diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 8ac1c0a7..3d17d5bb 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -10,10 +10,10 @@ jobs: runs-on: macos-latest steps: - uses: actions/checkout@v3 - - name: Lint - run: ./scripts/runSwiftFormat.sh -l + # - name: Lint + # run: ./scripts/runSwiftFormat.sh -l macos: - needs: lint + # needs: lint runs-on: macos-latest env: TESTS_PRIVATEKEY: ${{ secrets.TESTS_PRIVATEKEY }} @@ -26,10 +26,10 @@ jobs: - name: Tests run: swift test -v linux: - needs: lint + # needs: lint runs-on: ubuntu-latest container: - image: swift:5.5-bionic + image: swift:5.7-bionic env: TESTS_PRIVATEKEY: ${{ secrets.TESTS_PRIVATEKEY }} steps: diff --git a/Gemfile.lock b/Gemfile.lock index 792721a4..e8d0f433 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -3,7 +3,7 @@ GEM specs: CFPropertyList (3.0.5) rexml - activesupport (6.1.7.2) + activesupport (6.1.7.6) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) @@ -54,7 +54,7 @@ GEM netrc (~> 0.11) cocoapods-try (1.2.0) colored2 (3.1.2) - concurrent-ruby (1.2.0) + concurrent-ruby (1.2.2) escape (0.0.4) ethon (0.15.0) ffi (>= 1.15.0) @@ -63,10 +63,10 @@ GEM fuzzy_match (2.0.4) gh_inspector (1.1.3) httpclient (2.8.3) - i18n (1.12.0) + i18n (1.14.1) concurrent-ruby (~> 1.0) json (2.6.1) - minitest (5.17.0) + minitest (5.19.0) molinillo (0.8.0) nanaimo (0.3.0) nap (1.1.0) @@ -85,7 +85,7 @@ GEM colored2 (~> 3.1) nanaimo (~> 0.3.0) rexml (~> 3.2.4) - zeitwerk (2.6.6) + zeitwerk (2.6.11) PLATFORMS ruby diff --git a/Package.resolved b/Package.resolved index 08ffadef..8f70f53f 100644 --- a/Package.resolved +++ b/Package.resolved @@ -6,8 +6,8 @@ "repositoryURL": "https://github.com/attaswift/BigInt", "state": { "branch": null, - "revision": "889a1ecacd73ccc189c5cb29288048f186c44ed9", - "version": "5.2.1" + "revision": "0ed110f7555c34ff468e72e1686e59721f2b0da6", + "version": "5.3.0" } }, { @@ -24,8 +24,8 @@ "repositoryURL": "https://github.com/GigaBitcoin/secp256k1.swift.git", "state": { "branch": null, - "revision": "39dd39248e769ea88253a0ce300399b402a64529", - "version": "0.9.2" + "revision": "1a14e189def5eaa92f839afdd2faad8e43b61a6e", + "version": "0.12.2" } }, { diff --git a/Package.swift b/Package.swift index 9e4f7ccb..32f9386c 100644 --- a/Package.swift +++ b/Package.swift @@ -9,10 +9,11 @@ let package = Package( .watchOS(.v7) ], products: [ - .library(name: "web3.swift", targets: ["web3"]) + .library(name: "web3.swift", targets: ["web3"]), + .library(name: "web3-zksync.swift", targets: ["web3-zksync"]) ], dependencies: [ - .package(name: "BigInt", url: "https://github.com/attaswift/BigInt", from: "5.0.0"), + .package(name: "BigInt", url: "https://github.com/attaswift/BigInt", from: "5.3.0"), .package(name: "GenericJSON", url: "https://github.com/iwill/generic-json-swift", .upToNextMajor(from: "2.0.0")), .package(url: "https://github.com/GigaBitcoin/secp256k1.swift.git", .upToNextMajor(from: "0.6.0")), .package(url: "https://github.com/vapor/websocket-kit.git", from: "2.0.0"), @@ -32,7 +33,16 @@ let package = Package( .product(name: "WebSocketKit", package: "websocket-kit"), .product(name: "Logging", package: "swift-log") ], - path: "web3swift/src" + path: "web3swift/src", + exclude: ["ZKSync"] + ), + .target( + name: "web3-zksync", + dependencies: + [ + .target(name: "web3") + ], + path: "web3swift/src/ZKSync" ), .target( name: "keccaktiny", @@ -53,7 +63,7 @@ let package = Package( ), .testTarget( name: "web3swiftTests", - dependencies: ["web3"], + dependencies: ["web3", "web3-zksync"], path: "web3sTests", resources: [ .copy("Resources/rlptests.json"), diff --git a/README.md b/README.md index 05f5c662..b8f6b1d3 100644 --- a/README.md +++ b/README.md @@ -162,6 +162,14 @@ We support querying ERC721 token data via the `ERC721` struct. Including: - Retrieve `Transfer` events - Decode standard JSON for NFT metadata. Please be aware some smart contracts are not 100% compliant with standard. + +### ZKSync Era + +We also include additional helpers to interact with [ZKSync Era](https://zksync.io/), by importing `web3_zksync`. + +Take a look at [ZKSyncTransaction](https://github.com/argentlabs/web3.swift/blob/develop/web3swift/src/ZKSync/ZKSyncTransaction.swift) or use directly +[ZKSyncClient](https://github.com/argentlabs/web3.swift/blob/develop/web3swift/src/ZKSync/ZKSyncProvider.swift) which has similar API as the `EthereumClient` + ### Running Tests Some of the tests require a private key, which is not stored in the repository. You can ignore these while testing locally, as CI will use the encrypted secret key from Github. @@ -183,7 +191,7 @@ Package dependencies: - [Vapor Websocket](https://github.com/vapor/websocket-kit.git) - [Apple Swift-log](https://github.com/apple/swift-log.git) -Also for Linux build, we can't se Apple crypto APIs, so we embedded a small subset of CryptoSwift (instead of importing the whole library). Credit to [Marcin Krzyżanowski](https://github.com/krzyzanowskim/CryptoSwift) +Also for Linux build, we can't use Apple crypto APIs, so we embedded a small subset of CryptoSwift (instead of importing the whole library). Credit to [Marcin Krzyżanowski](https://github.com/krzyzanowskim/CryptoSwift) ## Contributors diff --git a/scripts/swiftformat.yml b/scripts/swiftformat.yml index 928456f5..d7512f18 100644 --- a/scripts/swiftformat.yml +++ b/scripts/swiftformat.yml @@ -75,5 +75,5 @@ --wraptypealiases preserve --xcodeindentation disabled --yodaswap always ---disable enumNamespaces,extensionAccessControl,fileHeader,genericExtensions,modifierOrder,numberFormatting,opaqueGenericParameters,preferKeyPath,redundantBackticks,redundantExtensionACL,redundantFileprivate,redundantPattern,redundantRawValues,redundantSelf,sortDeclarations,spaceAroundGenerics,strongOutlets,trailingClosures,trailingCommas,unusedArguments,wrap,wrapMultilineStatementBraces,wrapSingleLineComments,yodaConditions +--disable enumNamespaces,extensionAccessControl,fileHeader,genericExtensions,modifierOrder,numberFormatting,opaqueGenericParameters,preferKeyPath,redundantBackticks,redundantExtensionACL,redundantFileprivate,redundantPattern,redundantRawValues,redundantSelf,sortDeclarations,spaceAroundGenerics,strongOutlets,trailingClosures,trailingCommas,unusedArguments,wrap,wrapMultilineStatementBraces,wrapSingleLineComments,yodaConditions,consecutiveSpaces --enable blankLineAfterImports,blankLinesBetweenImports,isEmpty,wrapConditionalBodies diff --git a/web3.swift.podspec b/web3.swift.podspec index 3a27d3f7..d56bb5ad 100644 --- a/web3.swift.podspec +++ b/web3.swift.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'web3.swift' - s.version = '1.5.0' + s.version = '1.6.1' s.license = 'MIT' s.summary = 'Ethereum API for Swift' s.homepage = 'https://github.com/argentlabs/web3.swift' diff --git a/web3sTests/Account/EthereumAccount+SignTransactionTests.swift b/web3sTests/Account/EthereumAccount+SignTransactionTests.swift index bf8823c9..79a8acfe 100644 --- a/web3sTests/Account/EthereumAccount+SignTransactionTests.swift +++ b/web3sTests/Account/EthereumAccount+SignTransactionTests.swift @@ -49,18 +49,11 @@ class EthereumAccount_SignTransactionTests: XCTestCase { let gasLimit = BigUInt(hex: "0x5208")! let to = EthereumAddress("0x3535353535353535353535353535353535353535") let value = BigUInt(hex: "0x0")! - let v = Int(hex: "0x25")! - let r = Data(hex: "0x044852b2a670ade5407e78fb2863c51de9fcb96542a07186fe3aeda6bb8a116d")! - let s = Data(hex: "0x044852b2a670ade5407e78fb2863c51de9fcb96542a07186fe3aeda6bb8a116d")! - - var chainId = v - if chainId >= 37 { - chainId = (chainId - 35) / 2 - } - - let tx = EthereumTransaction(from: nil, to: to, value: value, data: nil, nonce: nonce, gasPrice: gasPrice, gasLimit: gasLimit, chainId: chainId) - let signed = SignedTransaction(transaction: tx, v: v, r: r, s: s) - + let signature = "0x044852b2a670ade5407e78fb2863c51de9fcb96542a07186fe3aeda6bb8a116d044852b2a670ade5407e78fb2863c51de9fcb96542a07186fe3aeda6bb8a116d25".web3.hexData! + + let tx = EthereumTransaction(from: nil, to: to, value: value, data: nil, nonce: nonce, gasPrice: gasPrice, gasLimit: gasLimit, chainId: 37) + let signed = SignedTransaction(transaction: tx, signature: signature) + let raw = signed.raw!.web3.hexString let hash = signed.hash!.web3.hexString diff --git a/web3sTests/Account/EthereumAccountTests.swift b/web3sTests/Account/EthereumAccountTests.swift index 699af655..6b0f0f8f 100644 --- a/web3sTests/Account/EthereumAccountTests.swift +++ b/web3sTests/Account/EthereumAccountTests.swift @@ -17,13 +17,13 @@ class EthereumAccountTests: XCTestCase { func testLoadAccountAndAddress() { let account = try! EthereumAccount(keyStorage: TestEthereumKeyStorage(privateKey: TestConfig.privateKey)) - XCTAssertEqual(account.address.value.lowercased(), TestConfig.publicKey.lowercased(), "Failed to load private key. Ensure key is valid in TestConfig.swift") + XCTAssertEqual(account.address, EthereumAddress(TestConfig.publicKey), "Failed to load private key. Ensure key is valid in TestConfig.swift") } func testLoadAccountAndAddressMultiple() { let storage = TestEthereumMultipleKeyStorage(privateKey: TestConfig.privateKey) let account = try! EthereumAccount(addressString: TestConfig.publicKey, keyStorage: storage) - XCTAssertEqual(account.address.value.lowercased(), TestConfig.publicKey.lowercased(), "Failed to load private key. Ensure key is valid in TestConfig.swift") + XCTAssertEqual(account.address, EthereumAddress(TestConfig.publicKey), "Failed to load private key. Ensure key is valid in TestConfig.swift") } func testCreateAccount() { @@ -42,14 +42,14 @@ class EthereumAccountTests: XCTestCase { let storage = EthereumKeyLocalStorage() let account = try! EthereumAccount.importAccount(replacing: storage, privateKey: "0x2639f727ded571d584643895d43d02a7a190f8249748a2c32200cfc12dde7173", keystorePassword: "PASSWORD") - XCTAssertEqual(account.address.value, "0x675f5810feb3b09528e5cd175061b4eb8de69075") + XCTAssertEqual(account.address, "0x675f5810feb3b09528e5cd175061b4eb8de69075") } func testImportAccountMultiple() { let storage = EthereumKeyLocalStorage() let account = try! EthereumAccount.importAccount(addingTo: storage, privateKey: "0x2639f727ded571d584643895d43d02a7a190f8249748a2c32200cfc12dde7173", keystorePassword: "PASSWORD") - XCTAssertEqual(account.address.value, "0x675f5810feb3b09528e5cd175061b4eb8de69075") + XCTAssertEqual(account.address, "0x675f5810feb3b09528e5cd175061b4eb8de69075") } func testFetchAccounts() { diff --git a/web3sTests/Address/EthereumAddressTests.swift b/web3sTests/Address/EthereumAddressTests.swift new file mode 100644 index 00000000..4e3cbea7 --- /dev/null +++ b/web3sTests/Address/EthereumAddressTests.swift @@ -0,0 +1,49 @@ +// +// web3.swift +// Copyright © 2023 Argent Labs Limited. All rights reserved. +// + +import XCTest +@testable import web3 + +class EthereumAddressTests: XCTestCase { + private var values: Set! + private let addr1 = EthereumAddress("0x162142f0508F557C02bEB7C473682D7C91Bcef41") + private let addr1Padded = EthereumAddress("0x0162142f0508F557C02bEB7C473682D7C91Bcef41") + private let addr2 = EthereumAddress("0x162142f0508F557C02bEB7C473682D7C91Bcef42") + + func testGivenAddress_WhenComparingWithSameAddressString_AddressIsEqual() { + XCTAssertEqual(addr1, addr1) + } + + func testGivenAddress_WhenHashingWithSameAddressString_AddressIsEqual() { + values = [addr1] + XCTAssertTrue(values.contains(addr1)) + } + + func testGivenAddress_WhenComparingWithDifferentAddressString_AddressNotEqual() { + XCTAssertNotEqual(addr1, addr2) + } + + func testGivenAddress_WhenComparingWith0PaddedAddress_AddressIsEqual() { + XCTAssertEqual(addr1, addr1Padded) + } + + func testGivenAddress_WhenHashingWith0PaddedAddress_AddressIsEqual() { + values = [addr1] + XCTAssertTrue(values.contains(addr1Padded)) + } + + func testGiven0PaddedAddress_WhenHashingWithNotPaddedAddress_AddressIsEqual() { + values = [addr1Padded] + XCTAssertTrue(values.contains(addr1)) + } + + func testGivenAddress_WhenHashing_EqualToSameAddressHash() { + XCTAssertEqual(addr1.hashValue, addr1.hashValue) + } + + func testGivenAddress_WhenHashing_EqualToPaddedAddressHash() { + XCTAssertEqual(addr1.hashValue, addr1Padded.hashValue) + } +} diff --git a/web3sTests/Client/EthereumClientTests.swift b/web3sTests/Client/EthereumClientTests.swift index d4c1b6b8..964576a5 100644 --- a/web3sTests/Client/EthereumClientTests.swift +++ b/web3sTests/Client/EthereumClientTests.swift @@ -35,9 +35,8 @@ class EthereumClientTests: XCTestCase { override func setUp() { super.setUp() - client = EthereumHttpClient(url: URL(string: TestConfig.clientUrl)!) + client = EthereumHttpClient(url: URL(string: TestConfig.clientUrl)!, network: TestConfig.network) account = try? EthereumAccount(keyStorage: TestEthereumKeyStorage(privateKey: TestConfig.privateKey)) - print("Public address: \(account?.address.value ?? "NONE")") } func testEthGetTransactionCount() async { @@ -116,7 +115,8 @@ class EthereumClientTests: XCTestCase { func testEthSendRawTransaction() async { do { - let tx = EthereumTransaction(from: nil, to: "0x3c1bd6b420448cf16a389c8b0115ccb3660bb854", value: BigUInt(1600000), data: nil, nonce: 2, gasPrice: BigUInt(4000000), gasLimit: BigUInt(500000), chainId: EthereumNetwork.goerli.intValue) + let gasPrice = try await client?.eth_gasPrice() + let tx = EthereumTransaction(from: nil, to: "0x3c1bd6b420448cf16a389c8b0115ccb3660bb854", value: BigUInt(1), data: nil, nonce: 2, gasPrice: gasPrice ?? BigUInt(9000000), gasLimit: BigUInt(30000), chainId: EthereumNetwork.goerli.intValue) let txHash = try await client?.eth_sendRawTransaction(tx, withAccount: account!) XCTAssertNotNil(txHash, "No tx hash, ensure key is valid in TestConfig.swift") @@ -208,8 +208,8 @@ class EthereumClientTests: XCTestCase { func testGivenMinedTransactionHash_ThenGetsTransactionByHash() async { do { let transaction = try await client?.eth_getTransaction(byHash: "0x706bbe6f2593235942b8e76c2f37af3824d47a64caf65f7ae5e0c5ee1e886132") - XCTAssertEqual(transaction?.from?.value, "0x64d0ea4fc60f27e74f1a70aa6f39d403bbe56793") - XCTAssertEqual(transaction?.to.value, "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984") + XCTAssertEqual(transaction?.from, "0x64d0ea4fc60f27e74f1a70aa6f39d403bbe56793") + XCTAssertEqual(transaction?.to, "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984") XCTAssertEqual(transaction?.gas, "85773") XCTAssertEqual(transaction?.gasPrice, BigUInt(14300000000)) XCTAssertEqual(transaction?.nonce, 23) @@ -435,14 +435,14 @@ class EthereumWebSocketClientTests: EthereumClientTests { override func setUp() { super.setUp() - client = EthereumWebSocketClient(url: URL(string: TestConfig.wssUrl)!, configuration: TestConfig.webSocketConfig) + client = EthereumWebSocketClient(url: URL(string: TestConfig.wssUrl)!, configuration: TestConfig.webSocketConfig, network: TestConfig.network) } #if os(Linux) // On Linux some tests are fail. Need investigation #else func testWebSocketNoAutomaticOpen() { - self.client = EthereumWebSocketClient(url: URL(string: TestConfig.wssUrl)!, configuration: .init(automaticOpen: false)) + self.client = EthereumWebSocketClient(url: URL(string: TestConfig.wssUrl)!, configuration: .init(automaticOpen: false), network: TestConfig.network) guard let client = client as? EthereumWebSocketClient else { XCTFail("Expected client to be EthereumWebSocketClient") @@ -453,7 +453,7 @@ class EthereumWebSocketClientTests: EthereumClientTests { } func testWebSocketConnect() { - self.client = EthereumWebSocketClient(url: URL(string: TestConfig.wssUrl)!, configuration: .init(automaticOpen: false)) + self.client = EthereumWebSocketClient(url: URL(string: TestConfig.wssUrl)!, configuration: .init(automaticOpen: false), network: TestConfig.network) guard let client = client as? EthereumWebSocketClient else { XCTFail("Expected client to be EthereumWebSocketClient") diff --git a/web3sTests/Contract/ABIEventTests.swift b/web3sTests/Contract/ABIEventTests.swift index 826d2972..733c1839 100644 --- a/web3sTests/Contract/ABIEventTests.swift +++ b/web3sTests/Contract/ABIEventTests.swift @@ -12,7 +12,7 @@ class ABIEventTests: XCTestCase { override func setUp() { super.setUp() - client = EthereumHttpClient(url: URL(string: TestConfig.clientUrl)!) + client = EthereumHttpClient(url: URL(string: TestConfig.clientUrl)!, network: TestConfig.network) } func test_givenEventWithData4_ItParsesCorrectly() async { @@ -60,7 +60,7 @@ class ABIEventTests: XCTestCase { class ABIEventWebSocketTests: ABIEventTests { override func setUp() { super.setUp() - client = EthereumWebSocketClient(url: URL(string: TestConfig.wssUrl)!, configuration: TestConfig.webSocketConfig) + client = EthereumWebSocketClient(url: URL(string: TestConfig.wssUrl)!, configuration: TestConfig.webSocketConfig, network: TestConfig.network) } } diff --git a/web3sTests/ENS/ENSOffchainTests.swift b/web3sTests/ENS/ENSOffchainTests.swift index 65493ae6..85527645 100644 --- a/web3sTests/ENS/ENSOffchainTests.swift +++ b/web3sTests/ENS/ENSOffchainTests.swift @@ -12,7 +12,7 @@ class ENSOffchainTests: XCTestCase { override func setUp() { super.setUp() - client = EthereumHttpClient(url: URL(string: TestConfig.clientUrl)!) + client = EthereumHttpClient(url: URL(string: TestConfig.clientUrl)!, network: TestConfig.network) } func testDNSEncode() { @@ -60,7 +60,7 @@ class ENSOffchainTests: XCTestCase { ens: "resolver.eth", mode: .allowOffchainLookup ) - XCTAssertEqual(EthereumAddress("0x19c2d5d0f035563344dbb7be5fd09c8dad62b001"), ens) + XCTAssertEqual(EthereumAddress("0xd7a4f6473f32ac2af804b3686ae8f1932bc35750"), ens) } catch { XCTFail("Expected ens but failed \(error).") } @@ -104,7 +104,7 @@ class ENSOffchainTests: XCTestCase { ens: "resolver.eth", mode: .allowOffchainLookup ) - XCTAssertEqual(EthereumAddress("0x19c2d5d0f035563344dbb7be5fd09c8dad62b001"), ens) + XCTAssertEqual(EthereumAddress("0xd7a4f6473f32ac2af804b3686ae8f1932bc35750"), ens) } catch { XCTFail("Expected ens but failed \(error).") } @@ -139,7 +139,7 @@ class ENSOffchainTests: XCTestCase { ens: "resolver.eth", mode: .allowOffchainLookup ) - XCTAssertEqual(EthereumAddress("0x19c2d5d0f035563344dbb7be5fd09c8dad62b001"), ens) + XCTAssertEqual(EthereumAddress("0xd7a4f6473f32ac2af804b3686ae8f1932bc35750"), ens) } catch { XCTFail("Expected ens but failed \(error).") } @@ -149,6 +149,6 @@ class ENSOffchainTests: XCTestCase { class ENSOffchainWebSocketTests: ENSOffchainTests { override func setUp() { super.setUp() - client = EthereumWebSocketClient(url: URL(string: TestConfig.wssUrl)!, configuration: TestConfig.webSocketConfig) + client = EthereumWebSocketClient(url: URL(string: TestConfig.wssUrl)!, configuration: TestConfig.webSocketConfig, network: TestConfig.network) } } diff --git a/web3sTests/ENS/ENSTests.swift b/web3sTests/ENS/ENSTests.swift index 85acf5f1..2cf6684e 100644 --- a/web3sTests/ENS/ENSTests.swift +++ b/web3sTests/ENS/ENSTests.swift @@ -9,10 +9,12 @@ import XCTest class ENSTests: XCTestCase { var account: EthereumAccount? var client: EthereumClientProtocol! + var mainnetClient: EthereumClientProtocol! override func setUp() { super.setUp() - client = EthereumHttpClient(url: URL(string: TestConfig.clientUrl)!) + client = EthereumHttpClient(url: URL(string: TestConfig.clientUrl)!, network: .goerli) + mainnetClient = EthereumHttpClient(url: URL(string: TestConfig.mainnetUrl)!, network: .mainnet) } func testGivenName_ThenResolvesNameHash() { @@ -69,7 +71,7 @@ class ENSTests: XCTestCase { func testGivenRegistry_WhenAddressHasSubdomain_AndReverseRecordNotSet_ThenDoesNotResolveCorrectly() async { do { let nameService = EthereumNameService(client: client!) - let ens = try await nameService.resolve( + let _ = try await nameService.resolve( address: "0x787411394Ccb38483a6F303FDee075f3EA67D65F", mode: .onchain ) @@ -296,11 +298,72 @@ class ENSTests: XCTestCase { XCTAssertEqual(error as? EthereumNameServiceError, .ensUnknown) } } + + func testGivenMainnetRegistry_WhenResolvingOnChain_ItResolvesName() async { + do { + let nameService = EthereumNameService(client: mainnetClient!) + + let address = try await nameService.resolve( + ens: "michael-brown.eth", + mode: .onchain + ) + + XCTAssertEqual("0x7bcf6af56f0e4e7498b2a76d4ac8b3262ac790bb", address) + } catch { + XCTFail("Error \(error)") + } + } + + func testGivenMainnetRegistry_WhenAllowsOffchain_AndDomainDoesNotHaveOffchain_ItResolvesName() async { + do { + let nameService = EthereumNameService(client: mainnetClient!) + + let address = try await nameService.resolve( + ens: "michael-brown.eth", + mode: .allowOffchainLookup + ) + + XCTAssertEqual("0x7bcf6af56f0e4e7498b2a76d4ac8b3262ac790bb", address) + } catch { + XCTFail("Error \(error)") + } + } + + func testGivenMainnetRegistry_WhenResolvingOnChain_ItResolvesAddress() async { + do { + let nameService = EthereumNameService(client: mainnetClient!) + + let name = try await nameService.resolve( + address: "0x7bcf6af56f0e4e7498b2a76d4ac8b3262ac790bb", + mode: .onchain + ) + + XCTAssertEqual("michael-brown.eth", name) + } catch { + XCTFail("Error \(error)") + } + } + + func testGivenMainnetRegistry_WhenAllowsOffchain_ItResolvesAddress() async { + do { + let nameService = EthereumNameService(client: mainnetClient!) + + let name = try await nameService.resolve( + address: "0x7bcf6af56f0e4e7498b2a76d4ac8b3262ac790bb", + mode: .allowOffchainLookup + ) + + XCTAssertEqual("michael-brown.eth", name) + } catch { + XCTFail("Error \(error)") + } + } } class ENSWebSocketTests: ENSTests { override func setUp() { super.setUp() - client = EthereumWebSocketClient(url: URL(string: TestConfig.wssUrl)!, configuration: TestConfig.webSocketConfig) + client = EthereumWebSocketClient(url: URL(string: TestConfig.wssUrl)!, configuration: TestConfig.webSocketConfig, network: TestConfig.network) + mainnetClient = EthereumWebSocketClient(url: URL(string: TestConfig.wssMainnetUrl)!, configuration: TestConfig.webSocketConfig, network: .mainnet) } } diff --git a/web3sTests/ERC1271/ERC1271Tests.swift b/web3sTests/ERC1271/ERC1271Tests.swift index a9969ca0..b983c84a 100644 --- a/web3sTests/ERC1271/ERC1271Tests.swift +++ b/web3sTests/ERC1271/ERC1271Tests.swift @@ -13,7 +13,7 @@ class ERC1271Tests: XCTestCase { override func setUp() { super.setUp() if self.client == nil { - self.client = EthereumHttpClient(url: URL(string: TestConfig.clientUrl)!) + self.client = EthereumHttpClient(url: URL(string: TestConfig.clientUrl)!, network: TestConfig.network) } self.erc1271 = ERC1271(client: self.client) } @@ -105,7 +105,7 @@ final class ERC1271WebSocketTests: ERC1271Tests { override func setUp() { if self.client == nil { - self.client = EthereumWebSocketClient(url: URL(string: TestConfig.wssUrl)!) + self.client = EthereumWebSocketClient(url: URL(string: TestConfig.wssUrl)!, network: TestConfig.network) } super.setUp() } diff --git a/web3sTests/ERC165/ERC165Tests.swift b/web3sTests/ERC165/ERC165Tests.swift index 410d9940..a4bdd31e 100644 --- a/web3sTests/ERC165/ERC165Tests.swift +++ b/web3sTests/ERC165/ERC165Tests.swift @@ -11,10 +11,11 @@ class ERC165Tests: XCTestCase { var client: EthereumClientProtocol! var erc165: ERC165! let address = EthereumAddress(TestConfig.erc165Contract) + let nonSupportedAddress = EthereumAddress(TestConfig.erc20Contract) override func setUp() { super.setUp() - client = EthereumHttpClient(url: URL(string: TestConfig.clientUrl)!) + client = EthereumHttpClient(url: URL(string: TestConfig.clientUrl)!, network: TestConfig.network) erc165 = ERC165(client: client) } @@ -39,11 +40,20 @@ class ERC165Tests: XCTestCase { XCTFail("Expected supported but failed \(error).") } } + + func test_GivenContractNotImplementingERC165_WhenCalling_ReturnsNotSupported() async { + do { + let supported = try await erc165.supportsInterface(contract: nonSupportedAddress, id: ERC165Functions.interfaceId) + XCTAssertEqual(supported, false) + } catch { + XCTFail("Expected unsupported but failed \(error).") + } + } } class ERC165WebSocketTests: ERC165Tests { override func setUp() { super.setUp() - client = EthereumWebSocketClient(url: URL(string: TestConfig.wssUrl)!, configuration: TestConfig.webSocketConfig) + client = EthereumWebSocketClient(url: URL(string: TestConfig.wssUrl)!, configuration: TestConfig.webSocketConfig, network: TestConfig.network) } } diff --git a/web3sTests/ERC20/ERC20Tests.swift b/web3sTests/ERC20/ERC20Tests.swift index 66f800c9..c745d313 100644 --- a/web3sTests/ERC20/ERC20Tests.swift +++ b/web3sTests/ERC20/ERC20Tests.swift @@ -14,7 +14,7 @@ class ERC20Tests: XCTestCase { override func setUp() { super.setUp() - client = EthereumHttpClient(url: URL(string: TestConfig.clientUrl)!) + client = EthereumHttpClient(url: URL(string: TestConfig.clientUrl)!, network: TestConfig.network) erc20 = ERC20(client: client!) } @@ -114,6 +114,6 @@ class ERC20Tests: XCTestCase { class ERC20WebSocketTests: ERC20Tests { override func setUp() { super.setUp() - client = EthereumWebSocketClient(url: URL(string: TestConfig.wssUrl)!, configuration: TestConfig.webSocketConfig) + client = EthereumWebSocketClient(url: URL(string: TestConfig.wssUrl)!, configuration: TestConfig.webSocketConfig, network: TestConfig.network) } } diff --git a/web3sTests/ERC721/ERC721Tests.swift b/web3sTests/ERC721/ERC721Tests.swift index 3fc157c3..767729bb 100644 --- a/web3sTests/ERC721/ERC721Tests.swift +++ b/web3sTests/ERC721/ERC721Tests.swift @@ -24,7 +24,7 @@ class ERC721Tests: XCTestCase { override func setUp() { super.setUp() - client = EthereumHttpClient(url: URL(string: TestConfig.clientUrl)!) + client = EthereumHttpClient(url: URL(string: TestConfig.clientUrl)!, network: TestConfig.network) erc721 = ERC721(client: client) } @@ -111,7 +111,7 @@ class ERC721MetadataTests: XCTestCase { override func setUp() { super.setUp() - client = EthereumHttpClient(url: URL(string: TestConfig.clientUrl)!) + client = EthereumHttpClient(url: URL(string: TestConfig.clientUrl)!, network: TestConfig.network) erc721 = ERC721Metadata(client: client, metadataSession: URLSession.shared) } @@ -163,7 +163,7 @@ class ERC721EnumerableTests: XCTestCase { override func setUp() { super.setUp() - client = EthereumHttpClient(url: URL(string: TestConfig.clientUrl)!) + client = EthereumHttpClient(url: URL(string: TestConfig.clientUrl)!, network: TestConfig.network) erc721 = ERC721Enumerable(client: client) } @@ -211,20 +211,20 @@ class ERC721EnumerableTests: XCTestCase { class ERC721WebSocketTests: ERC721Tests { override func setUp() { super.setUp() - client = EthereumWebSocketClient(url: URL(string: TestConfig.wssUrl)!, configuration: TestConfig.webSocketConfig) + client = EthereumWebSocketClient(url: URL(string: TestConfig.wssUrl)!, configuration: TestConfig.webSocketConfig, network: TestConfig.network) } } class ERC721MetadataWebSocketTests: ERC721MetadataTests { override func setUp() { super.setUp() - client = EthereumWebSocketClient(url: URL(string: TestConfig.wssUrl)!, configuration: TestConfig.webSocketConfig) + client = EthereumWebSocketClient(url: URL(string: TestConfig.wssUrl)!, configuration: TestConfig.webSocketConfig, network: TestConfig.network) } } class ERC721EnumerableWebSocketTests: ERC721EnumerableTests { override func setUp() { super.setUp() - client = EthereumWebSocketClient(url: URL(string: TestConfig.wssUrl)!, configuration: TestConfig.webSocketConfig) + client = EthereumWebSocketClient(url: URL(string: TestConfig.wssUrl)!, configuration: TestConfig.webSocketConfig, network: TestConfig.network) } } diff --git a/web3sTests/Multicall/MulticallTests.swift b/web3sTests/Multicall/MulticallTests.swift index 342b2774..79287585 100644 --- a/web3sTests/Multicall/MulticallTests.swift +++ b/web3sTests/Multicall/MulticallTests.swift @@ -13,7 +13,7 @@ class MulticallTests: XCTestCase { override func setUp() { super.setUp() - client = EthereumHttpClient(url: URL(string: TestConfig.clientUrl)!) + client = EthereumHttpClient(url: URL(string: TestConfig.clientUrl)!, network: TestConfig.network) multicall = Multicall(client: client!) } @@ -83,6 +83,6 @@ class MulticallTests: XCTestCase { class MulticallWebSocketTests: MulticallTests { override func setUp() { super.setUp() - client = EthereumWebSocketClient(url: URL(string: TestConfig.wssUrl)!, configuration: TestConfig.webSocketConfig) + client = EthereumWebSocketClient(url: URL(string: TestConfig.wssUrl)!, configuration: TestConfig.webSocketConfig, network: TestConfig.network) } } diff --git a/web3sTests/OffchainLookup/OffchainLookupTests.swift b/web3sTests/OffchainLookup/OffchainLookupTests.swift index 8c909b9b..19c8eb23 100644 --- a/web3sTests/OffchainLookup/OffchainLookupTests.swift +++ b/web3sTests/OffchainLookup/OffchainLookupTests.swift @@ -145,9 +145,8 @@ class OffchainLookupTests: XCTestCase { override func setUp() { super.setUp() - client = EthereumHttpClient(url: URL(string: TestConfig.clientUrl)!) + client = EthereumHttpClient(url: URL(string: TestConfig.clientUrl)!, network: TestConfig.network) account = try? EthereumAccount(keyStorage: TestEthereumKeyStorage(privateKey: TestConfig.privateKey)) - print("Public address: \(account?.address.value ?? "NONE")") } func test_GivenFunctionWithOffchainLookupError_ThenDecodesLookupParamsCorrectly() async throws { @@ -318,7 +317,7 @@ private func expectedResponse( sender: EthereumAddress, data: Data ) -> String { - let senderData = sender.value.web3.hexData! + let senderData = sender.asData()! return Data([ [UInt8(senderData.count)], senderData.web3.bytes, @@ -332,6 +331,6 @@ private func expectedResponse( class OffchainLookupWebSocketTests: OffchainLookupTests { override func setUp() { super.setUp() - client = EthereumWebSocketClient(url: URL(string: TestConfig.wssUrl)!, configuration: TestConfig.webSocketConfig) + client = EthereumWebSocketClient(url: URL(string: TestConfig.wssUrl)!, configuration: TestConfig.webSocketConfig, network: TestConfig.network) } } diff --git a/web3sTests/SIWE/SIWETests.swift b/web3sTests/SIWE/SIWETests.swift index 60eea08d..ac4fec92 100644 --- a/web3sTests/SIWE/SIWETests.swift +++ b/web3sTests/SIWE/SIWETests.swift @@ -14,7 +14,7 @@ class SIWETests: XCTestCase { override func setUp() { super.setUp() if self.client == nil { - self.client = EthereumHttpClient(url: URL(string: TestConfig.clientUrl)!) + self.client = EthereumHttpClient(url: URL(string: TestConfig.clientUrl)!, network: TestConfig.network) } self.verifier = SiweVerifier(client: self.client) } @@ -58,7 +58,7 @@ final class SIWEWebSocketTests: SIWETests { override func setUp() { if self.client == nil { - self.client = EthereumWebSocketClient(url: URL(string: TestConfig.wssUrl)!) + self.client = EthereumWebSocketClient(url: URL(string: TestConfig.wssUrl)!, network: TestConfig.network) } super.setUp() } diff --git a/web3sTests/SIWE/SiweVerifierTests.swift b/web3sTests/SIWE/SiweVerifierTests.swift index 5dfb50b7..c265e31b 100644 --- a/web3sTests/SIWE/SiweVerifierTests.swift +++ b/web3sTests/SIWE/SiweVerifierTests.swift @@ -226,7 +226,7 @@ final class SiweVerifierWebSocketTests: SiweVerifierTests { override func setUp() { if self.client == nil { - self.client = EthereumWebSocketClient(url: URL(string: TestConfig.wssUrl)!) + self.client = EthereumWebSocketClient(url: URL(string: TestConfig.wssUrl)!, network: TestConfig.network) } super.setUp() } diff --git a/web3sTests/TestConfig.swift b/web3sTests/TestConfig.swift index b3571891..35bdc1b5 100644 --- a/web3sTests/TestConfig.swift +++ b/web3sTests/TestConfig.swift @@ -9,9 +9,11 @@ import web3 struct TestConfig { // This is the proxy URL for connecting to the Blockchain. For testing we usually use the Goerli network on Infura. Using free tier, so might hit rate limits static let clientUrl = "https://goerli.infura.io/v3/b2f4b3f635d8425c96854c3d28ba6bb0" + static let mainnetUrl = "https://mainnet.infura.io/v3/b2f4b3f635d8425c96854c3d28ba6bb0" // This is the proxy wss URL for connecting to the Blockchain. For testing we usually use the Goerli network on Infura. Using free tier, so might hit rate limits static let wssUrl = "wss://goerli.infura.io/ws/v3/b2f4b3f635d8425c96854c3d28ba6bb0" + static let wssMainnetUrl = "wss://mainnet.infura.io/ws/v3/b2f4b3f635d8425c96854c3d28ba6bb0" // An EOA with some Ether, so that we can test sending transactions (pay for gas). Set by CI // static let privateKey = "SET_YOUR_KEY_HERE" @@ -29,4 +31,19 @@ struct TestConfig { static let erc165Contract = "0xA2618a1c426a1684E00cA85b5C736164AC391d35" static let webSocketConfig = WebSocketConfiguration(maxFrameSize: 1_000_000) + + static let network = EthereumNetwork.goerli + + enum ZKSync { + static let chainId = 280 + static let network = EthereumNetwork.custom("\(280)") + static let clientURL = URL(string: "https://zksync2-testnet.zksync.dev")! + } +} + + +@discardableResult public func with(_ root: Root, _ block: (inout Root) throws -> Void) rethrows -> Root { + var copy = root + try block(©) + return copy } diff --git a/web3sTests/Utils/KeyUtilTests.swift b/web3sTests/Utils/KeyUtilTests.swift index ba9ec81d..8e353de6 100644 --- a/web3sTests/Utils/KeyUtilTests.swift +++ b/web3sTests/Utils/KeyUtilTests.swift @@ -45,7 +45,7 @@ class KeyUtilTests: XCTestCase { let publicKey = try! KeyUtil.generatePublicKey(from: privateKey) let address = KeyUtil.generateAddress(from: publicKey) - XCTAssertEqual(address.value, "0x751e735a83a8142c1b9dc722ef559b898f1d77fa") + XCTAssertEqual(address, "0x751e735a83a8142c1b9dc722ef559b898f1d77fa") } func testRecoverPublicKey() { @@ -54,7 +54,7 @@ class KeyUtilTests: XCTestCase { let address = try! KeyUtil.recoverPublicKey(message: "Hello message!".web3.keccak256, signature: signature) - XCTAssertEqual(address, account.address.value.lowercased()) + XCTAssertEqual(address, account.address.asString().lowercased()) } func testRecoverPublicKeyMultiple() { @@ -64,6 +64,6 @@ class KeyUtilTests: XCTestCase { let address = try! KeyUtil.recoverPublicKey(message: "Hello message!".web3.keccak256, signature: signature) - XCTAssertEqual(address, account.address.value.lowercased()) + XCTAssertEqual(address, account.address.asString().lowercased()) } } diff --git a/web3sTests/ZKSync/EthereumClient+ZKSyncTests.swift b/web3sTests/ZKSync/EthereumClient+ZKSyncTests.swift new file mode 100644 index 00000000..209cdbaf --- /dev/null +++ b/web3sTests/ZKSync/EthereumClient+ZKSyncTests.swift @@ -0,0 +1,61 @@ +// +// web3.swift +// Copyright © 2022 Argent Labs Limited. All rights reserved. +// + +import Foundation +@testable import web3_zksync +@testable import web3 +import XCTest +import BigInt + + +final class EthereumClientZKSyncTests: XCTestCase { + let eoaAccount = try! EthereumAccount(keyStorage: TestEthereumKeyStorage(privateKey: TestConfig.privateKey)) + let client = ZKSyncClient(url: TestConfig.ZKSync.clientURL, network: TestConfig.ZKSync.network) + var eoaEthTransfer = ZKSyncTransaction( + from: .init(TestConfig.publicKey), + to: .init("0x64d0eA4FC60f27E74f1a70Aa6f39D403bBe56793"), + value: 100, + data: Data(), + gasLimit: 300000 + ) + + // TODO: Reintegrate test +// func test_GivenEOAAccount_WhenSendETH_ThenSendsCorrectly() async { +// do { +// let gasPrice = try await client.eth_gasPrice() +// eoaEthTransfer.gasPrice = gasPrice +// let txHash = try await client.eth_sendRawZKSyncTransaction(eoaEthTransfer, withAccount: eoaAccount) +// XCTAssertNotNil(txHash, "No tx hash, ensure key is valid in TestConfig.swift") +// } catch { +// XCTFail("Expected tx but failed \(error).") +// } +// } + + // TODO: Integrate paymaster +// func test_GivenEOAAccount_WhenSendETH_AndFeeIsInUSDC_ThenSendsCorrectly() async { +// do { +// let txHash = try await client.eth_sendRawZKSyncTransaction(with(eoaEthTransfer) { +// $0.feeToken = EthereumAddress("0x54a14D7559BAF2C8e8Fa504E019d32479739018c") +// }, withAccount: eoaAccount) +// XCTAssertNotNil(txHash, "No tx hash, ensure key is valid in TestConfig.swift") +// } catch { +// XCTFail("Expected tx but failed \(error).") +// } +// } + + func test_GivenEOATransaction_gasEstimationCorrect() async { + do { + let estimate = try await client.estimateGas( + with(eoaEthTransfer) { + $0.gasPrice = nil + $0.gasLimit = nil + } + ) + XCTAssertGreaterThan(estimate, 1000) + } catch { + XCTFail("Expected value but failed \(error).") + } + } +} diff --git a/web3sTests/ZKSync/ZKSyncTransactionTests.swift b/web3sTests/ZKSync/ZKSyncTransactionTests.swift new file mode 100644 index 00000000..25c3a643 --- /dev/null +++ b/web3sTests/ZKSync/ZKSyncTransactionTests.swift @@ -0,0 +1,126 @@ +// +// web3.swift +// Copyright © 2022 Argent Labs Limited. All rights reserved. +// + +import XCTest +@testable import web3_zksync +@testable import web3 +import BigInt + +final class ZKSyncTransactionTests: XCTestCase { + let signature = "0x55943b2228183717fd3be583bde0f6ec168247ea8d304eb13b3e7e76ebf6bf2c3c77734e163711c5963ac25a15f95d9ac63b82c2c427fd4eb011c5e3a22f89221b".web3.hexData! + let chainId = TestConfig.ZKSync.chainId + let nonce = 4 + let gasPrice = BigUInt(hex: "0x05f5e100")! + let gasLimit = BigUInt(hex: "0x080a22")! + let from = EthereumAddress(TestConfig.publicKey) + let to = EthereumAddress("0x64d0eA4FC60f27E74f1a70Aa6f39D403bBe56793") + + let eoaAccount = try! EthereumAccount(keyStorage: TestEthereumKeyStorage(privateKey: TestConfig.privateKey)) + + let eoaTransfer = ZKSyncTransaction( + from: .init(TestConfig.publicKey), + to: .init("0x64d0eA4FC60f27E74f1a70Aa6f39D403bBe56793"), + value: BigUInt(hex: "0x5af3107a4000")!, + data: Data(), + chainId: TestConfig.ZKSync.chainId, + nonce: 4, + gasPrice: BigUInt(hex: "0x05f5e100")!, + gasLimit: BigUInt(hex: "0x080a22")! + ) + + func test_GivenETHTransfer_EncodesCorrectly() { + let signed = ZKSyncSignedTransaction( + transaction: eoaTransfer, signature: .init(raw: signature) + ) + XCTAssertEqual(signed.raw?.web3.hexString, "0x71f891048405f5e1008405f5e10083080a229464d0ea4fc60f27e74f1a70aa6f39d403bbe56793865af3107a400080820118808082011894e78e5ecb061fe3dd1672ddda7b5116213b23b99a82c350c0b84155943b2228183717fd3be583bde0f6ec168247ea8d304eb13b3e7e76ebf6bf2c3c77734e163711c5963ac25a15f95d9ac63b82c2c427fd4eb011c5e3a22f89221bc0") + } + + func test_GivenETHTransfer_WhenSigningWithEOAAccount_ThenSignsAndEncodesCorrectly() { + let signed = try? eoaAccount.sign(zkTransaction: eoaTransfer) + + XCTAssertEqual(signed?.raw?.web3.hexString, + "0x71f891048405f5e1008405f5e10083080a229464d0ea4fc60f27e74f1a70aa6f39d403bbe56793865af3107a400080820118808082011894e78e5ecb061fe3dd1672ddda7b5116213b23b99a82c350c0b841c956ba7bfdf54a6d3f3b21c51465ad37df22b6258835b6e162259d6d3eec02ae11f9d17c3aafd47df49bd77e33befed87bbaff44e4c497228bfa8bcc9fa64bc31bc0") + } + + func test_GivenERC20Transfer_EncodesCorrectly() { + let ercTransfer = try! ERC20Functions.transfer( + contract: "0x0BfcE1D53451B4a8175DD94e6e029F7d8a701e9c", + from: from, + to: to, + value: 10000 + ).zkSyncTransaction( + gasPrice: gasPrice, + gasLimit: gasLimit, + chainId: chainId, + nonce: nonce + ) + + + let signed = ZKSyncSignedTransaction( + transaction: ercTransfer, signature: .init(raw: signature) + ) + XCTAssertEqual(signed.raw?.web3.hexString, "0x71f8d0048405f5e1008405f5e10083080a22940bfce1d53451b4a8175dd94e6e029f7d8a701e9c80b844a9059cbb00000000000000000000000064d0ea4fc60f27e74f1a70aa6f39d403bbe567930000000000000000000000000000000000000000000000000000000000002710820118808082011894e78e5ecb061fe3dd1672ddda7b5116213b23b99a82c350c0b84155943b2228183717fd3be583bde0f6ec168247ea8d304eb13b3e7e76ebf6bf2c3c77734e163711c5963ac25a15f95d9ac63b82c2c427fd4eb011c5e3a22f89221bc0") + } + + func test_GivenERC20TransferFrom_EncodesCorrectly() { + let ercTransfer = try! ERC20Functions.transferFrom( + contract: "0x0BfcE1D53451B4a8175DD94e6e029F7d8a701e9c", + from: from, + sender: from, + to: to, + value: 10000 + ).zkSyncTransaction( + gasPrice: gasPrice, + gasLimit: gasLimit, + chainId: chainId, + nonce: nonce + ) + + let signed = ZKSyncSignedTransaction( + transaction: ercTransfer, signature: .init(raw: signature) + ) + XCTAssertEqual(signed.raw?.web3.hexString, "0x71f8f0048405f5e1008405f5e10083080a22940bfce1d53451b4a8175dd94e6e029f7d8a701e9c80b86423b872dd000000000000000000000000e78e5ecb061fe3dd1672ddda7b5116213b23b99a00000000000000000000000064d0ea4fc60f27e74f1a70aa6f39d403bbe567930000000000000000000000000000000000000000000000000000000000002710820118808082011894e78e5ecb061fe3dd1672ddda7b5116213b23b99a82c350c0b84155943b2228183717fd3be583bde0f6ec168247ea8d304eb13b3e7e76ebf6bf2c3c77734e163711c5963ac25a15f95d9ac63b82c2c427fd4eb011c5e3a22f89221bc0") + } + + func test_GivenERC20Approve_EncodesCorrectly() { + let ercTransfer = try! ERC20Functions.approve( + contract: "0x0BfcE1D53451B4a8175DD94e6e029F7d8a701e9c", + from: from, + spender: to, + value: 10000 + ).zkSyncTransaction( + gasPrice: gasPrice, + gasLimit: gasLimit, + chainId: chainId, + nonce: nonce + ) + + + let signed = ZKSyncSignedTransaction( + transaction: ercTransfer, signature: .init(raw: signature) + ) + XCTAssertEqual(signed.raw?.web3.hexString, "0x71f8d0048405f5e1008405f5e10083080a22940bfce1d53451b4a8175dd94e6e029f7d8a701e9c80b844095ea7b300000000000000000000000064d0ea4fc60f27e74f1a70aa6f39d403bbe567930000000000000000000000000000000000000000000000000000000000002710820118808082011894e78e5ecb061fe3dd1672ddda7b5116213b23b99a82c350c0b84155943b2228183717fd3be583bde0f6ec168247ea8d304eb13b3e7e76ebf6bf2c3c77734e163711c5963ac25a15f95d9ac63b82c2c427fd4eb011c5e3a22f89221bc0") + } + + func test_GivenERC721Transfer_EncodesCorrectly() { + let ercTransfer = try! ERC721Functions.transferFrom( + contract: "0x0BfcE1D53451B4a8175DD94e6e029F7d8a701e9c", + from: from, + sender: from, + to: to, + tokenId: 100 + ).zkSyncTransaction( + gasPrice: gasPrice, + gasLimit: gasLimit, + chainId: chainId, + nonce: nonce + ) + + let signed = ZKSyncSignedTransaction( + transaction: ercTransfer, signature: .init(raw: signature) + ) + XCTAssertEqual(signed.raw?.web3.hexString, "0x71f8f0048405f5e1008405f5e10083080a22940bfce1d53451b4a8175dd94e6e029f7d8a701e9c80b86423b872dd000000000000000000000000e78e5ecb061fe3dd1672ddda7b5116213b23b99a00000000000000000000000064d0ea4fc60f27e74f1a70aa6f39d403bbe567930000000000000000000000000000000000000000000000000000000000000064820118808082011894e78e5ecb061fe3dd1672ddda7b5116213b23b99a82c350c0b84155943b2228183717fd3be583bde0f6ec168247ea8d304eb13b3e7e76ebf6bf2c3c77734e163711c5963ac25a15f95d9ac63b82c2c427fd4eb011c5e3a22f89221bc0") + } +} diff --git a/web3swift/src/Account/EthereumAccount+SignTransaction.swift b/web3swift/src/Account/EthereumAccount+SignTransaction.swift index 5c47ec4a..297b56e3 100644 --- a/web3swift/src/Account/EthereumAccount+SignTransaction.swift +++ b/web3swift/src/Account/EthereumAccount+SignTransaction.swift @@ -10,7 +10,7 @@ enum EthereumSignerError: Error { case unknownError } -public extension EthereumAccount { +public extension EthereumAccountProtocol { func signRaw(_ transaction: EthereumTransaction) throws -> Data { let signed: SignedTransaction = try sign(transaction: transaction) guard let raw = signed.raw else { @@ -28,14 +28,6 @@ public extension EthereumAccount { throw EthereumSignerError.unknownError } - let r = signature.subdata(in: 0 ..< 32) - let s = signature.subdata(in: 32 ..< 64) - - var v = Int(signature[64]) - if v < 37 { - v += (transaction.chainId ?? -1) * 2 + 35 - } - - return SignedTransaction(transaction: transaction, v: v, r: r, s: s) + return SignedTransaction(transaction: transaction, signature: signature) } } diff --git a/web3swift/src/Account/EthereumAccount.swift b/web3swift/src/Account/EthereumAccount.swift index 2d36829f..4c78bdf2 100644 --- a/web3swift/src/Account/EthereumAccount.swift +++ b/web3swift/src/Account/EthereumAccount.swift @@ -14,6 +14,8 @@ public protocol EthereumAccountProtocol { func sign(hex: String) throws -> Data func sign(message: Data) throws -> Data func sign(message: String) throws -> Data + func signMessage(message: Data) throws -> String + func signMessage(message: TypedData) throws -> String func sign(transaction: EthereumTransaction) throws -> SignedTransaction } @@ -89,8 +91,8 @@ public class EthereumAccount: EthereumAccountProtocol { do { try keyStorage.encryptAndStorePrivateKey(key: privateKey, keystorePassword: password) let publicKey = try KeyUtil.generatePublicKey(from: privateKey) - let address = KeyUtil.generateAddress(from: publicKey).value - return try self.init(addressString: address, keyStorage: keyStorage, keystorePassword: password) + let address = KeyUtil.generateAddress(from: publicKey) + return try self.init(addressString: address.asString(), keyStorage: keyStorage, keystorePassword: password) } catch { throw EthereumAccountError.createAccountError } @@ -116,8 +118,8 @@ public class EthereumAccount: EthereumAccountProtocol { do { try keyStorage.encryptAndStorePrivateKey(key: privateKey, keystorePassword: password) let publicKey = try KeyUtil.generatePublicKey(from: privateKey) - let address = KeyUtil.generateAddress(from: publicKey).value - return try self.init(addressString: address, keyStorage: keyStorage, keystorePassword: password) + let address = KeyUtil.generateAddress(from: publicKey) + return try self.init(addressString: address.asString(), keyStorage: keyStorage, keystorePassword: password) } catch { throw EthereumAccountError.importAccountError } diff --git a/web3swift/src/Account/EthereumKeyStorage.swift b/web3swift/src/Account/EthereumKeyStorage.swift index 420154bb..377f73d4 100644 --- a/web3swift/src/Account/EthereumKeyStorage.swift +++ b/web3swift/src/Account/EthereumKeyStorage.swift @@ -28,7 +28,7 @@ public enum EthereumKeyStorageError: Error { public class EthereumKeyLocalStorage: EthereumSingleKeyStorageProtocol { public init() {} - private var address: String? + private var address: EthereumAddress? private let localFileName = "ethereumkey" private var addressPath: String? { @@ -36,7 +36,7 @@ public class EthereumKeyLocalStorage: EthereumSingleKeyStorageProtocol { return nil } if let url = folderPath { - return url.appendingPathComponent(address).path + return url.appendingPathComponent(address.asString()).path } return nil } @@ -102,7 +102,7 @@ extension EthereumKeyLocalStorage: EthereumMultipleKeyStorageProtocol { } public func storePrivateKey(key: Data, with address: EthereumAddress) throws { - self.address = address.value + self.address = address defer { self.address = nil @@ -120,7 +120,7 @@ extension EthereumKeyLocalStorage: EthereumMultipleKeyStorageProtocol { } public func loadPrivateKey(for address: EthereumAddress) throws -> Data { - self.address = address.value + self.address = address defer { self.address = nil @@ -155,7 +155,7 @@ extension EthereumKeyLocalStorage: EthereumMultipleKeyStorageProtocol { public func deletePrivateKey(for address: EthereumAddress) throws { do { if let folderPath = folderPath { - let filePathName = folderPath.appendingPathComponent(address.value) + let filePathName = folderPath.appendingPathComponent(address.asString()) try fileManager.removeItem(at: filePathName) } } catch { diff --git a/web3swift/src/Account/Signature.swift b/web3swift/src/Account/Signature.swift new file mode 100644 index 00000000..ee7ecfc0 --- /dev/null +++ b/web3swift/src/Account/Signature.swift @@ -0,0 +1,58 @@ +// +// web3.swift +// Copyright © 2022 Argent Labs Limited. All rights reserved. +// + +import Foundation + +public struct Signature: Equatable { + public let r: Data + public let s: Data + public let v: Int + public let recoveryParam: Int + let raw: Data + + public var flattened: Data { + raw + } + + public init( + r: Data, + s: Data, + v: Int, + recoveryParam: Int + ) { + self.r = r + self.s = s + self.v = v + self.recoveryParam = recoveryParam + self.raw = r + s + Data([UInt8(v)]) + } + + public init( + raw: Data + ) { + self.raw = raw + (self.r, self.s, self.v) = raw.extractRSV() + self.recoveryParam = 1 - (self.v % 2) + } + + public static let zero: Signature = .init(raw: Data(repeating: 0, count: 65)) +} + +extension Data { + func extractRSV() -> (Data, Data, Int) { + guard count >= 65 else { + fatalError("Invalid usage: Need a correctly sized signature") + } + + let r = subdata(in: 0 ..< 32) + let s = subdata(in: 32 ..< 64) + var v = Int(self[64]) + if v < 27 { // recid == v + v += 27 + } + + return (r.web3.strippingZeroesFromBytes, s.web3.strippingZeroesFromBytes, v) + } +} diff --git a/web3swift/src/Account/TypedData.swift b/web3swift/src/Account/TypedData.swift index 4324bdba..cdc8d480 100644 --- a/web3swift/src/Account/TypedData.swift +++ b/web3swift/src/Account/TypedData.swift @@ -89,7 +89,7 @@ extension TypedData { let recursiveEncoded: [UInt8] = try valueTypes.flatMap { variable -> [UInt8] in - // Decomposit the type if it is array type + // Decomposite the type if it is array type let components = variable.type.components(separatedBy: CharacterSet(charactersIn: "[]")) let parsedType = components[0] @@ -128,7 +128,7 @@ extension TypedData { } private func getParsedType(primaryType: String) -> String { - // Decomposit the type if it is an array type + // Decomposite the type if it is an array type let components = primaryType.components(separatedBy: CharacterSet(charactersIn: "[]")) let parsedType = components[0] diff --git a/web3swift/src/Client/EthereumClient+Call.swift b/web3swift/src/Client/BaseEthereumClient+Call.swift similarity index 97% rename from web3swift/src/Client/EthereumClient+Call.swift rename to web3swift/src/Client/BaseEthereumClient+Call.swift index 8f6fea4e..fc9bae8d 100644 --- a/web3swift/src/Client/EthereumClient+Call.swift +++ b/web3swift/src/Client/BaseEthereumClient+Call.swift @@ -58,8 +58,8 @@ extension BaseEthereumClient { } let params = CallParams( - from: transaction.from?.value, - to: transaction.to.value, + from: transaction.from?.asString(), + to: transaction.to.asString(), data: transactionData.web3.hexString, block: block.stringValue ) @@ -150,7 +150,7 @@ extension BaseEthereumClient { let isGet = rawURL.contains("{data}") guard let url = URL(string: rawURL - .replacingOccurrences(of: "{sender}", with: sender.value.lowercased()) + .replacingOccurrences(of: "{sender}", with: sender.asString().lowercased()) .replacingOccurrences(of: "{data}", with: data.web3.hexString.lowercased())) else { throw OffchainReadError.invalidParams } diff --git a/web3swift/src/Client/BaseEthereumClient.swift b/web3swift/src/Client/BaseEthereumClient.swift index 7fb221db..c3b7e91c 100644 --- a/web3swift/src/Client/BaseEthereumClient.swift +++ b/web3swift/src/Client/BaseEthereumClient.swift @@ -11,310 +11,25 @@ import Foundation import FoundationNetworking #endif -public class BaseEthereumClient: EthereumClientProtocol { +open class BaseEthereumClient: EthereumClientProtocol { public let url: URL - let networkProvider: NetworkProviderProtocol + public let networkProvider: NetworkProviderProtocol private let logger: Logger - public var network: EthereumNetwork? + public var network: EthereumNetwork - init( + public init( networkProvider: NetworkProviderProtocol, url: URL, logger: Logger? = nil, - network: EthereumNetwork? + network: EthereumNetwork ) { self.url = url self.networkProvider = networkProvider self.logger = logger ?? Logger(label: "web3.swift.eth-client") self.network = network - - if network == nil { - let semaphore = DispatchSemaphore(value: 0) - Task { - self.network = await fetchNetwork() - semaphore.signal() - } - semaphore.wait() - } - } - - public func net_version() async throws -> EthereumNetwork { - let emptyParams: [Bool] = [] - do { - let data = try await networkProvider.send(method: "net_version", params: emptyParams, receive: String.self) - - if let resString = data as? String { - let network = EthereumNetwork.fromString(resString) - return network - } else { - throw EthereumClientError.unexpectedReturnValue - } - } catch { - throw failureHandler(error) - } - } - - public func eth_gasPrice() async throws -> BigUInt { - let emptyParams: [Bool] = [] - - do { - let data = try await networkProvider.send(method: "eth_gasPrice", params: emptyParams, receive: String.self) - if let hexString = data as? String, let bigUInt = BigUInt(hex: hexString) { - return bigUInt - } else { - throw EthereumClientError.unexpectedReturnValue - } - } catch { - throw failureHandler(error) - } - } - - public func eth_blockNumber() async throws -> Int { - let emptyParams: [Bool] = [] - - do { - let data = try await networkProvider.send(method: "eth_blockNumber", params: emptyParams, receive: String.self) - if let hexString = data as? String { - if let integerValue = Int(hex: hexString) { - return integerValue - } else { - throw EthereumClientError.decodeIssue - } - } else { - throw EthereumClientError.unexpectedReturnValue - } - } catch { - throw failureHandler(error) - } - } - - public func eth_getBalance(address: EthereumAddress, block: EthereumBlock) async throws -> BigUInt { - do { - let data = try await networkProvider.send(method: "eth_getBalance", params: [address.value, block.stringValue], receive: String.self) - if let resString = data as? String, let balanceInt = BigUInt(hex: resString.web3.noHexPrefix) { - return balanceInt - } else { - throw EthereumClientError.unexpectedReturnValue - } - } catch { - throw failureHandler(error) - } - } - - public func eth_getCode(address: EthereumAddress, block: EthereumBlock = .Latest) async throws -> String { - do { - let data = try await networkProvider.send(method: "eth_getCode", params: [address.value, block.stringValue], receive: String.self) - if let resDataString = data as? String { - return resDataString - } else { - throw EthereumClientError.unexpectedReturnValue - } - } catch { - throw failureHandler(error) - } - } - - public func eth_estimateGas(_ transaction: EthereumTransaction) async throws -> BigUInt { - struct CallParams: Encodable { - let from: String? - let to: String - let value: String? - let data: String? - - enum TransactionCodingKeys: String, CodingKey { - case from - case to - 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) - - let jsonRPCAmount: (String) -> String = { amount in - amount == "0x00" ? "0x0" : amount - } - - if let value = value.map(jsonRPCAmount) { - 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, - value: value?.web3.hexStringNoLeadingZeroes, - data: transaction.data?.web3.hexString - ) - - do { - let data = try await networkProvider.send(method: "eth_estimateGas", params: params, receive: String.self) - if let gasHex = data as? String, let gas = BigUInt(hex: gasHex) { - return gas - } else { - throw EthereumClientError.unexpectedReturnValue - } - } catch { - throw failureHandler(error) - } - } - - public func eth_sendRawTransaction(_ transaction: EthereumTransaction, withAccount account: EthereumAccountProtocol) async throws -> String { - do { - // Inject pending nonce - let nonce = try await eth_getTransactionCount(address: account.address, block: .Pending) - - var transaction = transaction - transaction.nonce = nonce - - if transaction.chainId == nil, let network = network { - transaction.chainId = network.intValue - } - - guard let _ = transaction.chainId, let signedTx = (try? account.sign(transaction: transaction)), let transactionHex = signedTx.raw?.web3.hexString else { - throw EthereumClientError.encodeIssue - } - - let data = try await networkProvider.send(method: "eth_sendRawTransaction", params: [transactionHex], receive: String.self) - if let resDataString = data as? String { - return resDataString - } else { - throw EthereumClientError.unexpectedReturnValue - } - } catch { - throw failureHandler(error) - } - } - - public func eth_getTransactionCount(address: EthereumAddress, block: EthereumBlock) async throws -> Int { - do { - let data = try await networkProvider.send(method: "eth_getTransactionCount", params: [address.value, block.stringValue], receive: String.self) - if let resString = data as? String, let count = Int(hex: resString) { - return count - } else { - throw EthereumClientError.unexpectedReturnValue - } - } catch { - throw failureHandler(error) - } - } - - public func eth_getTransaction(byHash txHash: String) async throws -> EthereumTransaction { - do { - let data = try await networkProvider.send(method: "eth_getTransactionByHash", params: [txHash], receive: EthereumTransaction.self) - if let transaction = data as? EthereumTransaction { - return transaction - } else { - throw EthereumClientError.unexpectedReturnValue - } - } catch { - throw failureHandler(error) - } - } - - public func eth_getTransactionReceipt(txHash: String) async throws -> EthereumTransactionReceipt { - do { - let data = try await networkProvider.send(method: "eth_getTransactionReceipt", params: [txHash], receive: EthereumTransactionReceipt.self) - if let receipt = data as? EthereumTransactionReceipt { - return receipt - } else { - throw EthereumClientError.unexpectedReturnValue - } - } catch { - throw failureHandler(error) - } - } - - public func eth_getLogs(addresses: [EthereumAddress]?, topics: [String?]?, fromBlock from: EthereumBlock = .Earliest, toBlock to: EthereumBlock = .Latest) async throws -> [EthereumLog] { - try await RecursiveLogCollector(ethClient: self).getAllLogs(addresses: addresses, topics: topics.map(Topics.plain), from: from, to: to) - } - - public func eth_getLogs(addresses: [EthereumAddress]?, orTopics topics: [[String]?]?, fromBlock from: EthereumBlock = .Earliest, toBlock to: EthereumBlock = .Latest) async throws -> [EthereumLog] { - try await RecursiveLogCollector(ethClient: self).getAllLogs(addresses: addresses, topics: topics.map(Topics.composed), from: from, to: to) - } - - public func getLogs(addresses: [EthereumAddress]?, topics: Topics?, fromBlock: EthereumBlock, toBlock: EthereumBlock) async throws -> [EthereumLog] { - struct CallParams: Encodable { - var fromBlock: String - var toBlock: String - let address: [EthereumAddress]? - let topics: Topics? - } - - let params = CallParams(fromBlock: fromBlock.stringValue, toBlock: toBlock.stringValue, address: addresses, topics: topics) - - do { - let data = try await networkProvider.send(method: "eth_getLogs", params: [params], receive: [EthereumLog].self) - - if let logs = data as? [EthereumLog] { - return logs - } else { - throw EthereumClientError.unexpectedReturnValue - } - } catch { - if let error = error as? JSONRPCError, - case let .executionError(innerError) = error, - innerError.error.code == JSONRPCErrorCode.tooManyResults { - throw EthereumClientError.tooManyResults - } else { - throw EthereumClientError.unexpectedReturnValue - } - } - } - - public func eth_getBlockByNumber(_ block: EthereumBlock) async throws -> EthereumBlockInfo { - struct CallParams: Encodable { - let block: EthereumBlock - let fullTransactions: Bool - - func encode(to encoder: Encoder) throws { - var container = encoder.unkeyedContainer() - try container.encode(block.stringValue) - try container.encode(fullTransactions) - } - } - - let params = CallParams(block: block, fullTransactions: false) - - do { - let data = try await networkProvider.send(method: "eth_getBlockByNumber", params: params, receive: EthereumBlockInfo.self) - if let blockData = data as? EthereumBlockInfo { - return blockData - } else { - throw EthereumClientError.unexpectedReturnValue - } - } catch { - throw failureHandler(error) - } - } - - private func fetchNetwork() async -> EthereumNetwork? { - do { - return try await net_version() - } catch { - logger.warning("Client has no network: \(error.localizedDescription)") - } - - return nil } func failureHandler(_ error: Error) -> EthereumClientError { diff --git a/web3swift/src/Client/EthereumHttpClient.swift b/web3swift/src/Client/HTTP/EthereumHttpClient.swift similarity index 95% rename from web3swift/src/Client/EthereumHttpClient.swift rename to web3swift/src/Client/HTTP/EthereumHttpClient.swift index f2f3ba1a..dda40f57 100644 --- a/web3swift/src/Client/EthereumHttpClient.swift +++ b/web3swift/src/Client/HTTP/EthereumHttpClient.swift @@ -17,7 +17,7 @@ public class EthereumHttpClient: BaseEthereumClient { url: URL, sessionConfig: URLSessionConfiguration = URLSession.shared.configuration, logger: Logger? = nil, - network: EthereumNetwork? = nil + network: EthereumNetwork ) { let networkQueue = OperationQueue() networkQueue.name = "web3swift.client.networkQueue" diff --git a/web3swift/src/Client/Models/EthereumAddress.swift b/web3swift/src/Client/Models/EthereumAddress.swift index da57cfe5..6850d91a 100644 --- a/web3swift/src/Client/Models/EthereumAddress.swift +++ b/web3swift/src/Client/Models/EthereumAddress.swift @@ -3,38 +3,70 @@ // Copyright © 2022 Argent Labs Limited. All rights reserved. // +import BigInt import Foundation public struct EthereumAddress: Codable, Hashable { - public let value: String + @available(*, deprecated, message: "Shouldn't rely on the actual String representation. Use asString() instead to get an unformatted representation") public var value: String { + raw + } + + private let raw: String + private let numberRepresentation: BigUInt? + private let numberRepresentationAsString: String? public static let zero: Self = "0x0000000000000000000000000000000000000000" public init(_ value: String) { - self.value = value.lowercased() + self.raw = value.lowercased() + self.numberRepresentation = BigUInt(hex: raw) + self.numberRepresentationAsString = self.numberRepresentation.map(String.init(describing:)) } public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() - self.value = try container.decode(String.self).lowercased() + let raw = try container.decode(String.self).lowercased() + self.raw = raw + self.numberRepresentation = BigUInt(hex: raw) + self.numberRepresentationAsString = self.numberRepresentation.map(String.init(describing:)) } public func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() - try container.encode(value) + try container.encode(raw) } public func hash(into hasher: inout Hasher) { - hasher.combine(value) + if let numberAsString = numberRepresentationAsString { + hasher.combine(numberAsString) + } else { + hasher.combine(asString()) + } } public static func == (lhs: EthereumAddress, rhs: EthereumAddress) -> Bool { - lhs.value == rhs.value + guard let lhs = lhs.numberRepresentationAsString, let rhs = rhs.numberRepresentationAsString else { + return false + } + // Comparing Number representation avoids issues with lowercase and 0-padding + return lhs == rhs } } public extension EthereumAddress { + func asString() -> String { + raw + } + + func asNumber() -> BigUInt? { + numberRepresentation + } + + func asData() -> Data? { + raw.web3.hexData + } + func toChecksumAddress() -> String { - let lowerCaseAddress = value.web3.noHexPrefix.lowercased() + let lowerCaseAddress = raw.web3.noHexPrefix.lowercased() let arr = Array(lowerCaseAddress) let keccaf = Array(lowerCaseAddress.web3.keccak256.web3.hexString.web3.noHexPrefix) var result = "0x" diff --git a/web3swift/src/Client/Models/EthereumNetwork.swift b/web3swift/src/Client/Models/EthereumNetwork.swift index e85779db..e41ddc4a 100644 --- a/web3swift/src/Client/Models/EthereumNetwork.swift +++ b/web3swift/src/Client/Models/EthereumNetwork.swift @@ -11,7 +11,7 @@ public enum EthereumNetwork: Equatable, Decodable { case goerli case sepolia case custom(String) - static func fromString(_ networkId: String) -> EthereumNetwork { + public static func fromString(_ networkId: String) -> EthereumNetwork { switch networkId { case "1": return .mainnet @@ -26,7 +26,7 @@ public enum EthereumNetwork: Equatable, Decodable { } } - var stringValue: String { + public var stringValue: String { switch self { case .mainnet: return "1" @@ -41,7 +41,7 @@ public enum EthereumNetwork: Equatable, Decodable { } } - var intValue: Int { + public var intValue: Int { switch self { case .mainnet: return 1 diff --git a/web3swift/src/Client/Models/EthereumTransaction.swift b/web3swift/src/Client/Models/EthereumTransaction.swift index 55d8165d..deedf9c4 100644 --- a/web3swift/src/Client/Models/EthereumTransaction.swift +++ b/web3swift/src/Client/Models/EthereumTransaction.swift @@ -44,7 +44,7 @@ public struct EthereumTransaction: EthereumTransactionProtocol, Equatable, Codab self.chainId = chainId self.gas = nil self.blockNumber = nil - let txArray: [Any?] = [self.nonce, self.gasPrice, self.gasLimit, self.to.value.web3.noHexPrefix, self.value, self.data, self.chainId, 0, 0] + let txArray: [Any?] = [self.nonce, self.gasPrice, self.gasLimit, self.to, self.value, self.data, self.chainId, 0, 0] self.hash = RLP.encode(txArray) self.input = nil } @@ -100,7 +100,7 @@ public struct EthereumTransaction: EthereumTransactionProtocol, Equatable, Codab } public var raw: Data? { - let txArray: [Any?] = [nonce, gasPrice, gasLimit, to.value.web3.noHexPrefix, value, data, chainId, 0, 0] + let txArray: [Any?] = [nonce, gasPrice, gasLimit, to, value, data, chainId, 0, 0] return RLP.encode(txArray) } @@ -162,19 +162,33 @@ public struct EthereumTransaction: EthereumTransactionProtocol, Equatable, Codab public struct SignedTransaction { public let transaction: EthereumTransaction - let v: Int - let r: Data - let s: Data + public let signature: Signature - public init(transaction: EthereumTransaction, v: Int, r: Data, s: Data) { + public init( + transaction: EthereumTransaction, + signature raw: Data + ) { self.transaction = transaction - self.v = v - self.r = r.web3.strippingZeroesFromBytes - self.s = s.web3.strippingZeroesFromBytes + self.signature = .init(raw: raw) + } + + var r: Data { + signature.r + } + + var s: Data { + signature.s + } + + var v: Int { + guard signature.v < 37 else { + return signature.v + } + return signature.v + (transaction.chainId ?? -1) * 2 + 8 } public var raw: Data? { - let txArray: [Any?] = [transaction.nonce, transaction.gasPrice, transaction.gasLimit, transaction.to.value.web3.noHexPrefix, transaction.value, transaction.data, v, r, s] + let txArray: [Any?] = [transaction.nonce, transaction.gasPrice, transaction.gasLimit, transaction.to, transaction.value, transaction.data, v, r, s] return RLP.encode(txArray) } diff --git a/web3swift/src/Client/NetworkProviders/HttpNetworkProvider.swift b/web3swift/src/Client/NetworkProviders/HttpNetworkProvider.swift index 0683ce3a..154ea02a 100644 --- a/web3swift/src/Client/NetworkProviders/HttpNetworkProvider.swift +++ b/web3swift/src/Client/NetworkProviders/HttpNetworkProvider.swift @@ -9,11 +9,11 @@ import Foundation import FoundationNetworking #endif -class HttpNetworkProvider: NetworkProviderProtocol { - let session: URLSession +public class HttpNetworkProvider: NetworkProviderProtocol { + public let session: URLSession private let url: URL - init(session: URLSession, url: URL) { + public init(session: URLSession, url: URL) { self.session = session self.url = url } @@ -22,7 +22,7 @@ class HttpNetworkProvider: NetworkProviderProtocol { session.invalidateAndCancel() } - func send(method: String, params: P, receive: U.Type) async throws -> Any where P: Encodable, U: Decodable { + public func send(method: String, params: P, receive: U.Type) async throws -> Any where P: Encodable, U: Decodable { if type(of: params) == [Any].self { // If params are passed in with Array and not caught, runtime fatal error throw JSONRPCError.encodingError diff --git a/web3swift/src/Client/NetworkProviders/NetworkProviderProtocol.swift b/web3swift/src/Client/NetworkProviders/NetworkProviderProtocol.swift index baea710b..900872dd 100644 --- a/web3swift/src/Client/NetworkProviders/NetworkProviderProtocol.swift +++ b/web3swift/src/Client/NetworkProviders/NetworkProviderProtocol.swift @@ -12,7 +12,7 @@ import Foundation import FoundationNetworking #endif -internal protocol NetworkProviderProtocol { +public protocol NetworkProviderProtocol { var session: URLSession { get } func send(method: String, params: P, receive: U.Type) async throws -> Any } diff --git a/web3swift/src/Client/EthereumClientProtocol.swift b/web3swift/src/Client/Protocols/EthereumClientProtocol.swift similarity index 71% rename from web3swift/src/Client/EthereumClientProtocol.swift rename to web3swift/src/Client/Protocols/EthereumClientProtocol.swift index a281b030..7e6dfc72 100644 --- a/web3swift/src/Client/EthereumClientProtocol.swift +++ b/web3swift/src/Client/Protocols/EthereumClientProtocol.swift @@ -11,30 +11,10 @@ public enum CallResolution { case offchainAllowed(maxRedirects: Int) } -public struct EquatableError: Error, Equatable { - let base: Error - - public static func == (lhs: EquatableError, rhs: EquatableError) -> Bool { - type(of: lhs.base) == type(of: rhs.base) && - lhs.base.localizedDescription == rhs.base.localizedDescription - } -} - -public enum EthereumClientError: Error, Equatable { - case tooManyResults - case executionError(JSONRPCErrorDetail) - case unexpectedReturnValue - case noResultFound - case decodeIssue - case encodeIssue - case noInputData - case webSocketError(EquatableError) - case connectionNotOpen -} - -public protocol EthereumClientProtocol: AnyObject { - var network: EthereumNetwork? { get } +// MARK: EthereumClient (HTTP or Websocket) +public protocol EthereumClientProtocol: EthereumRPCProtocol, AnyObject { + // Legacy result-based API func net_version(completionHandler: @escaping (Result) -> Void) func eth_gasPrice(completionHandler: @escaping (Result) -> Void) func eth_blockNumber(completionHandler: @escaping (Result) -> Void) @@ -60,32 +40,9 @@ public protocol EthereumClientProtocol: AnyObject { func eth_getLogs(addresses: [EthereumAddress]?, orTopics: [[String]?]?, fromBlock: EthereumBlock, toBlock: EthereumBlock, completionHandler: @escaping (Result<[EthereumLog], EthereumClientError>) -> Void) func eth_getBlockByNumber(_ block: EthereumBlock, completionHandler: @escaping (Result) -> Void) func getLogs(addresses: [EthereumAddress]?, topics: Topics?, fromBlock: EthereumBlock, toBlock: EthereumBlock) async throws -> [EthereumLog] - - // Async/Await - func net_version() async throws -> EthereumNetwork - func eth_gasPrice() async throws -> BigUInt - func eth_blockNumber() async throws -> Int - func eth_getBalance(address: EthereumAddress, block: EthereumBlock) async throws -> BigUInt - func eth_getCode(address: EthereumAddress, block: EthereumBlock) async throws -> String - func eth_estimateGas(_ transaction: EthereumTransaction) async throws -> BigUInt - func eth_sendRawTransaction(_ transaction: EthereumTransaction, withAccount account: EthereumAccountProtocol) async throws -> String - func eth_getTransactionCount(address: EthereumAddress, block: EthereumBlock) async throws -> Int - func eth_getTransaction(byHash txHash: String) async throws -> EthereumTransaction - func eth_getTransactionReceipt(txHash: String) async throws -> EthereumTransactionReceipt - func eth_call( - _ transaction: EthereumTransaction, - block: EthereumBlock - ) async throws -> String - func eth_call( - _ transaction: EthereumTransaction, - resolution: CallResolution, - block: EthereumBlock - ) async throws -> String - func eth_getLogs(addresses: [EthereumAddress]?, topics: [String?]?, fromBlock: EthereumBlock, toBlock: EthereumBlock) async throws -> [EthereumLog] - func eth_getLogs(addresses: [EthereumAddress]?, orTopics: [[String]?]?, fromBlock: EthereumBlock, toBlock: EthereumBlock) async throws -> [EthereumLog] - func eth_getBlockByNumber(_ block: EthereumBlock) async throws -> EthereumBlockInfo } +// MARK: - Websocket #if canImport(NIO) import NIOWebSocket diff --git a/web3swift/src/Client/Protocols/EthereumProvider.swift b/web3swift/src/Client/Protocols/EthereumProvider.swift new file mode 100644 index 00000000..03f62fc0 --- /dev/null +++ b/web3swift/src/Client/Protocols/EthereumProvider.swift @@ -0,0 +1,333 @@ +// +// web3.swift +// Copyright © 2022 Argent Labs Limited. All rights reserved. +// + +import BigInt + +public struct EquatableError: Error, Equatable { + let base: Error + + public static func == (lhs: EquatableError, rhs: EquatableError) -> Bool { + type(of: lhs.base) == type(of: rhs.base) && + lhs.base.localizedDescription == rhs.base.localizedDescription + } +} + +public enum EthereumClientError: Error, Equatable { + case tooManyResults + case executionError(JSONRPCErrorDetail) + case unexpectedReturnValue + case noResultFound + case decodeIssue + case encodeIssue + case noInputData + case webSocketError(EquatableError) + case connectionNotOpen +} + +public protocol EthereumRPCProtocol: AnyObject { + var networkProvider: NetworkProviderProtocol { get } + var network: EthereumNetwork { get } + + func eth_getTransactionCount(address: EthereumAddress, block: EthereumBlock) async throws -> Int + func net_version() async throws -> EthereumNetwork + func eth_gasPrice() async throws -> BigUInt + func eth_blockNumber() async throws -> Int + func eth_getBalance(address: EthereumAddress, block: EthereumBlock) async throws -> BigUInt + func eth_getCode(address: EthereumAddress, block: EthereumBlock) async throws -> String + func eth_estimateGas(_ transaction: EthereumTransaction) async throws -> BigUInt + func eth_sendRawTransaction(_ transaction: EthereumTransaction, withAccount account: EthereumAccountProtocol) async throws -> String + func eth_getTransaction(byHash txHash: String) async throws -> EthereumTransaction + func eth_getTransactionReceipt(txHash: String) async throws -> EthereumTransactionReceipt + func eth_call( + _ transaction: EthereumTransaction, + block: EthereumBlock + ) async throws -> String + func eth_call( + _ transaction: EthereumTransaction, + resolution: CallResolution, + block: EthereumBlock + ) async throws -> String + func eth_getLogs(addresses: [EthereumAddress]?, topics: [String?]?, fromBlock: EthereumBlock, toBlock: EthereumBlock) async throws -> [EthereumLog] + func eth_getLogs(addresses: [EthereumAddress]?, orTopics: [[String]?]?, fromBlock: EthereumBlock, toBlock: EthereumBlock) async throws -> [EthereumLog] + func eth_getBlockByNumber(_ block: EthereumBlock) async throws -> EthereumBlockInfo +} + +extension EthereumRPCProtocol { + public func eth_getTransactionCount(address: EthereumAddress, block: EthereumBlock) async throws -> Int { + do { + let data = try await networkProvider.send(method: "eth_getTransactionCount", params: [address.asString(), block.stringValue], receive: String.self) + if let resString = data as? String, let count = Int(hex: resString) { + return count + } else { + throw EthereumClientError.unexpectedReturnValue + } + } catch { + throw failureHandler(error) + } + } + + public func net_version() async throws -> EthereumNetwork { + let emptyParams: [Bool] = [] + do { + let data = try await networkProvider.send(method: "net_version", params: emptyParams, receive: String.self) + + if let resString = data as? String { + let network = EthereumNetwork.fromString(resString) + return network + } else { + throw EthereumClientError.unexpectedReturnValue + } + } catch { + throw failureHandler(error) + } + } + + public func eth_gasPrice() async throws -> BigUInt { + let emptyParams: [Bool] = [] + + do { + let data = try await networkProvider.send(method: "eth_gasPrice", params: emptyParams, receive: String.self) + if let hexString = data as? String, let bigUInt = BigUInt(hex: hexString) { + return bigUInt + } else { + throw EthereumClientError.unexpectedReturnValue + } + } catch { + throw failureHandler(error) + } + } + + public func eth_blockNumber() async throws -> Int { + let emptyParams: [Bool] = [] + + do { + let data = try await networkProvider.send(method: "eth_blockNumber", params: emptyParams, receive: String.self) + if let hexString = data as? String { + if let integerValue = Int(hex: hexString) { + return integerValue + } else { + throw EthereumClientError.decodeIssue + } + } else { + throw EthereumClientError.unexpectedReturnValue + } + } catch { + throw failureHandler(error) + } + } + + public func eth_getBalance(address: EthereumAddress, block: EthereumBlock) async throws -> BigUInt { + do { + let data = try await networkProvider.send(method: "eth_getBalance", params: [address.asString(), block.stringValue], receive: String.self) + if let resString = data as? String, let balanceInt = BigUInt(hex: resString.web3.noHexPrefix) { + return balanceInt + } else { + throw EthereumClientError.unexpectedReturnValue + } + } catch { + throw failureHandler(error) + } + } + + public func eth_getCode(address: EthereumAddress, block: EthereumBlock = .Latest) async throws -> String { + do { + let data = try await networkProvider.send(method: "eth_getCode", params: [address.asString(), block.stringValue], receive: String.self) + if let resDataString = data as? String { + return resDataString + } else { + throw EthereumClientError.unexpectedReturnValue + } + } catch { + throw failureHandler(error) + } + } + + public func eth_estimateGas(_ transaction: EthereumTransaction) async throws -> BigUInt { + let value: BigUInt? + if let txValue = transaction.value, txValue > .zero { + value = txValue + } else { + value = nil + } + + let params = EstimateCallParams( + from: transaction.from?.asString(), + to: transaction.to.asString(), + value: value?.web3.hexStringNoLeadingZeroes, + data: transaction.data?.web3.hexString + ) + + do { + let data = try await networkProvider.send(method: "eth_estimateGas", params: params, receive: String.self) + if let gasHex = data as? String, let gas = BigUInt(hex: gasHex) { + return gas + } else { + throw EthereumClientError.unexpectedReturnValue + } + } catch { + throw failureHandler(error) + } + } + + public func eth_sendRawTransaction(_ transaction: EthereumTransaction, withAccount account: EthereumAccountProtocol) async throws -> String { + do { + // Inject pending nonce + let nonce = try await eth_getTransactionCount(address: account.address, block: .Pending) + + var transaction = transaction + transaction.nonce = nonce + + if transaction.chainId == nil { + transaction.chainId = network.intValue + } + + guard let _ = transaction.chainId, let signedTx = (try? account.sign(transaction: transaction)), let transactionHex = signedTx.raw?.web3.hexString else { + throw EthereumClientError.encodeIssue + } + + let data = try await networkProvider.send(method: "eth_sendRawTransaction", params: [transactionHex], receive: String.self) + if let resDataString = data as? String { + return resDataString + } else { + throw EthereumClientError.unexpectedReturnValue + } + } catch { + throw failureHandler(error) + } + } + + public func eth_getTransaction(byHash txHash: String) async throws -> EthereumTransaction { + do { + let data = try await networkProvider.send(method: "eth_getTransactionByHash", params: [txHash], receive: EthereumTransaction.self) + if let transaction = data as? EthereumTransaction { + return transaction + } else { + throw EthereumClientError.unexpectedReturnValue + } + } catch { + throw failureHandler(error) + } + } + + public func eth_getTransactionReceipt(txHash: String) async throws -> EthereumTransactionReceipt { + do { + let data = try await networkProvider.send(method: "eth_getTransactionReceipt", params: [txHash], receive: EthereumTransactionReceipt.self) + if let receipt = data as? EthereumTransactionReceipt { + return receipt + } else { + throw EthereumClientError.unexpectedReturnValue + } + } catch { + throw failureHandler(error) + } + } + + public func eth_getLogs(addresses: [EthereumAddress]?, topics: [String?]?, fromBlock from: EthereumBlock = .Earliest, toBlock to: EthereumBlock = .Latest) async throws -> [EthereumLog] { + try await RecursiveLogCollector(ethClient: self).getAllLogs(addresses: addresses, topics: topics.map(Topics.plain), from: from, to: to) + } + + public func eth_getLogs(addresses: [EthereumAddress]?, orTopics topics: [[String]?]?, fromBlock from: EthereumBlock = .Earliest, toBlock to: EthereumBlock = .Latest) async throws -> [EthereumLog] { + try await RecursiveLogCollector(ethClient: self).getAllLogs(addresses: addresses, topics: topics.map(Topics.composed), from: from, to: to) + } + + public func getLogs(addresses: [EthereumAddress]?, topics: Topics?, fromBlock: EthereumBlock, toBlock: EthereumBlock) async throws -> [EthereumLog] { + let params = GetLogsCallParams(fromBlock: fromBlock.stringValue, toBlock: toBlock.stringValue, address: addresses, topics: topics) + + do { + let data = try await networkProvider.send(method: "eth_getLogs", params: [params], receive: [EthereumLog].self) + + if let logs = data as? [EthereumLog] { + return logs + } else { + throw EthereumClientError.unexpectedReturnValue + } + } catch { + if let error = error as? JSONRPCError, + case let .executionError(innerError) = error, + innerError.error.code == JSONRPCErrorCode.tooManyResults { + throw EthereumClientError.tooManyResults + } else { + throw EthereumClientError.unexpectedReturnValue + } + } + } + + public func eth_getBlockByNumber(_ block: EthereumBlock) async throws -> EthereumBlockInfo { + let params = GetBlockByNumberCallParams(block: block, fullTransactions: false) + + do { + let data = try await networkProvider.send(method: "eth_getBlockByNumber", params: params, receive: EthereumBlockInfo.self) + if let blockData = data as? EthereumBlockInfo { + return blockData + } else { + throw EthereumClientError.unexpectedReturnValue + } + } catch { + throw failureHandler(error) + } + } + + func failureHandler(_ error: Error) -> EthereumClientError { + if case let .executionError(result) = error as? JSONRPCError { + return EthereumClientError.executionError(result.error) + } else if case .executionError = error as? EthereumClientError, let error = error as? EthereumClientError { + return error + } else { + return EthereumClientError.unexpectedReturnValue + } + } +} + +fileprivate struct EstimateCallParams: Encodable { + let from: String? + let to: String + let value: String? + let data: String? + + enum TransactionCodingKeys: String, CodingKey { + case from + case to + 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) + + let jsonRPCAmount: (String) -> String = { amount in + amount == "0x00" ? "0x0" : amount + } + + if let value = value.map(jsonRPCAmount) { + try nested.encode(value, forKey: .value) + } + if let data = data { + try nested.encode(data, forKey: .data) + } + } +} + +fileprivate struct GetLogsCallParams: Encodable { + var fromBlock: String + var toBlock: String + let address: [EthereumAddress]? + let topics: Topics? +} + +fileprivate struct GetBlockByNumberCallParams: Encodable { + let block: EthereumBlock + let fullTransactions: Bool + + func encode(to encoder: Encoder) throws { + var container = encoder.unkeyedContainer() + try container.encode(block.stringValue) + try container.encode(fullTransactions) + } +} diff --git a/web3swift/src/Client/RecursiveLogCollector.swift b/web3swift/src/Client/RecursiveLogCollector.swift index dc12a2ee..94ef9f75 100644 --- a/web3swift/src/Client/RecursiveLogCollector.swift +++ b/web3swift/src/Client/RecursiveLogCollector.swift @@ -21,7 +21,7 @@ public enum Topics: Encodable { } struct RecursiveLogCollector { - let ethClient: EthereumClientProtocol + let ethClient: EthereumRPCProtocol func getAllLogs(addresses: [EthereumAddress]?, topics: Topics?, from: EthereumBlock, to: EthereumBlock) async throws -> [EthereumLog] { do { diff --git a/web3swift/src/Client/EthereumWebSocketClient.swift b/web3swift/src/Client/WSS/EthereumWebSocketClient.swift similarity index 99% rename from web3swift/src/Client/EthereumWebSocketClient.swift rename to web3swift/src/Client/WSS/EthereumWebSocketClient.swift index 534b9815..c27cb75c 100644 --- a/web3swift/src/Client/EthereumWebSocketClient.swift +++ b/web3swift/src/Client/WSS/EthereumWebSocketClient.swift @@ -56,7 +56,7 @@ configuration: WebSocketConfiguration = .init(), sessionConfig: URLSessionConfiguration = URLSession.shared.configuration, logger: Logger? = nil, - network: EthereumNetwork? = nil + network: EthereumNetwork ) { let networkQueue = OperationQueue() networkQueue.name = "web3swift.client.networkQueue" diff --git a/web3swift/src/Contract/Statically Typed/ABIDecoder+Static.swift b/web3swift/src/Contract/Statically Typed/ABIDecoder+Static.swift index 5543cd68..92c6480d 100644 --- a/web3swift/src/Contract/Statically Typed/ABIDecoder+Static.swift +++ b/web3swift/src/Contract/Statically Typed/ABIDecoder+Static.swift @@ -83,12 +83,11 @@ extension ABIDecoder { } public static func decode(_ data: ParsedABIEntry, to: EthereumAddress.Type) throws -> EthereumAddress { - let address = EthereumAddress(data) - guard address.value.hasPrefix("0x") else { + guard data.hasPrefix("0x") else { throw ABIError.invalidValue } - return address + return EthereumAddress(data) } public static func decode(_ data: ParsedABIEntry, to: BigInt.Type) throws -> BigInt { diff --git a/web3swift/src/Contract/Statically Typed/ABIEncoder+Static.swift b/web3swift/src/Contract/Statically Typed/ABIEncoder+Static.swift index ce8d23ef..bdf37918 100644 --- a/web3swift/src/Contract/Statically Typed/ABIEncoder+Static.swift +++ b/web3swift/src/Contract/Statically Typed/ABIEncoder+Static.swift @@ -19,7 +19,7 @@ extension ABIEncoder { case let value as Bool: return try ABIEncoder.encodeRaw(value ? "true" : "false", forType: type, padded: !packed) case let value as EthereumAddress: - return try ABIEncoder.encodeRaw(value.value, forType: type, padded: !packed) + return try ABIEncoder.encodeRaw(value.asString(), forType: type, padded: !packed) case let value as BigInt: return try ABIEncoder.encodeRaw(String(value), forType: type, padded: !packed) case let value as BigUInt: @@ -102,7 +102,8 @@ extension ABIEncoder { } else { return try ABIEncoder.encodeRaw(String(bytes: data.web3.bytes), forType: type, padded: !packed) } - + case let value as ABIArray: + return try encode(value.values) case let value as ABITuple: return try encodeTuple(value, type: type) default: diff --git a/web3swift/src/Contract/Statically Typed/ABITuple.swift b/web3swift/src/Contract/Statically Typed/ABITuple.swift index 7829a276..30655ab8 100644 --- a/web3swift/src/Contract/Statically Typed/ABITuple.swift +++ b/web3swift/src/Contract/Statically Typed/ABITuple.swift @@ -8,6 +8,7 @@ import Foundation /// A Tuple is a set of sequential types encoded together public protocol ABITupleDecodable { static var types: [ABIType.Type] { get } + init?(data: String) throws init?(values: [ABIDecoder.DecodedValue]) throws } diff --git a/web3swift/src/Contract/Statically Typed/EthereumClient+Static.swift b/web3swift/src/Contract/Statically Typed/EthereumClient+Static.swift index ca53ce6c..e80384c0 100644 --- a/web3swift/src/Contract/Statically Typed/EthereumClient+Static.swift +++ b/web3swift/src/Contract/Statically Typed/EthereumClient+Static.swift @@ -6,7 +6,7 @@ import Foundation public extension ABIFunction { - func execute(withClient client: EthereumClientProtocol, account: EthereumAccountProtocol) async throws -> String { + func execute(withClient client: EthereumRPCProtocol, account: EthereumAccountProtocol) async throws -> String { guard let tx = try? transaction() else { throw EthereumClientError.encodeIssue } @@ -15,7 +15,7 @@ public extension ABIFunction { } func call( - withClient client: EthereumClientProtocol, + withClient client: EthereumRPCProtocol, responseType: T.Type, block: EthereumBlock = .Latest, resolution: CallResolution = .noOffchain(failOnExecutionError: true) @@ -92,7 +92,7 @@ public struct Events { public let logs: [EthereumLog] } -public extension EthereumClientProtocol { +public extension EthereumRPCProtocol { func getEvents( addresses: [EthereumAddress]?, orTopics: [[String]?]?, diff --git a/web3swift/src/ENS/ENSContracts.swift b/web3swift/src/ENS/ENSContracts.swift index 67fab5ab..51419ebc 100644 --- a/web3swift/src/ENS/ENSContracts.swift +++ b/web3swift/src/ENS/ENSContracts.swift @@ -31,7 +31,7 @@ public enum ENSContracts { switch self { case let .address(address): nameHash = ENSContracts.nameHash( - name: address.value.web3.noHexPrefix + ".addr.reverse" + name: address.asString().web3.noHexPrefix + ".addr.reverse" ) case let .name(ens): nameHash = ENSContracts.nameHash(name: ens) @@ -43,7 +43,7 @@ public enum ENSContracts { switch self { case let .address(address): return ENSContracts.dnsEncode( - name: address.value.web3.noHexPrefix + ".addr.reverse" + name: address.asString().web3.noHexPrefix + ".addr.reverse" ) case let .name(name): return ENSContracts.dnsEncode(name: name) diff --git a/web3swift/src/ENS/ENSMultiResolver.swift b/web3swift/src/ENS/ENSMultiResolver.swift index 4358d322..8e2722f2 100644 --- a/web3swift/src/ENS/ENSMultiResolver.swift +++ b/web3swift/src/ENS/ENSMultiResolver.swift @@ -80,12 +80,12 @@ extension EthereumNameService { let nameHash: Data } - let client: EthereumClientProtocol + let client: EthereumRPCProtocol let registryAddress: EthereumAddress? let multicall: Multicall init( - client: EthereumClientProtocol, + client: EthereumRPCProtocol, registryAddress: EthereumAddress? = nil ) { self.client = client @@ -93,6 +93,10 @@ extension EthereumNameService { self.multicall = Multicall(client: client) } + private var network: EthereumNetwork { + client.network + } + func resolve(addresses: [EthereumAddress]) async throws -> [AddressResolveOutput] { let output = RegistryOutput(expectedResponsesCount: addresses.count) @@ -154,7 +158,7 @@ extension EthereumNameService { parameters: [ENSRegistryResolverParameter], handler: @escaping (Int, ENSRegistryResolverParameter, Result) -> Void ) async throws { - guard let network = client.network, let ensRegistryAddress = registryAddress ?? ENSContracts.registryAddress(for: network) else { + guard let ensRegistryAddress = registryAddress ?? ENSContracts.registryAddress(for: network) else { throw EthereumNameServiceError.noNetwork } diff --git a/web3swift/src/ENS/ENSResolver.swift b/web3swift/src/ENS/ENSResolver.swift index 7c1c16c3..50e0630f 100644 --- a/web3swift/src/ENS/ENSResolver.swift +++ b/web3swift/src/ENS/ENSResolver.swift @@ -10,11 +10,11 @@ class ENSResolver { let callResolution: CallResolution private(set) var supportsWildCard: Bool? - private let client: EthereumClientProtocol + private let client: EthereumRPCProtocol init( address: EthereumAddress, - client: EthereumClientProtocol, + client: EthereumRPCProtocol, callResolution: CallResolution, supportsWildCard: Bool? = nil ) { diff --git a/web3swift/src/ENS/EthereumNameService.swift b/web3swift/src/ENS/EthereumNameService.swift index 98b7139e..fd035782 100644 --- a/web3swift/src/ENS/EthereumNameService.swift +++ b/web3swift/src/ENS/EthereumNameService.swift @@ -37,7 +37,7 @@ public enum EthereumNameServiceError: Error, Equatable { } public class EthereumNameService: EthereumNameServiceProtocol { - let client: EthereumClientProtocol + let client: EthereumRPCProtocol let registryAddress: EthereumAddress? let maximumRedirections: Int private let syncQueue = DispatchQueue(label: "web3swift.ethereumNameService.syncQueue") @@ -57,7 +57,7 @@ public class EthereumNameService: EthereumNameServiceProtocol { } required public init( - client: EthereumClientProtocol, + client: EthereumRPCProtocol, registryAddress: EthereumAddress? = nil, maximumRedirections: Int = 5 ) { @@ -66,9 +66,12 @@ public class EthereumNameService: EthereumNameServiceProtocol { self.maximumRedirections = maximumRedirections } + private var network: EthereumNetwork { + client.network + } + public func resolve(address: EthereumAddress, mode: ResolutionMode) async throws -> String { - guard let network = client.network, - let registryAddress = registryAddress ?? ENSContracts.registryAddress(for: network) else { + guard let registryAddress = registryAddress ?? ENSContracts.registryAddress(for: network) else { throw EthereumNameServiceError.noNetwork } @@ -87,8 +90,7 @@ public class EthereumNameService: EthereumNameServiceProtocol { } public func resolve(ens: String, mode: ResolutionMode) async throws -> EthereumAddress { - guard let network = client.network, - let registryAddress = registryAddress ?? ENSContracts.registryAddress(for: network) else { + guard let registryAddress = registryAddress ?? ENSContracts.registryAddress(for: network) else { throw EthereumNameServiceError.noNetwork } do { diff --git a/web3swift/src/ERC1271/ERC1271.swift b/web3swift/src/ERC1271/ERC1271.swift index 12f9336c..3745ca7f 100644 --- a/web3swift/src/ERC1271/ERC1271.swift +++ b/web3swift/src/ERC1271/ERC1271.swift @@ -7,16 +7,16 @@ import BigInt import Foundation public protocol ERC1271Protocol { - init(client: EthereumClientProtocol) + init(client: EthereumRPCProtocol) func isValidSignature(contract: EthereumAddress, messageHash: Data, signature: Data) async throws -> Bool func isValidSignature(contract: EthereumAddress, messageHash: Data, signature: Data, completionHandler: @escaping (Result) -> Void) } public class ERC1271: ERC1271Protocol { - let client: EthereumClientProtocol + let client: EthereumRPCProtocol - required public init(client: EthereumClientProtocol) { + required public init(client: EthereumRPCProtocol) { self.client = client } diff --git a/web3swift/src/ERC165/ERC165.swift b/web3swift/src/ERC165/ERC165.swift index d7f03e02..eec491c2 100644 --- a/web3swift/src/ERC165/ERC165.swift +++ b/web3swift/src/ERC165/ERC165.swift @@ -7,17 +7,28 @@ import BigInt import Foundation open class ERC165 { - public let client: EthereumClientProtocol + public let client: EthereumRPCProtocol - required public init(client: EthereumClientProtocol) { + required public init(client: EthereumRPCProtocol) { self.client = client } public func supportsInterface(contract: EthereumAddress, id: Data) async throws -> Bool { let function = ERC165Functions.supportsInterface(contract: contract, interfaceId: id) - let data = try await function.call(withClient: client, responseType: ERC165Responses.supportsInterfaceResponse.self) - return data.supported + do { + let data = try await function.call(withClient: client, responseType: ERC165Responses.supportsInterfaceResponse.self) + return data.supported + } catch let error as EthereumClientError { + switch error { + case .executionError: + return false + default: + throw error + } + } catch { + throw error + } } } diff --git a/web3swift/src/ERC20/ERC20.swift b/web3swift/src/ERC20/ERC20.swift index 66c609ee..8810d0d9 100644 --- a/web3swift/src/ERC20/ERC20.swift +++ b/web3swift/src/ERC20/ERC20.swift @@ -7,7 +7,7 @@ import BigInt import Foundation public protocol ERC20Protocol { - init(client: EthereumClientProtocol) + init(client: EthereumRPCProtocol) func name(tokenContract: EthereumAddress) async throws -> String func symbol(tokenContract: EthereumAddress) async throws -> String @@ -28,9 +28,9 @@ public protocol ERC20Protocol { } open class ERC20: ERC20Protocol { - let client: EthereumClientProtocol + let client: EthereumRPCProtocol - required public init(client: EthereumClientProtocol) { + required public init(client: EthereumRPCProtocol) { self.client = client } diff --git a/web3swift/src/ERC721/ERC721.swift b/web3swift/src/ERC721/ERC721.swift index fbcb9341..b577964e 100644 --- a/web3swift/src/ERC721/ERC721.swift +++ b/web3swift/src/ERC721/ERC721.swift @@ -130,12 +130,12 @@ public class ERC721Metadata: ERC721 { public let session: URLSession - public init(client: EthereumClientProtocol, metadataSession: URLSession) { + public init(client: EthereumRPCProtocol, metadataSession: URLSession) { self.session = metadataSession super.init(client: client) } - required init(client: EthereumClientProtocol) { + required init(client: EthereumRPCProtocol) { fatalError("init(client:) has not been implemented") } diff --git a/web3swift/src/Multicall/Multicall.swift b/web3swift/src/Multicall/Multicall.swift index b308d9e2..60dc8abc 100644 --- a/web3swift/src/Multicall/Multicall.swift +++ b/web3swift/src/Multicall/Multicall.swift @@ -9,14 +9,18 @@ import Foundation public typealias MulticallResponse = Multicall.Response public struct Multicall { - private let client: EthereumClientProtocol + private let client: EthereumRPCProtocol - public init(client: EthereumClientProtocol) { + public init(client: EthereumRPCProtocol) { self.client = client } + private var network: EthereumNetwork { + client.network + } + public func aggregate(calls: [Call]) async throws -> MulticallResponse { - guard let network = client.network, let contract = Contract.registryAddress(for: network) else { + guard let contract = Contract.registryAddress(for: network) else { throw MulticallError.contractUnavailable } @@ -120,7 +124,7 @@ extension Multicall { public init?(values: [ABIDecoder.DecodedValue]) throws { self.success = try values[0].decoded() - self.returnData = try values[1].entry[0] + self.returnData = values[1].entry[0] } public func encode(to encoder: ABIFunctionEncoder) throws { diff --git a/web3swift/src/SIWE/SiweMessage+String.swift b/web3swift/src/SIWE/SiweMessage+String.swift index 8de8c5d9..9ec51146 100644 --- a/web3swift/src/SIWE/SiweMessage+String.swift +++ b/web3swift/src/SIWE/SiweMessage+String.swift @@ -82,7 +82,7 @@ extension SiweMessage: CustomStringConvertible { /// - https://example.com/my-web2-claim.json /// ``` /// - Parameter description: the SIWE string message following EIP-4361 standard. - /// - Throws: `SiweMessage.RegExError` if an error occured while parsing the string; + /// - Throws: `SiweMessage.RegExError` if an error occurred while parsing the string; /// `SiweMessage.ValidationError` in case regex parsing was successful but data in the message was invalid; /// might throw `DecodingError` since we use `Decodable` to transform parsed values into `SiweMessage`. public init(_ description: String) throws { diff --git a/web3swift/src/SIWE/SiweVerifier.swift b/web3swift/src/SIWE/SiweVerifier.swift index c759e99b..32e90fc3 100644 --- a/web3swift/src/SIWE/SiweVerifier.swift +++ b/web3swift/src/SIWE/SiweVerifier.swift @@ -8,7 +8,7 @@ import Foundation /// An object which will verify if a given `SiweMessage` and signature match with the EVM address provided public class SiweVerifier { - /// Errors thrown when verifing a given message agains a signature + /// Errors thrown when verifing a given message against a signature public enum Error: Swift.Error { /// The provided message is from a different network than the client's. case differentNetwork @@ -22,10 +22,10 @@ public class SiweVerifier { case invalidSignature } - private let client: EthereumClientProtocol + private let client: EthereumRPCProtocol private let dateProvider: () -> Date - public init(client: EthereumClientProtocol, dateProvider: @escaping () -> Date = Date.init) { + public init(client: EthereumRPCProtocol, dateProvider: @escaping () -> Date = Date.init) { self.client = client self.dateProvider = dateProvider } @@ -62,7 +62,7 @@ public class SiweVerifier { } } - guard message.chainId == client.network?.intValue else { + guard message.chainId == client.network.intValue else { throw Error.differentNetwork } diff --git a/web3swift/src/Utils/RLP.swift b/web3swift/src/Utils/RLP.swift index 95ba3299..339e058e 100644 --- a/web3swift/src/Utils/RLP.swift +++ b/web3swift/src/Utils/RLP.swift @@ -6,8 +6,8 @@ import BigInt import Foundation -struct RLP { - static func encode(_ item: Any) -> Data? { +public struct RLP { + public static func encode(_ item: Any) -> Data? { switch item { case let int as Int: return encodeInt(int) @@ -21,6 +21,8 @@ struct RLP { return encodeBigUInt(buint) case let data as Data: return encodeData(data) + case let address as EthereumAddress: + return encodeAddress(address) default: return nil } @@ -37,6 +39,10 @@ struct RLP { return encodeData(data) } + static func encodeAddress(_ address: EthereumAddress) -> Data? { + encodeString(address.asString()) + } + static func encodeInt(_ int: Int) -> Data? { guard int >= 0 else { return nil diff --git a/web3swift/src/ZKSync/ABIFunction+ZKSync.swift b/web3swift/src/ZKSync/ABIFunction+ZKSync.swift new file mode 100644 index 00000000..e26595d3 --- /dev/null +++ b/web3swift/src/ZKSync/ABIFunction+ZKSync.swift @@ -0,0 +1,37 @@ +// +// web3.swift +// Copyright © 2023 Argent Labs Limited. All rights reserved. +// + +import web3 +import BigInt +import Foundation + +extension ABIFunction { + public func zkSyncTransaction( + value: BigUInt? = nil, + gasPrice: BigUInt? = nil, + gasLimit: BigUInt? = nil, + chainId: Int? = nil, + nonce: Int? = nil + ) throws -> ZKSyncTransaction { + guard let from = from else { + throw ABIError.invalidValue + } + + let encoder = ABIFunctionEncoder(Self.name) + try encode(to: encoder) + let data = try encoder.encoded() + + return ZKSyncTransaction( + from: from, + to: contract, + value: value ?? 0, + data: data, + chainId: chainId, + nonce: nonce, + gasPrice: self.gasPrice ?? gasPrice ?? 0, + gasLimit: self.gasLimit ?? gasLimit ?? 0 + ) + } +} diff --git a/web3swift/src/ZKSync/EthereumAccount+ZKSync.swift b/web3swift/src/ZKSync/EthereumAccount+ZKSync.swift new file mode 100644 index 00000000..fdb7799f --- /dev/null +++ b/web3swift/src/ZKSync/EthereumAccount+ZKSync.swift @@ -0,0 +1,19 @@ +// +// web3.swift +// Copyright © 2022 Argent Labs Limited. All rights reserved. +// + +import web3 +import Foundation + +extension EthereumAccountProtocol { + func sign(zkTransaction: ZKSyncTransaction) throws -> ZKSyncSignedTransaction { + let typed = zkTransaction.eip712Representation + let signature = try signMessage(message: typed).web3.hexData! + + return .init( + transaction: zkTransaction, + signature: .init(raw: signature) + ) + } +} diff --git a/web3swift/src/ZKSync/ZKSyncProvider.swift b/web3swift/src/ZKSync/ZKSyncProvider.swift new file mode 100644 index 00000000..17eaba89 --- /dev/null +++ b/web3swift/src/ZKSync/ZKSyncProvider.swift @@ -0,0 +1,148 @@ +// +// web3.swift +// Copyright © 2022 Argent Labs Limited. All rights reserved. +// + +import web3 +import BigInt +import Logging +import Foundation + +#if canImport(FoundationNetworking) + import FoundationNetworking +#endif + +public protocol ZKSyncClientProtocol: EthereumRPCProtocol { + func eth_sendRawZKSyncTransaction(_ transaction: ZKSyncTransaction, withAccount account: EthereumAccountProtocol) async throws -> String + func gasPrice() async throws -> BigUInt + func estimateGas(_ transaction: ZKSyncTransaction) async throws -> BigUInt +} + +extension ZKSyncClientProtocol { + public func eth_sendRawZKSyncTransaction(_ transaction: ZKSyncTransaction, withAccount account: EthereumAccountProtocol) async throws -> String { + // Inject pending nonce + let nonce = try await self.eth_getTransactionCount(address: account.address, block: .Pending) + + var transaction = transaction + transaction.nonce = nonce + + if transaction.chainId == nil { + transaction.chainId = network.intValue + } + + guard let signedTx = try? account.sign(zkTransaction: transaction), + let transactionHex = signedTx.raw?.web3.hexString else { + throw EthereumClientError.encodeIssue + } + + guard let txHash = try await networkProvider.send( + method: "eth_sendRawTransaction", + params: [transactionHex], + receive: String.self + ) as? String else { + throw EthereumClientError.unexpectedReturnValue + } + + return txHash + } + + public func gasPrice() async throws -> BigUInt { + let emptyParams: [Bool] = [] + guard let data = try await networkProvider.send(method: "eth_gasPrice", params: emptyParams, receive: String.self) as? String else { + throw EthereumClientError.unexpectedReturnValue + } + + guard let value = BigUInt(hex: data) else { + throw EthereumClientError.unexpectedReturnValue + } + return value + } + + public func estimateGas(_ transaction: ZKSyncTransaction) async throws -> BigUInt { + let value = transaction.value > .zero ? transaction.value : nil + let params = EstimateGasParams( + from: transaction.from.asString(), + to: transaction.to.asString(), + gas: transaction.gasLimit?.web3.hexString, + gasPrice: transaction.gasPrice?.web3.hexString, + value: value?.web3.hexString, + data: transaction.data.web3.hexString + ) + + guard let data = try await networkProvider.send( + method: "eth_estimateGas", + params: params, + receive: String.self + ) as? String else { + throw EthereumClientError.unexpectedReturnValue + } + + guard let value = BigUInt(hex: data) else { + throw EthereumClientError.unexpectedReturnValue + } + return value + } +} + +struct EstimateGasParams: 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) + + 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.map(jsonRPCAmount) { + try nested.encode(gasPrice, forKey: .gasPrice) + } + if let value = value.map(jsonRPCAmount) { + try nested.encode(value, forKey: .value) + } + if let data = data { + try nested.encode(data, forKey: .data) + } + } +} + +public class ZKSyncClient: BaseEthereumClient, ZKSyncClientProtocol { + let networkQueue: OperationQueue + + public init( + url: URL, + sessionConfig: URLSessionConfiguration = URLSession.shared.configuration, + logger: Logger? = nil, + network: EthereumNetwork + ) { + let networkQueue = OperationQueue() + networkQueue.name = "web3swift.client.networkQueue" + networkQueue.maxConcurrentOperationCount = 4 + self.networkQueue = networkQueue + + let session = URLSession(configuration: sessionConfig, delegate: nil, delegateQueue: networkQueue) + super.init(networkProvider: HttpNetworkProvider(session: session, url: url), url: url, logger: logger, network: network) + } +} diff --git a/web3swift/src/ZKSync/ZKSyncTransaction.swift b/web3swift/src/ZKSync/ZKSyncTransaction.swift new file mode 100644 index 00000000..4b4ed42e --- /dev/null +++ b/web3swift/src/ZKSync/ZKSyncTransaction.swift @@ -0,0 +1,228 @@ +// +// web3.swift +// Copyright © 2022 Argent Labs Limited. All rights reserved. +// + +import web3 +import BigInt +import Foundation +import GenericJSON + +// to be filled in by client +public struct ZKSyncTransaction: Equatable { + public static let eip712Type: UInt8 = 0x71 + public static let defaultGasPerPubDataLimit: BigUInt = 50000 + public let txType: UInt8 = Self.eip712Type + public var from: EthereumAddress + public var to: EthereumAddress + public var value: BigUInt + public var data: Data + public var chainId: Int? + public var nonce: Int? + public var gasPrice: BigUInt? + public var gasLimit: BigUInt? + public var gasPerPubData: BigUInt + public var maxFeePerGas: BigUInt? + public var maxPriorityFeePerGas: BigUInt? + public var paymasterParams: PaymasterParams + + public init( + from: EthereumAddress, + to: EthereumAddress, + value: BigUInt, + data: Data, + chainId: Int? = nil, + nonce: Int? = nil, + gasPrice: BigUInt? = nil, + gasLimit: BigUInt? = nil, + gasPerPubData: BigUInt = ZKSyncTransaction.defaultGasPerPubDataLimit, + maxFeePerGas: BigUInt? = nil, + maxPriorityFeePerGas: BigUInt? = nil, + paymasterParams: PaymasterParams = .none + ) { + self.from = from + self.to = to + self.value = value + self.data = data + self.chainId = chainId + self.nonce = nonce + self.gasPrice = gasPrice + self.gasLimit = gasLimit + self.gasPerPubData = gasPerPubData + self.maxFeePerGas = maxFeePerGas + self.maxPriorityFeePerGas = maxPriorityFeePerGas + self.paymasterParams = paymasterParams + } + + public struct PaymasterParams: Equatable { + public var paymaster: EthereumAddress + public var input: Data + public init( + paymaster: EthereumAddress, + input: Data + ) { + self.paymaster = paymaster + self.input = input + } + + public var isEmpty: Bool { + self.paymaster == .zero + } + + public static let none: PaymasterParams = .init(paymaster: .zero, input: Data()) + } + + public var maxFee: BigUInt { + maxFeePerGas ?? gasPrice ?? 0 + } + + public var maxPriorityFee: BigUInt { + maxPriorityFeePerGas ?? maxFee + } + + public var paymaster: EthereumAddress { + paymasterParams.paymaster + } + + var paymasterInput: Data { + paymasterParams.input + } + + public var eip712Representation: TypedData { + let decoder = JSONDecoder() + let eip712 = try! decoder.decode(TypedData.self, from: eip712JSON) + return eip712 + } + + private var eip712JSON: Data { + """ + { + "types": { + "EIP712Domain": [ + {"name": "name", "type": "string"}, + {"name": "version", "type": "string"}, + {"name": "chainId", "type": "uint256"} + ], + "Transaction": [ + {"name": "txType","type": "uint256"}, + {"name": "from","type": "uint256"}, + {"name": "to","type": "uint256"}, + {"name": "gasLimit","type": "uint256"}, + {"name": "gasPerPubdataByteLimit","type": "uint256"}, + {"name": "maxFeePerGas", "type": "uint256"}, + {"name": "maxPriorityFeePerGas", "type": "uint256"}, + {"name": "paymaster", "type": "uint256"}, + {"name": "nonce","type": "uint256"}, + {"name": "value","type": "uint256"}, + {"name": "data","type": "bytes"}, + {"name": "factoryDeps","type": "bytes32[]"}, + {"name": "paymasterInput", "type": "bytes"} + ] + }, + "primaryType": "Transaction", + "domain": { + "name": "zkSync", + "version": "2", + "chainId": \(chainId!) + }, + "message": { + "txType" : \(txType), + "from" : "\(from.asNumber()!.description)", + "to" : "\(to.asNumber()!.description)", + "gasLimit" : "\(gasLimit!.description)", + "gasPerPubdataByteLimit" : "\(gasPerPubData.description)", + "maxFeePerGas" : "\(maxFee.description)", + "maxPriorityFeePerGas" : "\(maxPriorityFee.description)", + "paymaster" : "\(paymaster.asNumber()!.description)", + "nonce" : \(nonce!), + "value" : "\(value.description)", + "data" : "\(data.web3.hexString)", + "factoryDeps" : [], + "paymasterInput" : "\(paymasterInput.web3.hexString)" + } + } + """.data(using: .utf8)! + } +} + +public struct ZKSyncSignedTransaction { + public let transaction: ZKSyncTransaction + public let signature: Signature + + public init( + transaction: ZKSyncTransaction, + signature: Signature + ) { + self.transaction = transaction + self.signature = signature + } + + public var raw: Data? { + guard transaction.nonce != nil, transaction.chainId != nil, + transaction.gasPrice != nil, transaction.gasLimit != nil else { + return nil + } + + var txArray: [Any?] = [ + transaction.nonce, + transaction.maxPriorityFee, + transaction.maxFee, + transaction.gasLimit, + transaction.to, + transaction.value, + transaction.data + ] + + txArray.append(transaction.chainId) + txArray.append(Data()) + txArray.append(Data()) + + txArray.append(transaction.chainId) + txArray.append(transaction.from) + txArray.append(transaction.gasPerPubData) + // TODO: factorydeps + txArray.append([]) + + txArray.append(signature.flattened) + + if transaction.paymasterParams.isEmpty { + txArray.append([]) + } else { + txArray.append([ + transaction.paymaster, + transaction.paymasterInput + ]) + } + + return RLP.encode(txArray).flatMap { + Data([transaction.txType]) + $0.web3.bytes + } + } + + public var hash: Data? { + raw?.web3.keccak256 + } +} + +extension ABIFunction { + public func zkTransaction( + from: EthereumAddress, + value: BigUInt? = nil, + gasPrice: BigUInt? = nil, + gasLimit: BigUInt? = nil, + feeToken: EthereumAddress = .zero + ) throws -> ZKSyncTransaction { + let encoder = ABIFunctionEncoder(Self.name) + try self.encode(to: encoder) + let data = try encoder.encoded() + + return ZKSyncTransaction( + from: from, + to: contract, + value: value ?? 0, + data: data, + gasPrice: self.gasPrice ?? gasPrice ?? 0, + gasLimit: self.gasLimit ?? gasLimit ?? 0 + ) + } +}