Skip to content

Commit 2d4bffd

Browse files
Merge pull request #830 from zhangliugang/query-fileter
feat: implement eth_getLogs
2 parents dab2667 + d744289 commit 2d4bffd

File tree

7 files changed

+237
-3
lines changed

7 files changed

+237
-3
lines changed

Sources/Web3Core/Contract/ContractProtocol.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,13 @@ extension DefaultContractProtocol {
280280
return encodedData
281281
}
282282

283+
public func event(_ event: String, parameters: [Any]) -> [EventFilterParameters.Topic?] {
284+
guard let event = events[event] else {
285+
return []
286+
}
287+
return event.encodeParameters(parameters)
288+
}
289+
283290
public func parseEvent(_ eventLog: EventLog) -> (eventName: String?, eventData: [String: Any]?) {
284291
for (eName, ev) in self.events {
285292
if !ev.anonymous {

Sources/Web3Core/EthereumABI/ABIElements.swift

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,13 +211,85 @@ extension ABI.Element.Function {
211211
}
212212
}
213213

214-
// MARK: - Event logs decoding
214+
// MARK: - Event logs decoding & encoding
215215

216216
extension ABI.Element.Event {
217217
public func decodeReturnedLogs(eventLogTopics: [Data], eventLogData: Data) -> [String: Any]? {
218218
guard let eventContent = ABIDecoder.decodeLog(event: self, eventLogTopics: eventLogTopics, eventLogData: eventLogData) else { return nil }
219219
return eventContent
220220
}
221+
222+
public static func encodeTopic(input: ABI.Element.Event.Input, value: Any) -> EventFilterParameters.Topic? {
223+
switch input.type {
224+
case .string:
225+
guard let string = value as? String else {
226+
return nil
227+
}
228+
return .string(string.sha3(.keccak256).addHexPrefix())
229+
case .dynamicBytes:
230+
guard let data = ABIEncoder.convertToData(value) else {
231+
return nil
232+
}
233+
return .string(data.sha3(.keccak256).toHexString().addHexPrefix())
234+
case .bytes(length: _):
235+
guard let data = ABIEncoder.convertToData(value), let data = data.setLengthLeft(32) else {
236+
return nil
237+
}
238+
return .string(data.toHexString().addHexPrefix())
239+
case .address, .uint(bits: _), .int(bits: _), .bool:
240+
guard let encoded = ABIEncoder.encodeSingleType(type: input.type, value: value) else {
241+
return nil
242+
}
243+
return .string(encoded.toHexString().addHexPrefix())
244+
default:
245+
guard let data = try? ABIEncoder.abiEncode(value).setLengthLeft(32) else {
246+
return nil
247+
}
248+
return .string(data.toHexString().addHexPrefix())
249+
}
250+
}
251+
252+
public func encodeParameters(_ parameters: [Any?]) -> [EventFilterParameters.Topic?] {
253+
guard parameters.count <= inputs.count else {
254+
// too many arguments for fragment
255+
return []
256+
}
257+
var topics: [EventFilterParameters.Topic?] = []
258+
259+
if !anonymous {
260+
topics.append(.string(topic.toHexString().addHexPrefix()))
261+
}
262+
263+
for (i, p) in parameters.enumerated() {
264+
let input = inputs[i]
265+
if !input.indexed {
266+
// cannot filter non-indexed parameters; must be null
267+
return []
268+
}
269+
if p == nil {
270+
topics.append(nil)
271+
} else if input.type.isArray || input.type.isTuple {
272+
// filtering with tuples or arrays not supported
273+
return []
274+
} else if let p = p as? Array<Any> {
275+
topics.append(.strings(p.map { Self.encodeTopic(input: input, value: $0) }))
276+
} else {
277+
topics.append(Self.encodeTopic(input: input, value: p!))
278+
}
279+
}
280+
281+
// Trim off trailing nulls
282+
while let last = topics.last {
283+
if last == nil {
284+
topics.removeLast()
285+
} else if case .string(let string) = last, string == nil {
286+
topics.removeLast()
287+
} else {
288+
break
289+
}
290+
}
291+
return topics
292+
}
221293
}
222294

223295
// MARK: - Function input/output decoding

Sources/Web3Core/Transaction/EventfilterParameters.swift

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,8 @@ extension EventFilterParameters {
5050
var container = encoder.container(keyedBy: CodingKeys.self)
5151
try container.encode(fromBlock.description, forKey: .fromBlock)
5252
try container.encode(toBlock.description, forKey: .toBlock)
53-
try container.encode(address.description, forKey: .address)
54-
try container.encode(topics.textRepresentation, forKey: .topics)
53+
try container.encode(address, forKey: .address)
54+
try container.encode(topics, forKey: .topics)
5555
}
5656
}
5757

@@ -96,6 +96,17 @@ extension EventFilterParameters {
9696
case string(String?)
9797
case strings([Topic?]?)
9898

99+
public func encode(to encoder: Encoder) throws {
100+
switch self {
101+
case let .string(s):
102+
var container = encoder.singleValueContainer()
103+
try container.encode(s)
104+
case let .strings(ss):
105+
var container = encoder.unkeyedContainer()
106+
try container.encode(contentsOf: ss ?? [])
107+
}
108+
}
109+
99110
var rawValue: String {
100111
switch self {
101112
case let .string(string):

Sources/web3swift/EthereumAPICalls/Ethereum/IEth+Defaults.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,12 @@ public extension IEth {
130130
}
131131
}
132132

133+
public extension IEth {
134+
func getLogs(eventFilter: EventFilterParameters) async throws -> [EventLog] {
135+
try await APIRequest.sendRequest(with: self.provider, for: .getLogs(eventFilter)).result
136+
}
137+
}
138+
133139
public extension IEth {
134140
func send(_ transaction: CodableTransaction) async throws -> TransactionSendingResult {
135141
let request = APIRequest.sendTransaction(transaction)

Sources/web3swift/EthereumAPICalls/Ethereum/IEth.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ public protocol IEth {
2525

2626
func code(for address: EthereumAddress, onBlock: BlockNumber) async throws -> Hash
2727

28+
func getLogs(eventFilter: EventFilterParameters) async throws -> [EventLog]
29+
2830
func gasPrice() async throws -> BigUInt
2931

3032
func getTransactionCount(for address: EthereumAddress, onBlock: BlockNumber) async throws -> BigUInt
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
//
2+
// EventTests.swift
3+
//
4+
//
5+
// Created by liugang zhang on 2023/8/24.
6+
//
7+
8+
import XCTest
9+
import Web3Core
10+
import BigInt
11+
12+
@testable import web3swift
13+
14+
class EventTests: XCTestCase {
15+
16+
/// Solidity event allows up to 3 indexed field, this is just for test.
17+
let testEvent = """
18+
[{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"userOpHash","type":"bytes32"},{"indexed":true,"internalType":"address","name":"sender","type":"address"},{"indexed":true,"internalType":"string","name":"a","type":"string"},{"indexed":true,"internalType":"bool","name":"b","type":"bool"},{"indexed":true,"internalType":"bytes","name":"c","type":"bytes"},{"indexed":true,"internalType":"uint256","name":"d","type":"uint256"}],"name":"UserOperationEvent","type":"event"}]
19+
"""
20+
21+
func testEncodeTopicToJSON() throws {
22+
let encoder = JSONEncoder()
23+
let t1: [EventFilterParameters.Topic] = []
24+
let t2: [EventFilterParameters.Topic] = [.string(nil)]
25+
let t3: [EventFilterParameters.Topic] = [.strings([.string(nil), .string("1")])]
26+
let t4: [EventFilterParameters.Topic] = [.strings([nil, .string("1")])]
27+
XCTAssertNoThrow(try encoder.encode(t1))
28+
XCTAssertNoThrow(try encoder.encode(t2))
29+
XCTAssertNoThrow(try encoder.encode(t3))
30+
XCTAssertNoThrow(try encoder.encode(t4))
31+
32+
let topics: [EventFilterParameters.Topic] = [
33+
.string("1"),
34+
.strings([
35+
.string("2"),
36+
.string("3"),
37+
]
38+
)]
39+
let encoded = try encoder.encode(topics)
40+
let json = try JSONSerialization.jsonObject(with: encoded)
41+
XCTAssertEqual(json as? NSArray, ["1", ["2", "3"]])
42+
}
43+
44+
func testEncodeLogs() throws {
45+
let contract = try EthereumContract(testEvent)
46+
let topic = contract.events["UserOperationEvent"]!.topic
47+
let logs = contract.events["UserOperationEvent"]!.encodeParameters(
48+
[
49+
"0x2c16c07e1c68d502e9c7ad05f0402b365671a0e6517cb807b2de4edd95657042",
50+
"0x581074D2d9e50913eB37665b07CAFa9bFFdd1640",
51+
"hello,world",
52+
true,
53+
"0x02c16c07e1c68d50",
54+
nil
55+
]
56+
)
57+
XCTAssertEqual(logs.count, 6)
58+
59+
XCTAssertTrue(logs[0] == topic.toHexString().addHexPrefix())
60+
XCTAssertTrue(logs[1] == "0x2c16c07e1c68d502e9c7ad05f0402b365671a0e6517cb807b2de4edd95657042")
61+
XCTAssertTrue(logs[2] == "0x000000000000000000000000581074d2d9e50913eb37665b07cafa9bffdd1640")
62+
XCTAssertTrue(logs[3] == "0xab036729af8b8f9b610af4e11b14fa30c348f40c2c230cce92ef6ef37726fee7")
63+
XCTAssertTrue(logs[4] == "0x0000000000000000000000000000000000000000000000000000000000000001")
64+
XCTAssertTrue(logs[5] == "0x56f5a6cba57d26b32db8dc756fda960dcd3687770a300575a5f8107591eff63f")
65+
}
66+
67+
func testEncodeTopic() throws {
68+
XCTAssertTrue(ABI.Element.Event.encodeTopic(input: .init(name: "", type: .string, indexed: true), value: "hello,world") == "0xab036729af8b8f9b610af4e11b14fa30c348f40c2c230cce92ef6ef37726fee7")
69+
XCTAssertTrue(ABI.Element.Event.encodeTopic(input: .init(name: "", type: .address, indexed: true), value: "0x003e36550908907c2a2da960fd19a419b9a774b7") == "0x000000000000000000000000003e36550908907c2a2da960fd19a419b9a774b7")
70+
XCTAssertTrue(ABI.Element.Event.encodeTopic(input: .init(name: "", type: .address, indexed: true), value: EthereumAddress("0x003e36550908907c2a2da960fd19a419b9a774b7")!) == "0x000000000000000000000000003e36550908907c2a2da960fd19a419b9a774b7")
71+
XCTAssertTrue(ABI.Element.Event.encodeTopic(input: .init(name: "", type: .bool, indexed: true), value: true) == "0x0000000000000000000000000000000000000000000000000000000000000001")
72+
XCTAssertTrue(ABI.Element.Event.encodeTopic(input: .init(name: "", type: .bool, indexed: true), value: false) == "0x0000000000000000000000000000000000000000000000000000000000000000")
73+
XCTAssertTrue(ABI.Element.Event.encodeTopic(input: .init(name: "", type: .uint(bits: 256), indexed: true), value: BigUInt("dbe20a", radix: 16)!) == "0x0000000000000000000000000000000000000000000000000000000000dbe20a")
74+
XCTAssertTrue(ABI.Element.Event.encodeTopic(input: .init(name: "", type: .uint(bits: 256), indexed: true), value: "dbe20a") == "0x0000000000000000000000000000000000000000000000000000000000dbe20a")
75+
XCTAssertTrue(ABI.Element.Event.encodeTopic(input: .init(name: "", type: .int(bits: 32), indexed: true), value: 100) == "0x0000000000000000000000000000000000000000000000000000000000000064")
76+
XCTAssertTrue(ABI.Element.Event.encodeTopic(input: .init(name: "", type: .dynamicBytes, indexed: true), value: Data(hex: "6761766f66796f726b")) == "0xe0859ceea0a2fd2474deef2b2183f10f4c741ebba702e9a07d337522c0af55fb")
77+
XCTAssertTrue(ABI.Element.Event.encodeTopic(input: .init(name: "", type: .bytes(length: 32), indexed: true), value: Data(hex: "6761766f66796f726b")) == "0x00000000000000000000000000000000000000000000006761766f66796f726b")
78+
XCTAssertTrue(ABI.Element.Event.encodeTopic(input: .init(name: "", type: .bytes(length: 32), indexed: true), value: "0x6761766f66796f726b") == "0x00000000000000000000000000000000000000000000006761766f66796f726b")
79+
}
80+
}
81+
82+
private func ==(lhs: EventFilterParameters.Topic?, rhs: String?) -> Bool {
83+
if let lhs = lhs, case .string(let string) = lhs {
84+
return string == rhs
85+
}
86+
if lhs == nil && rhs == nil {
87+
return true
88+
}
89+
return false
90+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
//
2+
// EventFilterTests.swift
3+
//
4+
//
5+
// Created by liugang zhang on 2023/8/24.
6+
//
7+
8+
import XCTest
9+
import Web3Core
10+
import BigInt
11+
import CryptoSwift
12+
@testable import web3swift
13+
14+
class EventFilerTests: XCTestCase {
15+
16+
/// This test tx can be found at here:
17+
/// https://etherscan.io/tx/0x1a1daac5b3158f16399baec9abba2c8a4b4b7ffea5992490079b6bfc4ce70004
18+
func testErc20Transfer() async throws {
19+
let web3 = try await Web3.InfuraMainnetWeb3(accessToken: Constants.infuraToken)
20+
let address = EthereumAddress("0xdac17f958d2ee523a2206206994597c13d831ec7")!
21+
let erc20 = ERC20(web3: web3, provider: web3.provider, address: address)
22+
23+
let topics = erc20.contract.contract.event("Transfer", parameters: [
24+
"0x003e36550908907c2a2da960fd19a419b9a774b7"
25+
])
26+
27+
let parameters = EventFilterParameters(fromBlock: .exact(17983395), toBlock: .exact(17983395), address: [address], topics: topics)
28+
let result = try await web3.eth.getLogs(eventFilter: parameters)
29+
30+
XCTAssertEqual(result.count, 1)
31+
32+
let log = result.first!
33+
XCTAssertEqual(log.address.address.lowercased(), "0xdac17f958d2ee523a2206206994597c13d831ec7")
34+
XCTAssertEqual(log.transactionHash.toHexString().lowercased(), "1a1daac5b3158f16399baec9abba2c8a4b4b7ffea5992490079b6bfc4ce70004")
35+
36+
let logTopics = log.topics.map { $0.toHexString() }
37+
topics.compactMap { t -> String? in
38+
if let t = t, case EventFilterParameters.Topic.string(let topic) = t {
39+
return topic
40+
}
41+
return nil
42+
}.forEach { t in
43+
XCTAssertTrue(logTopics.contains(t.stripHexPrefix()))
44+
}
45+
}
46+
}

0 commit comments

Comments
 (0)