From 1f5dfc896ce813aef9addd50ef756d95f44b6f09 Mon Sep 17 00:00:00 2001 From: Miguel Angel Quinones Date: Tue, 28 Feb 2023 10:01:48 +0100 Subject: [PATCH] [ADD] ZK2: Experimental support --- Package.resolved | 4 +- Package.swift | 16 +- ...EthereumAccount+SignTransactionTests.swift | 17 +- web3sTests/TestConfig.swift | 12 + .../ZKSync/EthereumClient+ZKSyncTests.swift | 60 +++++ .../ZKSync/ZKSyncTransactionTests.swift | 40 +++ .../EthereumAccount+SignTransaction.swift | 12 +- web3swift/src/Account/EthereumAccount.swift | 2 + web3swift/src/Account/Signature.swift | 58 +++++ ...ll.swift => BaseEthereumClient+Call.swift} | 0 web3swift/src/Client/BaseEthereumClient.swift | 19 +- .../{ => HTTP}/EthereumHttpClient.swift | 0 .../src/Client/Models/EthereumNetwork.swift | 6 +- .../Client/Models/EthereumTransaction.swift | 28 ++- .../HttpNetworkProvider.swift | 8 +- .../NetworkProviderProtocol.swift | 2 +- .../EthereumClientProtocol.swift | 24 +- .../Client/Protocols/EthereumProvider.swift | 57 +++++ .../{ => WSS}/EthereumWebSocketClient.swift | 0 .../Statically Typed/ABIEncoder+Static.swift | 3 +- web3swift/src/Utils/RLP.swift | 4 +- .../src/ZKSync/EthereumAccount+ZKSync.swift | 19 ++ web3swift/src/ZKSync/ZKSyncProvider.swift | 148 ++++++++++++ web3swift/src/ZKSync/ZKSyncTransaction.swift | 228 ++++++++++++++++++ 24 files changed, 683 insertions(+), 84 deletions(-) create mode 100644 web3sTests/ZKSync/EthereumClient+ZKSyncTests.swift create mode 100644 web3sTests/ZKSync/ZKSyncTransactionTests.swift create mode 100644 web3swift/src/Account/Signature.swift rename web3swift/src/Client/{EthereumClient+Call.swift => BaseEthereumClient+Call.swift} (100%) rename web3swift/src/Client/{ => HTTP}/EthereumHttpClient.swift (100%) rename web3swift/src/Client/{ => Protocols}/EthereumClientProtocol.swift (90%) create mode 100644 web3swift/src/Client/Protocols/EthereumProvider.swift rename web3swift/src/Client/{ => WSS}/EthereumWebSocketClient.swift (100%) create mode 100644 web3swift/src/ZKSync/EthereumAccount+ZKSync.swift create mode 100644 web3swift/src/ZKSync/ZKSyncProvider.swift create mode 100644 web3swift/src/ZKSync/ZKSyncTransaction.swift diff --git a/Package.resolved b/Package.resolved index 08ffadef..f7d4e9cb 100644 --- a/Package.resolved +++ b/Package.resolved @@ -24,8 +24,8 @@ "repositoryURL": "https://github.com/GigaBitcoin/secp256k1.swift.git", "state": { "branch": null, - "revision": "39dd39248e769ea88253a0ce300399b402a64529", - "version": "0.9.2" + "revision": "48fb20fce4ca3aad89180448a127d5bc16f0e44c", + "version": "0.10.0" } }, { diff --git a/Package.swift b/Package.swift index 9e4f7ccb..b6ab33c0 100644 --- a/Package.swift +++ b/Package.swift @@ -9,7 +9,8 @@ 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"), @@ -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/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/TestConfig.swift b/web3sTests/TestConfig.swift index b3571891..d5b3dc20 100644 --- a/web3sTests/TestConfig.swift +++ b/web3sTests/TestConfig.swift @@ -29,4 +29,16 @@ struct TestConfig { static let erc165Contract = "0xA2618a1c426a1684E00cA85b5C736164AC391d35" static let webSocketConfig = WebSocketConfiguration(maxFrameSize: 1_000_000) + + enum ZKSync { + static let chainId = 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/ZKSync/EthereumClient+ZKSyncTests.swift b/web3sTests/ZKSync/EthereumClient+ZKSyncTests.swift new file mode 100644 index 00000000..177cc9a6 --- /dev/null +++ b/web3sTests/ZKSync/EthereumClient+ZKSyncTests.swift @@ -0,0 +1,60 @@ +// +// 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) + var eoaEthTransfer = ZKSyncTransaction( + from: .init(TestConfig.publicKey), + to: .init("0x64d0eA4FC60f27E74f1a70Aa6f39D403bBe56793"), + value: 100, + data: Data(), + gasLimit: 300000 + ) + + 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..eaceba62 --- /dev/null +++ b/web3sTests/ZKSync/ZKSyncTransactionTests.swift @@ -0,0 +1,40 @@ +// +// 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 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_GivenEOATransfer_EncodesCorrectly() { + let signature = "0x55943b2228183717fd3be583bde0f6ec168247ea8d304eb13b3e7e76ebf6bf2c3c77734e163711c5963ac25a15f95d9ac63b82c2c427fd4eb011c5e3a22f89221b".web3.hexData! + let signed = ZKSyncSignedTransaction( + transaction: eoaTransfer, signature: .init(raw: signature) + ) + XCTAssertEqual(signed.raw?.web3.hexString, "0x71f891048405f5e1008405f5e10083080a229464d0ea4fc60f27e74f1a70aa6f39d403bbe56793865af3107a400080820118808082011894e78e5ecb061fe3dd1672ddda7b5116213b23b99a82c350c0b84155943b2228183717fd3be583bde0f6ec168247ea8d304eb13b3e7e76ebf6bf2c3c77734e163711c5963ac25a15f95d9ac63b82c2c427fd4eb011c5e3a22f89221bc0") + } + + func test_GivenEOATransfer_WhenSigningWithEOAAccount_ThenSignsAndEncodesCorrectly() { + let signed = try? eoaAccount.sign(zkTransaction: eoaTransfer) + + XCTAssertEqual(signed?.raw?.web3.hexString, + "0x71f891048405f5e1008405f5e10083080a229464d0ea4fc60f27e74f1a70aa6f39d403bbe56793865af3107a400080820118808082011894e78e5ecb061fe3dd1672ddda7b5116213b23b99a82c350c0b841c956ba7bfdf54a6d3f3b21c51465ad37df22b6258835b6e162259d6d3eec02ae11f9d17c3aafd47df49bd77e33befed87bbaff44e4c497228bfa8bcc9fa64bc31bc0") + } +} 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 8305ad09..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 } 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/Client/EthereumClient+Call.swift b/web3swift/src/Client/BaseEthereumClient+Call.swift similarity index 100% rename from web3swift/src/Client/EthereumClient+Call.swift rename to web3swift/src/Client/BaseEthereumClient+Call.swift diff --git a/web3swift/src/Client/BaseEthereumClient.swift b/web3swift/src/Client/BaseEthereumClient.swift index b234c473..3e7c65e2 100644 --- a/web3swift/src/Client/BaseEthereumClient.swift +++ b/web3swift/src/Client/BaseEthereumClient.swift @@ -11,16 +11,16 @@ 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? - init( + public init( networkProvider: NetworkProviderProtocol, url: URL, logger: Logger? = nil, @@ -205,19 +205,6 @@ public class BaseEthereumClient: EthereumClientProtocol { } } - 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 eth_getTransaction(byHash txHash: String) async throws -> EthereumTransaction { do { let data = try await networkProvider.send(method: "eth_getTransactionByHash", params: [txHash], receive: EthereumTransaction.self) diff --git a/web3swift/src/Client/EthereumHttpClient.swift b/web3swift/src/Client/HTTP/EthereumHttpClient.swift similarity index 100% rename from web3swift/src/Client/EthereumHttpClient.swift rename to web3swift/src/Client/HTTP/EthereumHttpClient.swift 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 c067a817..deedf9c4 100644 --- a/web3swift/src/Client/Models/EthereumTransaction.swift +++ b/web3swift/src/Client/Models/EthereumTransaction.swift @@ -162,15 +162,29 @@ 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? { 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 90% rename from web3swift/src/Client/EthereumClientProtocol.swift rename to web3swift/src/Client/Protocols/EthereumClientProtocol.swift index a281b030..1aefc9d2 100644 --- a/web3swift/src/Client/EthereumClientProtocol.swift +++ b/web3swift/src/Client/Protocols/EthereumClientProtocol.swift @@ -11,28 +11,7 @@ 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 { +public protocol EthereumClientProtocol: EthereumRPCProtocol, AnyObject { var network: EthereumNetwork? { get } func net_version(completionHandler: @escaping (Result) -> Void) @@ -69,7 +48,6 @@ public protocol EthereumClientProtocol: AnyObject { 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( diff --git a/web3swift/src/Client/Protocols/EthereumProvider.swift b/web3swift/src/Client/Protocols/EthereumProvider.swift new file mode 100644 index 00000000..0b688e18 --- /dev/null +++ b/web3swift/src/Client/Protocols/EthereumProvider.swift @@ -0,0 +1,57 @@ +// +// web3.swift +// Copyright © 2022 Argent Labs Limited. All rights reserved. +// + +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 +} + +public extension EthereumRPCProtocol { + 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) + } + } + + 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 + } + } +} diff --git a/web3swift/src/Client/EthereumWebSocketClient.swift b/web3swift/src/Client/WSS/EthereumWebSocketClient.swift similarity index 100% rename from web3swift/src/Client/EthereumWebSocketClient.swift rename to web3swift/src/Client/WSS/EthereumWebSocketClient.swift diff --git a/web3swift/src/Contract/Statically Typed/ABIEncoder+Static.swift b/web3swift/src/Contract/Statically Typed/ABIEncoder+Static.swift index f61b44fe..bdf37918 100644 --- a/web3swift/src/Contract/Statically Typed/ABIEncoder+Static.swift +++ b/web3swift/src/Contract/Statically Typed/ABIEncoder+Static.swift @@ -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/Utils/RLP.swift b/web3swift/src/Utils/RLP.swift index c909ed01..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) 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..bd5088c7 --- /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, let network = self.network { + 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? = nil + ) { + 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 + ) + } +}