From 77193792d67e39c8f9111e5dee686125b10e4229 Mon Sep 17 00:00:00 2001 From: august Date: Thu, 24 Aug 2023 17:15:31 +0800 Subject: [PATCH 1/6] feat: enable encode Event and IETH.getLogs --- .../Web3Core/Contract/ContractProtocol.swift | 7 ++ .../Web3Core/EthereumABI/ABIElements.swift | 56 ++++++++++- .../Transaction/EventfilterParameters.swift | 15 ++- .../Ethereum/IEth+Defaults.swift | 6 ++ .../EthereumAPICalls/Ethereum/IEth.swift | 2 + .../localTests/EventTests.swift | 97 +++++++++++++++++++ .../remoteTests/EventFilterTests.swift | 30 ++++++ 7 files changed, 210 insertions(+), 3 deletions(-) create mode 100644 Tests/web3swiftTests/localTests/EventTests.swift create mode 100644 Tests/web3swiftTests/remoteTests/EventFilterTests.swift diff --git a/Sources/Web3Core/Contract/ContractProtocol.swift b/Sources/Web3Core/Contract/ContractProtocol.swift index 43a4c1543..b82d8aa8c 100755 --- a/Sources/Web3Core/Contract/ContractProtocol.swift +++ b/Sources/Web3Core/Contract/ContractProtocol.swift @@ -280,6 +280,13 @@ extension DefaultContractProtocol { return encodedData } + public func event(_ event: String, parameters: [Any]) -> [EventFilterParameters.Topic?] { + guard let event = events[event] else { + return [] + } + return event.encodeParameters(parameters) + } + public func parseEvent(_ eventLog: EventLog) -> (eventName: String?, eventData: [String: Any]?) { for (eName, ev) in self.events { if !ev.anonymous { diff --git a/Sources/Web3Core/EthereumABI/ABIElements.swift b/Sources/Web3Core/EthereumABI/ABIElements.swift index 5dca0b331..aac04199a 100755 --- a/Sources/Web3Core/EthereumABI/ABIElements.swift +++ b/Sources/Web3Core/EthereumABI/ABIElements.swift @@ -211,13 +211,67 @@ extension ABI.Element.Function { } } -// MARK: - Event logs decoding +// MARK: - Event logs decoding & encoding extension ABI.Element.Event { public func decodeReturnedLogs(eventLogTopics: [Data], eventLogData: Data) -> [String: Any]? { guard let eventContent = ABIDecoder.decodeLog(event: self, eventLogTopics: eventLogTopics, eventLogData: eventLogData) else { return nil } return eventContent } + + func encodeTopic(input: ABI.Element.Event.Input, value: Any) -> EventFilterParameters.Topic? { + if case .string = input.type { + guard let string = value as? String else { + return nil + } + return .string(string.sha3(.keccak256).addHexPrefix()) + } else if case .dynamicBytes = input.type { + guard let data = ABIEncoder.convertToData(value) else { + return nil + } + return .string(data.sha3(.keccak256).toHexString().addHexPrefix()) + } else if case .address = input.type { + guard let encoded = ABIEncoder.encode(types: [input.type], values: [value]) else { + return nil + } + return .string(encoded.toHexString().addHexPrefix()) + } + guard let data = ABIEncoder.convertToData(value)!.setLengthLeft(32) else { + return nil + } + return .string(data.toHexString().addHexPrefix()) + } + + public func encodeParameters(_ parameters: [Any?]) -> [EventFilterParameters.Topic?] { + guard parameters.count <= inputs.count else { + // too many arguments for fragment + return [] + } + var topics: [EventFilterParameters.Topic?] = [] + + if !anonymous { + topics.append(.string(topic.toHexString().addHexPrefix())) + } + + for (i, p) in parameters.enumerated() { + let input = inputs[i] + if !input.indexed { + // cannot filter non-indexed parameters; must be null + return [] + } + if p == nil { + topics.append(.string(nil)) + } else if input.type.isArray { + // filtering with tuples or arrays not supported + return [] + } else if let p = p as? Array { + topics.append(.strings(p.map { encodeTopic(input: input, value: $0) })) + } else { + topics.append(encodeTopic(input: input, value: p!)) + } + } + return topics + } } // MARK: - Function input/output decoding diff --git a/Sources/Web3Core/Transaction/EventfilterParameters.swift b/Sources/Web3Core/Transaction/EventfilterParameters.swift index 9850feb72..eb3c9342d 100755 --- a/Sources/Web3Core/Transaction/EventfilterParameters.swift +++ b/Sources/Web3Core/Transaction/EventfilterParameters.swift @@ -50,8 +50,8 @@ extension EventFilterParameters { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(fromBlock.description, forKey: .fromBlock) try container.encode(toBlock.description, forKey: .toBlock) - try container.encode(address.description, forKey: .address) - try container.encode(topics.textRepresentation, forKey: .topics) + try container.encode(address, forKey: .address) + try container.encode(topics, forKey: .topics) } } @@ -96,6 +96,17 @@ extension EventFilterParameters { case string(String?) case strings([Topic?]?) + public func encode(to encoder: Encoder) throws { + switch self { + case let .string(s): + var container = encoder.singleValueContainer() + try container.encode(s) + case let .strings(ss): + var container = encoder.unkeyedContainer() + try container.encode(contentsOf: ss ?? []) + } + } + var rawValue: String { switch self { case let .string(string): diff --git a/Sources/web3swift/EthereumAPICalls/Ethereum/IEth+Defaults.swift b/Sources/web3swift/EthereumAPICalls/Ethereum/IEth+Defaults.swift index c2732c66e..8d695cba2 100644 --- a/Sources/web3swift/EthereumAPICalls/Ethereum/IEth+Defaults.swift +++ b/Sources/web3swift/EthereumAPICalls/Ethereum/IEth+Defaults.swift @@ -130,6 +130,12 @@ public extension IEth { } } +public extension IEth { + func getLogs(eventFilter: EventFilterParameters) async throws -> [EventLog] { + try await APIRequest.sendRequest(with: self.provider, for: .getLogs(eventFilter)).result + } +} + public extension IEth { func send(_ transaction: CodableTransaction) async throws -> TransactionSendingResult { let request = APIRequest.sendTransaction(transaction) diff --git a/Sources/web3swift/EthereumAPICalls/Ethereum/IEth.swift b/Sources/web3swift/EthereumAPICalls/Ethereum/IEth.swift index ddade0c00..0ce33372f 100755 --- a/Sources/web3swift/EthereumAPICalls/Ethereum/IEth.swift +++ b/Sources/web3swift/EthereumAPICalls/Ethereum/IEth.swift @@ -25,6 +25,8 @@ public protocol IEth { func code(for address: EthereumAddress, onBlock: BlockNumber) async throws -> Hash + func getLogs(eventFilter: EventFilterParameters) async throws -> [EventLog] + func gasPrice() async throws -> BigUInt func getTransactionCount(for address: EthereumAddress, onBlock: BlockNumber) async throws -> BigUInt diff --git a/Tests/web3swiftTests/localTests/EventTests.swift b/Tests/web3swiftTests/localTests/EventTests.swift new file mode 100644 index 000000000..a05472a30 --- /dev/null +++ b/Tests/web3swiftTests/localTests/EventTests.swift @@ -0,0 +1,97 @@ +// +// EventTests.swift +// +// +// Created by liugang zhang on 2023/8/24. +// + +import XCTest +import Web3Core + +@testable import web3swift + +class EventTests: XCTestCase { + func testEncodeTopc() throws { + let encoder = JSONEncoder() + let t1: [EventFilterParameters.Topic] = [] + let t2: [EventFilterParameters.Topic] = [.string(nil)] + let t3: [EventFilterParameters.Topic] = [.strings([.string(nil), .string("1")])] + XCTAssertNoThrow(try encoder.encode(t1)) + XCTAssertNoThrow(try encoder.encode(t2)) + XCTAssertNoThrow(try encoder.encode(t3)) + + let t4: [EventFilterParameters.Topic] = [ + .string("1"), + .strings([ + .string("2"), + .string("3"), + ] + )] + let encoded = try encoder.encode(t4) + let json = try JSONSerialization.jsonObject(with: encoded) + XCTAssertEqual(json as? NSArray, ["1", ["2", "3"]]) + } + + func testEncodeLogs() throws { + let contract = try EthereumContract(TestEvent) + let logs = contract.events["UserOperationEvent"]?.encodeParameters( + [ + "0x2c16c07e1c68d502e9c7ad05f0402b365671a0e6517cb807b2de4edd95657042", + "0x581074D2d9e50913eB37665b07CAFa9bFFdd1640", + "hello,world", + true, + "0x02c16c07e1c68d50", + nil + ] + ) + + XCTAssert(logs?.count == 7) + } + + let TestEvent = """ + [{ + "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" + } + ] + """ +} diff --git a/Tests/web3swiftTests/remoteTests/EventFilterTests.swift b/Tests/web3swiftTests/remoteTests/EventFilterTests.swift new file mode 100644 index 000000000..d4c14e4bd --- /dev/null +++ b/Tests/web3swiftTests/remoteTests/EventFilterTests.swift @@ -0,0 +1,30 @@ +// +// EventFilterTests.swift +// +// +// Created by liugang zhang on 2023/8/24. +// + +import XCTest +import Web3Core + +@testable import web3swift + +class EventFilerTests: XCTestCase { + + func testErc20Transfer() async throws { + let web3 = try await Web3.InfuraMainnetWeb3(accessToken: Constants.infuraToken) + let address = EthereumAddress("0xdac17f958d2ee523a2206206994597c13d831ec7")! + let erc20 = ERC20(web3: web3, provider: web3.provider, address: address) + + let topics = erc20.contract.contract.event("Transfer", parameters: [ + "0x003e36550908907c2a2da960fd19a419b9a774b7" + ]) + let block = try await web3.eth.block(by: .latest) + let parameters = EventFilterParameters(fromBlock: .exact(block.number - 1000), address: [address], topics: topics) + let result = try await web3.eth.getLogs(eventFilter: parameters) + + // result not always has object in it. + print(result) + } +} From 909a1b044ebe87fa686cd1f5c4c54ac55a7c6283 Mon Sep 17 00:00:00 2001 From: august Date: Thu, 24 Aug 2023 17:27:35 +0800 Subject: [PATCH 2/6] remove a force unwrap --- Sources/Web3Core/EthereumABI/ABIElements.swift | 2 +- Tests/web3swiftTests/remoteTests/EventFilterTests.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Web3Core/EthereumABI/ABIElements.swift b/Sources/Web3Core/EthereumABI/ABIElements.swift index aac04199a..0582d01b7 100755 --- a/Sources/Web3Core/EthereumABI/ABIElements.swift +++ b/Sources/Web3Core/EthereumABI/ABIElements.swift @@ -236,7 +236,7 @@ extension ABI.Element.Event { } return .string(encoded.toHexString().addHexPrefix()) } - guard let data = ABIEncoder.convertToData(value)!.setLengthLeft(32) else { + guard let data = ABIEncoder.convertToData(value)?.setLengthLeft(32) else { return nil } return .string(data.toHexString().addHexPrefix()) diff --git a/Tests/web3swiftTests/remoteTests/EventFilterTests.swift b/Tests/web3swiftTests/remoteTests/EventFilterTests.swift index d4c14e4bd..d5f5ea216 100644 --- a/Tests/web3swiftTests/remoteTests/EventFilterTests.swift +++ b/Tests/web3swiftTests/remoteTests/EventFilterTests.swift @@ -24,7 +24,7 @@ class EventFilerTests: XCTestCase { let parameters = EventFilterParameters(fromBlock: .exact(block.number - 1000), address: [address], topics: topics) let result = try await web3.eth.getLogs(eventFilter: parameters) - // result not always has object in it. + // result not always has a log in it. print(result) } } From df870304448fb2bfc9d36570649436c1ea83e4c6 Mon Sep 17 00:00:00 2001 From: august Date: Thu, 24 Aug 2023 17:35:37 +0800 Subject: [PATCH 3/6] Trim Trailing Whitespace --- .../localTests/EventTests.swift | 47 +------------------ .../remoteTests/EventFilterTests.swift | 2 +- 2 files changed, 3 insertions(+), 46 deletions(-) diff --git a/Tests/web3swiftTests/localTests/EventTests.swift b/Tests/web3swiftTests/localTests/EventTests.swift index a05472a30..438cb7098 100644 --- a/Tests/web3swiftTests/localTests/EventTests.swift +++ b/Tests/web3swiftTests/localTests/EventTests.swift @@ -1,6 +1,6 @@ // // EventTests.swift -// +// // // Created by liugang zhang on 2023/8/24. // @@ -49,49 +49,6 @@ class EventTests: XCTestCase { } let TestEvent = """ - [{ - "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" - } - ] + [{"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"}] """ } diff --git a/Tests/web3swiftTests/remoteTests/EventFilterTests.swift b/Tests/web3swiftTests/remoteTests/EventFilterTests.swift index d5f5ea216..d6324e533 100644 --- a/Tests/web3swiftTests/remoteTests/EventFilterTests.swift +++ b/Tests/web3swiftTests/remoteTests/EventFilterTests.swift @@ -1,6 +1,6 @@ // // EventFilterTests.swift -// +// // // Created by liugang zhang on 2023/8/24. // From 89a98636a913f8fe6c8d441904cdcfb621c3fc1a Mon Sep 17 00:00:00 2001 From: august Date: Mon, 28 Aug 2023 18:28:39 +0800 Subject: [PATCH 4/6] add more test for encodeTopic --- .../Web3Core/EthereumABI/ABIElements.swift | 44 +++++++++++----- .../localTests/EventTests.swift | 51 +++++++++++++++---- .../remoteTests/EventFilterTests.swift | 26 ++++++++-- 3 files changed, 94 insertions(+), 27 deletions(-) diff --git a/Sources/Web3Core/EthereumABI/ABIElements.swift b/Sources/Web3Core/EthereumABI/ABIElements.swift index 0582d01b7..73004021c 100755 --- a/Sources/Web3Core/EthereumABI/ABIElements.swift +++ b/Sources/Web3Core/EthereumABI/ABIElements.swift @@ -219,27 +219,34 @@ extension ABI.Element.Event { return eventContent } - func encodeTopic(input: ABI.Element.Event.Input, value: Any) -> EventFilterParameters.Topic? { - if case .string = input.type { + public static func encodeTopic(input: ABI.Element.Event.Input, value: Any) -> EventFilterParameters.Topic? { + switch input.type { + case .string: guard let string = value as? String else { return nil } return .string(string.sha3(.keccak256).addHexPrefix()) - } else if case .dynamicBytes = input.type { + case .dynamicBytes: guard let data = ABIEncoder.convertToData(value) else { return nil } return .string(data.sha3(.keccak256).toHexString().addHexPrefix()) - } else if case .address = input.type { - guard let encoded = ABIEncoder.encode(types: [input.type], values: [value]) else { + case .bytes(length: _): + guard let data = value as? Data, let data = data.setLengthLeft(32) else { + return nil + } + return .string(data.toHexString().addHexPrefix()) + case .address, .uint(bits: _), .int(bits: _), .bool: + guard let encoded = ABIEncoder.encodeSingleType(type: input.type, value: value) else { return nil } return .string(encoded.toHexString().addHexPrefix()) + default: + guard let data = try? ABIEncoder.abiEncode(value).setLengthLeft(32) else { + return nil + } + return .string(data.toHexString().addHexPrefix()) } - guard let data = ABIEncoder.convertToData(value)?.setLengthLeft(32) else { - return nil - } - return .string(data.toHexString().addHexPrefix()) } public func encodeParameters(_ parameters: [Any?]) -> [EventFilterParameters.Topic?] { @@ -260,14 +267,25 @@ extension ABI.Element.Event { return [] } if p == nil { - topics.append(.string(nil)) - } else if input.type.isArray { + topics.append(nil) + } else if input.type.isArray || input.type.isTuple { // filtering with tuples or arrays not supported return [] } else if let p = p as? Array { - topics.append(.strings(p.map { encodeTopic(input: input, value: $0) })) + topics.append(.strings(p.map { Self.encodeTopic(input: input, value: $0) })) + } else { + topics.append(Self.encodeTopic(input: input, value: p!)) + } + } + + // Trim off trailing nulls + while let last = topics.last { + if last == nil { + topics.removeLast() + } else if case .string(let string) = last, string == nil { + topics.removeLast() } else { - topics.append(encodeTopic(input: input, value: p!)) + break } } return topics diff --git a/Tests/web3swiftTests/localTests/EventTests.swift b/Tests/web3swiftTests/localTests/EventTests.swift index 438cb7098..823a122da 100644 --- a/Tests/web3swiftTests/localTests/EventTests.swift +++ b/Tests/web3swiftTests/localTests/EventTests.swift @@ -7,34 +7,44 @@ import XCTest import Web3Core +import BigInt @testable import web3swift class EventTests: XCTestCase { - func testEncodeTopc() throws { + + /// Solidity event allows up to 3 indexed field, this is just for test. + let testEvent = """ + [{"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"}] + """ + + func testEncodeTopicToJSON() throws { let encoder = JSONEncoder() let t1: [EventFilterParameters.Topic] = [] let t2: [EventFilterParameters.Topic] = [.string(nil)] let t3: [EventFilterParameters.Topic] = [.strings([.string(nil), .string("1")])] + let t4: [EventFilterParameters.Topic] = [.strings([nil, .string("1")])] XCTAssertNoThrow(try encoder.encode(t1)) XCTAssertNoThrow(try encoder.encode(t2)) XCTAssertNoThrow(try encoder.encode(t3)) + XCTAssertNoThrow(try encoder.encode(t4)) - let t4: [EventFilterParameters.Topic] = [ + let topics: [EventFilterParameters.Topic] = [ .string("1"), .strings([ .string("2"), .string("3"), ] )] - let encoded = try encoder.encode(t4) + let encoded = try encoder.encode(topics) let json = try JSONSerialization.jsonObject(with: encoded) XCTAssertEqual(json as? NSArray, ["1", ["2", "3"]]) } func testEncodeLogs() throws { - let contract = try EthereumContract(TestEvent) - let logs = contract.events["UserOperationEvent"]?.encodeParameters( + let contract = try EthereumContract(testEvent) + let topic = contract.events["UserOperationEvent"]!.topic + let logs = contract.events["UserOperationEvent"]!.encodeParameters( [ "0x2c16c07e1c68d502e9c7ad05f0402b365671a0e6517cb807b2de4edd95657042", "0x581074D2d9e50913eB37665b07CAFa9bFFdd1640", @@ -44,11 +54,34 @@ class EventTests: XCTestCase { nil ] ) + XCTAssertEqual(logs.count, 6) - XCTAssert(logs?.count == 7) + XCTAssertTrue(logs[0] == topic.toHexString().addHexPrefix()) + XCTAssertTrue(logs[1] == nil) + XCTAssertTrue(logs[2] == "0x000000000000000000000000581074d2d9e50913eb37665b07cafa9bffdd1640") + XCTAssertTrue(logs[3] == "0xab036729af8b8f9b610af4e11b14fa30c348f40c2c230cce92ef6ef37726fee7") + XCTAssertTrue(logs[4] == "0x0000000000000000000000000000000000000000000000000000000000000001") + XCTAssertTrue(logs[5] == "0x56f5a6cba57d26b32db8dc756fda960dcd3687770a300575a5f8107591eff63f") } - let TestEvent = """ - [{"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"}] - """ + func testEncodeTopic() throws { + XCTAssertTrue(ABI.Element.Event.encodeTopic(input: .init(name: "", type: .string, indexed: true), value: "hello,world") == "0xab036729af8b8f9b610af4e11b14fa30c348f40c2c230cce92ef6ef37726fee7") + XCTAssertTrue(ABI.Element.Event.encodeTopic(input: .init(name: "", type: .address, indexed: true), value: "0x003e36550908907c2a2da960fd19a419b9a774b7") == "0x000000000000000000000000003e36550908907c2a2da960fd19a419b9a774b7") + XCTAssertTrue(ABI.Element.Event.encodeTopic(input: .init(name: "", type: .address, indexed: true), value: EthereumAddress("0x003e36550908907c2a2da960fd19a419b9a774b7")!) == "0x000000000000000000000000003e36550908907c2a2da960fd19a419b9a774b7") + XCTAssertTrue(ABI.Element.Event.encodeTopic(input: .init(name: "", type: .bool, indexed: true), value: true) == "0x0000000000000000000000000000000000000000000000000000000000000001") + XCTAssertTrue(ABI.Element.Event.encodeTopic(input: .init(name: "", type: .uint(bits: 256), indexed: true), value: BigUInt("dbe20a", radix: 16)!) == "0x0000000000000000000000000000000000000000000000000000000000dbe20a") + XCTAssertTrue(ABI.Element.Event.encodeTopic(input: .init(name: "", type: .int(bits: 32), indexed: true), value: 100) == "0x0000000000000000000000000000000000000000000000000000000000000064") + XCTAssertTrue(ABI.Element.Event.encodeTopic(input: .init(name: "", type: .dynamicBytes, indexed: true), value: Data(hex: "6761766f66796f726b")) == "0xe0859ceea0a2fd2474deef2b2183f10f4c741ebba702e9a07d337522c0af55fb") + XCTAssertTrue(ABI.Element.Event.encodeTopic(input: .init(name: "", type: .bytes(length: 32), indexed: true), value: Data(hex: "6761766f66796f726b")) == "0x00000000000000000000000000000000000000000000006761766f66796f726b") + } +} + +private func ==(lhs: EventFilterParameters.Topic?, rhs: String?) -> Bool { + if let lhs = lhs, case .string(let string) = lhs { + return string == rhs + } + if lhs == nil && rhs == nil { + return true + } + return false } diff --git a/Tests/web3swiftTests/remoteTests/EventFilterTests.swift b/Tests/web3swiftTests/remoteTests/EventFilterTests.swift index d6324e533..99a29683b 100644 --- a/Tests/web3swiftTests/remoteTests/EventFilterTests.swift +++ b/Tests/web3swiftTests/remoteTests/EventFilterTests.swift @@ -7,11 +7,14 @@ import XCTest import Web3Core - +import BigInt +import CryptoSwift @testable import web3swift class EventFilerTests: XCTestCase { + /// This test tx can be found at here: + /// https://etherscan.io/tx/0x1a1daac5b3158f16399baec9abba2c8a4b4b7ffea5992490079b6bfc4ce70004 func testErc20Transfer() async throws { let web3 = try await Web3.InfuraMainnetWeb3(accessToken: Constants.infuraToken) let address = EthereumAddress("0xdac17f958d2ee523a2206206994597c13d831ec7")! @@ -20,11 +23,24 @@ class EventFilerTests: XCTestCase { let topics = erc20.contract.contract.event("Transfer", parameters: [ "0x003e36550908907c2a2da960fd19a419b9a774b7" ]) - let block = try await web3.eth.block(by: .latest) - let parameters = EventFilterParameters(fromBlock: .exact(block.number - 1000), address: [address], topics: topics) + + let parameters = EventFilterParameters(fromBlock: .exact(17983395), toBlock: .exact(17983395), address: [address], topics: topics) let result = try await web3.eth.getLogs(eventFilter: parameters) - // result not always has a log in it. - print(result) + XCTAssertEqual(result.count, 1) + + let log = result.first! + XCTAssertEqual(log.address.address.lowercased(), "0xdac17f958d2ee523a2206206994597c13d831ec7") + XCTAssertEqual(log.transactionHash.toHexString().lowercased(), "1a1daac5b3158f16399baec9abba2c8a4b4b7ffea5992490079b6bfc4ce70004") + + let logTopics = log.topics.map { $0.toHexString() } + topics.compactMap { t -> String? in + if let t = t, case EventFilterParameters.Topic.string(let topic) = t { + return topic + } + return nil + }.forEach { t in + XCTAssertTrue(logTopics.contains(t.stripHexPrefix())) + } } } From d744289fdc87a7ae297bcd3cd996508ddfa51e6f Mon Sep 17 00:00:00 2001 From: august Date: Sat, 2 Sep 2023 21:22:44 +0800 Subject: [PATCH 5/6] encodeTopic bytes should accept hex string --- Sources/Web3Core/EthereumABI/ABIElements.swift | 2 +- Tests/web3swiftTests/localTests/EventTests.swift | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Sources/Web3Core/EthereumABI/ABIElements.swift b/Sources/Web3Core/EthereumABI/ABIElements.swift index 73004021c..f238e543f 100755 --- a/Sources/Web3Core/EthereumABI/ABIElements.swift +++ b/Sources/Web3Core/EthereumABI/ABIElements.swift @@ -232,7 +232,7 @@ extension ABI.Element.Event { } return .string(data.sha3(.keccak256).toHexString().addHexPrefix()) case .bytes(length: _): - guard let data = value as? Data, let data = data.setLengthLeft(32) else { + guard let data = ABIEncoder.convertToData(value), let data = data.setLengthLeft(32) else { return nil } return .string(data.toHexString().addHexPrefix()) diff --git a/Tests/web3swiftTests/localTests/EventTests.swift b/Tests/web3swiftTests/localTests/EventTests.swift index 823a122da..7c1b4187b 100644 --- a/Tests/web3swiftTests/localTests/EventTests.swift +++ b/Tests/web3swiftTests/localTests/EventTests.swift @@ -57,7 +57,7 @@ class EventTests: XCTestCase { XCTAssertEqual(logs.count, 6) XCTAssertTrue(logs[0] == topic.toHexString().addHexPrefix()) - XCTAssertTrue(logs[1] == nil) + XCTAssertTrue(logs[1] == "0x2c16c07e1c68d502e9c7ad05f0402b365671a0e6517cb807b2de4edd95657042") XCTAssertTrue(logs[2] == "0x000000000000000000000000581074d2d9e50913eb37665b07cafa9bffdd1640") XCTAssertTrue(logs[3] == "0xab036729af8b8f9b610af4e11b14fa30c348f40c2c230cce92ef6ef37726fee7") XCTAssertTrue(logs[4] == "0x0000000000000000000000000000000000000000000000000000000000000001") @@ -69,10 +69,13 @@ class EventTests: XCTestCase { XCTAssertTrue(ABI.Element.Event.encodeTopic(input: .init(name: "", type: .address, indexed: true), value: "0x003e36550908907c2a2da960fd19a419b9a774b7") == "0x000000000000000000000000003e36550908907c2a2da960fd19a419b9a774b7") XCTAssertTrue(ABI.Element.Event.encodeTopic(input: .init(name: "", type: .address, indexed: true), value: EthereumAddress("0x003e36550908907c2a2da960fd19a419b9a774b7")!) == "0x000000000000000000000000003e36550908907c2a2da960fd19a419b9a774b7") XCTAssertTrue(ABI.Element.Event.encodeTopic(input: .init(name: "", type: .bool, indexed: true), value: true) == "0x0000000000000000000000000000000000000000000000000000000000000001") + XCTAssertTrue(ABI.Element.Event.encodeTopic(input: .init(name: "", type: .bool, indexed: true), value: false) == "0x0000000000000000000000000000000000000000000000000000000000000000") XCTAssertTrue(ABI.Element.Event.encodeTopic(input: .init(name: "", type: .uint(bits: 256), indexed: true), value: BigUInt("dbe20a", radix: 16)!) == "0x0000000000000000000000000000000000000000000000000000000000dbe20a") + XCTAssertTrue(ABI.Element.Event.encodeTopic(input: .init(name: "", type: .uint(bits: 256), indexed: true), value: "dbe20a") == "0x0000000000000000000000000000000000000000000000000000000000dbe20a") XCTAssertTrue(ABI.Element.Event.encodeTopic(input: .init(name: "", type: .int(bits: 32), indexed: true), value: 100) == "0x0000000000000000000000000000000000000000000000000000000000000064") XCTAssertTrue(ABI.Element.Event.encodeTopic(input: .init(name: "", type: .dynamicBytes, indexed: true), value: Data(hex: "6761766f66796f726b")) == "0xe0859ceea0a2fd2474deef2b2183f10f4c741ebba702e9a07d337522c0af55fb") XCTAssertTrue(ABI.Element.Event.encodeTopic(input: .init(name: "", type: .bytes(length: 32), indexed: true), value: Data(hex: "6761766f66796f726b")) == "0x00000000000000000000000000000000000000000000006761766f66796f726b") + XCTAssertTrue(ABI.Element.Event.encodeTopic(input: .init(name: "", type: .bytes(length: 32), indexed: true), value: "0x6761766f66796f726b") == "0x00000000000000000000000000000000000000000000006761766f66796f726b") } } From bab86ca2258cabb7ee3cc4d6ff0a37ca6ce22dd5 Mon Sep 17 00:00:00 2001 From: "pharms.eth" <100330083+pharms-eth@users.noreply.github.com> Date: Tue, 7 Nov 2023 14:45:25 -0800 Subject: [PATCH 6/6] refactor: random bytes generation; data and string extensions update (#791) * Update HexDecodable+Extensions.swift * Update Data+Extension.swift * Update String+Extension.swift * chore: refactoring + documentation for Data.fromHex function * updates as per conversations * chore: updated docs for randomBytes + minor refactoring * chore: fixed typo in Data+Extension.swift * chore: lint issue fixed * add string spit test * Revert "add string spit test" github issue This reverts commit e8a5e2bf4e799d7bb35a0a3e280b00cd2da2ec0d. --------- Co-authored-by: Jenea Vranceanu Co-authored-by: Jenea Vranceanu <36865532+JeneaVranceanu@users.noreply.github.com> --- .../Utility/HexDecodable+Extensions.swift | 21 +++++----- Sources/Web3Core/Utility/Data+Extension.swift | 39 ++++++++++--------- .../Web3Core/Utility/String+Extension.swift | 20 ++++++++++ .../localTests/UncategorizedTests.swift | 11 ++++++ 4 files changed, 63 insertions(+), 28 deletions(-) diff --git a/Sources/Web3Core/EthereumNetwork/Utility/HexDecodable+Extensions.swift b/Sources/Web3Core/EthereumNetwork/Utility/HexDecodable+Extensions.swift index d64bb9a09..739442fca 100644 --- a/Sources/Web3Core/EthereumNetwork/Utility/HexDecodable+Extensions.swift +++ b/Sources/Web3Core/EthereumNetwork/Utility/HexDecodable+Extensions.swift @@ -17,16 +17,17 @@ extension BigInt: LiteralInitiableFromString { } extension BigUInt: LiteralInitiableFromString { } extension Data: LiteralInitiableFromString { + /// Converts hexadecimal string representation of some bytes into actual bytes. + /// Notes: + /// - empty string will return `nil`; + /// - empty hex string, meaning it's equal to `"0x"`, will return empty `Data` object. + /// - Parameter hex: bytes represented as string. + /// - Returns: optional raw bytes. public static func fromHex(_ hex: String) -> Data? { - let string = hex.lowercased().stripHexPrefix() - let array = [UInt8](hex: string) - if array.count == 0 { - if hex == "0x" || hex == "" { - return Data() - } else { - return nil - } - } - return Data(array) + let hex = hex.lowercased().trim() + guard !hex.isEmpty else { return nil } + guard hex != "0x" else { return Data() } + let bytes = [UInt8](hex: hex.stripHexPrefix()) + return bytes.isEmpty ? nil : Data(bytes) } } diff --git a/Sources/Web3Core/Utility/Data+Extension.swift b/Sources/Web3Core/Utility/Data+Extension.swift index 448728f1a..3a8185d0e 100755 --- a/Sources/Web3Core/Utility/Data+Extension.swift +++ b/Sources/Web3Core/Utility/Data+Extension.swift @@ -5,7 +5,8 @@ import Foundation -extension Data { +public extension Data { + init(fromArray values: [T]) { let values = values let ptrUB = values.withUnsafeBufferPointer { (ptr: UnsafeBufferPointer) in return ptr } @@ -33,32 +34,34 @@ extension Data { return difference == UInt8(0x00) } - public static func zero(_ data: inout Data) { + static func zero(_ data: inout Data) { let count = data.count data.withUnsafeMutableBytes { (body: UnsafeMutableRawBufferPointer) in body.baseAddress?.assumingMemoryBound(to: UInt8.self).initialize(repeating: 0, count: count) } } - public static func randomBytes(length: Int) -> Data? { - for _ in 0...1024 { - var data = Data(repeating: 0, count: length) - let result = data.withUnsafeMutableBytes { (body: UnsafeMutableRawBufferPointer) -> Int32? in - if let bodyAddress = body.baseAddress, body.count > 0 { - let pointer = bodyAddress.assumingMemoryBound(to: UInt8.self) - return SecRandomCopyBytes(kSecRandomDefault, length, pointer) - } else { - return nil - } - } - if let notNilResult = result, notNilResult == errSecSuccess { - return data - } + /** + Generates an array of random bytes of the specified length. + This function uses `SecRandomCopyBytes` to generate random bytes returning it as a `Data` object. + If an error occurs during random bytes generation, the function returns `nil`. + Error occurs only if `SecRandomCopyBytes` returns status that is not `errSecSuccess`. + See [all status codes](https://developer.apple.com/documentation/security/1542001-security_framework_result_codes) for possible error reasons. + Note: in v4 of web3swift this function will be deprecated and a new implementation will be provided that will throw occurred error. + - Parameter length: The number of random bytes to generate. + + - Returns: optional `Data` object containing the generated random bytes, or `nil` if an error occurred during generation. + */ + static func randomBytes(length: Int) -> Data? { + var entropyBytes = [UInt8](repeating: 0, count: length) + let status = SecRandomCopyBytes(kSecRandomDefault, entropyBytes.count, &entropyBytes) + guard status == errSecSuccess else { + return nil } - return nil + return Data(entropyBytes) } - public func bitsInRange(_ startingBit: Int, _ length: Int) -> UInt64? { // return max of 8 bytes for simplicity, non-public + func bitsInRange(_ startingBit: Int, _ length: Int) -> UInt64? { // return max of 8 bytes for simplicity, non-public if startingBit + length / 8 > self.count, length > 64, startingBit > 0, length >= 1 { return nil } let bytes = self[(startingBit/8) ..< (startingBit+length+7)/8] let padding = Data(repeating: 0, count: 8 - bytes.count) diff --git a/Sources/Web3Core/Utility/String+Extension.swift b/Sources/Web3Core/Utility/String+Extension.swift index dbe0a10ca..529fe2ff0 100755 --- a/Sources/Web3Core/Utility/String+Extension.swift +++ b/Sources/Web3Core/Utility/String+Extension.swift @@ -135,6 +135,26 @@ extension String { func trim() -> String { trimmingCharacters(in: .whitespacesAndNewlines) } + + /// Splits a string into groups of `every` n characters, grouping from left-to-right by default. If `backwards` is true, right-to-left. + public func split(every: Int, backwards: Bool = false) -> [String] { + var result = [String]() + + for i in stride(from: 0, to: self.count, by: every) { + switch backwards { + case true: + let endIndex = self.index(self.endIndex, offsetBy: -i) + let startIndex = self.index(endIndex, offsetBy: -every, limitedBy: self.startIndex) ?? self.startIndex + result.insert(String(self[startIndex..