diff --git a/Package.swift b/Package.swift index 0fa64b65..9639b587 100644 --- a/Package.swift +++ b/Package.swift @@ -4,8 +4,8 @@ import PackageDescription let package = Package( name: "web3.swift", platforms: [ - .iOS(SupportedPlatform.IOSVersion.v11), - .macOS(SupportedPlatform.MacOSVersion.v10_12) + .iOS(SupportedPlatform.IOSVersion.v13), + .macOS(SupportedPlatform.MacOSVersion.v11) ], products: [ .library(name: "web3.swift", targets: ["web3"]), diff --git a/web3sTests/Client/EthereumClientTests.swift b/web3sTests/Client/EthereumClientTests.swift index b2506986..1186c80c 100644 --- a/web3sTests/Client/EthereumClientTests.swift +++ b/web3sTests/Client/EthereumClientTests.swift @@ -334,7 +334,7 @@ extension EthereumClientTests { XCTAssertEqual( error as? EthereumClientError, .executionError( - .init(code: -32000, message: "execution reverted") + .init(code: -32000, message: "execution reverted", data: nil) ) ) } diff --git a/web3sTests/ENS/ENSOffchainTests.swift b/web3sTests/ENS/ENSOffchainTests.swift new file mode 100644 index 00000000..2c3a336a --- /dev/null +++ b/web3sTests/ENS/ENSOffchainTests.swift @@ -0,0 +1,72 @@ +// +// ENSOffchainTests.swift +// web3sTests +// +// Created by Miguel on 17/05/2022. +// Copyright © 2022 Argent Labs Limited. All rights reserved. +// + +import XCTest +@testable import web3 + +class ENSOffchainTests: XCTestCase { + var account: EthereumAccount? + var client: EthereumClient! + + override func setUp() { + super.setUp() + self.client = EthereumClient(url: URL(string: TestConfig.clientUrl)!) + } + + func testDNSEncode() { + XCTAssertEqual( + EthereumNameService.dnsEncode(name: "offchainexample.eth").web3.hexString, + "0x0f6f6666636861696e6578616d706c650365746800" + ) + XCTAssertEqual( + EthereumNameService.dnsEncode(name: "1.offchainexample.eth").web3.hexString, + "0x01310f6f6666636861696e6578616d706c650365746800" + ) + + } + + func testGivenRopstenRegistry_WhenResolvingOffchainENS_ResolvesCorrectly() async { + do { + let nameService = EthereumNameService(client: client!) + let ens = try await nameService.resolve( + ens: "offchainexample.eth", + mode: .allowOffchainLookup + ) + XCTAssertEqual(EthereumAddress("0xd8da6bf26964af9d7eed9e03e53415d37aa96045"), ens) + } catch { + XCTFail("Expected ens but failed \(error).") + } + } + + func testGivenRopstenRegistry_WhenResolvingOffchainENSAndDisabled_ThenFails() async { + do { + let nameService = EthereumNameService(client: client!) + let _ = try await nameService.resolve( + ens: "offchainexample.eth", + mode: .onchain + ) + XCTFail("Expecting error") + } catch let error { + XCTAssertEqual(error as? EthereumNameServiceError, .ensUnknown) + } + } + + func testGivenRopstenRegistry_WhenResolvingNonOffchainENS_ThenResolves() async { + do { + let nameService = EthereumNameService(client: client!) + let ens = try await nameService.resolve( + ens: "resolver.eth", + mode: .allowOffchainLookup + ) + XCTAssertEqual(EthereumAddress("0x42d63ae25990889e35f215bc95884039ba354115"), ens) + } catch { + XCTFail("Expected ens but failed \(error).") + } + } +} + diff --git a/web3sTests/ENS/ENSTests.swift b/web3sTests/ENS/ENSTests.swift index bbea8348..76ba2a36 100644 --- a/web3sTests/ENS/ENSTests.swift +++ b/web3sTests/ENS/ENSTests.swift @@ -24,184 +24,7 @@ class ENSTests: XCTestCase { XCTAssertEqual(nameHash, "0x3e58ef7a2e196baf0b9d36a65cc590ac9edafb3395b7cdeb8f39206049b4534c") } - func testGivenRopstenRegistry_WhenExistingDomainName_ResolvesOwnerAddressCorrectly() { - let expect = expectation(description: "Get the ENS owner") - - do { - let function = ENSContracts.ENSRegistryFunctions.owner(contract: ENSContracts.RopstenAddress, _node: EthereumNameService.nameHash(name: "test").web3.hexData ?? Data()) - - let tx = try function.transaction() - - client?.eth_call(tx, block: .Latest, completion: { (error, dataStr) in - guard let dataStr = dataStr else { - XCTFail() - expect.fulfill() - return - } - let owner = String(dataStr[dataStr.index(dataStr.endIndex, offsetBy: -40)...]) - XCTAssertEqual(owner.web3.noHexPrefix,"09b5bd82f3351a4c8437fc6d7772a9e6cd5d25a1") - expect.fulfill() - }) - - } catch { - XCTFail() - expect.fulfill() - } - - waitForExpectations(timeout: 20) - } - - func testGivenRopstenRegistry_WhenExistingAddress_ThenResolvesCorrectly() { - let expect = expectation(description: "Get the ENS address") - - let nameService = EthereumNameService(client: client!) - nameService.resolve(address: EthereumAddress("0xb0b874220ff95d62a676f58d186c832b3e6529c8"), completion: { (error, ens) in - XCTAssertEqual("julien.argent.test", ens) - expect.fulfill() - }) - - waitForExpectations(timeout: 20) - } - - func testGivenRopstenRegistry_WhenNotExistingAddress_ThenFailsCorrectly() { - let expect = expectation(description: "Get the ENS address") - - let nameService = EthereumNameService(client: client!) - nameService.resolve(address: EthereumAddress("0xb0b874220ff95d62a676f58d186c832b3e6529c9"), completion: { (error, ens) in - XCTAssertNil(ens) - XCTAssertEqual(error, .ensUnknown) - expect.fulfill() - }) - - waitForExpectations(timeout: 20) - } - - func testGivenCustomRegistry_WhenNotExistingAddress_ThenResolvesFailsCorrectly() { - let expect = expectation(description: "Get the ENS address") - - let nameService = EthereumNameService(client: client!, registryAddress: EthereumAddress("0x7D7C04B7A05539a92541105806e0971E45969F85")) - nameService.resolve(address: EthereumAddress("0xb0b874220ff95d62a676f58d186c832b3e6529c9"), completion: { (error, ens) in - XCTAssertNil(ens) - XCTAssertEqual(error, .ensUnknown) - expect.fulfill() - }) - - waitForExpectations(timeout: 20) - } - - func testGivenRopstenRegistry_WhenExistingENS_ThenResolvesAddressCorrectly() { - let expect = expectation(description: "Get the ENS reverse lookup address") - - let nameService = EthereumNameService(client: client!) - nameService.resolve(ens: "julien.argent.test", completion: { (error, ens) in - XCTAssertEqual(EthereumAddress("0xb0b874220ff95d62a676f58d186c832b3e6529c8"), ens) - expect.fulfill() - }) - - waitForExpectations(timeout: 20) - } - - func testGivenRopstenRegistry_WhenInvalidENS_ThenErrorsRequest() { - let expect = expectation(description: "Get the ENS reverse lookup address") - - let nameService = EthereumNameService(client: client!) - nameService.resolve(ens: "**somegarbage)_!!", completion: { (error, ens) in - XCTAssertNil(ens) - XCTAssertEqual(error, .ensUnknown) - expect.fulfill() - }) - - waitForExpectations(timeout: 20) - } - - func testGivenCustomRegistry_WhenInvalidENS_ThenErrorsRequest() { - let expect = expectation(description: "Get the ENS reverse lookup address") - - let nameService = EthereumNameService(client: client!, registryAddress: EthereumAddress("0x7D7C04B7A05539a92541105806e0971E45969F85")) - nameService.resolve(ens: "**somegarbage)_!!", completion: { (error, ens) in - XCTAssertNil(ens) - XCTAssertEqual(error, .ensUnknown) - expect.fulfill() - }) - - 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) - ] - ) - } -} - - -#if compiler(>=5.5) && canImport(_Concurrency) - -@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) -extension ENSTests { - func testGivenRopstenRegistry_WhenExistingDomainName_ResolvesOwnerAddressCorrectly_Async() async { + func testGivenRopstenRegistry_WhenExistingDomainName_ResolvesOwnerAddressCorrectly() async { do { let function = ENSContracts.ENSRegistryFunctions.owner(contract: ENSContracts.RopstenAddress, _node: EthereumNameService.nameHash(name: "test").web3.hexData ?? Data()) @@ -220,67 +43,85 @@ extension ENSTests { } } - func testGivenRopstenRegistry_WhenExistingAddress_ThenResolvesCorrectly_Async() async { + func testGivenRopstenRegistry_WhenExistingAddress_ThenResolvesCorrectly() async { do { let nameService = EthereumNameService(client: client!) - let ens = try await nameService.resolve(address: EthereumAddress("0xb0b874220ff95d62a676f58d186c832b3e6529c8")) + let ens = try await nameService.resolve( + address: EthereumAddress("0xb0b874220ff95d62a676f58d186c832b3e6529c8"), + mode: .onchain + ) XCTAssertEqual("julien.argent.test", ens) } catch { XCTFail("Expected ens but failed \(error).") } } - func testGivenRopstenRegistry_WhenNotExistingAddress_ThenFailsCorrectly_Async() async { + func testGivenRopstenRegistry_WhenNotExistingAddress_ThenFailsCorrectly() async { do { let nameService = EthereumNameService(client: client!) - _ = try await nameService.resolve(address: EthereumAddress("0xb0b874220ff95d62a676f58d186c832b3e6529c9")) + _ = try await nameService.resolve( + address: EthereumAddress("0xb0b874220ff95d62a676f58d186c832b3e6529c9"), + mode: .onchain + ) XCTFail("Expected to throw while awaiting, but succeeded") } catch { XCTAssertEqual(error as? EthereumNameServiceError, .ensUnknown) } } - func testGivenCustomRegistry_WhenNotExistingAddress_ThenResolvesFailsCorrectly_Async() async { + func testGivenCustomRegistry_WhenNotExistingAddress_ThenResolvesFailsCorrectly() async { do { let nameService = EthereumNameService(client: client!, registryAddress: EthereumAddress("0x7D7C04B7A05539a92541105806e0971E45969F85")) - _ = try await nameService.resolve(address: EthereumAddress("0xb0b874220ff95d62a676f58d186c832b3e6529c9")) + _ = try await nameService.resolve( + address: EthereumAddress("0xb0b874220ff95d62a676f58d186c832b3e6529c9"), + mode: .onchain + ) XCTFail("Expected to throw while awaiting, but succeeded") } catch { XCTAssertEqual(error as? EthereumNameServiceError, .ensUnknown) } } - func testGivenRopstenRegistry_WhenExistingENS_ThenResolvesAddressCorrectly_Async() async { + func testGivenRopstenRegistry_WhenExistingENS_ThenResolvesAddressCorrectly() async { do { let nameService = EthereumNameService(client: client!) - let ens = try await nameService.resolve(ens: "julien.argent.test") + let ens = try await nameService.resolve( + ens: "julien.argent.test", + mode: .onchain + ) XCTAssertEqual(EthereumAddress("0xb0b874220ff95d62a676f58d186c832b3e6529c8"), ens) } catch { XCTFail("Expected ens but failed \(error).") } } - func testGivenRopstenRegistry_WhenInvalidENS_ThenErrorsRequest_Async() async { + func testGivenRopstenRegistry_WhenInvalidENS_ThenErrorsRequest() async { do { let nameService = EthereumNameService(client: client!) - _ = try await nameService.resolve(ens: "**somegarbage)_!!") + _ = try await nameService.resolve( + ens: "**somegarbage)_!!", + mode: .onchain + ) XCTFail("Expected to throw while awaiting, but succeeded") } catch { XCTAssertEqual(error as? EthereumNameServiceError, .ensUnknown) } } - func testGivenCustomRegistry_WhenInvalidENS_ThenErrorsRequest_Async() async { + func testGivenCustomRegistry_WhenInvalidENS_ThenErrorsRequest() async { do { let nameService = EthereumNameService(client: client!, registryAddress: EthereumAddress("0x7D7C04B7A05539a92541105806e0971E45969F85")) - _ = try await nameService.resolve(ens: "**somegarbage)_!!") + _ = try await nameService.resolve( + ens: "**somegarbage)_!!", + mode: .onchain + ) XCTFail("Expected to throw while awaiting, but succeeded") } catch { XCTAssertEqual(error as? EthereumNameServiceError, .ensUnknown) } } - func testGivenRopstenRegistry_ThenResolvesMultipleAddressesInOneCall_Async() async { + func testGivenRopstenRegistry_ThenResolvesMultipleAddressesInOneCall() async { let nameService = EthereumNameService(client: client!) var results: [EthereumNameService.ResolveOutput]? @@ -308,7 +149,7 @@ extension ENSTests { ) } - func testGivenRopstenRegistry_ThenResolvesMultipleNamesInOneCall_Async() async { + func testGivenRopstenRegistry_ThenResolvesMultipleNamesInOneCall() async { let nameService = EthereumNameService(client: client!) var results: [EthereumNameService.ResolveOutput]? @@ -337,4 +178,3 @@ extension ENSTests { } } -#endif diff --git a/web3sTests/OffchainLookup/OffchainLookupTests.swift b/web3sTests/OffchainLookup/OffchainLookupTests.swift new file mode 100644 index 00000000..4ba29426 --- /dev/null +++ b/web3sTests/OffchainLookup/OffchainLookupTests.swift @@ -0,0 +1,334 @@ +// +// OffchainLookupTests.swift +// web3sTests +// +// Created by Miguel on 12/05/2022. +// Copyright © 2022 Argent Labs Limited. All rights reserved. +// + +@testable import web3 +import XCTest +import BigInt + +struct DummyOffchainENSResolve: ABIFunction { + static var name: String = "resolver" + var gasPrice: BigUInt? = nil + var gasLimit: BigUInt? = nil + + var contract = EthereumAddress("0x7A876E79a89b9B6dF935F2C1e832E15930FEf3f6") + + var from: EthereumAddress? = nil + var node: Data + + func encode(to encoder: ABIFunctionEncoder) throws { + try encoder.encode(node, staticSize: 32) + } +} + +enum EthersTestContract { + struct TestGet: ABIFunction { + static var name: String = "testGet" + var gasPrice: BigUInt? = nil + var gasLimit: BigUInt? = nil + + var contract = EthereumAddress("0xAe375B05A08204C809b3cA67C680765661998886") + + var from: EthereumAddress? = nil + var data: Data + + func encode(to encoder: ABIFunctionEncoder) throws { + try encoder.encode(data) + } + } + + struct TestGetFail: ABIFunction { + static var name: String = "testGetFail" + var gasPrice: BigUInt? = nil + var gasLimit: BigUInt? = nil + + var contract = EthereumAddress("0xAe375B05A08204C809b3cA67C680765661998886") + + var from: EthereumAddress? = nil + var data: Data + + func encode(to encoder: ABIFunctionEncoder) throws { + try encoder.encode(data) + } + } + + struct TestGetSenderFail: ABIFunction { + static var name: String = "testGetSenderFail" + var gasPrice: BigUInt? = nil + var gasLimit: BigUInt? = nil + + var contract = EthereumAddress("0xAe375B05A08204C809b3cA67C680765661998886") + + var from: EthereumAddress? = nil + var data: Data + + func encode(to encoder: ABIFunctionEncoder) throws { + try encoder.encode(data) + } + } + + struct TestGetMissing: ABIFunction { + static var name: String = "testGetMissing" + var gasPrice: BigUInt? = nil + var gasLimit: BigUInt? = nil + + var contract = EthereumAddress("0xAe375B05A08204C809b3cA67C680765661998886") + + var from: EthereumAddress? = nil + var data: Data + + func encode(to encoder: ABIFunctionEncoder) throws { + try encoder.encode(data) + } + } + + struct TestGetFallback: ABIFunction { + static var name: String = "testGetFallback" + var gasPrice: BigUInt? = nil + var gasLimit: BigUInt? = nil + + var contract = EthereumAddress("0xAe375B05A08204C809b3cA67C680765661998886") + + var from: EthereumAddress? = nil + var data: Data + + func encode(to encoder: ABIFunctionEncoder) throws { + try encoder.encode(data) + } + } + + struct TestPost: ABIFunction { + static var name: String = "testPost" + var gasPrice: BigUInt? = nil + var gasLimit: BigUInt? = nil + + var contract = EthereumAddress("0xAe375B05A08204C809b3cA67C680765661998886") + + var from: EthereumAddress? = nil + var data: Data + + func encode(to encoder: ABIFunctionEncoder) throws { + try encoder.encode(data) + } + } + + struct BytesResponse: ABIResponse { + static var types: [ABIType.Type] { + [Data32.self] + } + + let data: Data + + init?(values: [ABIDecoder.DecodedValue]) throws { + data = try values[0].decoded() + } + } +} + +extension EthereumClientError { + var executionError: JSONRPCErrorDetail? { + switch self { + case .executionError(let detail): + return detail + default: + return nil + } + } +} + +class OffchainLookupTests: XCTestCase { + var client: EthereumClient! + var account: EthereumAccount! + var offchainLookup = OffchainLookup(address: .zero, urls: [], callData: Data(), callbackFunction: Data(), extraData: Data()) + + override func setUp() { + super.setUp() + self.client = EthereumClient(url: URL(string: TestConfig.clientUrl)!) + self.account = try? EthereumAccount(keyStorage: TestEthereumKeyStorage(privateKey: TestConfig.privateKey)) + print("Public address: \(self.account?.address.value ?? "NONE")") + } + + func test_GivenFunctionWithOffchainLookupError_ThenDecodesLookupParamsCorrectly() async throws { + let function = DummyOffchainENSResolve( + node: EthereumNameService.nameHash(name: "hello.argent.xyz").web3.hexData! + ) + + let tx = try! function.transaction() + + do { + let _ = try await client.eth_call(tx) + XCTFail("Expecting error, not return value") + } catch let error { + let error = (error as? EthereumClientError)?.executionError + let decoded = try? error?.decode(error: offchainLookup) + + + XCTAssertEqual(error?.code, JSONRPCErrorCode.contractExecution) + XCTAssertEqual(try? decoded?[0].decoded(), EthereumAddress("0x7a876e79a89b9b6df935f2c1e832e15930fef3f6")) + XCTAssertEqual(try? decoded?[1].decodedArray(), ["https://argent.xyz"]) + XCTAssertEqual(try? decoded?[2].decoded(), Data(hex: "0x35b8485202b076a4e2d0173bf3d7e69546db3eb92389469473b2680c3cdb4427cafbcf2a")!) + XCTAssertEqual(try? decoded?[3].decoded(), Data(hex: "0xd2479f3e")!) + XCTAssertEqual(try? decoded?[4].decoded(), Data()) + } + } + + func test_GivenTestFunction_WhenLookupCorrect_ThenDecodesRetrievesValue() async throws { + let function = EthersTestContract.TestGet(data: "0x1234".web3.hexData!) + + do { + let response = try await function.call( + withClient: client, + responseType: EthersTestContract.BytesResponse.self, + resolution: .offchainAllowed(maxRedirects: 5) + ) + + XCTAssertEqual( + response.data.web3.hexString, + expectedResponse( + sender: function.contract, + data: "0x1234".web3.hexData!) + ) + } catch let error { + XCTFail("Error \(error)") + } + } + + func test_GivenTestFunction_WhenLookupDisabled_ThenFailsWithExecutionError() async throws { + let function = EthersTestContract.TestGet(data: "0x1234".web3.hexData!) + + do { + _ = try await function.call( + withClient: client, + responseType: EthersTestContract.BytesResponse.self, + resolution: .noOffchain(failOnExecutionError: true) + ) + XCTFail("Expecting error") + } catch let error { + let error = (error as? EthereumClientError)?.executionError + XCTAssertEqual(error?.code, 3) + } + } + + func test_GivenTestFunction_WhenGatewayFails_ThenFailsCall() async throws { + let function = EthersTestContract.TestGetFail(data: "0x1234".web3.hexData!) + + do { + _ = try await function.call( + withClient: client, + responseType: EthersTestContract.BytesResponse.self, + resolution: .offchainAllowed(maxRedirects: 5) + ) + XCTFail("Expecting error") + } catch let error { + XCTAssertEqual(error as? EthereumClientError, EthereumClientError.noResultFound) + } + } + + func test_GivenTestFunction_WhenSendersDoNotMatch_ThenFailsCall() async throws { + let function = EthersTestContract.TestGetSenderFail(data: "0x1234".web3.hexData!) + + do { + _ = try await function.call( + withClient: client, + responseType: EthersTestContract.BytesResponse.self, + resolution: .offchainAllowed(maxRedirects: 5) + ) + XCTFail("Expecting error") + } catch _ { + } + } + + func test_GivenTestFunction_WhenGatewayFailsWith4xx_ThenFailsCall() async throws { + let function = EthersTestContract.TestGetMissing(data: "0x1234".web3.hexData!) + + do { + _ = try await function.call( + withClient: client, + responseType: EthersTestContract.BytesResponse.self, + resolution: .offchainAllowed(maxRedirects: 5) + ) + XCTFail("Expecting error") + } catch let error { + XCTAssertEqual(error as? EthereumClientError, EthereumClientError.noResultFound) + } + } + + func test_GivenTestFunction_WhenLookupCorrectWithFallback_ThenDecodesRetrievesValue() async throws { + let function = EthersTestContract.TestGetFallback(data: "0x1234".web3.hexData!) + + do { + let response = try await function.call( + withClient: client, + responseType: EthersTestContract.BytesResponse.self, + resolution: .offchainAllowed(maxRedirects: 5) + ) + + XCTAssertEqual( + response.data.web3.hexString, + expectedResponse( + sender: function.contract, + data: "0x1234".web3.hexData!) + ) + } catch let error { + XCTFail("Error \(error)") + } + } + + func test_GivenTestFunction_WhenLookupCorrectWithFallbackAndNoRedirectsLeft_ThenFails() async throws { + let function = EthersTestContract.TestGetFallback(data: "0x1234".web3.hexData!) + + do { + let _ = try await function.call( + withClient: client, + responseType: EthersTestContract.BytesResponse.self, + resolution: .offchainAllowed(maxRedirects: 0) + ) + + XCTFail("Expecting error") + } catch let error { + XCTAssertEqual(error as? EthereumClientError, EthereumClientError.noResultFound) + } + } + + + func test_GivenTestFunction_WhenLookupCorrectWithPOSTData_ThenDecodesRetrievesValue() async throws { + let function = EthersTestContract.TestPost(data: "0x1234".web3.hexData!) + + do { + let response = try await function.call( + withClient: client, + responseType: EthersTestContract.BytesResponse.self, + resolution: .offchainAllowed(maxRedirects: 5) + ) + + XCTAssertEqual( + response.data.web3.hexString, + expectedResponse( + sender: function.contract, + data: "0x1234".web3.hexData!) + ) + } catch let error { + XCTFail("Error \(error)") + } + } +} + +// Expected hash of result, which is the same verification done in ethers contract +fileprivate func expectedResponse( + sender: EthereumAddress, + data: Data +) -> String { + let senderData = sender.value.web3.hexData! + return Data([ + [UInt8(senderData.count)], + senderData.web3.bytes, + [UInt8(data.count)], + data.web3.bytes + ] + .flatMap { $0 } + ).web3.keccak256.web3.hexString +} diff --git a/web3swift/src/Client/EthereumClient+Call.swift b/web3swift/src/Client/EthereumClient+Call.swift new file mode 100644 index 00000000..a14e2644 --- /dev/null +++ b/web3swift/src/Client/EthereumClient+Call.swift @@ -0,0 +1,235 @@ +// +// EthereumClient+Call.swift +// web3swift +// +// Created by Miguel on 16/05/2022. +// Copyright © 2022 Argent Labs Limited. All rights reserved. +// + +import Foundation +import Combine + +public enum OffchainReadError: Error { + case network + case server(code: Int, message: String?) + case invalidParams + case invalidResponse + case tooManyRedirections +} + +extension EthereumClient { + public func eth_call( + _ transaction: EthereumTransaction, + resolution: CallResolution = .noOffchain(failOnExecutionError: true), + block: EthereumBlock = .Latest, + completion: @escaping ((EthereumClientError?, String?) -> Void) + ) { + guard let transactionData = transaction.data else { + return completion(EthereumClientError.noInputData, nil) + } + + struct CallParams: Encodable { + let from: String? + let to: String + let data: String + let block: String + + enum TransactionCodingKeys: String, CodingKey { + case from + case to + 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) + try nested.encode(data, forKey: .data) + try container.encode(block) + } + } + + 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) + } else if case let .executionError(result) = error as? JSONRPCError { + switch resolution { + case .noOffchain: + completion(.executionError(result.error), nil) + case .offchainAllowed(let redirects): + if let lookup = result.offchainLookup, lookup.address == transaction.to { + self.offchainRead( + lookup: lookup, + maxReads: redirects + ).sink(receiveCompletion: { offchainCompletion in + if case .failure = offchainCompletion { + completion(.noResultFound, nil) + } + }, receiveValue: { data in + self.eth_call( + .init( + to: lookup.address, + data: lookup.encodeCall(withResponse: data) + ), + resolution: .noOffchain(failOnExecutionError: true), + block: block, completion: completion + ) + } + ) + .store(in: &cancellables) + } else { + completion(.executionError(result.error), nil) + } + } + } else { + completion(.unexpectedReturnValue, nil) + } + } + } + + private func offchainRead( + lookup: OffchainLookup, + attempt: Int = 1, + maxReads: Int = 4 + ) -> AnyPublisher { + guard !lookup.urls.isEmpty else { + return Fail(error: OffchainReadError.invalidResponse) + .eraseToAnyPublisher() + } + + let url = lookup.urls[0] + + return self.offchainRead( + sender: lookup.address, + data: lookup.callData, + rawURL: url, + attempt: attempt, + maxAttempts: maxReads + ) + .catch { error -> AnyPublisher in + guard error.isNextURLAllowed else { + return Fail(error: error) + .eraseToAnyPublisher() + } + var lookup = lookup + lookup.urls = Array(lookup.urls.dropFirst()) + return self.offchainRead( + lookup: lookup, + attempt: attempt + 1, + maxReads: maxReads + ).eraseToAnyPublisher() + } + .eraseToAnyPublisher() + } + + private func offchainRead( + sender: EthereumAddress, + data: Data, + rawURL: String, + attempt: Int, + maxAttempts: Int + ) -> AnyPublisher { + guard attempt <= maxAttempts else { + return Fail(error: OffchainReadError.tooManyRedirections) + .eraseToAnyPublisher() + } + + let isGet = rawURL.contains("{data}") + + guard + let url = URL( + string: rawURL + .replacingOccurrences(of: "{sender}", with: sender.value.lowercased()) + .replacingOccurrences(of: "{data}", with: data.web3.hexString.lowercased()) + ) + else { + return Fail(error: OffchainReadError.invalidParams) + .eraseToAnyPublisher() + } + + var request = URLRequest(url: url) + request.httpMethod = isGet ? "GET" : "POST" + if !isGet { + request.addValue("application/json", forHTTPHeaderField: "Content-Type") + request.httpBody = try? JSONEncoder().encode( + OffchainReadJSONBody( + sender: sender, + data: data.web3.hexString + ) + ) + } + request.addValue("application/json", forHTTPHeaderField: "Accept") + + return session.dataTaskPublisher(for: request) + .tryMap { data, response in + guard let res = response as? HTTPURLResponse else { + throw OffchainReadError.network + } + + guard res.statusCode >= 200, res.statusCode < 300 else { + let error = try? JSONDecoder().decode(OffchainReadErrorResponse.self, from: data) + throw OffchainReadError.server( + code: res.statusCode, + message: error?.message ?? nil + ) + } + + guard let decoded = try? JSONDecoder().decode(OffchainReadResponse.self, from: data) else { + throw OffchainReadError.invalidResponse + } + + return decoded.data + } + .mapError { error in + error as? OffchainReadError ?? OffchainReadError.network + } + .eraseToAnyPublisher() + } +} + +fileprivate struct OffchainReadJSONBody: Encodable { + let sender: EthereumAddress + let data: String +} + +fileprivate struct OffchainReadResponse: Decodable { + @DataStr + var data: Data +} + +fileprivate struct OffchainReadErrorResponse: Decodable { + let message: String? + let pathname: String +} + +fileprivate var cancellables = Set() + +fileprivate extension OffchainReadError { + var isNextURLAllowed: Bool { + switch self { + case let .server(code, _): + return code >= 500 // 4xx responses -> Don't continue with next url + case .network, .invalidParams, .invalidResponse: + return true + case .tooManyRedirections: + return false + } + } +} + diff --git a/web3swift/src/Client/EthereumClient.swift b/web3swift/src/Client/EthereumClient.swift index 0f83b5cf..dde97b6a 100644 --- a/web3swift/src/Client/EthereumClient.swift +++ b/web3swift/src/Client/EthereumClient.swift @@ -13,7 +13,12 @@ import BigInt import FoundationNetworking #endif -public protocol EthereumClientProtocol { +public enum CallResolution { + case noOffchain(failOnExecutionError: Bool) + case offchainAllowed(maxRedirects: Int) +} + +public protocol EthereumClientProtocol: AnyObject { init(url: URL, sessionConfig: URLSessionConfiguration) init(url: URL) var network: EthereumNetwork? { get } @@ -28,7 +33,12 @@ public protocol EthereumClientProtocol { func eth_getTransactionCount(address: EthereumAddress, block: EthereumBlock, completion: @escaping((EthereumClientError?, Int?) -> Void)) func eth_getTransaction(byHash txHash: String, completion: @escaping((EthereumClientError?, EthereumTransaction?) -> Void)) func eth_getTransactionReceipt(txHash: String, completion: @escaping((EthereumClientError?, EthereumTransactionReceipt?) -> Void)) - func eth_call(_ transaction: EthereumTransaction, block: EthereumBlock, completion: @escaping((EthereumClientError?, String?) -> Void)) + func eth_call( + _ transaction: EthereumTransaction, + resolution: CallResolution, + block: EthereumBlock, + completion: @escaping((EthereumClientError?, String?) -> Void) + ) func eth_getLogs(addresses: [EthereumAddress]?, topics: [String?]?, fromBlock: EthereumBlock, toBlock: EthereumBlock, completion: @escaping((EthereumClientError?, [EthereumLog]?) -> Void)) func eth_getLogs(addresses: [EthereumAddress]?, orTopics: [[String]?]?, fromBlock: EthereumBlock, toBlock: EthereumBlock, completion: @escaping((EthereumClientError?, [EthereumLog]?) -> Void)) func eth_getBlockByNumber(_ block: EthereumBlock, completion: @escaping((EthereumClientError?, EthereumBlockInfo?) -> Void)) @@ -65,7 +75,11 @@ public protocol EthereumClientProtocol { func eth_getTransactionReceipt(txHash: String) async throws -> EthereumTransactionReceipt @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) - func eth_call(_ transaction: EthereumTransaction, block: EthereumBlock) async throws -> String + func eth_call( + _ transaction: EthereumTransaction, + resolution: CallResolution, + block: EthereumBlock + ) async throws -> String @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) func eth_getLogs(addresses: [EthereumAddress]?, topics: [String?]?, fromBlock: EthereumBlock, toBlock: EthereumBlock) async throws -> [EthereumLog] @@ -347,47 +361,6 @@ public class EthereumClient: EthereumClientProtocol { } } - public func eth_call(_ transaction: EthereumTransaction, block: EthereumBlock = .Latest, completion: @escaping ((EthereumClientError?, String?) -> Void)) { - guard let transactionData = transaction.data else { - return completion(EthereumClientError.noInputData, nil) - } - - struct CallParams: Encodable { - let from: String? - let to: String - let data: String - let block: String - - enum TransactionCodingKeys: String, CodingKey { - case from - case to - 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) - try nested.encode(data, forKey: .data) - try container.encode(block) - } - } - - 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) - } else if case let .executionError(result) = error as? JSONRPCError { - completion(.executionError(result.error), nil) - } else { - completion(.unexpectedReturnValue, nil) - } - } - } - public func eth_getLogs(addresses: [EthereumAddress]?, 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) } @@ -586,9 +559,16 @@ extension EthereumClient { } } - public func eth_call(_ transaction: EthereumTransaction, block: EthereumBlock = .Latest) async throws -> String { + public func eth_call( + _ transaction: EthereumTransaction, + resolution: CallResolution = .noOffchain(failOnExecutionError: true), + block: EthereumBlock = .Latest) async throws -> String { return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in - eth_call(transaction, block: block) { error, txHash in + eth_call( + transaction, + resolution: resolution, + block: block + ) { error, txHash in if let error = error { continuation.resume(throwing: error) } else if let txHash = txHash { diff --git a/web3swift/src/Client/JSONRPC.swift b/web3swift/src/Client/JSONRPC.swift index 72eb298f..e11b762d 100644 --- a/web3swift/src/Client/JSONRPC.swift +++ b/web3swift/src/Client/JSONRPC.swift @@ -28,13 +28,16 @@ public struct JSONRPCResult: Decodable { public struct JSONRPCErrorDetail: Decodable, Equatable, CustomStringConvertible { public var code: Int public var message: String + public var data: String? public init( code: Int, - message: String + message: String, + data: String? ) { self.code = code self.message = message + self.data = data } public var description: String { @@ -104,7 +107,6 @@ public class EthereumRPC { let resultObjects = result.map{ return $0.result } return completion(nil, resultObjects) } else if let errorResult = try? JSONDecoder().decode(JSONRPCErrorResult.self, from: data) { - print("Ethereum response error: \(errorResult.error)") return completion(JSONRPCError.executionError(errorResult), nil) } else if let response = response as? HTTPURLResponse, response.statusCode < 200 || response.statusCode > 299 { return completion(JSONRPCError.requestRejected(data), nil) diff --git a/web3swift/src/Contract/Statically Typed/ABIFunction.swift b/web3swift/src/Contract/Statically Typed/ABIFunction.swift index 3d1979b5..3f33b114 100644 --- a/web3swift/src/Contract/Statically Typed/ABIFunction.swift +++ b/web3swift/src/Contract/Statically Typed/ABIFunction.swift @@ -9,13 +9,11 @@ import Foundation import BigInt -public protocol ABIFunction { - static var name: String { get } +public protocol ABIFunction: ABIFunctionEncodable { var gasPrice: BigUInt? { get } var gasLimit: BigUInt? { get } var contract: EthereumAddress { get } var from: EthereumAddress? { get } - func encode(to encoder: ABIFunctionEncoder) throws } public protocol ABIResponse: ABITupleDecodable {} diff --git a/web3swift/src/Contract/Statically Typed/ABIFunctionEncodable.swift b/web3swift/src/Contract/Statically Typed/ABIFunctionEncodable.swift new file mode 100644 index 00000000..5304d81c --- /dev/null +++ b/web3swift/src/Contract/Statically Typed/ABIFunctionEncodable.swift @@ -0,0 +1,42 @@ +// +// ABIFunctionEncodable.swift +// web3swift +// +// Created by Miguel on 12/05/2022. +// Copyright © 2022 Argent Labs Limited. All rights reserved. +// + +import Foundation + +public protocol ABIFunctionEncodable { + static var name: String { get } + func encode(to encoder: ABIFunctionEncoder) throws +} + +extension ABIFunctionEncodable { + public func decode( + _ data: Data, + expectedTypes: [ABIType.Type], + filteringEmptyEntries filterEmptyEntries: Bool = true + ) throws -> [ABIDecoder.DecodedValue] { + let encoder = ABIFunctionEncoder(Self.name) + try encode(to: encoder) + let rawTypes = encoder.types + let methodId = String(hexFromBytes: try ABIFunctionEncoder.methodId(name: Self.name, types: rawTypes)) + var raw = data.web3.hexString + + guard raw.hasPrefix(methodId) else { + throw ABIError.invalidSignature + } + raw = raw.replacingOccurrences(of: methodId, with: "") + let decoded = try ABIDecoder.decodeData(raw, types: expectedTypes) + let empty = decoded.flatMap { $0.entry.filter(\.isEmpty) } + guard + empty.count == 0 || !filterEmptyEntries, + decoded.count == expectedTypes.count else { + throw ABIError.invalidSignature + } + + return decoded + } +} diff --git a/web3swift/src/Contract/Statically Typed/ABIFunctionEncoder.swift b/web3swift/src/Contract/Statically Typed/ABIFunctionEncoder.swift index 9b744509..5aa0a90b 100644 --- a/web3swift/src/Contract/Statically Typed/ABIFunctionEncoder.swift +++ b/web3swift/src/Contract/Statically Typed/ABIFunctionEncoder.swift @@ -9,30 +9,6 @@ import Foundation import BigInt -extension ABIFunction { - public func decode(_ data: Data, expectedTypes: [ABIType.Type]) throws -> [ABIDecoder.DecodedValue] { - let encoder = ABIFunctionEncoder(Self.name) - try encode(to: encoder) - let rawTypes = encoder.types - let methodId = String(hexFromBytes: try ABIFunctionEncoder.methodId(name: Self.name, types: rawTypes)) - var raw = data.web3.hexString - - guard raw.hasPrefix(methodId) else { - throw ABIError.invalidSignature - } - raw = raw.replacingOccurrences(of: methodId, with: "") - let decoded = try ABIDecoder.decodeData(raw, types: expectedTypes) - let empty = decoded.flatMap { $0.entry.filter(\.isEmpty) } - guard - empty.count == 0, - decoded.count == expectedTypes.count else { - throw ABIError.invalidSignature - } - - return decoded - } -} - public class ABIFunctionEncoder { private let name: String private (set) var types: [ABIRawType] = [] diff --git a/web3swift/src/Contract/Statically Typed/ABIRevertError.swift b/web3swift/src/Contract/Statically Typed/ABIRevertError.swift new file mode 100644 index 00000000..41efd66b --- /dev/null +++ b/web3swift/src/Contract/Statically Typed/ABIRevertError.swift @@ -0,0 +1,30 @@ +// +// ABIRevertError.swift +// web3swift +// +// Created by Miguel on 12/05/2022. +// Copyright © 2022 Argent Labs Limited. All rights reserved. +// + +import Foundation + +// Technically an ABI error is the same as a function in the way it's encoded +// But use different type to be more explicit, as some extensions +// on ABIFunction don't matter for ABIError (i.e. to generate a transaction) +public protocol ABIRevertError: ABIFunctionEncodable { + var expectedTypes: [ABIType.Type] { get } +} + +extension JSONRPCErrorDetail { + public func decode(error: T) throws -> [ABIDecoder.DecodedValue] { + guard let data = data?.web3.hexData else { + throw ABIError.invalidType + } + + return try error.decode( + data, + expectedTypes: error.expectedTypes, + filteringEmptyEntries: false + ) + } +} diff --git a/web3swift/src/Contract/Statically Typed/EthereumClient+Static.swift b/web3swift/src/Contract/Statically Typed/EthereumClient+Static.swift index 9b9f50f6..185d445a 100644 --- a/web3swift/src/Contract/Statically Typed/EthereumClient+Static.swift +++ b/web3swift/src/Contract/Statically Typed/EthereumClient+Static.swift @@ -30,7 +30,7 @@ public extension ABIFunction { withClient client: EthereumClientProtocol, responseType: T.Type, block: EthereumBlock = .Latest, - failOnExecutionError: Bool = true, + resolution: CallResolution = .noOffchain(failOnExecutionError: true), completion: @escaping((EthereumClientError?, T?) -> Void) ) { @@ -38,7 +38,11 @@ public extension ABIFunction { return completion(EthereumClientError.encodeIssue, nil) } - client.eth_call(tx, block: block) { (error, res) in + client.eth_call( + tx, + resolution: resolution, + block: block + ) { (error, res) in let parseOrFail: (String) -> Void = { data in guard let response = (try? T(data: data)) else { return completion(EthereumClientError.decodeIssue, nil) @@ -49,7 +53,7 @@ public extension ABIFunction { switch (error, res) { case (.executionError, _): - if failOnExecutionError { + if resolution.failOnExecutionError { return completion(error, nil) } else { return parseOrFail("0x") @@ -65,6 +69,26 @@ public extension ABIFunction { } } +extension CallResolution { + var failOnExecutionError: Bool { + switch self { + case .noOffchain(let fail): + return fail + case .offchainAllowed: + return true + } + } + + var allowsOffchain: Bool { + switch self { + case .noOffchain: + return false + case .offchainAllowed: + return true + } + } +} + #if compiler(>=5.5) && canImport(_Concurrency) @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) @@ -85,14 +109,14 @@ public extension ABIFunction { withClient client: EthereumClientProtocol, responseType: T.Type, block: EthereumBlock = .Latest, - failOnExecutionError: Bool = true + resolution: CallResolution = .noOffchain(failOnExecutionError: true) ) async throws -> T { return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in call( withClient: client, responseType: responseType, block: block, - failOnExecutionError: failOnExecutionError + resolution: resolution ) { error, response in if let error = error { continuation.resume(throwing: error) @@ -116,7 +140,7 @@ public struct EventFilter { } } -public extension EthereumClient { +public extension EthereumClientProtocol { typealias EventsCompletion = (EthereumClientError?, [ABIEvent], [EthereumLog]) -> Void func getEvents(addresses: [EthereumAddress]?, orTopics: [[String]?]?, @@ -168,7 +192,7 @@ public extension EthereumClient { } } - private func handleLogs(_ error: EthereumClientError?, + func handleLogs(_ error: EthereumClientError?, _ logs: [EthereumLog]?, _ matches: [EventFilter], _ completion: EventsCompletion) { diff --git a/web3swift/src/ENS/ENSContracts.swift b/web3swift/src/ENS/ENSContracts.swift index c0c021f3..11d19de3 100644 --- a/web3swift/src/ENS/ENSContracts.swift +++ b/web3swift/src/ENS/ENSContracts.swift @@ -9,7 +9,7 @@ import Foundation import BigInt -public typealias ENSRegistryResolverParameter = ENSContracts.ENSRegistryFunctions.resolver.Parameter +public typealias ENSRegistryResolverParameter = ENSContracts.ResolveParameter public enum ENSContracts { static let RopstenAddress = EthereumAddress("0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e") @@ -25,6 +25,53 @@ public enum ENSContracts { return nil } } + + public enum ResolveParameter { + 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 dnsEncoded: Data { + switch self { + case .address(let address): + return ENSContracts.dnsEncode( + name: address.value.web3.noHexPrefix + ".addr.reverse" + ) + case .name(let name): + return ENSContracts.dnsEncode(name: name) + } + } + + 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 enum ENSResolverFunctions { public struct addr: ABIFunction { @@ -36,17 +83,33 @@ public enum ENSContracts { public let _node: Data - public init(contract: EthereumAddress, - from: EthereumAddress? = nil, - gasPrice: BigUInt? = nil, - gasLimit: BigUInt? = nil, - _node: Data) { + public init( + contract: EthereumAddress, + from: EthereumAddress? = nil, + gasPrice: BigUInt? = nil, + gasLimit: BigUInt? = nil, + _node: Data + ) { self.contract = contract self.from = from self.gasPrice = gasPrice self.gasLimit = gasLimit self._node = _node } + + public init( + contract: EthereumAddress, + from: EthereumAddress? = nil, + gasPrice: BigUInt? = nil, + gasLimit: BigUInt? = nil, + parameter: ENSRegistryResolverParameter + ) { + self.contract = contract + self.from = from + self.gasPrice = gasPrice + self.gasLimit = gasLimit + self._node = parameter.nameHash + } public func encode(to encoder: ABIFunctionEncoder) throws { try encoder.encode(_node, staticSize: 32) @@ -62,61 +125,112 @@ public enum ENSContracts { public let _node: Data - init(contract: EthereumAddress, + init( + contract: EthereumAddress, from: EthereumAddress? = nil, gasPrice: BigUInt? = nil, gasLimit: BigUInt? = nil, - _node: Data) { + _node: Data + ) { self.contract = contract self.from = from self.gasPrice = gasPrice self.gasLimit = gasLimit self._node = _node } + + public init( + contract: EthereumAddress, + from: EthereumAddress? = nil, + gasPrice: BigUInt? = nil, + gasLimit: BigUInt? = nil, + parameter: ENSRegistryResolverParameter + ) { + self.contract = contract + self.from = from + self.gasPrice = gasPrice + self.gasLimit = gasLimit + self._node = parameter.nameHash + } public func encode(to encoder: ABIFunctionEncoder) throws { try encoder.encode(_node, staticSize: 32) } } } - - 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() - } + public enum ENSOffchainResolverFunctions { + public static var interfaceId: Data { + return "0x9061b923".web3.hexData! + } - var name: String? { - switch self { - case .name(let ens): - return ens - case .address: - return nil - } - } + public struct resolve: ABIFunction { + public static var name: String = "resolve" + public let gasPrice: BigUInt? + public let gasLimit: BigUInt? + public var contract: EthereumAddress + public let from: EthereumAddress? + + public let name: Data + public let data: Data + + public init( + contract: EthereumAddress, + from: EthereumAddress? = nil, + gasPrice: BigUInt? = nil, + gasLimit: BigUInt? = nil, + name: Data, + data: Data + ) { + self.contract = contract + self.from = from + self.gasPrice = gasPrice + self.gasLimit = gasLimit + self.name = name + self.data = data + } - var address: EthereumAddress? { - switch self { - case .address(let address): - return address - case .name: - return nil - } + public init( + contract: EthereumAddress, + from: EthereumAddress? = nil, + gasPrice: BigUInt? = nil, + gasLimit: BigUInt? = nil, + parameter: ENSRegistryResolverParameter + ) { + self.contract = contract + self.from = from + self.gasPrice = gasPrice + self.gasLimit = gasLimit + self.name = parameter.dnsEncoded + switch parameter { + case .address: + self.data = try! ENSResolverFunctions.name( + contract: contract, + from: from, + gasPrice: gasPrice, + gasLimit: gasLimit, + parameter: parameter + ).transaction().data! + case .name: + self.data = try! ENSResolverFunctions.addr( + contract: contract, + from: from, + gasPrice: gasPrice, + gasLimit: gasLimit, + parameter: parameter + ).transaction().data! } } + public func encode(to encoder: ABIFunctionEncoder) throws { + try encoder.encode(name) + try encoder.encode(data) + } + } + } + + public enum ENSRegistryFunctions { + public struct resolver: ABIFunction { public static let name = "resolver" public let gasPrice: BigUInt? public let gasLimit: BigUInt? @@ -125,11 +239,13 @@ public enum ENSContracts { let _node: Data - init(contract: EthereumAddress, - from: EthereumAddress? = nil, - gasPrice: BigUInt? = nil, - gasLimit: BigUInt? = nil, - _node: Data) { + init( + contract: EthereumAddress, + from: EthereumAddress? = nil, + gasPrice: BigUInt? = nil, + gasLimit: BigUInt? = nil, + _node: Data + ) { self.contract = contract self.from = from self.gasPrice = gasPrice @@ -137,11 +253,13 @@ public enum ENSContracts { self._node = _node } - public init(contract: EthereumAddress, - from: EthereumAddress? = nil, - gasPrice: BigUInt? = nil, - gasLimit: BigUInt? = nil, - parameter: Parameter) { + public init( + contract: EthereumAddress, + from: EthereumAddress? = nil, + gasPrice: BigUInt? = nil, + gasLimit: BigUInt? = nil, + parameter: ResolveParameter + ) { self.init( contract: contract, from: from, @@ -165,17 +283,33 @@ public enum ENSContracts { let _node: Data - init(contract: EthereumAddress, - from: EthereumAddress? = nil, - gasPrice: BigUInt? = nil, - gasLimit: BigUInt? = nil, - _node: Data) { + init( + contract: EthereumAddress, + from: EthereumAddress? = nil, + gasPrice: BigUInt? = nil, + gasLimit: BigUInt? = nil, + _node: Data + ) { self.contract = contract self.from = from self.gasPrice = gasPrice self.gasLimit = gasLimit self._node = _node } + + public init( + contract: EthereumAddress, + from: EthereumAddress? = nil, + gasPrice: BigUInt? = nil, + gasLimit: BigUInt? = nil, + parameter: ENSRegistryResolverParameter + ) { + self.contract = contract + self.from = from + self.gasPrice = gasPrice + self.gasLimit = gasLimit + self._node = parameter.nameHash + } public func encode(to encoder: ABIFunctionEncoder) throws { try encoder.encode(_node, staticSize: 32) @@ -183,6 +317,44 @@ public enum ENSContracts { } } + public struct AddressResponse: ABIResponse, MulticallDecodableResponse { + public static var types: [ABIType.Type] = [ EthereumAddress.self ] + public let value: EthereumAddress + + public init?(values: [ABIDecoder.DecodedValue]) throws { + self.value = try values[0].decoded() + } + } + + public struct StringResponse: ABIResponse, MulticallDecodableResponse { + public static var types: [ABIType.Type] = [ String.self ] + public let value: String + + public init?(values: [ABIDecoder.DecodedValue]) throws { + self.value = try values[0].decoded() + } + } + + public struct AddressAsDataResponse: ABIResponse, MulticallDecodableResponse { + public static var types: [ABIType.Type] = [ Data.self ] + public let value: EthereumAddress + + public init?(values: [ABIDecoder.DecodedValue]) throws { + let data: Data = try values[0].decoded() + self.value = try ABIDecoder.decodeData(data.web3.hexString, types: [EthereumAddress.self])[0].decoded() + } + } + + public struct StringAsDataResponse: ABIResponse, MulticallDecodableResponse { + public static var types: [ABIType.Type] = [ Data.self ] + public let value: String + + public init?(values: [ABIDecoder.DecodedValue]) throws { + let data: Data = try values[0].decoded() + self.value = try ABIDecoder.decodeData(data.web3.hexString, types: [String.self])[0].decoded() + } + } + static func nameHash(name: String) -> String { var node = Data.init(count: 32) let labels = name.components(separatedBy: ".") @@ -192,4 +364,20 @@ public enum ENSContracts { } return node.web3.hexString } + + static func dnsEncode(name: String) -> Data { + let encoded = name.split(separator: ".") + .compactMap { part -> [UInt8]? in + guard part.count < 63 else { // Max byte size + return nil + } + guard var utf8 = "_\(part)".data(using: .utf8)?.web3.bytes else { + return nil + } + utf8[0] = UInt8(utf8.count - 1) + return utf8 + } + .flatMap { $0 } + return Data(encoded + [0x00]) + } } diff --git a/web3swift/src/ENS/ENSMultiResolver.swift b/web3swift/src/ENS/ENSMultiResolver.swift index 01b05987..0f616182 100644 --- a/web3swift/src/ENS/ENSMultiResolver.swift +++ b/web3swift/src/ENS/ENSMultiResolver.swift @@ -111,7 +111,6 @@ extension EthereumNameService { 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): @@ -155,7 +154,6 @@ extension EthereumNameService { 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): diff --git a/web3swift/src/ENS/ENSResolver.swift b/web3swift/src/ENS/ENSResolver.swift new file mode 100644 index 00000000..ea3576b9 --- /dev/null +++ b/web3swift/src/ENS/ENSResolver.swift @@ -0,0 +1,110 @@ +// +// ENSResolver.swift +// web3swift +// +// Created by Miguel on 17/05/2022. +// Copyright © 2022 Argent Labs Limited. All rights reserved. +// + +import Foundation + +class ENSResolver { + + let address: EthereumAddress + let callResolution: CallResolution + private (set) var supportsWildCard: Bool? + + private let client: EthereumClientProtocol + + init( + address: EthereumAddress, + client: EthereumClientProtocol, + callResolution: CallResolution, + supportsWildCard: Bool? = nil + ) { + self.address = address + self.callResolution = callResolution + self.client = client + self.supportsWildCard = supportsWildCard + } + + func resolve( + name: String + ) async throws -> EthereumAddress { + let wildcardResolution: Bool + if let supportsWildCard = self.supportsWildCard { + wildcardResolution = supportsWildCard + } else { + wildcardResolution = try await supportsWildcard() + } + self.supportsWildCard = wildcardResolution + + if wildcardResolution && callResolution.allowsOffchain { + let response = try await ENSContracts.ENSOffchainResolverFunctions.resolve( + contract: address, + parameter: .name(name) + ).call( + withClient: client, + responseType: ENSContracts.AddressAsDataResponse.self, + resolution: callResolution + ) + return response.value + } else { + let response = try await ENSContracts.ENSResolverFunctions.addr( + contract: address, + parameter: .name(name) + ).call( + withClient: client, + responseType: ENSContracts.AddressResponse.self, + resolution: callResolution + ) + return response.value + } + } + + func resolve( + address: EthereumAddress + ) async throws -> String { + let wildcardResolution: Bool + if let supportsWildCard = self.supportsWildCard { + wildcardResolution = supportsWildCard + } else { + wildcardResolution = try await supportsWildcard() + } + self.supportsWildCard = wildcardResolution + + if wildcardResolution && callResolution.allowsOffchain { + let response = try await ENSContracts.ENSOffchainResolverFunctions.resolve( + contract: self.address, + parameter: .address(address) + ).call( + withClient: client, + responseType: ENSContracts.StringAsDataResponse.self, + resolution: callResolution + ) + return response.value + } else { + let response = try await ENSContracts.ENSResolverFunctions.name( + contract: self.address, + parameter: .address(address) + ).call( + withClient: client, + responseType: ENSContracts.StringResponse.self, + resolution: callResolution + ) + + if response.value.isEmpty { + throw EthereumNameServiceError.ensUnknown + } + + return response.value + } + } + + private func supportsWildcard() async throws -> Bool { + try await ERC165(client: client).supportsInterface( + contract: address, + id: ENSContracts.ENSOffchainResolverFunctions.interfaceId + ) + } +} diff --git a/web3swift/src/ENS/EthereumNameService.swift b/web3swift/src/ENS/EthereumNameService.swift index 096496b4..b72645ff 100644 --- a/web3swift/src/ENS/EthereumNameService.swift +++ b/web3swift/src/ENS/EthereumNameService.swift @@ -7,19 +7,34 @@ // import Foundation +import BigInt -protocol EthereumNameServiceProtocol { - init(client: EthereumClientProtocol, registryAddress: EthereumAddress?) - func resolve(address: EthereumAddress, completion: @escaping((EthereumNameServiceError?, String?) -> Void)) -> Void - func resolve(ens: String, completion: @escaping((EthereumNameServiceError?, EthereumAddress?) -> Void)) -> Void - -#if compiler(>=5.5) && canImport(_Concurrency) - @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) - func resolve(address: EthereumAddress) async throws -> String +public enum ResolutionMode { + case onchain + case allowOffchainLookup +} - @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) - func resolve(ens: String) async throws -> EthereumAddress -#endif +protocol EthereumNameServiceProtocol { + func resolve( + address: EthereumAddress, + mode: ResolutionMode, + completion: @escaping((EthereumNameServiceError?, String?) -> Void) + ) -> Void + func resolve( + ens: String, + mode: ResolutionMode, + completion: @escaping((EthereumNameServiceError?, EthereumAddress?) -> Void) + ) -> Void + + func resolve( + address: EthereumAddress, + mode: ResolutionMode + ) async throws -> String + + func resolve( + ens: String, + mode: ResolutionMode + ) async throws -> EthereumAddress } public enum EthereumNameServiceError: Error, Equatable { @@ -28,143 +43,137 @@ public enum EthereumNameServiceError: Error, Equatable { case ensUnknown case invalidInput case decodeIssue + case tooManyRedirections } -// This is an example of interacting via a JSON Definition contract API public class EthereumNameService: EthereumNameServiceProtocol { let client: EthereumClientProtocol let registryAddress: EthereumAddress? + let maximumRedirections: Int - required public init(client: EthereumClientProtocol, registryAddress: EthereumAddress? = nil) { + required public init( + client: EthereumClientProtocol, + registryAddress: EthereumAddress? = nil, + maximumRedirections: Int = 5 + ) { self.client = client self.registryAddress = registryAddress + self.maximumRedirections = maximumRedirections } - public func resolve(address: EthereumAddress, completion: @escaping ((EthereumNameServiceError?, String?) -> Void)) { + public func resolve( + address: EthereumAddress, + mode: ResolutionMode, + completion: @escaping ((EthereumNameServiceError?, String?) -> Void) + ) { guard let network = client.network, let registryAddress = self.registryAddress ?? ENSContracts.registryAddress(for: network) else { return completion(EthereumNameServiceError.noNetwork, nil) } - let ensReverse = address.value.web3.noHexPrefix + ".addr.reverse" - let nameHash = Self.nameHash(name: ensReverse) - - let function = ENSContracts.ENSRegistryFunctions.resolver(contract: registryAddress, - _node: nameHash.web3.hexData ?? Data()) - guard let registryTransaction = try? function.transaction() else { - completion(EthereumNameServiceError.invalidInput, nil) - return - } - - client.eth_call(registryTransaction, block: .Latest, completion: { (error, resolverData) in + let function = ENSContracts.ENSRegistryFunctions.resolver( + contract: registryAddress, + parameter: .address(address) + ) + + function.call( + withClient: client, + responseType: ENSContracts.AddressResponse.self, + block: .Latest, + resolution: .noOffchain(failOnExecutionError: true) + ) { (error, response) in if case .executionError = error { return completion(.ensUnknown, nil) } - guard let resolverData = resolverData else { + guard let resolverAddress = response?.value else { return completion(EthereumNameServiceError.noResolver, nil) } - guard resolverData != "0x" else { - return completion(EthereumNameServiceError.ensUnknown, nil) - } - - let idx = resolverData.index(resolverData.endIndex, offsetBy: -40) - let resolverAddress = EthereumAddress(String(resolverData[idx...]).web3.withHexPrefix) - - let function = ENSContracts.ENSResolverFunctions.name(contract: resolverAddress, - _node: nameHash.web3.hexData ?? Data()) - guard let addressTransaction = try? function.transaction() else { - completion(EthereumNameServiceError.invalidInput, nil) - return - } - - self.client.eth_call(addressTransaction, block: .Latest, completion: { (error, data) in - guard let data = data, data != "0x" else { - return completion(EthereumNameServiceError.ensUnknown, nil) - } - if let ensHex: String = try? (try? ABIDecoder.decodeData(data, types: [String.self]))?.first?.decoded() { - completion(nil, ensHex) - } else { - completion(EthereumNameServiceError.decodeIssue, nil) + Task { + let resolver = ENSResolver( + address: resolverAddress, + client: self.client, + callResolution: mode.callResolution(maxRedirects: self.maximumRedirections) + ) + + do { + let name = try await resolver.resolve(address: address) + completion(nil, name) + } catch let error { + completion(error as? EthereumNameServiceError ?? .ensUnknown, nil) } - - }) - }) + } + } } - public func resolve(ens: String, completion: @escaping ((EthereumNameServiceError?, EthereumAddress?) -> Void)) { - + public func resolve( + ens: String, + mode: ResolutionMode, + completion: @escaping ((EthereumNameServiceError?, EthereumAddress?) -> Void) + ) { guard let network = client.network, let registryAddress = self.registryAddress ?? ENSContracts.registryAddress(for: network) else { return completion(EthereumNameServiceError.noNetwork, nil) } - let nameHash = Self.nameHash(name: ens) - let function = ENSContracts.ENSRegistryFunctions.resolver(contract: registryAddress, - _node: nameHash.web3.hexData ?? Data()) - - guard let registryTransaction = try? function.transaction() else { - completion(EthereumNameServiceError.invalidInput, nil) - return - } - - client.eth_call(registryTransaction, block: .Latest, completion: { (error, resolverData) in + let function = ENSContracts.ENSRegistryFunctions.resolver( + contract: registryAddress, + parameter: .name(ens) + ) + + function.call( + withClient: client, + responseType: ENSContracts.AddressResponse.self, + block: .Latest, + resolution: .noOffchain(failOnExecutionError: true) + ) { (error, response) in if case .executionError = error { return completion(.ensUnknown, nil) } - - guard let resolverData = resolverData else { - return completion(EthereumNameServiceError.noResolver, nil) - } - - guard resolverData != "0x" else { - return completion(EthereumNameServiceError.ensUnknown, nil) - } - let idx = resolverData.index(resolverData.endIndex, offsetBy: -40) - let resolverAddress = EthereumAddress(String(resolverData[idx...]).web3.withHexPrefix) - - let function = ENSContracts.ENSResolverFunctions.addr(contract: resolverAddress, _node: nameHash.web3.hexData ?? Data()) - guard let addressTransaction = try? function.transaction() else { - completion(EthereumNameServiceError.invalidInput, nil) - return + guard let resolverAddress = response?.value else { + return completion(EthereumNameServiceError.noResolver, nil) } - self.client.eth_call(addressTransaction, block: .Latest, completion: { (error, data) in - guard let data = data, data != "0x" else { - return completion(EthereumNameServiceError.ensUnknown, nil) - } - - if let ensAddress: EthereumAddress = try? (try? ABIDecoder.decodeData(data, types: [EthereumAddress.self]))?.first?.decoded() { - completion(nil, ensAddress) - } else { - completion(EthereumNameServiceError.decodeIssue, nil) + Task { + let resolver = ENSResolver( + address: resolverAddress, + client: self.client, + callResolution: mode.callResolution(maxRedirects: self.maximumRedirections) + ) + do { + let address = try await resolver.resolve(name: ens) + completion(nil, address) + } catch let error { + completion(error as? EthereumNameServiceError ?? .ensUnknown, nil) } - }) - }) + } + } } 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 + ENSContracts.nameHash(name: name) } + static func dnsEncode( + name: String + ) -> Data { + ENSContracts.dnsEncode(name: name) + } } -#if compiler(>=5.5) && canImport(_Concurrency) - -@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) extension EthereumNameService { - public func resolve(address: EthereumAddress) async throws -> String { + public func resolve( + address: EthereumAddress, + mode: ResolutionMode + ) async throws -> String { return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in - resolve(address: address) { error, ensHex in + resolve( + address: address, + mode: mode + ) { error, ensHex in if let error = error { continuation.resume(throwing: error) } else if let ensHex = ensHex { @@ -174,9 +183,15 @@ extension EthereumNameService { } } - public func resolve(ens: String) async throws -> EthereumAddress { + public func resolve( + ens: String, + mode: ResolutionMode + ) async throws -> EthereumAddress { return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in - resolve(ens: ens) { error, address in + resolve( + ens: ens, + mode: mode + ) { error, address in if let error = error { continuation.resume(throwing: error) } else if let address = address { @@ -187,4 +202,14 @@ extension EthereumNameService { } } -#endif + +fileprivate extension ResolutionMode { + func callResolution(maxRedirects: Int) -> CallResolution { + switch self { + case .allowOffchainLookup: + return .offchainAllowed(maxRedirects: maxRedirects) + case .onchain: + return .noOffchain(failOnExecutionError: true) + } + } +} diff --git a/web3swift/src/ERC165/ERC165.swift b/web3swift/src/ERC165/ERC165.swift index aa78dc2c..d7a53e18 100644 --- a/web3swift/src/ERC165/ERC165.swift +++ b/web3swift/src/ERC165/ERC165.swift @@ -10,8 +10,8 @@ import Foundation import BigInt public class ERC165 { - public let client: EthereumClient - required public init(client: EthereumClient) { + public let client: EthereumClientProtocol + required public init(client: EthereumClientProtocol) { self.client = client } diff --git a/web3swift/src/ERC20/ERC20.swift b/web3swift/src/ERC20/ERC20.swift index 777b2b95..911ee771 100644 --- a/web3swift/src/ERC20/ERC20.swift +++ b/web3swift/src/ERC20/ERC20.swift @@ -69,7 +69,7 @@ public class ERC20: ERC20Protocol { function.call( withClient: self.client, responseType: ERC20Responses.decimalsResponse.self, - failOnExecutionError: false + resolution: .noOffchain(failOnExecutionError: false) ) { (error, decimalsResponse) in return completion(error, decimalsResponse?.value) } diff --git a/web3swift/src/ERC721/ERC721.swift b/web3swift/src/ERC721/ERC721.swift index 27f15d09..028d6110 100644 --- a/web3swift/src/ERC721/ERC721.swift +++ b/web3swift/src/ERC721/ERC721.swift @@ -195,12 +195,12 @@ public class ERC721Metadata: ERC721 { public let session: URLSession - public init(client: EthereumClient, metadataSession: URLSession) { + public init(client: EthereumClientProtocol, metadataSession: URLSession) { self.session = metadataSession super.init(client: client) } - required init(client: EthereumClient) { + required init(client: EthereumClientProtocol) { fatalError("init(client:) has not been implemented") } @@ -350,7 +350,7 @@ public class ERC721Enumerable: ERC721 { function.call( withClient: client, responseType: ERC721EnumerableResponses.numberResponse.self, - failOnExecutionError: false + resolution: .noOffchain(failOnExecutionError: false) ) { error, response in return completion(error, response?.value) } diff --git a/web3swift/src/OffchainLookup/OffchainLookup.swift b/web3swift/src/OffchainLookup/OffchainLookup.swift new file mode 100644 index 00000000..d05f59ab --- /dev/null +++ b/web3swift/src/OffchainLookup/OffchainLookup.swift @@ -0,0 +1,113 @@ +// +// OffchainLookup.swift +// web3swift +// +// Created by Miguel on 12/05/2022. +// Copyright © 2022 Argent Labs Limited. All rights reserved. +// + +import Foundation + +public struct OffchainLookup: ABIRevertError { + public var expectedTypes: [ABIType.Type] { + [ + EthereumAddress.self, + ABIArray.self, + Data.self, + Data4.self, + Data.self + ] + } + + public static var name: String = "OffchainLookup" + + public var address: EthereumAddress + public var urls: [String] + public var callData: Data + public var callbackFunction: Data + public var extraData: Data + + public func encode(to encoder: ABIFunctionEncoder) throws { + try encoder.encode(address) + try encoder.encode(urls) + try encoder.encode(callData) + try encoder.encode(callbackFunction, staticSize: 4) + try encoder.encode(extraData) + } + + public init( + address: EthereumAddress, + urls: [String], + callData: Data, + callbackFunction: Data, + extraData: Data + ) { + self.address = address + self.urls = urls + self.callData = callData + self.callbackFunction = callbackFunction + self.extraData = extraData + } + + init?( + decoded: [ABIDecoder.DecodedValue] + ) { + guard let sender = decoded.sender, + let urls = decoded.urls, + let callData = decoded.callData, + let callbackFunction = decoded.callbackFunction, + let extraData = decoded.extraData + else { + return nil + } + + self.init( + address: sender, + urls: urls, + callData: callData, + callbackFunction: callbackFunction, + extraData: extraData + ) + } +} + +extension JSONRPCErrorResult { + var offchainLookup: OffchainLookup? { + return (try? error.decode(error: expected)).flatMap(OffchainLookup.init(decoded:)) + } +} + +extension OffchainLookup { + func encodeCall(withResponse data: Data) -> Data { + let encodedCall = try? [data, extraData].map { + try ABIEncoder.encode($0) + }.encoded(isDynamic: false) + return callbackFunction + Data(encodedCall ?? []) + } +} + +fileprivate let expected = OffchainLookup( + address: .zero, + urls: [], + callData: Data(), + callbackFunction: Data(), + extraData: Data() +) + +fileprivate extension Array where Element == ABIDecoder.DecodedValue { + var sender: EthereumAddress? { + try? self[0].decoded() + } + var urls: [String]? { + try? self[1].decodedArray() + } + var callData: Data? { + try? self[2].decoded() + } + var callbackFunction: Data? { + try? self[3].decoded() + } + var extraData: Data? { + try? self[4].decoded() + } +} diff --git a/web3swift/src/Utils/PropertyWrappers.swift b/web3swift/src/Utils/PropertyWrappers.swift new file mode 100644 index 00000000..4c11ca14 --- /dev/null +++ b/web3swift/src/Utils/PropertyWrappers.swift @@ -0,0 +1,41 @@ +// +// PropertyWrappers.swift +// web3swift +// +// Created by Miguel on 16/05/2022. +// Copyright © 2022 Argent Labs Limited. All rights reserved. +// + + +import Foundation + +@propertyWrapper +struct DataStr: Codable, Equatable, Hashable { + private var value: Data + + public init(wrappedValue: Data) { + self.value = wrappedValue + } + + public init(_ value: Data) { + self.init(wrappedValue: value) + } + + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let str = try container.decode(String.self) + guard let data = Data(hex: str) else { throw DecodingError.dataCorruptedError(in: container, debugDescription: "Data not in '0x' format") } + self.value = data + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(value.web3.hexString) + } + + public var wrappedValue: Data { + get { value } + set { self.value = newValue } + } + +}