diff --git a/Sources/Web3Core/Contract/ContractProtocol.swift b/Sources/Web3Core/Contract/ContractProtocol.swift index 65f254d3b..c532a1934 100755 --- a/Sources/Web3Core/Contract/ContractProtocol.swift +++ b/Sources/Web3Core/Contract/ContractProtocol.swift @@ -284,6 +284,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 7c8f7f60e..4c58f08e7 100755 --- a/Sources/Web3Core/EthereumABI/ABIElements.swift +++ b/Sources/Web3Core/EthereumABI/ABIElements.swift @@ -211,13 +211,85 @@ 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 } + + 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()) + case .dynamicBytes: + guard let data = ABIEncoder.convertToData(value) else { + return nil + } + return .string(data.sha3(.keccak256).toHexString().addHexPrefix()) + case .bytes(length: _): + guard let data = ABIEncoder.convertToData(value), 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()) + } + } + + 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(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 { 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 { + break + } + } + return topics + } } // MARK: - Decode custom error 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/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/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 e0e9b94ca..16e6ff66b 100755 --- a/Sources/Web3Core/Utility/String+Extension.swift +++ b/Sources/Web3Core/Utility/String+Extension.swift @@ -120,7 +120,7 @@ extension String { let to16 = utf16.index(utf16.startIndex, offsetBy: nsRange.location + nsRange.length, limitedBy: utf16.endIndex), let from = from16.samePosition(in: self), let to = to16.samePosition(in: self) - else { return nil } + else { return nil } return from ..< to } @@ -139,6 +139,26 @@ extension String { public var isHex: Bool { stripHexPrefix().reduce(true, { $0 && $1.isHexDigit } ) } + + /// 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.. [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..7c1b4187b --- /dev/null +++ b/Tests/web3swiftTests/localTests/EventTests.swift @@ -0,0 +1,90 @@ +// +// EventTests.swift +// +// +// Created by liugang zhang on 2023/8/24. +// + +import XCTest +import Web3Core +import BigInt + +@testable import web3swift + +class EventTests: XCTestCase { + + /// 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 topics: [EventFilterParameters.Topic] = [ + .string("1"), + .strings([ + .string("2"), + .string("3"), + ] + )] + 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 topic = contract.events["UserOperationEvent"]!.topic + let logs = contract.events["UserOperationEvent"]!.encodeParameters( + [ + "0x2c16c07e1c68d502e9c7ad05f0402b365671a0e6517cb807b2de4edd95657042", + "0x581074D2d9e50913eB37665b07CAFa9bFFdd1640", + "hello,world", + true, + "0x02c16c07e1c68d50", + nil + ] + ) + XCTAssertEqual(logs.count, 6) + + XCTAssertTrue(logs[0] == topic.toHexString().addHexPrefix()) + XCTAssertTrue(logs[1] == "0x2c16c07e1c68d502e9c7ad05f0402b365671a0e6517cb807b2de4edd95657042") + XCTAssertTrue(logs[2] == "0x000000000000000000000000581074d2d9e50913eb37665b07cafa9bffdd1640") + XCTAssertTrue(logs[3] == "0xab036729af8b8f9b610af4e11b14fa30c348f40c2c230cce92ef6ef37726fee7") + XCTAssertTrue(logs[4] == "0x0000000000000000000000000000000000000000000000000000000000000001") + XCTAssertTrue(logs[5] == "0x56f5a6cba57d26b32db8dc756fda960dcd3687770a300575a5f8107591eff63f") + } + + 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: .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") + } +} + +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/localTests/UncategorizedTests.swift b/Tests/web3swiftTests/localTests/UncategorizedTests.swift index 440dd9dbd..fa6bcf4d7 100755 --- a/Tests/web3swiftTests/localTests/UncategorizedTests.swift +++ b/Tests/web3swiftTests/localTests/UncategorizedTests.swift @@ -51,6 +51,17 @@ class UncategorizedTests: LocalTestCase { XCTAssert(biguint == BigUInt("126978086000000000")) } + func testStringSplit() { + XCTAssertEqual("abcdefgh".split(every: 3), ["abc", "def", "gh"]) + XCTAssertEqual("abcdefgh".split(every: 3, backwards: true), ["ab", "cde", "fgh"]) + + XCTAssertEqual("abcdefgh".split(every: 10), ["abcdefgh"]) + XCTAssertEqual("".split(every: 3), []) + + XCTAssertEqual("abcdefgh".split(every: 1), ["a", "b", "c", "d", "e", "f", "g", "h"]) + XCTAssertEqual("abcdefgh".split(every: 1, backwards: true), ["a", "b", "c", "d", "e", "f", "g", "h"]) // should be the same as from the front + } + func testBloom() throws { let positive = [ "testtest", diff --git a/Tests/web3swiftTests/remoteTests/EventFilterTests.swift b/Tests/web3swiftTests/remoteTests/EventFilterTests.swift new file mode 100644 index 000000000..99a29683b --- /dev/null +++ b/Tests/web3swiftTests/remoteTests/EventFilterTests.swift @@ -0,0 +1,46 @@ +// +// EventFilterTests.swift +// +// +// Created by liugang zhang on 2023/8/24. +// + +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")! + let erc20 = ERC20(web3: web3, provider: web3.provider, address: address) + + let topics = erc20.contract.contract.event("Transfer", parameters: [ + "0x003e36550908907c2a2da960fd19a419b9a774b7" + ]) + + let parameters = EventFilterParameters(fromBlock: .exact(17983395), toBlock: .exact(17983395), address: [address], topics: topics) + let result = try await web3.eth.getLogs(eventFilter: parameters) + + 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())) + } + } +}