From c9410ace6f538e98d478afe837ef797aecdbe29e Mon Sep 17 00:00:00 2001 From: Matt Marshall Date: Tue, 18 Aug 2020 16:38:16 +0100 Subject: [PATCH 01/20] [ADD] eth_estimateGas support (#107) --- web3swift/src/Client/EthereumClient.swift | 59 +++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/web3swift/src/Client/EthereumClient.swift b/web3swift/src/Client/EthereumClient.swift index 6b32a895..3249b9a9 100644 --- a/web3swift/src/Client/EthereumClient.swift +++ b/web3swift/src/Client/EthereumClient.swift @@ -19,6 +19,7 @@ public protocol EthereumClientProtocol { func eth_blockNumber(completion: @escaping((EthereumClientError?, Int?) -> Void)) func eth_getBalance(address: String, block: EthereumBlock, completion: @escaping((EthereumClientError?, BigUInt?) -> Void)) func eth_getCode(address: String, block: EthereumBlock, completion: @escaping((EthereumClientError?, String?) -> Void)) + func eth_estimateGas(_ transaction: EthereumTransaction, withAccount account: EthereumAccount, completion: @escaping((EthereumClientError?, BigUInt?) -> Void)) func eth_sendRawTransaction(_ transaction: EthereumTransaction, withAccount account: EthereumAccount, completion: @escaping((EthereumClientError?, String?) -> Void)) func eth_getTransactionCount(address: String, block: EthereumBlock, completion: @escaping((EthereumClientError?, Int?) -> Void)) func eth_getTransaction(byHash txHash: String, completion: @escaping((EthereumClientError?, EthereumTransaction?) -> Void)) @@ -150,6 +151,64 @@ public class EthereumClient: EthereumClientProtocol { } } + public func eth_estimateGas(_ transaction: EthereumTransaction, withAccount account: EthereumAccount, completion: @escaping((EthereumClientError?, BigUInt?) -> Void)) { + + struct CallParams: Encodable { + let from: String? + let to: String + let gas: String? + let gasPrice: String? + let value: String? + let data: String? + + enum TransactionCodingKeys: String, CodingKey { + case from + case to + case gas + case gasPrice + case value + case data + } + + func encode(to encoder: Encoder) throws { + var container = encoder.unkeyedContainer() + var nested = container.nestedContainer(keyedBy: TransactionCodingKeys.self) + if let from = from { + try nested.encode(from, forKey: .from) + } + try nested.encode(to, forKey: .to) + if let gas = gas { + try nested.encode(gas, forKey: .gas) + } + if let gasPrice = gasPrice { + try nested.encode(gasPrice, forKey: .gasPrice) + } + if let value = value { + try nested.encode(value, forKey: .value) + } + if let data = data { + try nested.encode(data, forKey: .data) + } + } + } + + let value: BigUInt? + if let txValue = transaction.value, txValue > .zero { + value = txValue + } else { + value = nil + } + + let params = CallParams(from: transaction.from?.value, to: transaction.to.value, gas: transaction.gasLimit?.web3.hexString, gasPrice: transaction.gasPrice?.web3.hexString, value: value?.web3.hexString, data: transaction.data?.web3.hexString) + EthereumRPC.execute(session: session, url: url, method: "eth_estimateGas", params: params, receive: String.self) { (error, response) in + if let gasHex = response as? String, let gas = BigUInt(hex: gasHex) { + completion(nil, gas) + } else { + completion(EthereumClientError.unexpectedReturnValue, nil) + } + } + } + public func eth_sendRawTransaction(_ transaction: EthereumTransaction, withAccount account: EthereumAccount, completion: @escaping ((EthereumClientError?, String?) -> Void)) { concurrentQueue.addOperation { From 07615c1695a344d4279da6f05f4665a5987f10f5 Mon Sep 17 00:00:00 2001 From: Miguel Angel Quinones Date: Fri, 21 Aug 2020 16:12:26 +0200 Subject: [PATCH 02/20] [FIX] Arrays in tuples not correcly nesting the encoding API needs static call to use generics correctly, so moved the encoding of a tuple to the type (exactly as functions) It results in a temporary encoder to get the encoded values, but the tradeoff is good to work with. --- .../Contract/ABIFunctionEncoderTests.swift | 65 ++++++++++++++++++- .../Statically Typed/ABIEncoder+Static.swift | 10 ++- .../Statically Typed/ABIFunctionEncoder.swift | 2 +- .../Contract/Statically Typed/ABITuple.swift | 1 + 4 files changed, 74 insertions(+), 4 deletions(-) diff --git a/web3sTests/Contract/ABIFunctionEncoderTests.swift b/web3sTests/Contract/ABIFunctionEncoderTests.swift index d61bd081..1f473594 100644 --- a/web3sTests/Contract/ABIFunctionEncoderTests.swift +++ b/web3sTests/Contract/ABIFunctionEncoderTests.swift @@ -151,7 +151,7 @@ class ABIFunctionEncoderTests: XCTestCase { } // See example: https://solidity.readthedocs.io/en/v0.6.11/abi-spec.html#use-of-dynamic-types - func test_ArrayOfArraysSample_ThenEncodesCorrectly() { + func test_GivenArrayOfArraysSample_ThenEncodesCorrectly() { encoder = ABIFunctionEncoder("f") do { @@ -165,6 +165,30 @@ class ABIFunctionEncoderTests: XCTestCase { XCTFail() } } + + func test_GivenArrayOfComplexTuples_WhenEncodesOneEntry_ThenEncodesCorrectly() { + do { + let tuple = ComplexTupleWithArray(address: EthereumAddress("0xdF136715f7bafD40881cFb16eAa5595C2562972b"), amount: 2, owners: [SimpleTuple(address: EthereumAddress("0xdF136715f7bafD40881cFb16eAa5595C2562972b"), amount: 100)]) + + try encoder.encode([tuple]) + XCTAssertEqual(try encoder.encoded().web3.hexString, + "0x07e0fd75000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000df136715f7bafd40881cfb16eaa5595c2562972b000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000001000000000000000000000000df136715f7bafd40881cfb16eaa5595c2562972b0000000000000000000000000000000000000000000000000000000000000064") + } catch { + XCTFail() + } + } + + func test_GivenArrayOfComplexTuples_WhenEncodesTwoEntries_ThenEncodesCorrectly() { + do { + let tuple1 = ComplexTupleWithArray(address: EthereumAddress("0xdF136715f7bafD40881cFb16eAa5595C2562972b"), amount: 2, owners: [SimpleTuple(address: EthereumAddress("0x4bf21a47b608841e974ff4147fd1a005da7fdf9b"), amount: 100)]) + let tuple2 = ComplexTupleWithArray(address: EthereumAddress("0x69F84b91E7107206E841748C2B52294A1176D45e"), amount: 3, owners: [SimpleTuple(address: EthereumAddress("0xc07d381fFadB957e0FC9218AaBa88556f5C4BB7a"), amount: 200)]) + try encoder.encode([tuple1, tuple2]) + XCTAssertEqual(try encoder.encoded().web3.hexString, + "0x07e0fd750000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000100000000000000000000000000df136715f7bafd40881cfb16eaa5595c2562972b0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000010000000000000000000000004bf21a47b608841e974ff4147fd1a005da7fdf9b000000000000000000000000000000000000000000000000000000000000006400000000000000000000000069f84b91e7107206e841748c2b52294a1176d45e000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000001000000000000000000000000c07d381ffadb957e0fc9218aaba88556f5c4bb7a00000000000000000000000000000000000000000000000000000000000000c8") + } catch { + XCTFail() + } + } } fileprivate struct SimpleTuple: ABITuple { @@ -184,6 +208,11 @@ fileprivate struct SimpleTuple: ABITuple { self.amount = try values[1].decoded() } + func encode(to encoder: ABIFunctionEncoder) throws { + try encoder.encode(address) + try encoder.encode(amount) + } + var encodableValues: [ABIType] { [address, amount] } } @@ -200,9 +229,43 @@ fileprivate struct DynamicContentTuple: ABITuple { self.message = try values[0].decoded() } + func encode(to encoder: ABIFunctionEncoder) throws { + try encoder.encode(message) + } + var encodableValues: [ABIType] { [message] } } +fileprivate struct ComplexTupleWithArray: ABITuple { + static var types: [ABIType.Type] { [EthereumAddress.self, BigUInt.self, ABIArray.self] } + + var address: EthereumAddress + var amount: BigUInt + var owners: [SimpleTuple] + + init(address: EthereumAddress, + amount: BigUInt, + owners: [SimpleTuple]) { + self.address = address + self.amount = amount + self.owners = owners + } + + init?(values: [ABIDecoder.DecodedValue]) throws { + self.address = try values[0].decoded() + self.amount = try values[1].decoded() + self.owners = try values[2].decodedArray() + } + + func encode(to encoder: ABIFunctionEncoder) throws { + try encoder.encode(address) + try encoder.encode(amount) + try encoder.encode(owners) + } + + var encodableValues: [ABIType] { [address, amount, ABIArray(values: owners)] } +} + fileprivate struct RelayerExecute: ABIFunction { static let name = "execute" let contract = EthereumAddress.zero diff --git a/web3swift/src/Contract/Statically Typed/ABIEncoder+Static.swift b/web3swift/src/Contract/Statically Typed/ABIEncoder+Static.swift index ebf4e080..e9d35ead 100644 --- a/web3swift/src/Contract/Statically Typed/ABIEncoder+Static.swift +++ b/web3swift/src/Contract/Statically Typed/ABIEncoder+Static.swift @@ -40,8 +40,7 @@ extension ABIEncoder { } case let value as ABITuple: - let sizeToEncode = type.isDynamic && value.encodableValues.count > 1 ? value.encodableValues.count : nil - return try ABIEncoder.encodeArray(elements: value.encodableValues.map { (value: $0, size: nil)}, isDynamic: type.isDynamic, size: sizeToEncode) + return try encodeTuple(value, type: type) default: throw ABIError.notCurrentlySupported } @@ -60,4 +59,11 @@ extension ABIEncoder { return .container(values: values, isDynamic: isDynamic, size: size) } + + private static func encodeTuple(_ tuple: ABITuple, type: ABIRawType) throws -> EncodedValue { + let encoder = ABIFunctionEncoder("") + try tuple.encode(to: encoder) + + return .container(values: encoder.encodedValues, isDynamic: type.isDynamic, size: nil) + } } diff --git a/web3swift/src/Contract/Statically Typed/ABIFunctionEncoder.swift b/web3swift/src/Contract/Statically Typed/ABIFunctionEncoder.swift index 1b8ef766..bd8b4f84 100644 --- a/web3swift/src/Contract/Statically Typed/ABIFunctionEncoder.swift +++ b/web3swift/src/Contract/Statically Typed/ABIFunctionEncoder.swift @@ -56,7 +56,7 @@ public class ABIFunctionEncoder { types.append(.DynamicArray(T.rawType)) } - private var encodedValues = [ABIEncoder.EncodedValue]() + internal var encodedValues = [ABIEncoder.EncodedValue]() public init(_ name: String) { self.name = name diff --git a/web3swift/src/Contract/Statically Typed/ABITuple.swift b/web3swift/src/Contract/Statically Typed/ABITuple.swift index bf853a14..c9a176d4 100644 --- a/web3swift/src/Contract/Statically Typed/ABITuple.swift +++ b/web3swift/src/Contract/Statically Typed/ABITuple.swift @@ -23,6 +23,7 @@ public extension ABITupleDecodable { public protocol ABITupleEncodable { var encodableValues: [ABIType] { get } + func encode(to encoder: ABIFunctionEncoder) throws } public protocol ABITuple: ABIType, ABITupleEncodable, ABITupleDecodable {} From 39eb2f202fe6818a18eb8cb7bcee4f0fa74d7655 Mon Sep 17 00:00:00 2001 From: Miguel Angel Quinones Date: Wed, 26 Aug 2020 16:58:32 +0200 Subject: [PATCH 03/20] [FIX] Encoding of amounts in RPC estimate_gas --- web3sTests/Client/EthereumClientTests.swift | 42 +++++++++++++++++++++ web3swift/src/Client/EthereumClient.swift | 18 +++++++-- 2 files changed, 56 insertions(+), 4 deletions(-) diff --git a/web3sTests/Client/EthereumClientTests.swift b/web3sTests/Client/EthereumClientTests.swift index e51851a6..5601e51c 100644 --- a/web3sTests/Client/EthereumClientTests.swift +++ b/web3sTests/Client/EthereumClientTests.swift @@ -375,6 +375,24 @@ class EthereumClientTests: XCTestCase { waitForExpectations(timeout: 10) } + + func test_GivenValidTransaction_ThenEstimatesGas() { + let expect = expectation(description: "estimateOK") + let function = TransferToken(wallet: EthereumAddress("0x2A6295C34b4136F2C3c1445c6A0338D784fe0ddd"), + token: EthereumAddress("0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"), + to: EthereumAddress("0x2A6295C34b4136F2C3c1445c6A0338D784fe0ddd"), + amount: 1, + data: Data(), + gasPrice: 0, + gasLimit: 0) + client!.eth_estimateGas(try! function.transaction(), withAccount: account!) { (error, value) in + XCTAssertNil(error) + XCTAssert(value != 0) + expect.fulfill() + } + + waitForExpectations(timeout: 10) + } } struct GetGuardians: ABIFunction { @@ -401,3 +419,27 @@ struct GetGuardians: ABIFunction { } } + +struct TransferToken: ABIFunction { + static let name = "transferToken" + let contract = EthereumAddress("0xA721E249c185ea3Ed98aBDd29F047db91Df36011") + let from: EthereumAddress? = EthereumAddress("0xA721E249c185ea3Ed98aBDd29F047db91Df36011") + + let wallet: EthereumAddress + let token: EthereumAddress + let to: EthereumAddress + let amount: BigUInt + let data: Data + + let gasPrice: BigUInt? + let gasLimit: BigUInt? + + func encode(to encoder: ABIFunctionEncoder) throws { + try encoder.encode(wallet) + try encoder.encode(token) + try encoder.encode(to) + try encoder.encode(amount) + try encoder.encode(data) + } +} + diff --git a/web3swift/src/Client/EthereumClient.swift b/web3swift/src/Client/EthereumClient.swift index 3249b9a9..1151f2f3 100644 --- a/web3swift/src/Client/EthereumClient.swift +++ b/web3swift/src/Client/EthereumClient.swift @@ -177,13 +177,18 @@ public class EthereumClient: EthereumClientProtocol { try nested.encode(from, forKey: .from) } try nested.encode(to, forKey: .to) - if let gas = gas { + + let jsonRPCAmount: (String) -> String = { amount in + amount == "0x00" ? "0x0" : amount + } + + if let gas = gas.map(jsonRPCAmount) { try nested.encode(gas, forKey: .gas) } - if let gasPrice = gasPrice { + if let gasPrice = gasPrice.map(jsonRPCAmount) { try nested.encode(gasPrice, forKey: .gasPrice) } - if let value = value { + if let value = gas.map(jsonRPCAmount) { try nested.encode(value, forKey: .value) } if let data = data { @@ -199,7 +204,12 @@ public class EthereumClient: EthereumClientProtocol { value = nil } - let params = CallParams(from: transaction.from?.value, to: transaction.to.value, gas: transaction.gasLimit?.web3.hexString, gasPrice: transaction.gasPrice?.web3.hexString, value: value?.web3.hexString, data: transaction.data?.web3.hexString) + let params = CallParams(from: transaction.from?.value, + to: transaction.to.value, + gas: transaction.gasLimit?.web3.hexString, + gasPrice: transaction.gasPrice?.web3.hexString, + value: value?.web3.hexString, + data: transaction.data?.web3.hexString) EthereumRPC.execute(session: session, url: url, method: "eth_estimateGas", params: params, receive: String.self) { (error, response) in if let gasHex = response as? String, let gas = BigUInt(hex: gasHex) { completion(nil, gas) From 17459e243850f3f41a72b221dfa36da5ff49f269 Mon Sep 17 00:00:00 2001 From: Miguel Angel Quinones Date: Wed, 26 Aug 2020 16:59:54 +0200 Subject: [PATCH 04/20] [CHANGE] Allow caller to modify gasPrice and gasLimit used to build a transaction from a function --- web3swift/src/Contract/Statically Typed/ABIFunction.swift | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/web3swift/src/Contract/Statically Typed/ABIFunction.swift b/web3swift/src/Contract/Statically Typed/ABIFunction.swift index cd2c9610..da3cec52 100644 --- a/web3swift/src/Contract/Statically Typed/ABIFunction.swift +++ b/web3swift/src/Contract/Statically Typed/ABIFunction.swift @@ -16,17 +16,16 @@ public protocol ABIFunction { var contract: EthereumAddress { get } var from: EthereumAddress? { get } func encode(to encoder: ABIFunctionEncoder) throws - func transaction() throws -> EthereumTransaction } public protocol ABIResponse: ABITupleDecodable {} extension ABIFunction { - public func transaction() throws -> EthereumTransaction { + public func transaction(gasPrice: BigUInt? = nil, gasLimit: BigUInt? = nil) throws -> EthereumTransaction { let encoder = ABIFunctionEncoder(Self.name) try self.encode(to: encoder) let data = try encoder.encoded() - return EthereumTransaction(from: from, to: contract, data: data, gasPrice: gasPrice ?? BigUInt(0), gasLimit: gasLimit ?? BigUInt(0)) + return EthereumTransaction(from: from, to: contract, data: data, gasPrice: self.gasPrice ?? gasPrice ?? BigUInt(0), gasLimit: self.gasLimit ?? gasLimit ?? BigUInt(0)) } } From a094646f6283d28538cb53d690608ceefe325502 Mon Sep 17 00:00:00 2001 From: Miguel Angel Quinones Date: Wed, 26 Aug 2020 17:25:35 +0200 Subject: [PATCH 05/20] [FIX} Typo --- web3swift/src/Client/EthereumClient.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web3swift/src/Client/EthereumClient.swift b/web3swift/src/Client/EthereumClient.swift index 1151f2f3..cd15d5fb 100644 --- a/web3swift/src/Client/EthereumClient.swift +++ b/web3swift/src/Client/EthereumClient.swift @@ -188,7 +188,7 @@ public class EthereumClient: EthereumClientProtocol { if let gasPrice = gasPrice.map(jsonRPCAmount) { try nested.encode(gasPrice, forKey: .gasPrice) } - if let value = gas.map(jsonRPCAmount) { + if let value = value.map(jsonRPCAmount) { try nested.encode(value, forKey: .value) } if let data = data { From 4602009fa694e3496ffb370fc5a93962fbb1ac46 Mon Sep 17 00:00:00 2001 From: David Rodrigues Date: Tue, 29 Sep 2020 11:32:13 +0100 Subject: [PATCH 06/20] [CHANGE] Notify upstream if the query fails with too many results --- web3swift/src/Client/EthereumClient.swift | 9 ++++++++- web3swift/src/Client/JSONRPC.swift | 7 ++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/web3swift/src/Client/EthereumClient.swift b/web3swift/src/Client/EthereumClient.swift index cd15d5fb..67915008 100644 --- a/web3swift/src/Client/EthereumClient.swift +++ b/web3swift/src/Client/EthereumClient.swift @@ -31,6 +31,7 @@ public protocol EthereumClientProtocol { } public enum EthereumClientError: Error { + case tooManyResults case unexpectedReturnValue case noResultFound case decodeIssue @@ -346,7 +347,13 @@ public class EthereumClient: EthereumClientProtocol { if let log = response as? [EthereumLog] { completion(nil, log) } else { - completion(EthereumClientError.unexpectedReturnValue, nil) + if let error = error as? JSONRPCError, + case let .executionError(innerError) = error, + innerError.error.code == JSONRPCErrorCode.tooManyResults { + completion(EthereumClientError.tooManyResults, nil) + } else { + completion(EthereumClientError.unexpectedReturnValue, nil) + } } } diff --git a/web3swift/src/Client/JSONRPC.swift b/web3swift/src/Client/JSONRPC.swift index 396cce16..2ae70658 100644 --- a/web3swift/src/Client/JSONRPC.swift +++ b/web3swift/src/Client/JSONRPC.swift @@ -32,7 +32,12 @@ struct JSONRPCErrorResult: Decodable { var error: JSONRPCErrorDetail } +enum JSONRPCErrorCode { + static var tooManyResults = -32005 +} + enum JSONRPCError: Error { + case executionError(JSONRPCErrorResult) case responseError case encodingError case decodingError @@ -73,7 +78,7 @@ public class EthereumRPC { return completion(nil, resultObjects) } else if let errorResult = try? JSONDecoder().decode(JSONRPCErrorResult.self, from: data) { print("Ethereum response error: \(errorResult.error)") - return completion(JSONRPCError.responseError, nil) + return completion(JSONRPCError.executionError(errorResult), nil) } else if let response = response as? HTTPURLResponse, response.statusCode < 200 || response.statusCode > 299 { return completion(JSONRPCError.responseError, nil) } else { From 56384fbabca6d1a33700b9526a5e4cc7e431ea32 Mon Sep 17 00:00:00 2001 From: Miguel Angel Quinones Date: Tue, 29 Sep 2020 14:18:20 +0200 Subject: [PATCH 07/20] [FIX] Broken tests on ropsten --- web3sTests/Client/EthereumClientTests.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web3sTests/Client/EthereumClientTests.swift b/web3sTests/Client/EthereumClientTests.swift index 5601e51c..18156a9f 100644 --- a/web3sTests/Client/EthereumClientTests.swift +++ b/web3sTests/Client/EthereumClientTests.swift @@ -378,7 +378,7 @@ class EthereumClientTests: XCTestCase { func test_GivenValidTransaction_ThenEstimatesGas() { let expect = expectation(description: "estimateOK") - let function = TransferToken(wallet: EthereumAddress("0x2A6295C34b4136F2C3c1445c6A0338D784fe0ddd"), + let function = TransferToken(wallet: EthereumAddress("0xd5b919520259e1174274420E3291ab77215C3D13"), token: EthereumAddress("0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"), to: EthereumAddress("0x2A6295C34b4136F2C3c1445c6A0338D784fe0ddd"), amount: 1, @@ -422,8 +422,8 @@ struct GetGuardians: ABIFunction { struct TransferToken: ABIFunction { static let name = "transferToken" - let contract = EthereumAddress("0xA721E249c185ea3Ed98aBDd29F047db91Df36011") - let from: EthereumAddress? = EthereumAddress("0xA721E249c185ea3Ed98aBDd29F047db91Df36011") + let contract = EthereumAddress("0xe4f5384d96cc4e6929b63546082788906250b60b") + let from: EthereumAddress? = EthereumAddress("0xe4f5384d96cc4e6929b63546082788906250b60b") let wallet: EthereumAddress let token: EthereumAddress From 55d42833cd8486bc309b3bb5458e8fa25afccd5f Mon Sep 17 00:00:00 2001 From: Miguel Angel Quinones Date: Thu, 8 Oct 2020 12:23:23 +0200 Subject: [PATCH 08/20] [FIX] ERC20 'decimals' response should be decoded to UInt8 --- web3sTests/ERC20/ERC20Tests.swift | 4 ++-- web3swift/src/ERC20/ERC20.swift | 2 +- web3swift/src/ERC20/ERC20Responses.swift | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/web3sTests/ERC20/ERC20Tests.swift b/web3sTests/ERC20/ERC20Tests.swift index 44c57a9b..66baede0 100644 --- a/web3sTests/ERC20/ERC20Tests.swift +++ b/web3sTests/ERC20/ERC20Tests.swift @@ -39,7 +39,7 @@ class ERC20Tests: XCTestCase { let expect = expectation(description: "Get token decimals") erc20?.decimals(tokenContract: self.testContractAddress, completion: { (error, decimals) in XCTAssertNil(error) - XCTAssertEqual(decimals, BigUInt(18)) + XCTAssertEqual(decimals, 18) expect.fulfill() }) waitForExpectations(timeout: 10) @@ -49,7 +49,7 @@ class ERC20Tests: XCTestCase { let expect = expectation(description: "Get token decimals (0)") erc20?.decimals(tokenContract: EthereumAddress("0x40dd3ac2481960cf34d96e647dd0bc52a1f03f52"), completion: { (error, decimals) in XCTAssertNil(error) - XCTAssertEqual(decimals, BigUInt(0)) + XCTAssertEqual(decimals, 0) expect.fulfill() }) waitForExpectations(timeout: 10) diff --git a/web3swift/src/ERC20/ERC20.swift b/web3swift/src/ERC20/ERC20.swift index 6e66e0da..9721c158 100644 --- a/web3swift/src/ERC20/ERC20.swift +++ b/web3swift/src/ERC20/ERC20.swift @@ -30,7 +30,7 @@ public class ERC20 { } } - public func decimals(tokenContract: EthereumAddress, completion: @escaping((Error?, BigUInt?) -> Void)) { + public func decimals(tokenContract: EthereumAddress, completion: @escaping((Error?, UInt8?) -> Void)) { let function = ERC20Functions.decimals(contract: tokenContract) function.call(withClient: self.client, responseType: ERC20Responses.decimalsResponse.self) { (error, decimalsResponse) in return completion(error, decimalsResponse?.value) diff --git a/web3swift/src/ERC20/ERC20Responses.swift b/web3swift/src/ERC20/ERC20Responses.swift index c4570c62..d8fadba2 100644 --- a/web3swift/src/ERC20/ERC20Responses.swift +++ b/web3swift/src/ERC20/ERC20Responses.swift @@ -29,8 +29,8 @@ enum ERC20Responses { } public struct decimalsResponse: ABIResponse { - public static var types: [ABIType.Type] = [ BigUInt.self ] - public let value: BigUInt + public static var types: [ABIType.Type] = [ UInt8.self ] + public let value: UInt8 public init?(values: [ABIDecoder.DecodedValue]) throws { self.value = try values[0].decoded() From a1f157a4b7de06374221ed9cd20e0808e1b68345 Mon Sep 17 00:00:00 2001 From: David Rodrigues Date: Wed, 4 Nov 2020 15:07:01 +0000 Subject: [PATCH 09/20] Multicall (#116) * [ADD] Add support for dynamic arrays with a dynamic type, e.g. `bytes[]` * [ADD] Support for Multicall to batch multiple calls into a single one * [CHANGE] Extended multicall API to allow a more ergonomic usage by consumers * [ADD] Resolve multiple ENSs in a single call * [ADD] Support to resolve multiple names in a single call * [FIX] Rename tests --- web3sTests/Contract/ABIDecoderTests.swift | 50 +++ web3sTests/ENS/ENSTests.swift | 69 +++- web3sTests/Multicall/MulticallTests.swift | 62 ++++ web3swift.xcodeproj/project.pbxproj | 36 ++ web3swift/src/Contract/ABIDecoder.swift | 25 ++ .../EthereumClient+Static.swift | 4 +- web3swift/src/ENS/ENSContracts.swift | 111 ++++-- web3swift/src/ENS/ENSMultiResolver.swift | 317 ++++++++++++++++++ web3swift/src/ENS/ENSResponses.swift | 50 +++ web3swift/src/ENS/EthereumNameService.swift | 6 +- web3swift/src/ERC20/ERC20Responses.swift | 10 +- web3swift/src/ERC721/ERC721Responses.swift | 20 +- web3swift/src/Multicall/Multicall.swift | 156 +++++++++ .../src/Multicall/MulticallContract.swift | 52 +++ 14 files changed, 924 insertions(+), 44 deletions(-) create mode 100644 web3sTests/Multicall/MulticallTests.swift create mode 100644 web3swift/src/ENS/ENSMultiResolver.swift create mode 100644 web3swift/src/ENS/ENSResponses.swift create mode 100644 web3swift/src/Multicall/Multicall.swift create mode 100644 web3swift/src/Multicall/MulticallContract.swift diff --git a/web3sTests/Contract/ABIDecoderTests.swift b/web3sTests/Contract/ABIDecoderTests.swift index 8f0679ca..ccc4139d 100644 --- a/web3sTests/Contract/ABIDecoderTests.swift +++ b/web3sTests/Contract/ABIDecoderTests.swift @@ -115,6 +115,56 @@ class ABIDecoderTests: XCTestCase { XCTFail() } } + + func testDecodeBytesArray() { + do { + let decoded = try ABIDecoder.decodeData( + "0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001753796e746865746978204e6574776f726b20546f6b656e000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003534e5800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000012" + , types: [ABIArray.self], asArray: false) + + XCTAssertEqual(try ERC20Responses.nameResponse(data: decoded[0].entry[0])?.value, "Synthetix Network Token") + XCTAssertEqual(try ERC20Responses.symbolResponse(data: decoded[0].entry[1])?.value, "SNX") + XCTAssertEqual(try ERC20Responses.balanceResponse(data: decoded[0].entry[2])?.value, BigUInt(integerLiteral: 0)) + XCTAssertEqual(try ERC20Responses.decimalsResponse(data: decoded[0].entry[3])?.value, 18) + } catch let error { + print(error.localizedDescription) + XCTFail() + } + } + + func testDecodeMulticallOutputWithoutFailures() { + do { + let decoded = try ABIDecoder.decodeData( + "0x0000000000000000000000000000000000000000000000000000000000a9f60c0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001753796e746865746978204e6574776f726b20546f6b656e000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003534e5800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000012" + , types: [BigInt.self, ABIArray.self]) + + XCTAssertEqual(try decoded[0].decoded(), BigInt(integerLiteral: 11138572)) + XCTAssertEqual(try ERC20Responses.nameResponse(data: decoded[1].entry[0])?.value, "Synthetix Network Token") + XCTAssertEqual(try ERC20Responses.symbolResponse(data: decoded[1].entry[1])?.value, "SNX") + XCTAssertEqual(try ERC20Responses.balanceResponse(data: decoded[1].entry[2])?.value, BigUInt(integerLiteral: 0)) + XCTAssertEqual(try ERC20Responses.decimalsResponse(data: decoded[1].entry[3])?.value, 18) + } catch let error { + print(error.localizedDescription) + XCTFail() + } + } + + func testDecodeMulticallOutputWithOneFailure() { + do { + let decoded = try ABIDecoder.decodeData( + "0x0000000000000000000000000000000000000000000000000000000000a9f60c0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001753796e746865746978204e6574776f726b20546f6b656e000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003534e580000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020aface4ed2e287b2f681c32da24383f2fb691f0b6962be5ae7950a5dd793e61ad" + , types: [BigInt.self, ABIArray.self]) + + XCTAssertEqual(try decoded[0].decoded(), BigInt(integerLiteral: 11138572)) + XCTAssertEqual(try ERC20Responses.nameResponse(data: decoded[1].entry[0])?.value, "Synthetix Network Token") + XCTAssertEqual(try ERC20Responses.symbolResponse(data: decoded[1].entry[1])?.value, "SNX") + XCTAssertEqual(try ERC20Responses.balanceResponse(data: decoded[1].entry[2])?.value, BigUInt(integerLiteral: 0)) + XCTAssertEqual(decoded[1].entry[3], Multicall.Response.multicallFailedError) + } catch let error { + print(error.localizedDescription) + XCTFail() + } + } func test_GivenSimpleURL_ThenDecodesCorrectly() { do { diff --git a/web3sTests/ENS/ENSTests.swift b/web3sTests/ENS/ENSTests.swift index c9296b30..ba4b1513 100644 --- a/web3sTests/ENS/ENSTests.swift +++ b/web3sTests/ENS/ENSTests.swift @@ -126,5 +126,72 @@ class ENSTests: XCTestCase { waitForExpectations(timeout: 20) } - + + func testGivenRopstenRegistry_ThenResolvesMultipleAddressesInOneCall() { + let expect = expectation(description: "Get the ENS reverse lookup address") + + let nameService = EthereumNameService(client: client!) + + var results: [EthereumNameService.ResolveOutput]? + + nameService.resolve(addresses: [ + EthereumAddress("0xb0b874220ff95d62a676f58d186c832b3e6529c8"), + EthereumAddress("0x09b5bd82f3351a4c8437fc6d7772a9e6cd5d25a1"), + EthereumAddress("0x7e691d7ffb007abe91d8a24d7f22fc74307dab06") + + ]) { result in + switch result { + case .success(let resolutions): + results = resolutions.map { $0.output } + case .failure: + break + } + expect.fulfill() + } + + waitForExpectations(timeout: 5) + + XCTAssertEqual( + results, + [ + .resolved("julien.argent.test"), + .couldNotBeResolved(.ensUnknown), + .resolved("davidtests.argent.xyz") + ] + ) + } + + func testGivenRopstenRegistry_ThenResolvesMultipleNamesInOneCall() { + let expect = expectation(description: "Get the ENS reverse lookup address") + + let nameService = EthereumNameService(client: client!) + + var results: [EthereumNameService.ResolveOutput]? + + nameService.resolve(names: [ + "julien.argent.test", + "davidtests.argent.xyz", + "somefakeens.argent.xyz" + + ]) { result in + switch result { + case .success(let resolutions): + results = resolutions.map { $0.output } + case .failure: + break + } + expect.fulfill() + } + + waitForExpectations(timeout: 5) + + XCTAssertEqual( + results, + [ + .resolved(EthereumAddress("0xb0b874220ff95d62a676f58d186c832b3e6529c8")), + .resolved(EthereumAddress("0x7e691d7ffb007abe91d8a24d7f22fc74307dab06")), + .couldNotBeResolved(.ensUnknown) + ] + ) + } } diff --git a/web3sTests/Multicall/MulticallTests.swift b/web3sTests/Multicall/MulticallTests.swift new file mode 100644 index 00000000..2584c860 --- /dev/null +++ b/web3sTests/Multicall/MulticallTests.swift @@ -0,0 +1,62 @@ +// +// MulticallTests.swift +// web3swiftTests +// +// Created by David Rodrigues on 28/10/2020. +// Copyright © 2020 Argent Labs Limited. All rights reserved. +// + +import XCTest +@testable import web3swift + +class MulticallTests: XCTestCase { + var client: EthereumClient! + var multicall: Multicall! + let testContractAddress = EthereumAddress(TestConfig.erc20Contract) + + override func setUp() { + super.setUp() + self.client = EthereumClient(url: URL(string: TestConfig.clientUrl)!) + self.multicall = Multicall(client: client!) + } + + func testNameAndSymbol() throws { + var aggregator = Multicall.Aggregator() + + var name: String? + var decimals: UInt8? + + try aggregator.append(ERC20Functions.decimals(contract: testContractAddress)) { output in + decimals = try ERC20Responses.decimalsResponse(data: output.get())?.value + } + + try aggregator.append( + function: ERC20Functions.name(contract: testContractAddress), + response: ERC20Responses.nameResponse.self + ) { result in + name = try? result.get() + } + + try aggregator.append(ERC20Functions.symbol(contract: testContractAddress)) + + let expect = expectation(description: "Get token name and symbol") + multicall.aggregate(calls: aggregator.calls) { result in + do { + switch result { + case .failure(let error): + XCTFail("Multicall failed with error: \(error)") + case .success(let response): + let symbol = try ERC20Responses.symbolResponse(data: try response.outputs[2].get())?.value + XCTAssertEqual(symbol, "BOKKY") + } + } catch { + XCTFail("Unexpected failure while handling output") + } + expect.fulfill() + } + waitForExpectations(timeout: 10) + + XCTAssertEqual(decimals, 18) + XCTAssertEqual(name, "BokkyPooBah Test Token") + } +} diff --git a/web3swift.xcodeproj/project.pbxproj b/web3swift.xcodeproj/project.pbxproj index a54cdb86..9f7aa1c5 100644 --- a/web3swift.xcodeproj/project.pbxproj +++ b/web3swift.xcodeproj/project.pbxproj @@ -45,6 +45,11 @@ 47F2EAAD22848BA7007063C2 /* ERC721Events.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47F2EAAC22848BA7007063C2 /* ERC721Events.swift */; }; 47F2EAAF22848BB2007063C2 /* ERC721Responses.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47F2EAAE22848BB2007063C2 /* ERC721Responses.swift */; }; 47F2EAB32285BBDF007063C2 /* ERC165Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47F2EAB12285BAB6007063C2 /* ERC165Tests.swift */; }; + 63002262255192D900C97B80 /* ENSMultiResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63002261255192D900C97B80 /* ENSMultiResolver.swift */; }; + 63C41E6D254C4CFF0055ED7F /* ENSResponses.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63C41E6C254C4CFF0055ED7F /* ENSResponses.swift */; }; + 63DAC6252549D8700059FF9C /* MulticallContract.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63DAC6242549D8700059FF9C /* MulticallContract.swift */; }; + 63DAC6292549D9CE0059FF9C /* Multicall.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63DAC6282549D9CE0059FF9C /* Multicall.swift */; }; + 63DAC6302549E2640059FF9C /* MulticallTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63DAC62F2549E2640059FF9C /* MulticallTests.swift */; }; 6C4ABDDAA3313A1B32EAB620 /* Pods_web3swiftTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5E4FAB97C27DB6CBB355E0F1 /* Pods_web3swiftTests.framework */; }; 9164E8EEDA7FB33A6638D561 /* Pods_web3swift.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BD54E6F686FEE6C1126AB6A0 /* Pods_web3swift.framework */; }; C91A7E80205C1F120074D3B4 /* ABIEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = C91A7E7F205C1F120074D3B4 /* ABIEncoder.swift */; }; @@ -155,6 +160,11 @@ 52B3D38DF87EAFCBBD99CCF1 /* Pods-web3swiftTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-web3swiftTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-web3swiftTests/Pods-web3swiftTests.debug.xcconfig"; sourceTree = ""; }; 595F7375549A81C8950C1F8E /* Pods-web3s.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-web3s.release.xcconfig"; path = "Pods/Target Support Files/Pods-web3s/Pods-web3s.release.xcconfig"; sourceTree = ""; }; 5E4FAB97C27DB6CBB355E0F1 /* Pods_web3swiftTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_web3swiftTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 63002261255192D900C97B80 /* ENSMultiResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ENSMultiResolver.swift; sourceTree = ""; }; + 63C41E6C254C4CFF0055ED7F /* ENSResponses.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ENSResponses.swift; sourceTree = ""; }; + 63DAC6242549D8700059FF9C /* MulticallContract.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MulticallContract.swift; sourceTree = ""; }; + 63DAC6282549D9CE0059FF9C /* Multicall.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Multicall.swift; sourceTree = ""; }; + 63DAC62F2549E2640059FF9C /* MulticallTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MulticallTests.swift; sourceTree = ""; }; 673183DE4025E9D5F5EAF360 /* Pods-web3swift.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-web3swift.release.xcconfig"; path = "Pods/Target Support Files/Pods-web3swift/Pods-web3swift.release.xcconfig"; sourceTree = ""; }; 7DE9D050B2C662A66E6B59CE /* Pods-web3swiftTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-web3swiftTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-web3swiftTests/Pods-web3swiftTests.release.xcconfig"; sourceTree = ""; }; A1CC3A57435EC4F90800C052 /* Pods_web3s.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_web3s.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -304,6 +314,7 @@ C967AAAC2080C35500A24D15 /* ERC20 */, 47F2EAB02285BA9C007063C2 /* ERC165 */, C9236B952052FD2800CE5557 /* Extensions */, + 63DAC62E2549E24D0059FF9C /* Multicall */, 3CF37C3C203437D70030520E /* Info.plist */, C9A78E6E20595B3000CF8316 /* Mocks */, 3C8D94F9204024DE00D0D5D7 /* Resources */, @@ -344,6 +355,7 @@ 47F2EA9C22848981007063C2 /* ERC721 */, C927FD60204ECFE3005922DC /* ENS */, C927FD6E20527A67005922DC /* Extensions */, + 63DAC6232549D8700059FF9C /* Multicall */, 3CF37C6220343BDD0030520E /* Utils */, ); path = src; @@ -436,6 +448,23 @@ path = ERC165; sourceTree = ""; }; + 63DAC6232549D8700059FF9C /* Multicall */ = { + isa = PBXGroup; + children = ( + 63DAC6282549D9CE0059FF9C /* Multicall.swift */, + 63DAC6242549D8700059FF9C /* MulticallContract.swift */, + ); + path = Multicall; + sourceTree = ""; + }; + 63DAC62E2549E24D0059FF9C /* Multicall */ = { + isa = PBXGroup; + children = ( + 63DAC62F2549E2640059FF9C /* MulticallTests.swift */, + ); + path = Multicall; + sourceTree = ""; + }; C9236B832052C6AC00CE5557 /* Models */ = { isa = PBXGroup; children = ( @@ -519,7 +548,9 @@ isa = PBXGroup; children = ( C927FD61204EF4E1005922DC /* EthereumNameService.swift */, + 63002261255192D900C97B80 /* ENSMultiResolver.swift */, C9A78E5C205871E500CF8316 /* ENSContracts.swift */, + 63C41E6C254C4CFF0055ED7F /* ENSResponses.swift */, ); path = ENS; sourceTree = ""; @@ -787,6 +818,7 @@ C9F836B52078049E004F45A9 /* EthereumAddress.swift in Sources */, C9236B802052C68D00CE5557 /* EthereumLog.swift in Sources */, 47F2EAAB22848B9C007063C2 /* ERC721Functions.swift in Sources */, + 63DAC6252549D8700059FF9C /* MulticallContract.swift in Sources */, C9236B8B2052E12200CE5557 /* HexExtensions.swift in Sources */, C927FD62204EF4E1005922DC /* EthereumNameService.swift in Sources */, C927FD57204EAD6E005922DC /* EthereumKeyStorage.swift in Sources */, @@ -808,6 +840,7 @@ C94C2BD02090C70D00F024EE /* ERC20Events.swift in Sources */, C91A7E84205C1F260074D3B4 /* ABIRawType.swift in Sources */, 3C8D94FE20402D8400D0D5D7 /* EthereumTransaction.swift in Sources */, + 63DAC6292549D9CE0059FF9C /* Multicall.swift in Sources */, 3CF37CBE2035A8240030520E /* EthereumAccount.swift in Sources */, 47F2EAA622848B5D007063C2 /* ERC721.swift in Sources */, 3CF37CE3203720F30030520E /* KeyDerivation.swift in Sources */, @@ -818,10 +851,12 @@ C9D43D2D207D0DB800685FB6 /* ABIEncoder+Static.swift in Sources */, 478E08CE249D16CE000810B0 /* TypedData.swift in Sources */, C9A78E5D205871E500CF8316 /* ENSContracts.swift in Sources */, + 63C41E6D254C4CFF0055ED7F /* ENSResponses.swift in Sources */, 3C8D94F6203ECA2D00D0D5D7 /* RLP.swift in Sources */, C91A7E80205C1F120074D3B4 /* ABIEncoder.swift in Sources */, C967AAA72080BF7100A24D15 /* ERC20Functions.swift in Sources */, C9D43D31207D140E00685FB6 /* EthereumClient+Static.swift in Sources */, + 63002262255192D900C97B80 /* ENSMultiResolver.swift in Sources */, 47B9C62824C730B30028DAAF /* ABITuple.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -840,6 +875,7 @@ C9A78E4F20583F1300CF8316 /* KeccakExtensionsTests.swift in Sources */, C967AAB02080C38000A24D15 /* ERC20Tests.swift in Sources */, C9236BA32057CE6F00CE5557 /* EthereumAccountTests.swift in Sources */, + 63DAC6302549E2640059FF9C /* MulticallTests.swift in Sources */, 3C8D94F820400E7200D0D5D7 /* RLPTests.swift in Sources */, C9A78E4A205826CC00CF8316 /* String+NumericTests.swift in Sources */, 3CF37CEB20372CA10030520E /* KeyDerivationTests.swift in Sources */, diff --git a/web3swift/src/Contract/ABIDecoder.swift b/web3swift/src/Contract/ABIDecoder.swift index 258ffd48..aae30503 100644 --- a/web3swift/src/Contract/ABIDecoder.swift +++ b/web3swift/src/Contract/ABIDecoder.swift @@ -102,6 +102,31 @@ public class ABIDecoder { var newOffset = offset try deepDecode(data: data, type: arrayType, result: &result, offset: &newOffset, size: &size) + return result + // NOTE: Needs analysis to confirm it can handle an inner `DynamicArray` too + case .DynamicArray(let arrayType) where arrayType.isDynamic: + var result: [String] = [] + var currentOffset = offset + + guard let offsetHex = (try decode(data, forType: ABIRawType.FixedUInt(256), offset: currentOffset)).first else { + throw ABIError.invalidValue + } + + currentOffset = Int(hex: offsetHex) ?? currentOffset + + guard let lengthHex = (try decode(data, forType: ABIRawType.FixedUInt(256), offset: currentOffset)).first else { + throw ABIError.invalidValue + } + guard let length = Int(hex: lengthHex) else { + throw ABIError.invalidValue + } + + currentOffset += 32 + + for instanceOffset in 0 ..< length { + result += try decode(Array(data.dropFirst(currentOffset)), forType: arrayType, offset: instanceOffset * 32) + } + return result case .DynamicArray(let arrayType): var result: [String] = [] diff --git a/web3swift/src/Contract/Statically Typed/EthereumClient+Static.swift b/web3swift/src/Contract/Statically Typed/EthereumClient+Static.swift index b0649a98..3f6528de 100644 --- a/web3swift/src/Contract/Statically Typed/EthereumClient+Static.swift +++ b/web3swift/src/Contract/Statically Typed/EthereumClient+Static.swift @@ -9,7 +9,7 @@ import Foundation public extension ABIFunction { - func execute(withClient client: EthereumClient, account: EthereumAccount, completion: @escaping((EthereumClientError?, String?) -> Void)) { + func execute(withClient client: EthereumClientProtocol, account: EthereumAccount, completion: @escaping((EthereumClientError?, String?) -> Void)) { guard let tx = try? self.transaction() else { return completion(EthereumClientError.encodeIssue, nil) @@ -26,7 +26,7 @@ public extension ABIFunction { } - func call(withClient client: EthereumClient, responseType: T.Type, block: EthereumBlock = .Latest, completion: @escaping((EthereumClientError?, T?) -> Void)) { + func call(withClient client: EthereumClientProtocol, responseType: T.Type, block: EthereumBlock = .Latest, completion: @escaping((EthereumClientError?, T?) -> Void)) { guard let tx = try? self.transaction() else { return completion(EthereumClientError.encodeIssue, nil) diff --git a/web3swift/src/ENS/ENSContracts.swift b/web3swift/src/ENS/ENSContracts.swift index 06095069..c0c021f3 100644 --- a/web3swift/src/ENS/ENSContracts.swift +++ b/web3swift/src/ENS/ENSContracts.swift @@ -9,11 +9,13 @@ import Foundation import BigInt -enum ENSContracts { +public typealias ENSRegistryResolverParameter = ENSContracts.ENSRegistryFunctions.resolver.Parameter + +public enum ENSContracts { static let RopstenAddress = EthereumAddress("0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e") static let MainnetAddress = EthereumAddress("0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e") - static func registryAddress(for network: EthereumNetwork) -> EthereumAddress? { + public static func registryAddress(for network: EthereumNetwork) -> EthereumAddress? { switch network { case .Ropsten: return ENSContracts.RopstenAddress @@ -24,17 +26,17 @@ enum ENSContracts { } } - enum ENSResolverFunctions { - struct addr: ABIFunction { - static let name = "addr" - let gasPrice: BigUInt? - let gasLimit: BigUInt? - var contract: EthereumAddress - let from: EthereumAddress? + public enum ENSResolverFunctions { + public struct addr: ABIFunction { + public static let name = "addr" + public let gasPrice: BigUInt? + public let gasLimit: BigUInt? + public var contract: EthereumAddress + public let from: EthereumAddress? - let _node: Data + public let _node: Data - init(contract: EthereumAddress, + public init(contract: EthereumAddress, from: EthereumAddress? = nil, gasPrice: BigUInt? = nil, gasLimit: BigUInt? = nil, @@ -51,14 +53,14 @@ enum ENSContracts { } } - struct name: ABIFunction { - static let name = "name" - let gasPrice: BigUInt? - let gasLimit: BigUInt? - var contract: EthereumAddress - let from: EthereumAddress? + public struct name: ABIFunction { + public static let name = "name" + public let gasPrice: BigUInt? + public let gasLimit: BigUInt? + public var contract: EthereumAddress + public let from: EthereumAddress? - let _node: Data + public let _node: Data init(contract: EthereumAddress, from: EthereumAddress? = nil, @@ -78,13 +80,48 @@ enum ENSContracts { } } - enum ENSRegistryFunctions { - struct resolver: ABIFunction { - static let name = "resolver" - let gasPrice: BigUInt? - let gasLimit: BigUInt? - var contract: EthereumAddress - let from: EthereumAddress? + public enum ENSRegistryFunctions { + public struct resolver: ABIFunction { + + public enum Parameter { + case address(EthereumAddress) + case name(String) + + var nameHash: Data { + let nameHash: String + switch self { + case .address(let address): + nameHash = ENSContracts.nameHash(name: address.value.web3.noHexPrefix + ".addr.reverse") + case .name(let ens): + nameHash = ENSContracts.nameHash(name: ens) + } + return nameHash.web3.hexData ?? Data() + } + + var name: String? { + switch self { + case .name(let ens): + return ens + case .address: + return nil + } + } + + var address: EthereumAddress? { + switch self { + case .address(let address): + return address + case .name: + return nil + } + } + } + + public static let name = "resolver" + public let gasPrice: BigUInt? + public let gasLimit: BigUInt? + public var contract: EthereumAddress + public let from: EthereumAddress? let _node: Data @@ -99,6 +136,20 @@ enum ENSContracts { self.gasLimit = gasLimit self._node = _node } + + public init(contract: EthereumAddress, + from: EthereumAddress? = nil, + gasPrice: BigUInt? = nil, + gasLimit: BigUInt? = nil, + parameter: Parameter) { + self.init( + contract: contract, + from: from, + gasPrice: gasPrice, + gasLimit: gasLimit, + _node: parameter.nameHash + ) + } public func encode(to encoder: ABIFunctionEncoder) throws { try encoder.encode(_node, staticSize: 32) @@ -131,4 +182,14 @@ enum ENSContracts { } } } + + static func nameHash(name: String) -> String { + var node = Data.init(count: 32) + let labels = name.components(separatedBy: ".") + for label in labels.reversed() { + node.append(label.web3.keccak256) + node = node.web3.keccak256 + } + return node.web3.hexString + } } diff --git a/web3swift/src/ENS/ENSMultiResolver.swift b/web3swift/src/ENS/ENSMultiResolver.swift new file mode 100644 index 00000000..1e0f61aa --- /dev/null +++ b/web3swift/src/ENS/ENSMultiResolver.swift @@ -0,0 +1,317 @@ +// +// ENSMultiResolver.swift +// web3swift +// +// Created by David Rodrigues on 03/11/2020. +// Copyright © 2020 Argent Labs Limited. All rights reserved. +// + +import Foundation + +extension EthereumNameService { + + public func resolve( + addresses: [EthereumAddress], + completion: @escaping (Result<[AddressResolveOutput], EthereumNameServiceError>) -> Void + ) { + MultiResolver( + client: client, + registryAddress: registryAddress + ).resolve(addresses: addresses, completion: completion) + } + + public func resolve( + names: [String], + completion: @escaping (Result<[NameResolveOutput], EthereumNameServiceError>) -> Void + ) { + MultiResolver( + client: client, + registryAddress: registryAddress + ).resolve(names: names, completion: completion) + } +} + +extension EthereumNameService { + + public enum ResolveOutput: Equatable { + case couldNotBeResolved(EthereumNameServiceError) + case resolved(Value) + } + + public struct AddressResolveOutput: Equatable { + public let address: EthereumAddress + public let output: ResolveOutput + } + + public struct NameResolveOutput: Equatable { + public let ens: String + public let output: ResolveOutput + } + + private class MultiResolver { + + private class RegistryOutput { + var queries: [ResolverQuery] = [] + var intermediaryResponses: [ResolveOutput?] + + init(expectedResponsesCount: Int) { + intermediaryResponses = Array(repeating: nil, count: expectedResponsesCount) + } + } + + private struct ResolverQuery { + let index: Int + let parameter: ENSRegistryResolverParameter + let resolverAddress: EthereumAddress + let nameHash: Data + } + + let client: EthereumClientProtocol + let registryAddress: EthereumAddress? + let multicall: Multicall + + init( + client: EthereumClientProtocol, + registryAddress: EthereumAddress? = nil + ) { + self.client = client + self.registryAddress = registryAddress + self.multicall = Multicall(client: client) + } + + func resolve( + addresses: [EthereumAddress], + completion: @escaping (Result<[AddressResolveOutput], EthereumNameServiceError>) -> Void + ) { + let output = RegistryOutput(expectedResponsesCount: addresses.count) + + resolveRegistry( + parameters: addresses.map(ENSRegistryResolverParameter.address), + handler: { index, parameter, result in + // TODO: Temporary solution + guard let address = parameter.address else { return } + switch result { + case .success(let resolverAddress): + output.queries.append( + ResolverQuery( + index: index, + parameter: parameter, + resolverAddress: resolverAddress, + nameHash: parameter.nameHash + ) + ) + case .failure(let error): + output.intermediaryResponses[index] = AddressResolveOutput( + address: address, + output: Self.output(from: error) + ) + } + }, completion: { result in + switch result { + case .success: + self.resolveQueries(registryOutput: output) { result in + switch result { + case .success(let output): + completion(.success(output)) + case .failure: + completion(result) + } + } + case .failure(let error): + completion(.failure(error)) + } + }) + } + + func resolve( + names: [String], + completion: @escaping (Result<[NameResolveOutput], EthereumNameServiceError>) -> Void + ) { + let output = RegistryOutput(expectedResponsesCount: names.count) + + resolveRegistry( + parameters: names.map(ENSRegistryResolverParameter.name), + handler: { index, parameter, result in + // TODO: Temporary solution + guard let name = parameter.name else { return } + switch result { + case .success(let resolverAddress): + output.queries.append( + ResolverQuery( + index: index, + parameter: parameter, + resolverAddress: resolverAddress, + nameHash: parameter.nameHash + ) + ) + case .failure(let error): + output.intermediaryResponses[index] = NameResolveOutput( + ens: name, + output: Self.output(from: error) + ) + } + }, completion: { result in + switch result { + case .success: + self.resolveQueries(registryOutput: output) { result in + switch result { + case .success(let output): + completion(.success(output)) + case .failure: + completion(result) + } + } + case .failure(let error): + completion(.failure(error)) + } + }) + } + + private func resolveRegistry( + parameters: [ENSRegistryResolverParameter], + handler: @escaping (Int, ENSRegistryResolverParameter, Result) -> Void, + completion: @escaping (Result) -> Void + ) { + + guard + let network = client.network, + let ensRegistryAddress = self.registryAddress ?? ENSContracts.registryAddress(for: network) + else { return completion(.failure(.noNetwork)) } + + + var aggegator = Multicall.Aggregator() + + do { + try parameters.enumerated().forEach { index, parameter in + + let function = ENSContracts.ENSRegistryFunctions.resolver(contract: ensRegistryAddress, parameter: parameter) + + try aggegator.append( + function: function, + response: ENSContracts.ENSRegistryResponses.RegistryResponse.self + ) { result in handler(index, parameter, result) } + } + + multicall.aggregate(calls: aggegator.calls) { result in + switch result { + case .success: + completion(.success(())) + case .failure: + completion(.failure(.noNetwork)) + } + } + } catch { + completion(.failure(.invalidInput)) + } + } + + private func resolveQueries( + registryOutput: RegistryOutput, + completion: @escaping (Result<[ResolverOutput], EthereumNameServiceError>) -> Void + ) { + var aggegator = Multicall.Aggregator() + + registryOutput.queries.forEach { query in + switch query.parameter { + case .address(let address): + guard let registryOutput = registryOutput as? RegistryOutput + else { fatalError("Invalid registry output provided") } + resolveAddress(query, address: address, aggegator: &aggegator, registryOutput: registryOutput) + case .name(let name): + guard let registryOutput = registryOutput as? RegistryOutput + else { fatalError("Invalid registry output provided") } + resolveName(query, ens: name, aggegator: &aggegator, registryOutput: registryOutput) + } + } + + multicall.aggregate(calls: aggegator.calls) { result in + switch result { + case .success: + completion(.success(registryOutput.intermediaryResponses.compactMap { $0 })) + case .failure: + completion(.failure(.noNetwork)) + } + } + } + + private func resolveAddress( + _ query: EthereumNameService.MultiResolver.ResolverQuery, + address: EthereumAddress, + aggegator: inout Multicall.Aggregator, + registryOutput: RegistryOutput + ) { + do { + try aggegator.append( + function: ENSContracts.ENSResolverFunctions.name(contract: query.resolverAddress, _node: query.nameHash), + response: ENSContracts.ENSRegistryResponses.AddressResolverResponse.self + ) { result in + switch result { + case .success(let name): + registryOutput.intermediaryResponses[query.index] = AddressResolveOutput( + address: address, + output: .resolved(name) + ) + case .failure(let error): + registryOutput.intermediaryResponses[query.index] = AddressResolveOutput( + address: address, + output: Self.output(from: error) + ) + } + } + } catch let error { + registryOutput.intermediaryResponses[query.index] = AddressResolveOutput( + address: address, + output: Self.output(from: error) + ) + } + } + + private func resolveName( + _ query: EthereumNameService.MultiResolver.ResolverQuery, + ens: String, + aggegator: inout Multicall.Aggregator, + registryOutput: RegistryOutput + ) { + do { + try aggegator.append( + function: ENSContracts.ENSResolverFunctions.addr(contract: query.resolverAddress, _node: query.nameHash), + response: ENSContracts.ENSRegistryResponses.NameResolverResponse.self + ) { result in + switch result { + case .success(let address): + registryOutput.intermediaryResponses[query.index] = NameResolveOutput( + ens: ens, + output: .resolved(address) + ) + case .failure(let error): + registryOutput.intermediaryResponses[query.index] = NameResolveOutput( + ens: ens, + output: Self.output(from: error) + ) + } + } + } catch let error { + registryOutput.intermediaryResponses[query.index] = NameResolveOutput( + ens: ens, + output: Self.output(from: error) + ) + } + } + + private static func output(from error: Error) -> ResolveOutput { + guard let error = error as? Multicall.CallError + else { return .couldNotBeResolved(.invalidInput) } + + switch error { + case .contractFailure: + return .couldNotBeResolved(.invalidInput) + case .couldNotDecodeResponse(let error): + if let specificError = error as? EthereumNameServiceError { + return .couldNotBeResolved(specificError) + } else { + return .couldNotBeResolved(.invalidInput) + } + } + } + } +} diff --git a/web3swift/src/ENS/ENSResponses.swift b/web3swift/src/ENS/ENSResponses.swift new file mode 100644 index 00000000..706713be --- /dev/null +++ b/web3swift/src/ENS/ENSResponses.swift @@ -0,0 +1,50 @@ +// +// ENSRegistryResponses.swift +// web3swift +// +// Created by David Rodrigues on 30/10/2020. +// Copyright © 2020 Argent Labs Limited. All rights reserved. +// + +import Foundation + +extension ENSContracts { + enum ENSRegistryResponses { + struct RegistryResponse: MulticallDecodableResponse { + var value: EthereumAddress + + init?(data: String) throws { + guard data != "0x" else { + throw EthereumNameServiceError.ensUnknown + } + + let idx = data.index(data.endIndex, offsetBy: -40) + self.value = EthereumAddress(String(data[idx...]).web3.withHexPrefix) + + guard self.value != .zero else { + throw EthereumNameServiceError.ensUnknown + } + } + } + + struct AddressResolverResponse: ABIResponse, MulticallDecodableResponse { + static var types: [ABIType.Type] { [String.self] } + + var value: String + + init?(values: [ABIDecoder.DecodedValue]) throws { + self.value = try values[0].decoded() + } + } + + struct NameResolverResponse: ABIResponse, MulticallDecodableResponse { + static var types: [ABIType.Type] { [EthereumAddress.self] } + + var value: EthereumAddress + + init?(values: [ABIDecoder.DecodedValue]) throws { + self.value = try values[0].decoded() + } + } + } +} diff --git a/web3swift/src/ENS/EthereumNameService.swift b/web3swift/src/ENS/EthereumNameService.swift index ee445749..afe7d3ad 100644 --- a/web3swift/src/ENS/EthereumNameService.swift +++ b/web3swift/src/ENS/EthereumNameService.swift @@ -31,7 +31,7 @@ public class EthereumNameService: EthereumNameServiceProtocol { self.client = client self.registryAddress = registryAddress } - + public func resolve(address: EthereumAddress, completion: @escaping ((EthereumNameServiceError?, String?) -> Void)) { guard let network = client.network, @@ -129,7 +129,7 @@ public class EthereumNameService: EthereumNameServiceProtocol { }) }) } - + static func nameHash(name: String) -> String { var node = Data.init(count: 32) let labels = name.components(separatedBy: ".") @@ -139,5 +139,5 @@ public class EthereumNameService: EthereumNameServiceProtocol { } return node.web3.hexString } - + } diff --git a/web3swift/src/ERC20/ERC20Responses.swift b/web3swift/src/ERC20/ERC20Responses.swift index d8fadba2..627be184 100644 --- a/web3swift/src/ERC20/ERC20Responses.swift +++ b/web3swift/src/ERC20/ERC20Responses.swift @@ -9,8 +9,8 @@ import Foundation import BigInt -enum ERC20Responses { - public struct nameResponse: ABIResponse { +public enum ERC20Responses { + public struct nameResponse: ABIResponse, MulticallDecodableResponse { public static var types: [ABIType.Type] = [ String.self ] public let value: String @@ -19,7 +19,7 @@ enum ERC20Responses { } } - public struct symbolResponse: ABIResponse { + public struct symbolResponse: ABIResponse, MulticallDecodableResponse { public static var types: [ABIType.Type] = [ String.self ] public let value: String @@ -28,7 +28,7 @@ enum ERC20Responses { } } - public struct decimalsResponse: ABIResponse { + public struct decimalsResponse: ABIResponse, MulticallDecodableResponse { public static var types: [ABIType.Type] = [ UInt8.self ] public let value: UInt8 @@ -37,7 +37,7 @@ enum ERC20Responses { } } - public struct balanceResponse: ABIResponse { + public struct balanceResponse: ABIResponse, MulticallDecodableResponse { public static var types: [ABIType.Type] = [ BigUInt.self ] public let value: BigUInt diff --git a/web3swift/src/ERC721/ERC721Responses.swift b/web3swift/src/ERC721/ERC721Responses.swift index b079c539..8e546938 100644 --- a/web3swift/src/ERC721/ERC721Responses.swift +++ b/web3swift/src/ERC721/ERC721Responses.swift @@ -10,7 +10,7 @@ import Foundation import BigInt public enum ERC721Responses { - public struct balanceResponse: ABIResponse { + public struct balanceResponse: ABIResponse, MulticallDecodableResponse { public static var types: [ABIType.Type] = [ BigUInt.self ] public let value: BigUInt @@ -19,7 +19,7 @@ public enum ERC721Responses { } } - public struct ownerResponse: ABIResponse { + public struct ownerResponse: ABIResponse, MulticallDecodableResponse { public static var types: [ABIType.Type] = [ EthereumAddress.self ] public let value: EthereumAddress @@ -30,7 +30,7 @@ public enum ERC721Responses { } public enum ERC721MetadataResponses { - public struct nameResponse: ABIResponse { + public struct nameResponse: ABIResponse, MulticallDecodableResponse { public static var types: [ABIType.Type] = [ String.self ] public let value: String @@ -39,7 +39,7 @@ public enum ERC721MetadataResponses { } } - public struct symbolResponse: ABIResponse { + public struct symbolResponse: ABIResponse, MulticallDecodableResponse { public static var types: [ABIType.Type] = [ String.self ] public let value: String @@ -48,18 +48,22 @@ public enum ERC721MetadataResponses { } } - public struct tokenURIResponse: ABIResponse { + public struct tokenURIResponse: ABIResponse, MulticallDecodableResponse { public static var types: [ABIType.Type] = [ URL.self ] - public let uri: URL + + @available(*, deprecated, renamed: "value") + public var uri: URL { value } + + public let value: URL public init?(values: [ABIDecoder.DecodedValue]) throws { - self.uri = try values[0].decoded() + self.value = try values[0].decoded() } } } public enum ERC721EnumerableResponses { - public struct numberResponse: ABIResponse { + public struct numberResponse: ABIResponse, MulticallDecodableResponse { public static var types: [ABIType.Type] = [ BigUInt.self ] public let value: BigUInt diff --git a/web3swift/src/Multicall/Multicall.swift b/web3swift/src/Multicall/Multicall.swift new file mode 100644 index 00000000..a5e1cb8e --- /dev/null +++ b/web3swift/src/Multicall/Multicall.swift @@ -0,0 +1,156 @@ +// +// Multicall.swift +// web3swift +// +// Created by David Rodrigues on 28/10/2020. +// Copyright © 2020 Argent Labs Limited. All rights reserved. +// + +import Foundation +import BigInt + +public typealias MulticallResponse = Multicall.Response + +public struct Multicall { + private let client: EthereumClientProtocol + + public init(client: EthereumClientProtocol) { + self.client = client + } + + public func aggregate( + calls: [Call], + completion: @escaping (Result) -> Void + ) { + guard + let network = client.network, + let contract = Contract.registryAddress(for: network) + else { return completion(.failure(MulticallError.contractUnavailable)) } + + let function = Contract.Functions.aggregate(contract: contract, calls: calls) + + function.call(withClient: client, responseType: Response.self) { (error, response) in + if let response = response { + guard calls.count == response.outputs.count + else { fatalError("Outputs do not match the number of calls done") } + + zip(calls, response.outputs) + .forEach { call, output in + try? call.handler?(output) + } + + completion(.success(response)) + } else { + completion(.failure(MulticallError.executionFailed(error))) + } + } + } +} + +extension Multicall { + + public enum MulticallError: Error { + case contractUnavailable + case executionFailed(Error?) + } + + public enum CallError: Error { + case contractFailure + case couldNotDecodeResponse(Error?) + } + + public typealias Output = Result + + public struct Response: ABIResponse { + static let multicallFailedError = "MULTICALL_FAIL".web3.keccak256.web3.hexString + + public static var types: [ABIType.Type] = [BigUInt.self, ABIArray.self] + + public let block: BigUInt + public let outputs: [Output] + + public init?(values: [ABIDecoder.DecodedValue]) throws { + block = try values[0].decoded() + outputs = values[1].entry.map { result in + guard result != Self.multicallFailedError + else { return .failure(.contractFailure) } + + return .success(result) + } + } + } + + public struct Call: ABITuple { + public static var types: [ABIType.Type] = [EthereumAddress.self, Data.self] + public var encodableValues: [ABIType] { [target, encodedFunction] } + + public let target: EthereumAddress + public let encodedFunction: Data + public let handler: ((Output) throws -> Void)? + + public init(function: Function, handler: ((Output) throws -> Void)? = nil) throws { + self.target = function.contract + self.encodedFunction = try { + let encoder = ABIFunctionEncoder(Function.name) + try function.encode(to: encoder) + return try encoder.encoded() + }() + self.handler = handler + } + + public init?(values: [ABIDecoder.DecodedValue]) throws { + self.target = try values[0].decoded() + self.encodedFunction = try values[1].decoded() + self.handler = nil + } + + public func encode(to encoder: ABIFunctionEncoder) throws { + try encoder.encode(target) + try encoder.encode(encodedFunction) + } + } + + public struct Aggregator { + public private(set) var calls: [Call] = [] + + public init() {} + + public mutating func append(_ f: Function) throws { + try calls.append(.init(function: f)) + } + + public mutating func append(_ f: Function, handler: @escaping (Output) throws -> Void) throws { + try calls.append(.init(function: f, handler: handler)) + } + + public mutating func append( + function f: Function, + response: Response.Type, + handler: @escaping (Result) throws -> Void + ) throws { + try calls.append(.init(function: f, handler: { output in + try handler( + output.flatMap { + do { + if let response = try Response(data: $0) { + return .success(response.value) + } else { + return .failure(.couldNotDecodeResponse(nil)) + } + } catch let error { + return .failure(.couldNotDecodeResponse(error)) + } + } + ) + })) + } + } +} + +public protocol MulticallDecodableResponse { + associatedtype Value + + var value: Value { get } + + init?(data: String) throws +} diff --git a/web3swift/src/Multicall/MulticallContract.swift b/web3swift/src/Multicall/MulticallContract.swift new file mode 100644 index 00000000..89418441 --- /dev/null +++ b/web3swift/src/Multicall/MulticallContract.swift @@ -0,0 +1,52 @@ +// +// Copyright © 2020 Argent Labs Limited. All rights reserved. +// + +import Foundation +import BigInt + +extension Multicall { + public enum Contract { + + static let ropstenAddress = EthereumAddress("0x604D19Ba889A223693B0E78bC1269760B291b9Df") + static let mainnetAddress = EthereumAddress("0xF34D2Cb31175a51B23fb6e08cA06d7208FaD379F") + + public static func registryAddress(for network: EthereumNetwork) -> EthereumAddress? { + switch network { + case .Ropsten: + return Self.ropstenAddress + case .Mainnet: + return Self.mainnetAddress + default: + return nil + } + } + + public enum Functions { + public struct aggregate: ABIFunction { + public static let name = "aggregate" + public let gasPrice: BigUInt? + public let gasLimit: BigUInt? + public var contract: EthereumAddress + public let from: EthereumAddress? + public let calls: [Call] + + public init(contract: EthereumAddress, + from: EthereumAddress? = nil, + gasPrice: BigUInt? = nil, + gasLimit: BigUInt? = nil, + calls: [Call]) { + self.contract = contract + self.from = from + self.gasPrice = gasPrice + self.gasLimit = gasLimit + self.calls = calls + } + + public func encode(to encoder: ABIFunctionEncoder) throws { + try encoder.encode(calls) + } + } + } + } +} From 740fa4a381fa75bd265b9894032bc09dfb086827 Mon Sep 17 00:00:00 2001 From: Miguel Angel Quinones Date: Thu, 12 Nov 2020 12:07:37 +0100 Subject: [PATCH 10/20] [FIX] Memory leaks when generating signature/hash --- web3swift/src/Extensions/KeccakExtensions.swift | 5 ++++- web3swift/src/Utils/AesUtil.swift | 6 ++++++ web3swift/src/Utils/KeyUtil.swift | 15 +++++++++++++++ 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/web3swift/src/Extensions/KeccakExtensions.swift b/web3swift/src/Extensions/KeccakExtensions.swift index efb4e2ab..578f8908 100644 --- a/web3swift/src/Extensions/KeccakExtensions.swift +++ b/web3swift/src/Extensions/KeccakExtensions.swift @@ -11,9 +11,12 @@ import keccaktiny public extension Web3Extensions where Base == Data { var keccak256: Data { + let result = UnsafeMutablePointer.allocate(capacity: 32) + defer { + result.deallocate() + } let nsData = base as NSData let input = nsData.bytes.bindMemory(to: UInt8.self, capacity: base.count) - let result = UnsafeMutablePointer.allocate(capacity: 32) keccak_256(result, 32, input, base.count) return Data(bytes: result, count: 32) } diff --git a/web3swift/src/Utils/AesUtil.swift b/web3swift/src/Utils/AesUtil.swift index 4b3acb65..37a469fd 100644 --- a/web3swift/src/Utils/AesUtil.swift +++ b/web3swift/src/Utils/AesUtil.swift @@ -20,6 +20,9 @@ class Aes128Util { func xcrypt(input: Data) -> Data { let ctx = UnsafeMutablePointer.allocate(capacity: 1) + defer { + ctx.deallocate() + } let keyPtr = (self.key as NSData).bytes.assumingMemoryBound(to: UInt8.self) @@ -33,6 +36,9 @@ class Aes128Util { let inputPtr = (input as NSData).bytes.assumingMemoryBound(to: UInt8.self) let length = input.count let outputPtr = UnsafeMutablePointer.allocate(capacity: length) + defer { + outputPtr.deallocate() + } outputPtr.assign(from: inputPtr, count: length) AES_CTR_xcrypt_buffer(ctx, outputPtr, UInt32(length)) diff --git a/web3swift/src/Utils/KeyUtil.swift b/web3swift/src/Utils/KeyUtil.swift index ee422fdf..6817f4c5 100644 --- a/web3swift/src/Utils/KeyUtil.swift +++ b/web3swift/src/Utils/KeyUtil.swift @@ -40,6 +40,9 @@ class KeyUtil { } let publicKeyPtr = UnsafeMutablePointer.allocate(capacity: 1) + defer { + publicKeyPtr.deallocate() + } guard secp256k1_ec_pubkey_create(ctx, publicKeyPtr, privateKeyPtr) == 1 else { print("Failed to generate a public key: public key could not be created.") throw KeyUtilError.unknownError @@ -47,6 +50,9 @@ class KeyUtil { var publicKeyLength = 65 let outputPtr = UnsafeMutablePointer.allocate(capacity: publicKeyLength) + defer { + outputPtr.deallocate() + } secp256k1_ec_pubkey_serialize(ctx, outputPtr, &publicKeyLength, publicKeyPtr, UInt32(SECP256K1_EC_UNCOMPRESSED)) let publicKey = Data(bytes: outputPtr, count: publicKeyLength).subdata(in: 1...allocate(capacity: 1) + defer { + signaturePtr.deallocate() + } guard secp256k1_ecdsa_sign_recoverable(ctx, signaturePtr, msg, privateKeyPtr, nil, nil) == 1 else { print("Failed to sign message: recoverable ECDSA signature creation failed.") throw KeyUtilError.signatureFailure } let outputPtr = UnsafeMutablePointer.allocate(capacity: 64) + defer { + outputPtr.deallocate() + } var recid: Int32 = 0 secp256k1_ecdsa_recoverable_signature_serialize_compact(ctx, outputPtr, &recid, signaturePtr) let outputWithRecidPtr = UnsafeMutablePointer.allocate(capacity: 65) + defer { + outputWithRecidPtr.deallocate() + } outputWithRecidPtr.assign(from: outputPtr, count: 64) outputWithRecidPtr.advanced(by: 64).pointee = UInt8(recid) From 464f6155131f1e2a0cb756affd7ce009c02e179a Mon Sep 17 00:00:00 2001 From: Miguel Angel Quinones Date: Thu, 12 Nov 2020 16:40:21 +0100 Subject: [PATCH 11/20] [FIX] Ropsten contract config issue (test failing) --- web3sTests/Client/EthereumClientTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web3sTests/Client/EthereumClientTests.swift b/web3sTests/Client/EthereumClientTests.swift index 18156a9f..a2513c92 100644 --- a/web3sTests/Client/EthereumClientTests.swift +++ b/web3sTests/Client/EthereumClientTests.swift @@ -378,7 +378,7 @@ class EthereumClientTests: XCTestCase { func test_GivenValidTransaction_ThenEstimatesGas() { let expect = expectation(description: "estimateOK") - let function = TransferToken(wallet: EthereumAddress("0xd5b919520259e1174274420E3291ab77215C3D13"), + let function = TransferToken(wallet: EthereumAddress("0xD18dE36e6FB4a5A069f673723Fab71cc00C6CE5F"), token: EthereumAddress("0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"), to: EthereumAddress("0x2A6295C34b4136F2C3c1445c6A0338D784fe0ddd"), amount: 1, From 3646d0f27da6bda21d0461c94fbf4040447ad718 Mon Sep 17 00:00:00 2001 From: Miguel Angel Quinones Date: Thu, 12 Nov 2020 17:46:25 +0100 Subject: [PATCH 12/20] [FIX] Support old geth behaviour returning '0x' as response to 'eth_call' when method is not implemented in a contract (execution reverted) We'll support the proper error later but keeping functionality as-is as clients might rely on this error specifically. --- web3sTests/Client/EthereumClientTests.swift | 45 +++++++++++++++++++++ web3sTests/TestConfig.swift | 3 ++ web3swift/src/Client/EthereumClient.swift | 7 +++- web3swift/src/Client/JSONRPC.swift | 1 + 4 files changed, 55 insertions(+), 1 deletion(-) diff --git a/web3sTests/Client/EthereumClientTests.swift b/web3sTests/Client/EthereumClientTests.swift index a2513c92..ae88b744 100644 --- a/web3sTests/Client/EthereumClientTests.swift +++ b/web3sTests/Client/EthereumClientTests.swift @@ -34,12 +34,14 @@ struct TransferMatchingSignatureEvent: ABIEvent { class EthereumClientTests: XCTestCase { var client: EthereumClient? + var mainnetClient: EthereumClient? var account: EthereumAccount? let timeout = 10.0 override func setUp() { super.setUp() self.client = EthereumClient(url: URL(string: TestConfig.clientUrl)!) + self.mainnetClient = EthereumClient(url: URL(string: TestConfig.mainnetClientUrl)!) self.account = try? EthereumAccount(keyStorage: TestEthereumKeyStorage(privateKey: TestConfig.privateKey)) print("Public address: \(self.account?.address ?? "NONE")") } @@ -376,6 +378,28 @@ class EthereumClientTests: XCTestCase { waitForExpectations(timeout: 10) } + // This is how geth used to work up until a recent version + // see https://github.com/ethereum/go-ethereum/pull/21083/ + // Used to return '0x' in response, and would fail decoding + // We'll continue to support this as user of library (and Argent in our case) + // works with this assumption. + // NOTE: This behaviour will be removed at a later time to fail as expected + // NOTE: At the time of writing, this test succeeds as-is in ropsten as nodes behaviour is different. That's why we use a mainnet check here + func test_GivenUnimplementedMethod_WhenCallingContract_ThenFailsWith0x() { + let expect = expectation(description: "graceful_failure") + + let function = InvalidMethod(param: .zero) + + function.call(withClient: self.mainnetClient!, + responseType: InvalidMethod.BoolResponse.self) { (error, response) in + XCTAssertEqual(error, .decodeIssue) + XCTAssertNil(response) + expect.fulfill() + } + + waitForExpectations(timeout: 10) + } + func test_GivenValidTransaction_ThenEstimatesGas() { let expect = expectation(description: "estimateOK") let function = TransferToken(wallet: EthereumAddress("0xD18dE36e6FB4a5A069f673723Fab71cc00C6CE5F"), @@ -443,3 +467,24 @@ struct TransferToken: ABIFunction { } } +struct InvalidMethod: ABIFunction { + static let name = "invalidMethodCallBoolResponse" + let contract = EthereumAddress("0xed0439eacf4c4965ae4613d77a5c2efe10e5f183") + let from: EthereumAddress? = EthereumAddress("0xed0439eacf4c4965ae4613d77a5c2efe10e5f183") + let gasPrice: BigUInt? = nil + let gasLimit: BigUInt? = nil + + let param: EthereumAddress + + struct BoolResponse: ABIResponse { + static var types: [ABIType.Type] = [Bool.self] + let value: Bool + + init?(values: [ABIDecoder.DecodedValue]) throws { + self.value = try values[0].decoded() + } + } + + func encode(to encoder: ABIFunctionEncoder) throws { + } +} diff --git a/web3sTests/TestConfig.swift b/web3sTests/TestConfig.swift index bbba4142..6fbd5046 100644 --- a/web3sTests/TestConfig.swift +++ b/web3sTests/TestConfig.swift @@ -12,6 +12,9 @@ struct TestConfig { // This is the proxy URL for connecting to the Blockchain. For testing we usually use the Ropsten network on Infura. Using free tier, so might hit rate limits static let clientUrl = "https://ropsten.infura.io/v3/b2f4b3f635d8425c96854c3d28ba6bb0" + // Same for mainnet + static let mainnetClientUrl = "https://mainnet.infura.io/v3/b2f4b3f635d8425c96854c3d28ba6bb0" + // An EOA with some Ether, so that we can test sending transactions (pay for gas) static let privateKey = "0xef4e182ae2cf32192d2a62c1159c8c4f7f2d658c303d0dfca5791a205456a132" diff --git a/web3swift/src/Client/EthereumClient.swift b/web3swift/src/Client/EthereumClient.swift index 67915008..d22d8188 100644 --- a/web3swift/src/Client/EthereumClient.swift +++ b/web3swift/src/Client/EthereumClient.swift @@ -325,7 +325,12 @@ public class EthereumClient: EthereumClientProtocol { let params = CallParams(from: transaction.from?.value, to: transaction.to.value, data: transactionData.web3.hexString, block: block.stringValue) EthereumRPC.execute(session: session, url: url, method: "eth_call", params: params, receive: String.self) { (error, response) in if let resDataString = response as? String { - completion(nil, resDataString) + completion(nil, resDataString) + } else if + let error = error, + case let JSONRPCError.executionError(result) = error, + result.error.code == JSONRPCErrorCode.executionReverted { + completion(nil, "0x") } else { completion(EthereumClientError.unexpectedReturnValue, nil) } diff --git a/web3swift/src/Client/JSONRPC.swift b/web3swift/src/Client/JSONRPC.swift index 2ae70658..311a9367 100644 --- a/web3swift/src/Client/JSONRPC.swift +++ b/web3swift/src/Client/JSONRPC.swift @@ -34,6 +34,7 @@ struct JSONRPCErrorResult: Decodable { enum JSONRPCErrorCode { static var tooManyResults = -32005 + static var executionReverted = -32000 } enum JSONRPCError: Error { From 02c567b84533da09fee41adbe3d713d35fd7dc77 Mon Sep 17 00:00:00 2001 From: Miguel Angel Quinones Date: Tue, 17 Nov 2020 15:15:32 +0100 Subject: [PATCH 13/20] [TIDY] Include 'execution error', and also rename error as per spec Errors spec (moving target, but still useful): https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1474.md: https://eth.wiki/json-rpc/json-rpc-error-codes-improvement-proposal --- web3sTests/Client/EthereumClientTests.swift | 42 +++++++++++++++++++-- web3swift/src/Client/EthereumClient.swift | 3 +- web3swift/src/Client/JSONRPC.swift | 3 +- 3 files changed, 43 insertions(+), 5 deletions(-) diff --git a/web3sTests/Client/EthereumClientTests.swift b/web3sTests/Client/EthereumClientTests.swift index ae88b744..5b78a8c7 100644 --- a/web3sTests/Client/EthereumClientTests.swift +++ b/web3sTests/Client/EthereumClientTests.swift @@ -388,10 +388,24 @@ class EthereumClientTests: XCTestCase { func test_GivenUnimplementedMethod_WhenCallingContract_ThenFailsWith0x() { let expect = expectation(description: "graceful_failure") - let function = InvalidMethod(param: .zero) + let function = InvalidMethodA(param: .zero) function.call(withClient: self.mainnetClient!, - responseType: InvalidMethod.BoolResponse.self) { (error, response) in + responseType: InvalidMethodA.BoolResponse.self) { (error, response) in + XCTAssertEqual(error, .decodeIssue) + XCTAssertNil(response) + expect.fulfill() + } + + waitForExpectations(timeout: 10) + } + func test_GivenFailingCallMethod_WhenCallingContract_ThenFailsWith0x() { + let expect = expectation(description: "graceful_failure") + + let function = InvalidMethodB(param: .zero) + + function.call(withClient: self.mainnetClient!, + responseType: InvalidMethodB.BoolResponse.self) { (error, response) in XCTAssertEqual(error, .decodeIssue) XCTAssertNil(response) expect.fulfill() @@ -467,7 +481,7 @@ struct TransferToken: ABIFunction { } } -struct InvalidMethod: ABIFunction { +struct InvalidMethodA: ABIFunction { static let name = "invalidMethodCallBoolResponse" let contract = EthereumAddress("0xed0439eacf4c4965ae4613d77a5c2efe10e5f183") let from: EthereumAddress? = EthereumAddress("0xed0439eacf4c4965ae4613d77a5c2efe10e5f183") @@ -488,3 +502,25 @@ struct InvalidMethod: ABIFunction { func encode(to encoder: ABIFunctionEncoder) throws { } } + +struct InvalidMethodB: ABIFunction { + static let name = "invalidMethodCallBoolResponse" + let contract = EthereumAddress("0xC011A72400E58ecD99Ee497CF89E3775d4bd732F") + let from: EthereumAddress? = EthereumAddress("0xC011A72400E58ecD99Ee497CF89E3775d4bd732F") + let gasPrice: BigUInt? = nil + let gasLimit: BigUInt? = nil + + let param: EthereumAddress + + struct BoolResponse: ABIResponse { + static var types: [ABIType.Type] = [Bool.self] + let value: Bool + + init?(values: [ABIDecoder.DecodedValue]) throws { + self.value = try values[0].decoded() + } + } + + func encode(to encoder: ABIFunctionEncoder) throws { + } +} diff --git a/web3swift/src/Client/EthereumClient.swift b/web3swift/src/Client/EthereumClient.swift index d22d8188..29a0a5ec 100644 --- a/web3swift/src/Client/EthereumClient.swift +++ b/web3swift/src/Client/EthereumClient.swift @@ -32,6 +32,7 @@ public protocol EthereumClientProtocol { public enum EthereumClientError: Error { case tooManyResults + case executionError case unexpectedReturnValue case noResultFound case decodeIssue @@ -329,7 +330,7 @@ public class EthereumClient: EthereumClientProtocol { } else if let error = error, case let JSONRPCError.executionError(result) = error, - result.error.code == JSONRPCErrorCode.executionReverted { + (result.error.code == JSONRPCErrorCode.invalidInput || result.error.code == JSONRPCErrorCode.contractExecution) { completion(nil, "0x") } else { completion(EthereumClientError.unexpectedReturnValue, nil) diff --git a/web3swift/src/Client/JSONRPC.swift b/web3swift/src/Client/JSONRPC.swift index 311a9367..b1171dac 100644 --- a/web3swift/src/Client/JSONRPC.swift +++ b/web3swift/src/Client/JSONRPC.swift @@ -33,8 +33,9 @@ struct JSONRPCErrorResult: Decodable { } enum JSONRPCErrorCode { + static var invalidInput = -32000 static var tooManyResults = -32005 - static var executionReverted = -32000 + static var contractExecution = 3 } enum JSONRPCError: Error { From 0dbb0800018b7a3536e3ab4082e2ba1d3209a148 Mon Sep 17 00:00:00 2001 From: David Rodrigues Date: Wed, 6 Jan 2021 10:45:37 +0000 Subject: [PATCH 14/20] [FIX] Return all logs from the starting block to the end block by doing recursive queries when needed (#124) --- web3swift.xcodeproj/project.pbxproj | 4 + web3swift/src/Client/EthereumClient.swift | 67 +++++----- .../src/Client/RecursiveLogCollector.swift | 122 ++++++++++++++++++ 3 files changed, 161 insertions(+), 32 deletions(-) create mode 100644 web3swift/src/Client/RecursiveLogCollector.swift diff --git a/web3swift.xcodeproj/project.pbxproj b/web3swift.xcodeproj/project.pbxproj index 9f7aa1c5..0193a92f 100644 --- a/web3swift.xcodeproj/project.pbxproj +++ b/web3swift.xcodeproj/project.pbxproj @@ -46,6 +46,7 @@ 47F2EAAF22848BB2007063C2 /* ERC721Responses.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47F2EAAE22848BB2007063C2 /* ERC721Responses.swift */; }; 47F2EAB32285BBDF007063C2 /* ERC165Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47F2EAB12285BAB6007063C2 /* ERC165Tests.swift */; }; 63002262255192D900C97B80 /* ENSMultiResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63002261255192D900C97B80 /* ENSMultiResolver.swift */; }; + 6327068225A48A8A0061388C /* RecursiveLogCollector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6327068125A48A8A0061388C /* RecursiveLogCollector.swift */; }; 63C41E6D254C4CFF0055ED7F /* ENSResponses.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63C41E6C254C4CFF0055ED7F /* ENSResponses.swift */; }; 63DAC6252549D8700059FF9C /* MulticallContract.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63DAC6242549D8700059FF9C /* MulticallContract.swift */; }; 63DAC6292549D9CE0059FF9C /* Multicall.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63DAC6282549D9CE0059FF9C /* Multicall.swift */; }; @@ -161,6 +162,7 @@ 595F7375549A81C8950C1F8E /* Pods-web3s.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-web3s.release.xcconfig"; path = "Pods/Target Support Files/Pods-web3s/Pods-web3s.release.xcconfig"; sourceTree = ""; }; 5E4FAB97C27DB6CBB355E0F1 /* Pods_web3swiftTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_web3swiftTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 63002261255192D900C97B80 /* ENSMultiResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ENSMultiResolver.swift; sourceTree = ""; }; + 6327068125A48A8A0061388C /* RecursiveLogCollector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecursiveLogCollector.swift; sourceTree = ""; }; 63C41E6C254C4CFF0055ED7F /* ENSResponses.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ENSResponses.swift; sourceTree = ""; }; 63DAC6242549D8700059FF9C /* MulticallContract.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MulticallContract.swift; sourceTree = ""; }; 63DAC6282549D9CE0059FF9C /* Multicall.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Multicall.swift; sourceTree = ""; }; @@ -390,6 +392,7 @@ children = ( C9236B832052C6AC00CE5557 /* Models */, 3CF37CBF2035ABE90030520E /* EthereumClient.swift */, + 6327068125A48A8A0061388C /* RecursiveLogCollector.swift */, C927FD58204EB1F4005922DC /* JSONRPC.swift */, ); path = Client; @@ -818,6 +821,7 @@ C9F836B52078049E004F45A9 /* EthereumAddress.swift in Sources */, C9236B802052C68D00CE5557 /* EthereumLog.swift in Sources */, 47F2EAAB22848B9C007063C2 /* ERC721Functions.swift in Sources */, + 6327068225A48A8A0061388C /* RecursiveLogCollector.swift in Sources */, 63DAC6252549D8700059FF9C /* MulticallContract.swift in Sources */, C9236B8B2052E12200CE5557 /* HexExtensions.swift in Sources */, C927FD62204EF4E1005922DC /* EthereumNameService.swift in Sources */, diff --git a/web3swift/src/Client/EthereumClient.swift b/web3swift/src/Client/EthereumClient.swift index 29a0a5ec..1ab3c771 100644 --- a/web3swift/src/Client/EthereumClient.swift +++ b/web3swift/src/Client/EthereumClient.swift @@ -338,52 +338,55 @@ public class EthereumClient: EthereumClientProtocol { } } - public func eth_getLogs(addresses: [String]?, topics: [String?]?, fromBlock: EthereumBlock = .Earliest, toBlock: EthereumBlock = .Latest, completion: @escaping ((EthereumClientError?, [EthereumLog]?) -> Void)) { - + public func eth_getLogs(addresses: [String]?, topics: [String?]?, fromBlock from: EthereumBlock = .Earliest, toBlock to: EthereumBlock = .Latest, completion: @escaping ((EthereumClientError?, [EthereumLog]?) -> Void)) { + eth_getLogs(addresses: addresses, topics: topics.map(Topics.plain), fromBlock: from, toBlock: to, completion: completion) + } + + public func eth_getLogs(addresses: [String]?, orTopics topics: [[String]?]?, fromBlock from: EthereumBlock = .Earliest, toBlock to: EthereumBlock = .Latest, completion: @escaping((EthereumClientError?, [EthereumLog]?) -> Void)) { + eth_getLogs(addresses: addresses, topics: topics.map(Topics.composed), fromBlock: from, toBlock: to, completion: completion) + } + + private func eth_getLogs(addresses: [String]?, topics: Topics?, fromBlock from: EthereumBlock, toBlock to: EthereumBlock, completion: @escaping((EthereumClientError?, [EthereumLog]?) -> Void)) { + DispatchQueue.global(qos: .default) + .async { + let result = RecursiveLogCollector(ethClient: self) + .getAllLogs(addresses: addresses, topics: topics, from: from, to: to) + + switch result { + case .success(let logs): + completion(nil, logs) + case .failure(let error): + completion(error, nil) + } + } + } + + internal func getLogs(addresses: [String]?, topics: Topics?, fromBlock: EthereumBlock, toBlock: EthereumBlock, completion: @escaping((Result<[EthereumLog], EthereumClientError>) -> Void)) { + struct CallParams: Encodable { - let fromBlock: String - let toBlock: String + var fromBlock: String + var toBlock: String let address: [String]? - let topics: [String?]? + let topics: Topics? } - + let params = CallParams(fromBlock: fromBlock.stringValue, toBlock: toBlock.stringValue, address: addresses, topics: topics) - + EthereumRPC.execute(session: session, url: url, method: "eth_getLogs", params: [params], receive: [EthereumLog].self) { (error, response) in - if let log = response as? [EthereumLog] { - completion(nil, log) + if let logs = response as? [EthereumLog] { + completion(.success(logs)) } else { if let error = error as? JSONRPCError, case let .executionError(innerError) = error, innerError.error.code == JSONRPCErrorCode.tooManyResults { - completion(EthereumClientError.tooManyResults, nil) + completion(.failure(.tooManyResults)) } else { - completion(EthereumClientError.unexpectedReturnValue, nil) + completion(.failure(.unexpectedReturnValue)) } } } - } - - public func eth_getLogs(addresses: [String]?, orTopics: [[String]?]?, fromBlock: EthereumBlock, toBlock: EthereumBlock, completion: @escaping((EthereumClientError?, [EthereumLog]?) -> Void)) { - struct CallParams: Encodable { - let fromBlock: String - let toBlock: String - let address: [String]? - let topics: [[String]?]? - } - - let params = CallParams(fromBlock: fromBlock.stringValue, toBlock: toBlock.stringValue, address: addresses, topics: orTopics) - - EthereumRPC.execute(session: session, url: url, method: "eth_getLogs", params: [params], receive: [EthereumLog].self) { (error, response) in - if let log = response as? [EthereumLog] { - completion(nil, log) - } else { - completion(EthereumClientError.unexpectedReturnValue, nil) - } - } - } - + public func eth_getBlockByNumber(_ block: EthereumBlock, completion: @escaping((EthereumClientError?, EthereumBlockInfo?) -> Void)) { struct CallParams: Encodable { diff --git a/web3swift/src/Client/RecursiveLogCollector.swift b/web3swift/src/Client/RecursiveLogCollector.swift new file mode 100644 index 00000000..9e8b62fd --- /dev/null +++ b/web3swift/src/Client/RecursiveLogCollector.swift @@ -0,0 +1,122 @@ +// +// RecursiveLogCollector.swift +// web3swift +// +// Created by David Rodrigues on 05/01/2021. +// Copyright © 2021 Argent Labs Limited. All rights reserved. +// + +import Foundation + +enum Topics: Encodable { + case plain([String?]) + case composed([[String]?]) + + func encode(to encoder: Encoder) throws { + var container = encoder.unkeyedContainer() + switch self { + case .plain(let values): + try container.encode(contentsOf: values) + case .composed(let values): + try container.encode(contentsOf: values) + } + } +} + +struct RecursiveLogCollector { + let ethClient: EthereumClient + + func getAllLogs( + addresses: [String]?, + topics: Topics?, + from: EthereumBlock, + to: EthereumBlock + ) -> Result<[EthereumLog], EthereumClientError> { + + switch getLogs(addresses: addresses, topics: topics, from: from, to: to) { + case .success(let logs): + return .success(logs) + case.failure(.tooManyResults): + guard let middleBlock = getMiddleBlock(from: from, to: to) + else { return .failure(.unexpectedReturnValue) } + + guard + case let .success(lhs) = getAllLogs( + addresses: addresses, + topics: topics, + from: from, + to: middleBlock + ), + case let .success(rhs) = getAllLogs( + addresses: addresses, + topics: topics, + from: middleBlock, + to: to + ) + else { return .failure(.unexpectedReturnValue) } + + return .success(lhs + rhs) + case .failure(let error): + return .failure(error) + } + } + + private func getLogs( + addresses: [String]?, + topics: Topics? = nil, + from: EthereumBlock, + to: EthereumBlock + ) -> Result<[EthereumLog], EthereumClientError> { + + let sem = DispatchSemaphore(value: 0) + + var response: Result<[EthereumLog], EthereumClientError>! + + ethClient.getLogs(addresses: addresses, topics: topics, fromBlock: from, toBlock: to) { result in + response = result + sem.signal() + } + + sem.wait() + + return response + } + + private func getMiddleBlock( + from: EthereumBlock, + to: EthereumBlock + ) -> EthereumBlock? { + + func toBlockNumber() -> Int? { + if let toBlockNumber = to.intValue { + return toBlockNumber + } else if case let .success(currentBlock) = getCurrentBlock(), let currentBlockNumber = currentBlock.intValue { + return currentBlockNumber + } else { + return nil + } + } + + guard + let fromBlockNumber = from.intValue, + let toBlockNumber = toBlockNumber() + else { return nil } + + return EthereumBlock(rawValue: fromBlockNumber + (toBlockNumber - fromBlockNumber) / 2) + } + + private func getCurrentBlock() -> Result { + let sem = DispatchSemaphore(value: 0) + var responseValue: EthereumBlock? + + self.ethClient.eth_blockNumber { (error, blockInt) in + if let blockInt = blockInt { + responseValue = EthereumBlock(rawValue: blockInt) + } + sem.signal() + } + sem.wait() + + return responseValue.map(Result.success) ?? .failure(.unexpectedReturnValue) + } +} From 6bf2079df79085e227fc0a2ace033c97fffb6853 Mon Sep 17 00:00:00 2001 From: David Rodrigues Date: Thu, 7 Jan 2021 18:20:44 +0000 Subject: [PATCH 15/20] [FIX] Validate if the error is an execution error for estimate gas and propagate upstream --- web3swift/src/Client/EthereumClient.swift | 2 ++ web3swift/src/Client/JSONRPC.swift | 9 +++++++++ 2 files changed, 11 insertions(+) diff --git a/web3swift/src/Client/EthereumClient.swift b/web3swift/src/Client/EthereumClient.swift index 1ab3c771..b84b379c 100644 --- a/web3swift/src/Client/EthereumClient.swift +++ b/web3swift/src/Client/EthereumClient.swift @@ -215,6 +215,8 @@ public class EthereumClient: EthereumClientProtocol { EthereumRPC.execute(session: session, url: url, method: "eth_estimateGas", params: params, receive: String.self) { (error, response) in if let gasHex = response as? String, let gas = BigUInt(hex: gasHex) { completion(nil, gas) + } else if let error = error as? JSONRPCError, error.isExecutionError { + completion(EthereumClientError.executionError, nil) } else { completion(EthereumClientError.unexpectedReturnValue, nil) } diff --git a/web3swift/src/Client/JSONRPC.swift b/web3swift/src/Client/JSONRPC.swift index b1171dac..35daa5e4 100644 --- a/web3swift/src/Client/JSONRPC.swift +++ b/web3swift/src/Client/JSONRPC.swift @@ -45,6 +45,15 @@ enum JSONRPCError: Error { case decodingError case unknownError case noResult + + var isExecutionError: Bool { + switch self { + case .executionError: + return true + default: + return false + } + } } public class EthereumRPC { From 9fda1cc0daeb57db7533d1f4bbbe0647e134515d Mon Sep 17 00:00:00 2001 From: Miguel Angel Quinones Date: Mon, 11 Jan 2021 16:09:50 +0100 Subject: [PATCH 16/20] [FIX] Use cached network response --- web3swift/src/Client/EthereumClient.swift | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/web3swift/src/Client/EthereumClient.swift b/web3swift/src/Client/EthereumClient.swift index b84b379c..4a673011 100644 --- a/web3swift/src/Client/EthereumClient.swift +++ b/web3swift/src/Client/EthereumClient.swift @@ -61,8 +61,11 @@ public class EthereumClient: EthereumClientProtocol { self.net_version { (error, retreivedNetwork) in if let error = error { print("Client has no network: \(error.localizedDescription)") + } else { + network = retreivedNetwork + self.retreivedNetwork = network } - network = retreivedNetwork + group.leave() } @@ -328,7 +331,7 @@ public class EthereumClient: EthereumClientProtocol { let params = CallParams(from: transaction.from?.value, to: transaction.to.value, data: transactionData.web3.hexString, block: block.stringValue) EthereumRPC.execute(session: session, url: url, method: "eth_call", params: params, receive: String.self) { (error, response) in if let resDataString = response as? String { - completion(nil, resDataString) + completion(nil, resDataString) } else if let error = error, case let JSONRPCError.executionError(result) = error, From c6cdd5393d940386f0e7da3b816c41de7bec2e68 Mon Sep 17 00:00:00 2001 From: Miguel Angel Quinones Date: Mon, 1 Feb 2021 12:05:09 +0100 Subject: [PATCH 17/20] [UPDATE] Local testing against BigInt 2.5.0 --- Podfile | 2 +- Podfile.lock | 8 +- Pods/BigInt/README.md | 2 +- Pods/BigInt/Sources/Data Conversion.swift | 69 +++++++++++ Pods/BigInt/Sources/Random.swift | 116 +++++++++++------- Pods/Manifest.lock | 8 +- .../BigInt/BigInt-Info.plist | 2 +- 7 files changed, 153 insertions(+), 54 deletions(-) diff --git a/Podfile b/Podfile index 643fd5bd..2f50bcd9 100644 --- a/Podfile +++ b/Podfile @@ -1,5 +1,5 @@ def all_pods - pod 'BigInt', '5.0.0' + pod 'BigInt', '5.2.0' pod 'secp256k1.swift', '~> 0.1' pod 'GenericJSON', '~> 2.0' end diff --git a/Podfile.lock b/Podfile.lock index 488c86ab..7e11964a 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -1,10 +1,10 @@ PODS: - - BigInt (5.0.0) + - BigInt (5.2.0) - GenericJSON (2.0.1) - secp256k1.swift (0.1.4) DEPENDENCIES: - - BigInt (= 5.0.0) + - BigInt (= 5.2.0) - GenericJSON (~> 2.0) - secp256k1.swift (~> 0.1) @@ -15,10 +15,10 @@ SPEC REPOS: - secp256k1.swift SPEC CHECKSUMS: - BigInt: 74b4d88367b0e819d9f77393549226d36faeb0d8 + BigInt: f668a80089607f521586bbe29513d708491ef2f7 GenericJSON: a6e74e2c457f8693caab08e0eafde7d97e6666de secp256k1.swift: a7e7a214f6db6ce5db32cc6b2b45e5c4dd633634 -PODFILE CHECKSUM: 3d84a95ae54221cea1e840cd6b62e55c04ed01a3 +PODFILE CHECKSUM: 3df5c5e61ce857dec84394cba74c01a6b4c15d3a COCOAPODS: 1.8.4 diff --git a/Pods/BigInt/README.md b/Pods/BigInt/README.md index 4a90d640..2256249c 100644 --- a/Pods/BigInt/README.md +++ b/Pods/BigInt/README.md @@ -111,7 +111,7 @@ BigInt deploys to macOS 10.10, iOS 9, watchOS 2 and tvOS 9. It has been tested on the latest OS releases only---however, as the module uses very few platform-provided APIs, there should be very few issues with earlier versions. -BigInt uses no APIs specific to Apple platforms except for `arc4random_buf` in `BigUInt Random.swift`, so +BigInt uses no APIs specific to Apple platforms, so it should be easy to port it to other operating systems. Setup instructions: diff --git a/Pods/BigInt/Sources/Data Conversion.swift b/Pods/BigInt/Sources/Data Conversion.swift index e21b7bb0..25c65521 100644 --- a/Pods/BigInt/Sources/Data Conversion.swift +++ b/Pods/BigInt/Sources/Data Conversion.swift @@ -11,6 +11,7 @@ import Foundation extension BigUInt { //MARK: NSData Conversion + /// Initialize a BigInt from bytes accessed from an UnsafeRawBufferPointer public init(_ buffer: UnsafeRawBufferPointer) { // This assumes Word is binary. precondition(Word.bitWidth % 8 == 0) @@ -108,3 +109,71 @@ extension BigUInt { } } +extension BigInt { + + /// Initialize a BigInt from bytes accessed from an UnsafeRawBufferPointer, + /// where the first byte indicates sign (0 for positive, 1 for negative) + public init(_ buffer: UnsafeRawBufferPointer) { + // This assumes Word is binary. + precondition(Word.bitWidth % 8 == 0) + + self.init() + + let length = buffer.count + + // Serialized data for a BigInt should contain at least 2 bytes: one representing + // the sign, and another for the non-zero magnitude. Zero is represented by an + // empty Data struct, and negative zero is not supported. + guard length > 1, let firstByte = buffer.first else { return } + + // The first byte gives the sign + // This byte is compared to a bitmask to allow additional functionality to be added + // to this byte in the future. + self.sign = firstByte & 0b1 == 0 ? .plus : .minus + + self.magnitude = BigUInt(UnsafeRawBufferPointer(rebasing: buffer.dropFirst(1))) + } + + /// Initializes an integer from the bits stored inside a piece of `Data`. + /// The data is assumed to be in network (big-endian) byte order with a first + /// byte to represent the sign (0 for positive, 1 for negative) + public init(_ data: Data) { + // This assumes Word is binary. + // This is the same assumption made when initializing BigUInt from Data + precondition(Word.bitWidth % 8 == 0) + + self.init() + + // Serialized data for a BigInt should contain at least 2 bytes: one representing + // the sign, and another for the non-zero magnitude. Zero is represented by an + // empty Data struct, and negative zero is not supported. + guard data.count > 1, let firstByte = data.first else { return } + + // The first byte gives the sign + // This byte is compared to a bitmask to allow additional functionality to be added + // to this byte in the future. + self.sign = firstByte & 0b1 == 0 ? .plus : .minus + + // The remaining bytes are read and stored as the magnitude + self.magnitude = BigUInt(data.dropFirst(1)) + } + + /// Return a `Data` value that contains the base-256 representation of this integer, in network (big-endian) byte order and a prepended byte to indicate the sign (0 for positive, 1 for negative) + public func serialize() -> Data { + // Create a data object for the magnitude portion of the BigInt + let magnitudeData = self.magnitude.serialize() + + // Similar to BigUInt, a value of 0 should return an initialized, empty Data struct + guard magnitudeData.count > 0 else { return magnitudeData } + + // Create a new Data struct for the signed BigInt value + var data = Data(capacity: magnitudeData.count + 1) + + // The first byte should be 0 for a positive value, or 1 for a negative value + // i.e., the sign bit is the LSB + data.append(self.sign == .plus ? 0 : 1) + + data.append(magnitudeData) + return data + } +} diff --git a/Pods/BigInt/Sources/Random.swift b/Pods/BigInt/Sources/Random.swift index 5813b4bd..bea98caf 100644 --- a/Pods/BigInt/Sources/Random.swift +++ b/Pods/BigInt/Sources/Random.swift @@ -6,66 +6,96 @@ // Copyright © 2016-2017 Károly Lőrentey. // -import Foundation -#if os(Linux) || os(FreeBSD) - import Glibc -#endif - - extension BigUInt { - //MARK: Random Integers - - /// Create a big integer consisting of `width` uniformly distributed random bits. + /// Create a big unsigned integer consisting of `width` uniformly distributed random bits. /// - /// - Returns: A big integer less than `1 << width`. - /// - Note: This function uses `arc4random_buf` to generate random bits. - public static func randomInteger(withMaximumWidth width: Int) -> BigUInt { - guard width > 0 else { return 0 } - - let byteCount = (width + 7) / 8 - assert(byteCount > 0) - - let buffer = UnsafeMutablePointer.allocate(capacity: byteCount) - #if os(Linux) || os(FreeBSD) - let fd = open("/dev/urandom", O_RDONLY) - defer { - close(fd) + /// - Parameter width: The maximum number of one bits in the result. + /// - Parameter generator: The source of randomness. + /// - Returns: A big unsigned integer less than `1 << width`. + public static func randomInteger(withMaximumWidth width: Int, using generator: inout RNG) -> BigUInt { + var result = BigUInt.zero + var bitsLeft = width + var i = 0 + let wordsNeeded = (width + Word.bitWidth - 1) / Word.bitWidth + if wordsNeeded > 2 { + result.reserveCapacity(wordsNeeded) } - let _ = read(fd, buffer, MemoryLayout.size * byteCount) - #else - arc4random_buf(buffer, byteCount) - #endif - if width % 8 != 0 { - buffer[0] &= UInt8(1 << (width % 8) - 1) + while bitsLeft >= Word.bitWidth { + result[i] = generator.next() + i += 1 + bitsLeft -= Word.bitWidth } - defer { - buffer.deinitialize(count: byteCount) - buffer.deallocate() + if bitsLeft > 0 { + let mask: Word = (1 << bitsLeft) - 1 + result[i] = (generator.next() as Word) & mask } - return BigUInt(Data(bytesNoCopy: buffer, count: byteCount, deallocator: .none)) + return result } - /// Create a big integer consisting of `width-1` uniformly distributed random bits followed by a one bit. + /// Create a big unsigned integer consisting of `width` uniformly distributed random bits. /// - /// - Returns: A random big integer whose width is `width`. - /// - Note: This function uses `arc4random_buf` to generate random bits. - public static func randomInteger(withExactWidth width: Int) -> BigUInt { + /// - Note: I use a `SystemRandomGeneratorGenerator` as the source of randomness. + /// + /// - Parameter width: The maximum number of one bits in the result. + /// - Returns: A big unsigned integer less than `1 << width`. + public static func randomInteger(withMaximumWidth width: Int) -> BigUInt { + var rng = SystemRandomNumberGenerator() + return randomInteger(withMaximumWidth: width, using: &rng) + } + + /// Create a big unsigned integer consisting of `width-1` uniformly distributed random bits followed by a one bit. + /// + /// - Note: If `width` is zero, the result is zero. + /// + /// - Parameter width: The number of bits required to represent the answer. + /// - Parameter generator: The source of randomness. + /// - Returns: A random big unsigned integer whose width is `width`. + public static func randomInteger(withExactWidth width: Int, using generator: inout RNG) -> BigUInt { + // width == 0 -> return 0 because there is no room for a one bit. + // width == 1 -> return 1 because there is no room for any random bits. guard width > 1 else { return BigUInt(width) } - var result = randomInteger(withMaximumWidth: width - 1) + var result = randomInteger(withMaximumWidth: width - 1, using: &generator) result[(width - 1) / Word.bitWidth] |= 1 << Word((width - 1) % Word.bitWidth) return result } - /// Create a uniformly distributed random integer that's less than the specified limit. + /// Create a big unsigned integer consisting of `width-1` uniformly distributed random bits followed by a one bit. /// - /// - Returns: A random big integer that is less than `limit`. - /// - Note: This function uses `arc4random_buf` to generate random bits. - public static func randomInteger(lessThan limit: BigUInt) -> BigUInt { + /// - Note: If `width` is zero, the result is zero. + /// - Note: I use a `SystemRandomGeneratorGenerator` as the source of randomness. + /// + /// - Returns: A random big unsigned integer whose width is `width`. + public static func randomInteger(withExactWidth width: Int) -> BigUInt { + var rng = SystemRandomNumberGenerator() + return randomInteger(withExactWidth: width, using: &rng) + } + + /// Create a uniformly distributed random unsigned integer that's less than the specified limit. + /// + /// - Precondition: `limit > 0`. + /// + /// - Parameter limit: The upper bound on the result. + /// - Parameter generator: The source of randomness. + /// - Returns: A random big unsigned integer that is less than `limit`. + public static func randomInteger(lessThan limit: BigUInt, using generator: inout RNG) -> BigUInt { + precondition(limit > 0, "\(#function): 0 is not a valid limit") let width = limit.bitWidth - var random = randomInteger(withMaximumWidth: width) + var random = randomInteger(withMaximumWidth: width, using: &generator) while random >= limit { - random = randomInteger(withMaximumWidth: width) + random = randomInteger(withMaximumWidth: width, using: &generator) } return random } + + /// Create a uniformly distributed random unsigned integer that's less than the specified limit. + /// + /// - Precondition: `limit > 0`. + /// - Note: I use a `SystemRandomGeneratorGenerator` as the source of randomness. + /// + /// - Parameter limit: The upper bound on the result. + /// - Returns: A random big unsigned integer that is less than `limit`. + public static func randomInteger(lessThan limit: BigUInt) -> BigUInt { + var rng = SystemRandomNumberGenerator() + return randomInteger(lessThan: limit, using: &rng) + } } diff --git a/Pods/Manifest.lock b/Pods/Manifest.lock index 488c86ab..7e11964a 100644 --- a/Pods/Manifest.lock +++ b/Pods/Manifest.lock @@ -1,10 +1,10 @@ PODS: - - BigInt (5.0.0) + - BigInt (5.2.0) - GenericJSON (2.0.1) - secp256k1.swift (0.1.4) DEPENDENCIES: - - BigInt (= 5.0.0) + - BigInt (= 5.2.0) - GenericJSON (~> 2.0) - secp256k1.swift (~> 0.1) @@ -15,10 +15,10 @@ SPEC REPOS: - secp256k1.swift SPEC CHECKSUMS: - BigInt: 74b4d88367b0e819d9f77393549226d36faeb0d8 + BigInt: f668a80089607f521586bbe29513d708491ef2f7 GenericJSON: a6e74e2c457f8693caab08e0eafde7d97e6666de secp256k1.swift: a7e7a214f6db6ce5db32cc6b2b45e5c4dd633634 -PODFILE CHECKSUM: 3d84a95ae54221cea1e840cd6b62e55c04ed01a3 +PODFILE CHECKSUM: 3df5c5e61ce857dec84394cba74c01a6b4c15d3a COCOAPODS: 1.8.4 diff --git a/Pods/Target Support Files/BigInt/BigInt-Info.plist b/Pods/Target Support Files/BigInt/BigInt-Info.plist index e2771ff4..82c355f5 100644 --- a/Pods/Target Support Files/BigInt/BigInt-Info.plist +++ b/Pods/Target Support Files/BigInt/BigInt-Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 5.0.0 + 5.2.0 CFBundleSignature ???? CFBundleVersion From 48533162b7adf3dcd138819aaf9f22758d4eed5b Mon Sep 17 00:00:00 2001 From: Miguel Angel Quinones Date: Mon, 1 Feb 2021 12:06:48 +0100 Subject: [PATCH 18/20] [TIDY] Warning --- web3swift/src/ERC721/ERC721.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web3swift/src/ERC721/ERC721.swift b/web3swift/src/ERC721/ERC721.swift index dc9be674..e72af364 100644 --- a/web3swift/src/ERC721/ERC721.swift +++ b/web3swift/src/ERC721/ERC721.swift @@ -141,7 +141,7 @@ public class ERC721Metadata: ERC721 { let function = ERC721MetadataFunctions.tokenURI(contract: contract, tokenID: tokenID) function.call(withClient: client, responseType: ERC721MetadataResponses.tokenURIResponse.self) { error, response in - return completion(error, response?.uri) + return completion(error, response?.value) } } From ddfaf298757f65fe455f3437bd11161c2c578221 Mon Sep 17 00:00:00 2001 From: Miguel Angel Quinones Date: Mon, 1 Feb 2021 15:10:17 +0100 Subject: [PATCH 19/20] [ADD] Support custom int/uint types < 256 (BigInt/BigUInt was assumed always 256) [FIX] Guard against invalid Data sizes > 32 --- .../Contract/ABIFunctionEncoderTests.swift | 65 ++++++++++++++++--- .../Statically Typed/ABIFunctionEncoder.swift | 13 ++++ 2 files changed, 69 insertions(+), 9 deletions(-) diff --git a/web3sTests/Contract/ABIFunctionEncoderTests.swift b/web3sTests/Contract/ABIFunctionEncoderTests.swift index 1f473594..df276aa5 100644 --- a/web3sTests/Contract/ABIFunctionEncoderTests.swift +++ b/web3sTests/Contract/ABIFunctionEncoderTests.swift @@ -15,41 +15,88 @@ class ABIFunctionEncoderTests: XCTestCase { override func setUp() { encoder = ABIFunctionEncoder("test") - } func testGivenEmptyString_ThenEncodesCorrectly() { - try! encoder.encode("") + XCTAssertNoThrow(try encoder.encode("")) let encoded = try! encoder.encoded() XCTAssertEqual(String(hexFromBytes: encoded.web3.bytes), "0xf9fbd554000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000") } func testGivenNonEmptyString_ThenEncodesCorrectly() { - try! encoder.encode("hi") + XCTAssertNoThrow(try encoder.encode("hi")) let encoded = try! encoder.encoded() XCTAssertEqual(String(hexFromBytes: encoded.web3.bytes), "0xf9fbd554000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000026869000000000000000000000000000000000000000000000000000000000000") } + + func testGivenUInt_WhenNoExplicitSize_ThenEncodesAs256() { + XCTAssertNoThrow(try encoder.encode(BigUInt(100))) + let encoded = try! encoder.encoded() + XCTAssertEqual(String(hexFromBytes: encoded.web3.bytes), "0x29e99f070000000000000000000000000000000000000000000000000000000000000064") + } + + func testGivenUInt_WhenExplicitSize256_ThenEncodesAs256() { + XCTAssertNoThrow(try encoder.encode(BigUInt(100), staticSize: 256)) + let encoded = try! encoder.encoded() + XCTAssertEqual(String(hexFromBytes: encoded.web3.bytes), "0x29e99f070000000000000000000000000000000000000000000000000000000000000064") + } + + func testGivenUInt_WhenValidExplicitSize100_ThenEncodesAs100() { + XCTAssertNoThrow(try encoder.encode(BigUInt(100), staticSize: 100)) + let encoded = try! encoder.encoded() + XCTAssertEqual(String(hexFromBytes: encoded.web3.bytes), "0xbc39e7b50000000000000000000000000000000000000000000000000000000000000064") + } + + func testGivenUInt_WhenInvalidSizeBiggerThan256_ThenFailsEncoding() { + XCTAssertThrowsError(try encoder.encode(BigUInt(100), staticSize: 257)) + } + + func testGivenPositiveInt_WhenNoExplicitSize_ThenEncodesAs256() { + XCTAssertNoThrow(try encoder.encode(BigInt(100))) + let encoded = try! encoder.encoded() + XCTAssertEqual(String(hexFromBytes: encoded.web3.bytes), "0x9b22c05d0000000000000000000000000000000000000000000000000000000000000064") + } + + func testGivenPositiveInt_WhenValidSize256_ThenEncodesAs256() { + XCTAssertNoThrow(try encoder.encode(BigInt(100), staticSize: 256)) + let encoded = try! encoder.encoded() + XCTAssertEqual(String(hexFromBytes: encoded.web3.bytes), "0x9b22c05d0000000000000000000000000000000000000000000000000000000000000064") + } + + func testGivenPositiveInt_WhenValidExplicitSize100_ThenEncodesAs100() { + XCTAssertNoThrow(try encoder.encode(BigInt(100), staticSize: 100)) + let encoded = try! encoder.encoded() + XCTAssertEqual(String(hexFromBytes: encoded.web3.bytes), "0x868147590000000000000000000000000000000000000000000000000000000000000064") + } + + func testGivenPositiveInt_WhenInvalidSizeBiggerThan256_ThenFailsEncoding() { + XCTAssertThrowsError(try encoder.encode(BigInt(100), staticSize: 257)) + } func testGivenEmptyData_ThenEncodesCorrectly() { - try! encoder.encode(Data()) + XCTAssertNoThrow(try encoder.encode(Data())) let encoded = try! encoder.encoded() XCTAssertEqual(String(hexFromBytes: encoded.web3.bytes), "0x2f570a2300000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000") } func testGivenNonEmptyData_ThenEncodesCorrectly() { - try! encoder.encode(Data("hi".web3.bytes)) + XCTAssertNoThrow(try encoder.encode(Data("hi".web3.bytes))) let encoded = try! encoder.encoded() XCTAssertEqual(String(hexFromBytes: encoded.web3.bytes), "0x2f570a23000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000026869000000000000000000000000000000000000000000000000000000000000") } func testGivenStaticSizeData4_ThenEncodesCorrectly() { - try! encoder.encode(Data(hex: "0xffffffff")!, staticSize: 4) + XCTAssertNoThrow(try encoder.encode(Data(hex: "0xffffffff")!, staticSize: 4)) let encoded = try! encoder.encoded() XCTAssertEqual(String(hexFromBytes: encoded.web3.bytes), "0xda67eb8affffffff00000000000000000000000000000000000000000000000000000000") } + func testGivenStaticSizeDataBiggerThan32_ThenFailsEncoding() { + XCTAssertThrowsError(try encoder.encode(Data(hex: "0xffffffff")!, staticSize: 33)) + } + func testGivenEmptyArrayOfAddressses_ThenEncodesCorrectly() { - try! encoder.encode([EthereumAddress]()) + XCTAssertNoThrow(try encoder.encode([EthereumAddress]())) let encoded = try! encoder.encoded() XCTAssertEqual(String(hexFromBytes: encoded.web3.bytes), "0xd57498ea00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000") } @@ -62,7 +109,7 @@ class ABIFunctionEncoderTests: XCTestCase { "0x8c2dc702371d73febc50c6e6ced100bf9dbcb029", "0x007eedb5044ed5512ed7b9f8b42fe3113452491e"].map(EthereumAddress.init) - try! encoder.encode(addresses) + XCTAssertNoThrow(try encoder.encode(addresses)) let encoded = try! encoder.encoded() XCTAssertEqual(String(hexFromBytes: encoded.web3.bytes), "0xd57498ea0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000500000000000000000000000026fc876db425b44bf6c377a7beef65e9ebad0ec300000000000000000000000025a01a05c188dacbcf1d61af55d4a5b4021f7eed000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0000000000000000000000008c2dc702371d73febc50c6e6ced100bf9dbcb029000000000000000000000000007eedb5044ed5512ed7b9f8b42fe3113452491e") } @@ -81,7 +128,7 @@ class ABIFunctionEncoderTests: XCTestCase { func testGivenArrayOfBigUInt_ThenEncodesCorrectly() { let values = [BigUInt(1),BigUInt(2),BigUInt(3)] - try! encoder.encode(values) + XCTAssertNoThrow(try encoder.encode(values)) let encoded = try! encoder.encoded() XCTAssertEqual(String(hexFromBytes: encoded.web3.bytes), "0xca16068400000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003") } diff --git a/web3swift/src/Contract/Statically Typed/ABIFunctionEncoder.swift b/web3swift/src/Contract/Statically Typed/ABIFunctionEncoder.swift index bd8b4f84..9b744509 100644 --- a/web3swift/src/Contract/Statically Typed/ABIFunctionEncoder.swift +++ b/web3swift/src/Contract/Statically Typed/ABIFunctionEncoder.swift @@ -44,7 +44,20 @@ public class ABIFunctionEncoder { encodedValues.append(encoded) switch (staticSize, rawType) { case (let size?, .DynamicBytes): + guard size <= 32 else { + throw ABIError.invalidType + } types.append(.FixedBytes(size)) + case (let size?, .FixedUInt): + guard size <= 256 else { + throw ABIError.invalidType + } + types.append(.FixedUInt(size)) + case (let size?, .FixedInt): + guard size <= 256 else { + throw ABIError.invalidType + } + types.append(.FixedInt(size)) default: types.append(rawType) } From d8b6d1fdddc70e066852fe310f0a25a8d86af6b8 Mon Sep 17 00:00:00 2001 From: Miguel Angel Quinones Date: Tue, 2 Feb 2021 18:16:45 +0100 Subject: [PATCH 20/20] [UPDATE] Version --- web3.swift.podspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web3.swift.podspec b/web3.swift.podspec index ec38c2a0..af84edef 100644 --- a/web3.swift.podspec +++ b/web3.swift.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'web3.swift' - s.version = '0.5.0' + s.version = '0.666666.0' s.license = 'MIT' s.summary = 'Ethereum API for Swift' s.homepage = 'https://github.com/argentlabs/web3.swift'