From 3e7ab99a4973eeaac9a0a18c5b2eb6f8c82765a2 Mon Sep 17 00:00:00 2001 From: Dionysios Karatzas Date: Thu, 2 Jun 2022 11:02:30 +0300 Subject: [PATCH] Add WebSocket Client MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add to ability to use an elg or create a new Remove request after calling its callback Finalize WebSocket EthereumClientProtocol methods - Add WebSocket tests Optimizations / additional features: - WebSocket reconnect functionallity - Remove pending requests on socket close - Disable testSimpleEthGetLogs till fixed - Remove pending request after failure - Rename request to requestQueue - Add SwiftLogger Add Queue’s Fix promise failure handling Add subscriptions logic, optimizations Add newPendingTransaction, newBlockHeader, syncing subscriptions Clear subscriptions on reconnection, disable failing ws tests Add input property on EthereumTransaction Formatting, optimizatios Add eth_getLogs errorHandling Fix EthereumSync decoding Re-enable testSimpleEthGetLogs test, add webSocketConfig for tests with higher maxFrameSize to resolve `message too large ws error` Log close code for debug Add WebSocketClient tests Disable some tests on linux to find the reason that are failing Make use of Logger Rebase, fix conflicts Reciew fixes, rebase Add ThreadSafety to shared resources Remove deprecated methods --- Package.resolved | 36 + Package.swift | 8 +- web3sTests/Client/EthereumClientTests.swift | 157 ++- web3sTests/Contract/ABIEventTests.swift | 11 +- web3sTests/ENS/ENSOffchainTests.swift | 11 +- web3sTests/ENS/ENSTests.swift | 13 +- web3sTests/ERC165/ERC165Tests.swift | 8 +- web3sTests/ERC20/ERC20Tests.swift | 8 +- web3sTests/ERC721/ERC721Tests.swift | 27 +- web3sTests/Multicall/MulticallTests.swift | 8 +- .../OffchainLookup/OffchainLookupTests.swift | 14 +- web3sTests/TestConfig.swift | 6 + web3swift/src/Account/EthereumAccount.swift | 10 +- .../src/Client/EthereumClient+Call.swift | 39 +- web3swift/src/Client/EthereumClient.swift | 283 +---- .../src/Client/EthereumClientProtocol.swift | 135 +++ .../src/Client/EthereumWebSocketClient.swift | 1029 +++++++++++++++++ web3swift/src/Client/JSONRPC.swift | 19 +- .../src/Client/Models/EthereumBlock.swift | 14 +- .../src/Client/Models/EthereumBlockInfo.swift | 13 +- .../src/Client/Models/EthereumHeader.swift | 28 + web3swift/src/Client/Models/EthereumLog.swift | 22 +- .../src/Client/Models/EthereumNetwork.swift | 10 +- .../Client/Models/EthereumSubscription.swift | 40 + .../Client/Models/EthereumSyncStatus.swift | 86 ++ .../Client/Models/EthereumTransaction.swift | 37 +- .../Models/EthereumTransactionReceipt.swift | 18 +- .../src/Client/RecursiveLogCollector.swift | 6 +- .../EthereumClient+Static.swift | 167 --- web3swift/src/ENS/EthereumNameService.swift | 36 +- web3swift/src/ERC165/ERC165.swift | 15 - web3swift/src/ERC20/ERC20.swift | 102 +- web3swift/src/ERC721/ERC721.swift | 161 --- .../src/Extensions/ResultExtensions.swift | 25 + web3swift/src/Utils/KeyUtil.swift | 19 +- 35 files changed, 1758 insertions(+), 863 deletions(-) create mode 100644 web3swift/src/Client/EthereumClientProtocol.swift create mode 100644 web3swift/src/Client/EthereumWebSocketClient.swift create mode 100644 web3swift/src/Client/Models/EthereumHeader.swift create mode 100644 web3swift/src/Client/Models/EthereumSubscription.swift create mode 100644 web3swift/src/Client/Models/EthereumSyncStatus.swift create mode 100644 web3swift/src/Extensions/ResultExtensions.swift diff --git a/Package.resolved b/Package.resolved index 0e5dba65..4c9e8aae 100644 --- a/Package.resolved +++ b/Package.resolved @@ -36,6 +36,42 @@ "revision": "1a796f738bdcd84b41d05f92593188b23163e60b", "version": "0.7.0" } + }, + { + "package": "swift-log", + "repositoryURL": "https://github.com/apple/swift-log.git", + "state": { + "branch": null, + "revision": "5d66f7ba25daf4f94100e7022febf3c75e37a6c7", + "version": "1.4.2" + } + }, + { + "package": "swift-nio", + "repositoryURL": "https://github.com/apple/swift-nio.git", + "state": { + "branch": null, + "revision": "124119f0bb12384cef35aa041d7c3a686108722d", + "version": "2.40.0" + } + }, + { + "package": "swift-nio-ssl", + "repositoryURL": "https://github.com/apple/swift-nio-ssl.git", + "state": { + "branch": null, + "revision": "1750873bce84b4129b5303655cce2c3d35b9ed3a", + "version": "2.19.0" + } + }, + { + "package": "websocket-kit", + "repositoryURL": "https://github.com/vapor/websocket-kit.git", + "state": { + "branch": null, + "revision": "e32033ad3c68ebec1b761bc961be7bd56bad02f8", + "version": "2.3.1" + } } ] }, diff --git a/Package.swift b/Package.swift index c57053db..401539d1 100644 --- a/Package.swift +++ b/Package.swift @@ -14,7 +14,9 @@ let package = Package( .package(name: "BigInt", url: "https://github.com/attaswift/BigInt", from: "5.0.0"), .package(name: "GenericJSON", url: "https://github.com/zoul/generic-json-swift", from: "2.0.0"), .package(url: "https://github.com/GigaBitcoin/secp256k1.swift.git", .upToNextMajor(from: "0.6.0")), - .package(url: "https://github.com/OpenCombine/OpenCombine.git", from: "0.13.0") + .package(url: "https://github.com/OpenCombine/OpenCombine.git", from: "0.13.0"), + .package(url: "https://github.com/vapor/websocket-kit.git", from: "2.0.0"), + .package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"), ], targets: [ .target( @@ -29,7 +31,9 @@ let package = Package( .product(name: "BigInt", package: "BigInt"), "OpenCombine", .product(name: "OpenCombineFoundation", package: "OpenCombine"), - .product(name: "secp256k1", package: "secp256k1.swift") + .product(name: "secp256k1", package: "secp256k1.swift"), + .product(name: "WebSocketKit", package: "websocket-kit"), + .product(name: "Logging", package: "swift-log") ], path: "web3swift/src" ), diff --git a/web3sTests/Client/EthereumClientTests.swift b/web3sTests/Client/EthereumClientTests.swift index 46ea8e86..d7309047 100644 --- a/web3sTests/Client/EthereumClientTests.swift +++ b/web3sTests/Client/EthereumClientTests.swift @@ -9,6 +9,7 @@ import XCTest @testable import web3 import BigInt +import NIO struct TransferMatchingSignatureEvent: ABIEvent { public static let name = "Transfer" @@ -31,9 +32,8 @@ struct TransferMatchingSignatureEvent: ABIEvent { } } - class EthereumClientTests: XCTestCase { - var client: EthereumClient? + var client: EthereumClientProtocol? var account: EthereumAccount? override func setUp() { @@ -110,7 +110,7 @@ class EthereumClientTests: XCTestCase { func testEthGetCode() async { do { - let code = try await client?.eth_getCode(address: EthereumAddress("0x112234455c3a32fd11230c42e7bccd4a84e02010")) + let code = try await client?.eth_getCode(address: EthereumAddress("0x112234455c3a32fd11230c42e7bccd4a84e02010"), block: .Latest) XCTAssertNotNil(code, "Contract code not available") } catch { XCTFail("Expected code but failed \(error).") @@ -443,3 +443,154 @@ struct InvalidMethodB: ABIFunction { func encode(to encoder: ABIFunctionEncoder) throws { } } + +class EthereumWebSocketClientTests: EthereumClientTests { + var delegateExpectation: XCTestExpectation? + + override func setUp() { + super.setUp() + self.client = EthereumWebSocketClient(url: TestConfig.wssUrl, configuration: TestConfig.webSocketConfig) + + } +#if os(Linux) +// On Linux some tests are fail. Need investigation +#else + func testWebSocketState() { + guard let client = client as? EthereumWebSocketClient else { + XCTFail("Expected client to be EthereumWebSocketClient") + return + } + XCTAssertEqual(client.currentState, EthereumWebSocketClient.State.open) + + let clientClose = client.eventLoopGroup.next().makePromise(of: Void.self) + client.exposeWebSocket()?.onClose.cascade(to: clientClose) + client.disconnect() + XCTAssertNoThrow(try clientClose.futureResult.wait()) + XCTAssertEqual(client.currentState, EthereumWebSocketClient.State.closed) + + client.connect() + XCTAssertEqual(client.currentState, EthereumWebSocketClient.State.open) + } + + func testWebSocketNoAutomaticOpen() { + self.client = EthereumWebSocketClient(url: TestConfig.wssUrl, configuration: .init(automaticOpen: false)) + + guard let client = client as? EthereumWebSocketClient else { + XCTFail("Expected client to be EthereumWebSocketClient") + return + } + + XCTAssertEqual(client.currentState, EthereumWebSocketClient.State.closed) + } + + func testWebSocketConnect() { + self.client = EthereumWebSocketClient(url: TestConfig.wssUrl, configuration: .init(automaticOpen: false)) + + guard let client = client as? EthereumWebSocketClient else { + XCTFail("Expected client to be EthereumWebSocketClient") + return + } + + XCTAssertEqual(client.currentState, EthereumWebSocketClient.State.closed) + + client.connect() + + XCTAssertEqual(client.currentState, EthereumWebSocketClient.State.open) + XCTAssertTrue(!client.exposeWebSocket()!.isClosed) + } + + func testWebSocketPendingTransactions() async { + do { + guard let client = client as? EthereumWebSocketClient else { + XCTFail("Expected client to be EthereumWebSocketClient") + return + } + + var expectation: XCTestExpectation? = self.expectation(description: "Pending Transaction") + let subscription = try await client.pendingTransactions { txHash in + expectation?.fulfill() + expectation = nil + } + + await waitForExpectations(timeout: 5, handler: nil) + + XCTAssertNotEqual(subscription.id, "") + XCTAssertEqual(subscription.type, .pendingTransactions) + } catch { + XCTFail("Expected subscription but failed \(error).") + } + } + + func testWebSocketNewBlockHeaders() async { + do { + guard let client = client as? EthereumWebSocketClient else { + XCTFail("Expected client to be EthereumWebSocketClient") + return + } + + var expectation: XCTestExpectation? = self.expectation(description: "New Block Headers") + let subscription = try await client.newBlockHeaders { header in + expectation?.fulfill() + expectation = nil + } + + // we need a high timeout as new block might take a while + await waitForExpectations(timeout: 2500, handler: nil) + + XCTAssertNotEqual(subscription.id, "") + XCTAssertEqual(subscription.type, .newBlockHeaders) + } catch { + XCTFail("Expected subscription but failed \(error).") + } + } + + func testWebSocketSubscribe() async { + do { + guard let client = client as? EthereumWebSocketClient else { + XCTFail("Expected client to be EthereumWebSocketClient") + return + } + client.delegate = self + + delegateExpectation = expectation(description: "onNewPendingTransaction delegate call") + var subscription = try await client.subscribe(type: .pendingTransactions) + await waitForExpectations(timeout: 10) + _ = try await client.unsubscribe(subscription) + + delegateExpectation = expectation(description: "onNewBlockHeader delegate call") + subscription = try await client.subscribe(type: .newBlockHeaders) + await waitForExpectations(timeout: 2500) + _ = try await client.unsubscribe(subscription) + } catch { + XCTFail("Expected subscription but failed \(error).") + } + } + + func testWebSocketUnsubscribe() async { + do { + guard let client = client as? EthereumWebSocketClient else { + XCTFail("Expected client to be EthereumWebSocketClient") + return + } + + let subscription = try await client.subscribe(type: .newBlockHeaders) + let result = try await client.unsubscribe(subscription) + XCTAssertTrue(result) + } catch { + XCTFail("Expected subscription but failed \(error).") + } + } +#endif +} + +extension EthereumWebSocketClientTests: EthereumWebSocketClientDelegate { + func onNewPendingTransaction(subscription: EthereumSubscription, txHash: String) { + delegateExpectation?.fulfill() + delegateExpectation = nil + } + + func onNewBlockHeader(subscription: EthereumSubscription, header: EthereumHeader) { + delegateExpectation?.fulfill() + delegateExpectation = nil + } +} diff --git a/web3sTests/Contract/ABIEventTests.swift b/web3sTests/Contract/ABIEventTests.swift index 37a14216..5d734f3b 100644 --- a/web3sTests/Contract/ABIEventTests.swift +++ b/web3sTests/Contract/ABIEventTests.swift @@ -11,9 +11,10 @@ import BigInt @testable import web3 class ABIEventTests: XCTestCase { - var client: EthereumClient! + var client: EthereumClientProtocol! override func setUp() { + super.setUp() self.client = EthereumClient(url: URL(string: TestConfig.clientUrl)!) } @@ -59,6 +60,13 @@ class ABIEventTests: XCTestCase { } } +class ABIEventWebSocketTests: ABIEventTests { + override func setUp() { + super.setUp() + self.client = EthereumWebSocketClient(url: TestConfig.wssUrl, configuration: TestConfig.webSocketConfig) + } +} + struct EnabledStaticCall: ABIEvent { static let name = "EnabledStaticCall" static let types: [ABIType.Type] = [EthereumAddress.self,Data4.self] @@ -95,3 +103,4 @@ struct UpgraderRegistered: ABIEvent { self.name = try data[0].decoded() } } + diff --git a/web3sTests/ENS/ENSOffchainTests.swift b/web3sTests/ENS/ENSOffchainTests.swift index f918f2ad..8f994522 100644 --- a/web3sTests/ENS/ENSOffchainTests.swift +++ b/web3sTests/ENS/ENSOffchainTests.swift @@ -11,7 +11,7 @@ import XCTest class ENSOffchainTests: XCTestCase { var account: EthereumAccount? - var client: EthereumClient! + var client: EthereumClientProtocol! override func setUp() { super.setUp() @@ -149,3 +149,12 @@ class ENSOffchainTests: XCTestCase { } } +// TODO: Disable till feature implementation +/* +class ENSOffchainWebSocketTests: ENSOffchainTests { + override func setUp() { + super.setUp() + self.client = EthereumWebSocketClient(url: TestConfig.wssUrl, configuration: TestConfig.webSocketConfig) + } +} +*/ diff --git a/web3sTests/ENS/ENSTests.swift b/web3sTests/ENS/ENSTests.swift index 686150b4..40e50a2a 100644 --- a/web3sTests/ENS/ENSTests.swift +++ b/web3sTests/ENS/ENSTests.swift @@ -11,7 +11,7 @@ import XCTest class ENSTests: XCTestCase { var account: EthereumAccount? - var client: EthereumClient! + var client: EthereumClientProtocol! override func setUp() { super.setUp() @@ -30,7 +30,7 @@ class ENSTests: XCTestCase { let tx = try function.transaction() - let dataStr = try await client?.eth_call(tx, block: .Latest) + let dataStr = try await client?.eth_call(tx, resolution: .noOffchain(failOnExecutionError: true), block: .Latest) guard let dataStr = dataStr else { XCTFail() return @@ -208,3 +208,12 @@ class ENSTests: XCTestCase { } } +// TODO: Disable till feature implementation +/* +class ENSWebSocketTests: ENSTests { + override func setUp() { + super.setUp() + self.client = EthereumWebSocketClient(url: TestConfig.wssUrl, configuration: TestConfig.webSocketConfig) + } +} +*/ diff --git a/web3sTests/ERC165/ERC165Tests.swift b/web3sTests/ERC165/ERC165Tests.swift index 910cc846..a1a57254 100644 --- a/web3sTests/ERC165/ERC165Tests.swift +++ b/web3sTests/ERC165/ERC165Tests.swift @@ -11,7 +11,7 @@ import BigInt @testable import web3 class ERC165Tests: XCTestCase { - var client: EthereumClient! + var client: EthereumClientProtocol! var erc165: ERC165! let address = EthereumAddress(TestConfig.erc165Contract) @@ -44,3 +44,9 @@ class ERC165Tests: XCTestCase { } } +class ERC165WebSocketTests: ERC165Tests { + override func setUp() { + super.setUp() + self.client = EthereumWebSocketClient(url: TestConfig.wssUrl, configuration: TestConfig.webSocketConfig) + } +} diff --git a/web3sTests/ERC20/ERC20Tests.swift b/web3sTests/ERC20/ERC20Tests.swift index ee9a12d8..d213aa1a 100644 --- a/web3sTests/ERC20/ERC20Tests.swift +++ b/web3sTests/ERC20/ERC20Tests.swift @@ -11,7 +11,7 @@ import BigInt @testable import web3 class ERC20Tests: XCTestCase { - var client: EthereumClient? + var client: EthereumClientProtocol? var erc20: ERC20? let testContractAddress = EthereumAddress(TestConfig.erc20Contract) @@ -115,3 +115,9 @@ class ERC20Tests: XCTestCase { } } +class ERC20WebSocketTests: ERC20Tests { + override func setUp() { + super.setUp() + self.client = EthereumWebSocketClient(url: TestConfig.wssUrl, configuration: TestConfig.webSocketConfig) + } +} diff --git a/web3sTests/ERC721/ERC721Tests.swift b/web3sTests/ERC721/ERC721Tests.swift index bbacb4ad..f5e4455b 100644 --- a/web3sTests/ERC721/ERC721Tests.swift +++ b/web3sTests/ERC721/ERC721Tests.swift @@ -21,7 +21,7 @@ let nftImageURL = URL(string: "https://ipfs.io/ipfs/QmUDJMmiJEsueLbr6jxh7vhSSFAv let nftURL = URL(string: "https://ipfs.io/ipfs/QmUtKP7LnZnL2pWw2ERvNDndP9v5EPoJH7g566XNdgoRfE")! class ERC721Tests: XCTestCase { - var client: EthereumClient! + var client: EthereumClientProtocol! var erc721: ERC721! let address = EthereumAddress(TestConfig.erc721Contract) @@ -100,7 +100,7 @@ class ERC721Tests: XCTestCase { } class ERC721MetadataTests: XCTestCase { - var client: EthereumClient! + var client: EthereumClientProtocol! var erc721: ERC721Metadata! let address = EthereumAddress(TestConfig.erc721Contract) let nftDetails = ERC721Metadata.Token(title: "Asset Metadata", @@ -158,7 +158,7 @@ class ERC721MetadataTests: XCTestCase { } class ERC721EnumerableTests: XCTestCase { - var client: EthereumClient! + var client: EthereumClientProtocol! var erc721: ERC721Enumerable! let address = EthereumAddress(TestConfig.erc721Contract) @@ -208,3 +208,24 @@ class ERC721EnumerableTests: XCTestCase { } } } + +class ERC721WebSocketTests: ERC721Tests { + override func setUp() { + super.setUp() + self.client = EthereumWebSocketClient(url: TestConfig.wssUrl, configuration: TestConfig.webSocketConfig) + } +} + +class ERC721MetadataWebSocketTests: ERC721MetadataTests { + override func setUp() { + super.setUp() + self.client = EthereumWebSocketClient(url: TestConfig.wssUrl, configuration: TestConfig.webSocketConfig) + } +} + +class ERC721EnumerableWebSocketTests: ERC721EnumerableTests { + override func setUp() { + super.setUp() + self.client = EthereumWebSocketClient(url: TestConfig.wssUrl, configuration: TestConfig.webSocketConfig) + } +} diff --git a/web3sTests/Multicall/MulticallTests.swift b/web3sTests/Multicall/MulticallTests.swift index 2e085ebf..da2c4173 100644 --- a/web3sTests/Multicall/MulticallTests.swift +++ b/web3sTests/Multicall/MulticallTests.swift @@ -10,7 +10,7 @@ import XCTest @testable import web3 class MulticallTests: XCTestCase { - var client: EthereumClient! + var client: EthereumClientProtocol! var multicall: Multicall! let testContractAddress = EthereumAddress(TestConfig.erc20Contract) @@ -58,3 +58,9 @@ class MulticallTests: XCTestCase { } } +class MulticallWebSocketTests: MulticallTests { + override func setUp() { + super.setUp() + self.client = EthereumWebSocketClient(url: TestConfig.wssUrl, configuration: TestConfig.webSocketConfig) + } +} diff --git a/web3sTests/OffchainLookup/OffchainLookupTests.swift b/web3sTests/OffchainLookup/OffchainLookupTests.swift index 4ba29426..ad38dad1 100644 --- a/web3sTests/OffchainLookup/OffchainLookupTests.swift +++ b/web3sTests/OffchainLookup/OffchainLookupTests.swift @@ -141,7 +141,7 @@ extension EthereumClientError { } class OffchainLookupTests: XCTestCase { - var client: EthereumClient! + var client: EthereumClientProtocol! var account: EthereumAccount! var offchainLookup = OffchainLookup(address: .zero, urls: [], callData: Data(), callbackFunction: Data(), extraData: Data()) @@ -160,7 +160,7 @@ class OffchainLookupTests: XCTestCase { let tx = try! function.transaction() do { - let _ = try await client.eth_call(tx) + let _ = try await client.eth_call(tx, resolution: .noOffchain(failOnExecutionError: true), block: .Latest) XCTFail("Expecting error, not return value") } catch let error { let error = (error as? EthereumClientError)?.executionError @@ -332,3 +332,13 @@ fileprivate func expectedResponse( .flatMap { $0 } ).web3.keccak256.web3.hexString } + +// TODO: Disable till feature implementation +/* +class OffchainLookupWebSocketTests: OffchainLookupTests { + override func setUp() { + super.setUp() + self.client = EthereumWebSocketClient(url: TestConfig.wssUrl, configuration: TestConfig.webSocketConfig) + } +} +*/ diff --git a/web3sTests/TestConfig.swift b/web3sTests/TestConfig.swift index bbba4142..85561a5c 100644 --- a/web3sTests/TestConfig.swift +++ b/web3sTests/TestConfig.swift @@ -7,10 +7,14 @@ // import Foundation +import web3 struct TestConfig { // This is the proxy URL for connecting to the Blockchain. For testing we usually use the Ropsten network on Infura. Using free tier, so might hit rate limits static let clientUrl = "https://ropsten.infura.io/v3/b2f4b3f635d8425c96854c3d28ba6bb0" + + // This is the proxy wss URL for connecting to the Blockchain. For testing we usually use the Ropsten network on Infura. Using free tier, so might hit rate limits + static let wssUrl = "wss://ropsten.infura.io/ws/v3/467b3cd9ab2a4cbda33f14e608362e32" // An EOA with some Ether, so that we can test sending transactions (pay for gas) static let privateKey = "0xef4e182ae2cf32192d2a62c1159c8c4f7f2d658c303d0dfca5791a205456a132" @@ -26,4 +30,6 @@ struct TestConfig { // ERC165 compliant contract static let erc165Contract = "0x5c007a1d8051dfda60b3692008b9e10731b67fde" + + static let webSocketConfig = EthereumWebSocketClient.Configuration(maxFrameSize: 1_000_000) } diff --git a/web3swift/src/Account/EthereumAccount.swift b/web3swift/src/Account/EthereumAccount.swift index 9ae260f7..c745c091 100644 --- a/web3swift/src/Account/EthereumAccount.swift +++ b/web3swift/src/Account/EthereumAccount.swift @@ -7,6 +7,7 @@ // import Foundation +import Logging public protocol EthereumAccountProtocol { var address: EthereumAddress { get } @@ -29,6 +30,7 @@ public enum EthereumAccountError: Error { public class EthereumAccount: EthereumAccountProtocol { private let privateKeyData: Data private let publicKeyData: Data + private let log: Logger public lazy var publicKey: String = { return self.publicKeyData.web3.hexString @@ -38,18 +40,20 @@ public class EthereumAccount: EthereumAccountProtocol { return KeyUtil.generateAddress(from: self.publicKeyData) }() - required public init(keyStorage: EthereumKeyStorageProtocol, keystorePassword password: String) throws { + required public init(keyStorage: EthereumKeyStorageProtocol, keystorePassword password: String, logger: Logger? = nil) throws { + self.log = logger ?? Logger(label: "web3.swift.eth-account") do { let decodedKey = try keyStorage.loadAndDecryptPrivateKey(keystorePassword: password) self.privateKeyData = decodedKey self.publicKeyData = try KeyUtil.generatePublicKey(from: decodedKey) } catch let error { - print("Error loading key data: \(error)") + self.log.warning("Error loading key data: \(error)") throw EthereumAccountError.loadAccountError } } - required public init(keyStorage: EthereumKeyStorageProtocol) throws { + required public init(keyStorage: EthereumKeyStorageProtocol, logger: Logger? = nil) throws { + self.log = logger ?? Logger(label: "web3.swift.eth-account") do { let data = try keyStorage.loadPrivateKey() self.privateKeyData = data diff --git a/web3swift/src/Client/EthereumClient+Call.swift b/web3swift/src/Client/EthereumClient+Call.swift index 55b26230..02ccd312 100644 --- a/web3swift/src/Client/EthereumClient+Call.swift +++ b/web3swift/src/Client/EthereumClient+Call.swift @@ -26,6 +26,12 @@ public enum OffchainReadError: Error { } extension EthereumClient { + public func eth_call(_ transaction: EthereumTransaction, + block: EthereumBlock = .Latest, + completionHandler: @escaping (Result) -> Void) { + eth_call(transaction, resolution: .noOffchain(failOnExecutionError: true), block: block, completionHandler: completionHandler) + } + public func eth_call( _ transaction: EthereumTransaction, resolution: CallResolution = .noOffchain(failOnExecutionError: true), @@ -219,6 +225,11 @@ extension EthereumClient { // MARK: - Async/Await extension EthereumClient { + public func eth_call(_ transaction: EthereumTransaction, + block: EthereumBlock = .Latest) async throws -> String { + return try await eth_call(transaction, resolution: .noOffchain(failOnExecutionError: true), block: block) + } + public func eth_call(_ transaction: EthereumTransaction, resolution: CallResolution = .noOffchain(failOnExecutionError: true), block: EthereumBlock = .Latest) async throws -> String { @@ -232,41 +243,22 @@ extension EthereumClient { } } -// MARK: - Deprecated -extension EthereumClient { - @available(*, deprecated, renamed: "eth_call(_:resolution:block:completionHandler:)") - public func eth_call( _ transaction: EthereumTransaction, - resolution: CallResolution = .noOffchain(failOnExecutionError: true), - block: EthereumBlock = .Latest, - completion: @escaping ((EthereumClientError?, String?) -> Void) - ) { - eth_call(transaction, resolution: resolution, block: block) { result in - switch result { - case .success(let data): - completion(nil, data) - case .failure(let error): - completion(error, nil) - } - } - } -} - -fileprivate struct OffchainReadJSONBody: Encodable { +private struct OffchainReadJSONBody: Encodable { let sender: EthereumAddress let data: String } -fileprivate struct OffchainReadResponse: Decodable { +private struct OffchainReadResponse: Decodable { @DataStr var data: Data } -fileprivate struct OffchainReadErrorResponse: Decodable { +private struct OffchainReadErrorResponse: Decodable { let message: String? let pathname: String } -fileprivate var cancellables = Set() +private var cancellables = Set() fileprivate extension OffchainReadError { var isNextURLAllowed: Bool { @@ -280,4 +272,3 @@ fileprivate extension OffchainReadError { } } } - diff --git a/web3swift/src/Client/EthereumClient.swift b/web3swift/src/Client/EthereumClient.swift index d4a9381a..cd1abda0 100644 --- a/web3swift/src/Client/EthereumClient.swift +++ b/web3swift/src/Client/EthereumClient.swift @@ -8,96 +8,17 @@ import Foundation import BigInt +import Logging #if canImport(FoundationNetworking) import FoundationNetworking #endif -public enum CallResolution { - case noOffchain(failOnExecutionError: Bool) - case offchainAllowed(maxRedirects: Int) -} - -public protocol EthereumClientProtocol: AnyObject { - init(url: URL, sessionConfig: URLSessionConfiguration) - init(url: URL) - var network: EthereumNetwork? { get } - - func net_version(completionHandler: @escaping(Result) -> Void) - func eth_gasPrice(completionHandler: @escaping(Result) -> Void) - func eth_blockNumber(completionHandler: @escaping(Result) -> Void) - func eth_getBalance(address: EthereumAddress, block: EthereumBlock, completionHandler: @escaping(Result) -> Void) - func eth_getCode(address: EthereumAddress, block: EthereumBlock, completionHandler: @escaping(Result) -> Void) - func eth_estimateGas(_ transaction: EthereumTransaction, completionHandler: @escaping(Result) -> Void) - func eth_sendRawTransaction(_ transaction: EthereumTransaction, withAccount account: EthereumAccountProtocol, completionHandler: @escaping(Result) -> Void) - func eth_getTransactionCount(address: EthereumAddress, block: EthereumBlock, completionHandler: @escaping(Result) -> Void) - func eth_getTransaction(byHash txHash: String, completionHandler: @escaping(Result) -> Void) - func eth_getTransactionReceipt(txHash: String, completionHandler: @escaping(Result) -> Void) - func eth_call( - _ transaction: EthereumTransaction, - resolution: CallResolution, - block: EthereumBlock, - completionHandler: @escaping(Result) -> Void) - func eth_getLogs(addresses: [EthereumAddress]?, topics: [String?]?, fromBlock: EthereumBlock, toBlock: EthereumBlock, completionHandler: @escaping(Result<[EthereumLog], EthereumClientError>) -> Void) - 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) - - // 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, - 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 - - // Deprecated - func net_version(completion: @escaping((EthereumClientError?, EthereumNetwork?) -> Void)) - func eth_gasPrice(completion: @escaping((EthereumClientError?, BigUInt?) -> Void)) - func eth_blockNumber(completion: @escaping((EthereumClientError?, Int?) -> Void)) - func eth_getBalance(address: EthereumAddress, block: EthereumBlock, completion: @escaping((EthereumClientError?, BigUInt?) -> Void)) - func eth_getCode(address: EthereumAddress, block: EthereumBlock, completion: @escaping((EthereumClientError?, String?) -> Void)) - func eth_estimateGas(_ transaction: EthereumTransaction, withAccount account: EthereumAccountProtocol, completion: @escaping((EthereumClientError?, BigUInt?) -> Void)) - func eth_sendRawTransaction(_ transaction: EthereumTransaction, withAccount account: EthereumAccountProtocol, completion: @escaping((EthereumClientError?, String?) -> Void)) - func eth_getTransactionCount(address: EthereumAddress, block: EthereumBlock, completion: @escaping((EthereumClientError?, Int?) -> Void)) - func eth_getTransaction(byHash txHash: String, completion: @escaping((EthereumClientError?, EthereumTransaction?) -> Void)) - func eth_getTransactionReceipt(txHash: String, completion: @escaping((EthereumClientError?, EthereumTransactionReceipt?) -> Void)) - func eth_call( - _ transaction: EthereumTransaction, - resolution: CallResolution, - block: EthereumBlock, - completion: @escaping((EthereumClientError?, String?) -> Void) - ) - func eth_getLogs(addresses: [EthereumAddress]?, topics: [String?]?, fromBlock: EthereumBlock, toBlock: EthereumBlock, completion: @escaping((EthereumClientError?, [EthereumLog]?) -> Void)) - func eth_getLogs(addresses: [EthereumAddress]?, orTopics: [[String]?]?, fromBlock: EthereumBlock, toBlock: EthereumBlock, completion: @escaping((EthereumClientError?, [EthereumLog]?) -> Void)) - func eth_getBlockByNumber(_ block: EthereumBlock, completion: @escaping((EthereumClientError?, EthereumBlockInfo?) -> Void)) -} - -public enum EthereumClientError: Error, Equatable { - case tooManyResults - case executionError(JSONRPCErrorDetail) - case unexpectedReturnValue - case noResultFound - case decodeIssue - case encodeIssue - case noInputData -} - public class EthereumClient: EthereumClientProtocol { public let url: URL private var retreivedNetwork: EthereumNetwork? + private let log: Logger private let networkQueue: OperationQueue private let concurrentQueue: OperationQueue @@ -118,7 +39,7 @@ public class EthereumClient: EthereumClientProtocol { network = data self.retreivedNetwork = network case .failure(let error): - print("Client has no network: \(error.localizedDescription)") + self.log.warning("Client has no network: \(error.localizedDescription)") } group.leave() @@ -128,7 +49,7 @@ public class EthereumClient: EthereumClientProtocol { return network } - required public init(url: URL, sessionConfig: URLSessionConfiguration) { + required public init(url: URL, sessionConfig: URLSessionConfiguration, logger: Logger? = nil) { self.url = url let networkQueue = OperationQueue() networkQueue.name = "web3swift.client.networkQueue" @@ -143,6 +64,7 @@ public class EthereumClient: EthereumClientProtocol { self.concurrentQueue = txQueue self.session = URLSession(configuration: sessionConfig, delegate: nil, delegateQueue: networkQueue) + self.log = logger ?? Logger(label: "web3.swift.eth-client") } required public convenience init(url: URL) { @@ -154,7 +76,7 @@ public class EthereumClient: EthereumClientProtocol { } public func net_version(completionHandler: @escaping (Result) -> Void) { - let emptyParams: Array = [] + let emptyParams: [Bool] = [] EthereumRPC.execute(session: session, url: url, method: "net_version", params: emptyParams, receive: String.self) { result in switch result { case .success(let data): @@ -171,7 +93,7 @@ public class EthereumClient: EthereumClientProtocol { } public func eth_gasPrice(completionHandler: @escaping (Result) -> Void) { - let emptyParams: Array = [] + let emptyParams: [Bool] = [] EthereumRPC.execute(session: session, url: url, method: "eth_gasPrice", params: emptyParams, receive: String.self) { result in switch result { case .success(let data): @@ -187,7 +109,7 @@ public class EthereumClient: EthereumClientProtocol { } public func eth_blockNumber(completionHandler: @escaping (Result) -> Void) { - let emptyParams: Array = [] + let emptyParams: [Bool] = [] EthereumRPC.execute(session: session, url: url, method: "eth_blockNumber", params: emptyParams, receive: String.self) { result in switch result { case .success(let data): @@ -420,17 +342,7 @@ public class EthereumClient: EthereumClientProtocol { } } - private func eth_getLogs(addresses: [EthereumAddress]?, topics: Topics?, fromBlock from: EthereumBlock, toBlock to: EthereumBlock, completion: @escaping((Result<[EthereumLog], EthereumClientError>) -> Void)) { - DispatchQueue.global(qos: .default) - .async { - let result = RecursiveLogCollector(ethClient: self) - .getAllLogs(addresses: addresses, topics: topics, from: from, to: to) - - completion(result) - } - } - - internal func getLogs(addresses: [EthereumAddress]?, topics: Topics?, fromBlock: EthereumBlock, toBlock: EthereumBlock, completion: @escaping((Result<[EthereumLog], EthereumClientError>) -> Void)) { + public func getLogs(addresses: [EthereumAddress]?, topics: Topics?, fromBlock: EthereumBlock, toBlock: EthereumBlock, completionHandler: @escaping((Result<[EthereumLog], EthereumClientError>) -> Void)) { struct CallParams: Encodable { var fromBlock: String @@ -445,22 +357,32 @@ public class EthereumClient: EthereumClientProtocol { switch result { case .success(let data): if let logs = data as? [EthereumLog] { - completion(.success(logs)) + completionHandler(.success(logs)) } else { - completion(.failure(.unexpectedReturnValue)) + completionHandler(.failure(.unexpectedReturnValue)) } case .failure(let error): if let error = error as? JSONRPCError, case let .executionError(innerError) = error, innerError.error.code == JSONRPCErrorCode.tooManyResults { - completion(.failure(.tooManyResults)) + completionHandler(.failure(.tooManyResults)) } else { - completion(.failure(.unexpectedReturnValue)) + completionHandler(.failure(.unexpectedReturnValue)) } } } } + private func eth_getLogs(addresses: [EthereumAddress]?, topics: Topics?, fromBlock from: EthereumBlock, toBlock to: EthereumBlock, completion: @escaping((Result<[EthereumLog], EthereumClientError>) -> Void)) { + DispatchQueue.global(qos: .default) + .async { + let result = RecursiveLogCollector(ethClient: self) + .getAllLogs(addresses: addresses, topics: topics, from: from, to: to) + + completion(result) + } + } + private func failureHandler(_ error: Error, completionHandler: @escaping (Result) -> Void) { if case let .executionError(result) = error as? JSONRPCError { completionHandler(.failure(.executionError(result.error))) @@ -550,162 +472,3 @@ extension EthereumClient { } } } - -// MARK: - Deprecated -extension EthereumClient { - @available(*, deprecated, renamed: "net_version(completionHandler:)") - public func net_version(completion: @escaping ((EthereumClientError?, EthereumNetwork?) -> Void)) { - net_version { result in - switch result { - case .success(let data): - completion(nil, data) - case .failure(let error): - completion(error, nil) - } - } - } - - @available(*, deprecated, renamed: "eth_gasPrice(completionHandler:)") - public func eth_gasPrice(completion: @escaping ((EthereumClientError?, BigUInt?) -> Void)) { - eth_gasPrice { result in - switch result { - case .success(let data): - completion(nil, data) - case .failure(let error): - completion(error, nil) - } - } - } - - @available(*, deprecated, renamed: "eth_blockNumber(completionHandler:)") - public func eth_blockNumber(completion: @escaping ((EthereumClientError?, Int?) -> Void)) { - eth_blockNumber { result in - switch result { - case .success(let data): - completion(nil, data) - case .failure(let error): - completion(error, nil) - } - } - } - - @available(*, deprecated, renamed: "eth_getBalance(address:block:completionHandler:)") - public func eth_getBalance(address: EthereumAddress, block: EthereumBlock, completion: @escaping ((EthereumClientError?, BigUInt?) -> Void)) { - eth_getBalance(address: address, block: block) { result in - switch result { - case .success(let data): - completion(nil, data) - case .failure(let error): - completion(error, nil) - } - } - } - - @available(*, deprecated, renamed: "eth_getCode(address:block:completionHandler:)") - public func eth_getCode(address: EthereumAddress, block: EthereumBlock = .Latest, completion: @escaping((EthereumClientError?, String?) -> Void)) { - eth_getCode(address: address, block: block) { result in - switch result { - case .success(let data): - completion(nil, data) - case .failure(let error): - completion(error, nil) - } - } - } - - @available(*, deprecated, renamed: "eth_estimateGas(_:completionHandler:)") - public func eth_estimateGas(_ transaction: EthereumTransaction, withAccount account: EthereumAccountProtocol, completion: @escaping((EthereumClientError?, BigUInt?) -> Void)) { - eth_estimateGas(transaction) { result in - switch result { - case .success(let data): - completion(nil, data) - case .failure(let error): - completion(error, nil) - } - } - } - - @available(*, deprecated, renamed: "eth_sendRawTransaction(_:withAccount:completionHandler:)") - public func eth_sendRawTransaction(_ transaction: EthereumTransaction, withAccount account: EthereumAccountProtocol, completion: @escaping ((EthereumClientError?, String?) -> Void)) { - eth_sendRawTransaction(transaction, withAccount: account) { result in - switch result { - case .success(let data): - completion(nil, data) - case .failure(let error): - completion(error, nil) - } - } - } - - @available(*, deprecated, renamed: "eth_getTransactionCount(address:block:completionHandler:)") - public func eth_getTransactionCount(address: EthereumAddress, block: EthereumBlock, completion: @escaping ((EthereumClientError?, Int?) -> Void)) { - eth_getTransactionCount(address: address, block: block) { result in - switch result { - case .success(let data): - completion(nil, data) - case .failure(let error): - completion(error, nil) - } - } - } - - @available(*, deprecated, renamed: "eth_getTransactionReceipt(txHash:completionHandler:)") - public func eth_getTransactionReceipt(txHash: String, completion: @escaping ((EthereumClientError?, EthereumTransactionReceipt?) -> Void)) { - eth_getTransactionReceipt(txHash: txHash) { result in - switch result { - case .success(let data): - completion(nil, data) - case .failure(let error): - completion(error, nil) - } - } - } - - @available(*, deprecated, renamed: "eth_getTransaction(byHash:completionHandler:)") - public func eth_getTransaction(byHash txHash: String, completion: @escaping((EthereumClientError?, EthereumTransaction?) -> Void)) { - eth_getTransaction(byHash: txHash) { result in - switch result { - case .success(let data): - completion(nil, data) - case .failure(let error): - completion(error, nil) - } - } - } - - @available(*, deprecated, renamed: "eth_getLogs(addresses:topics:fromBlock:toBlock:completionHandler:)") - public func eth_getLogs(addresses: [EthereumAddress]?, topics: [String?]?, fromBlock from: EthereumBlock = .Earliest, toBlock to: EthereumBlock = .Latest, completion: @escaping ((EthereumClientError?, [EthereumLog]?) -> Void)) { - eth_getLogs(addresses: addresses, topics: topics) { result in - switch result { - case .success(let data): - completion(nil, data) - case .failure(let error): - completion(error, nil) - } - } - } - - @available(*, deprecated, renamed: "eth_getLogs(addresses:orTopics:fromBlock:toBlock:completionHandler:)") - public func eth_getLogs(addresses: [EthereumAddress]?, orTopics topics: [[String]?]?, fromBlock from: EthereumBlock = .Earliest, toBlock to: EthereumBlock = .Latest, completion: @escaping((EthereumClientError?, [EthereumLog]?) -> Void)) { - eth_getLogs(addresses: addresses, orTopics: topics) { result in - switch result { - case .success(let data): - completion(nil, data) - case .failure(let error): - completion(error, nil) - } - } - } - - @available(*, deprecated, renamed: "eth_getBlockByNumber(_:completionHandler:)") - public func eth_getBlockByNumber(_ block: EthereumBlock, completion: @escaping((EthereumClientError?, EthereumBlockInfo?) -> Void)) { - eth_getBlockByNumber(block) { result in - switch result { - case .success(let data): - completion(nil, data) - case .failure(let error): - completion(error, nil) - } - } - } -} diff --git a/web3swift/src/Client/EthereumClientProtocol.swift b/web3swift/src/Client/EthereumClientProtocol.swift new file mode 100644 index 00000000..cf085106 --- /dev/null +++ b/web3swift/src/Client/EthereumClientProtocol.swift @@ -0,0 +1,135 @@ +// +// EthereumClient.swift +// web3swift +// +// Created by Dionisis Karatzas on 6/6/22. +// Copyright © 2018 Argent Labs Limited. All rights reserved. +// + +import Foundation +import BigInt +import NIOWebSocket + +public enum CallResolution { + case noOffchain(failOnExecutionError: Bool) + case offchainAllowed(maxRedirects: Int) +} + +public struct EquatableError: Error, Equatable { + let base: Error + + public static func ==(lhs: EquatableError, rhs: EquatableError) -> Bool { + return 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 + // WebSocket + case invalidConnection + case connectionNotOpen + case connectionTimeout + case pendingRequestsOnReconnecting + case maxAttemptsReachedOnReconnecting + case webSocketError(EquatableError) +} + +public protocol EthereumClientProtocol: AnyObject { + var network: EthereumNetwork? { get } + + func net_version(completionHandler: @escaping(Result) -> Void) + func eth_gasPrice(completionHandler: @escaping(Result) -> Void) + func eth_blockNumber(completionHandler: @escaping(Result) -> Void) + func eth_getBalance(address: EthereumAddress, block: EthereumBlock, completionHandler: @escaping(Result) -> Void) + func eth_getCode(address: EthereumAddress, block: EthereumBlock, completionHandler: @escaping(Result) -> Void) + func eth_estimateGas(_ transaction: EthereumTransaction, completionHandler: @escaping(Result) -> Void) + func eth_sendRawTransaction(_ transaction: EthereumTransaction, withAccount account: EthereumAccountProtocol, completionHandler: @escaping(Result) -> Void) + func eth_getTransactionCount(address: EthereumAddress, block: EthereumBlock, completionHandler: @escaping(Result) -> Void) + func eth_getTransaction(byHash txHash: String, completionHandler: @escaping(Result) -> Void) + func eth_getTransactionReceipt(txHash: String, completionHandler: @escaping(Result) -> Void) + func eth_call( + _ transaction: EthereumTransaction, + block: EthereumBlock, + completionHandler: @escaping(Result) -> Void) + func eth_call( + _ transaction: EthereumTransaction, + resolution: CallResolution, + block: EthereumBlock, + completionHandler: @escaping(Result) -> Void) + func eth_getLogs(addresses: [EthereumAddress]?, topics: [String?]?, fromBlock: EthereumBlock, toBlock: EthereumBlock, completionHandler: @escaping(Result<[EthereumLog], EthereumClientError>) -> Void) + 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, completionHandler: @escaping((Result<[EthereumLog], EthereumClientError>) -> Void)) + + // 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 +} + +public protocol EthereumClientWebSocketProtocol: EthereumClientProtocol { + var delegate: EthereumWebSocketClientDelegate? { get set } + + func connect() + func disconnect(code: WebSocketErrorCode) + func refresh() + + func subscribe(type: EthereumSubscriptionType, completionHandler: @escaping(Result) -> Void) + func subscribe(type: EthereumSubscriptionType) async throws -> EthereumSubscription + + func unsubscribe(_ subscription: EthereumSubscription, completionHandler: @escaping(Result) -> Void) + func unsubscribe(_ subscription: EthereumSubscription) async throws -> Bool + + func pendingTransactions(onSubscribe: @escaping(Result) -> Void, onData: @escaping(String) -> Void) + func pendingTransactions(onData: @escaping(String) -> Void) async throws -> EthereumSubscription + + func newBlockHeaders(onSubscribe: @escaping(Result) -> Void, onData: @escaping(EthereumHeader) -> Void) + func newBlockHeaders(onData: @escaping(EthereumHeader) -> Void) async throws -> EthereumSubscription + + func syncing(onSubscribe: @escaping(Result) -> Void, onData: @escaping(EthereumSyncStatus) -> Void) + func syncing(onData: @escaping(EthereumSyncStatus) -> Void) async throws -> EthereumSubscription +} + +public protocol EthereumWebSocketClientDelegate: AnyObject { + func onNewPendingTransaction(subscription: EthereumSubscription, txHash: String) + func onNewBlockHeader(subscription: EthereumSubscription, header: EthereumHeader) + func onSyncing(subscription: EthereumSubscription, sync: EthereumSyncStatus) + func onWebSocketReconnect() +} + +extension EthereumWebSocketClientDelegate { + func onNewPendingTransaction(subscription: EthereumSubscription, txHash: String) {} + + func onNewBlockHeader(subscription: EthereumSubscription, header: EthereumHeader) {} + + func onSyncing(subscription: EthereumSubscription, sync: EthereumSyncStatus) {} + + func onWebSocketReconnect() {} +} diff --git a/web3swift/src/Client/EthereumWebSocketClient.swift b/web3swift/src/Client/EthereumWebSocketClient.swift new file mode 100644 index 00000000..5d6ef71c --- /dev/null +++ b/web3swift/src/Client/EthereumWebSocketClient.swift @@ -0,0 +1,1029 @@ +// +// EthereumWebSocketClient.swift +// web3swift +// +// Created by Dionisis Karatzas on 1/6/22. +// Copyright © 2018 Argent Labs Limited. All rights reserved. +// + +import BigInt +import Foundation +import NIO +import WebSocketKit +import Logging +import NIOSSL +import NIOCore +import NIOWebSocket +import GenericJSON + +public class EthereumWebSocketClient: EthereumClientWebSocketProtocol { + private struct JSONRPCSubscriptionParams: Decodable { + public var subscription: String + public var result: T + } + + private struct JSONRPCSubscriptionResponse: Decodable { + public var jsonrpc: String + public var method: String + public var params: JSONRPCSubscriptionParams + } + + private struct WebSocketRequest { + var payload: String + var callback: (Result) -> Void + } + + private class SharedResources { + private let semaphore = DispatchSemaphore(value: 1) + // Requests that have not sent yet + private(set) var requestQueue: [Int: WebSocketRequest] = [:] + // Requests that have been sent and waiting for Response + private(set) var responseQueue: [Int: WebSocketRequest] = [:] + + private(set) var subscriptions: [EthereumSubscription: (Any) -> Void] = [:] + + private(set) var counter: Int = 0 { + didSet { + if counter == Int.max { + counter = 0 + } + } + } + + func addRequest(_ key: Int, request: WebSocketRequest) { + semaphore.wait() + requestQueue[key] = request + semaphore.signal() + } + + func removeRequest(_ key: Int) { + semaphore.wait() + requestQueue.removeValue(forKey: key) + semaphore.signal() + } + + func addResponse(_ key: Int, request: WebSocketRequest) { + semaphore.wait() + responseQueue[key] = request + semaphore.signal() + } + + func removeResponse(_ key: Int) { + semaphore.wait() + responseQueue.removeValue(forKey: key) + semaphore.signal() + } + + func addSubscription(_ subscription: EthereumSubscription, callback: @escaping (Any) -> Void) { + semaphore.wait() + subscriptions[subscription] = callback + semaphore.signal() + } + + func removeSubscription(_ subscription: EthereumSubscription) { + semaphore.wait() + subscriptions.removeValue(forKey: subscription) + semaphore.signal() + } + + func incrementCounter() { + semaphore.wait() + counter += 1 + semaphore.signal() + } + + func cleanSubscriptions() { + semaphore.wait() + subscriptions.removeAll() + semaphore.signal() + } + } + + public enum EventLoopGroupProvider { + case shared(EventLoopGroup) + case createNew + } + + public enum State { + case connecting + case open + case closed + } + + public struct Configuration { + /// The TLS configuration for client use. + public var tlsConfiguration: TLSConfiguration? + /// The largest incoming `WebSocketFrame` size in bytes. Default is 16,384 bytes. + public var maxFrameSize: Int + /// Whether or not the websocket should attempt to connect immediately upon instantiation. + public var automaticOpen: Bool + /// The number of milliseconds to delay before attempting to reconnect. + public var reconnectInterval: Int + /// The maximum number of milliseconds to delay a reconnection attempt. + public var maxReconnectInterval: Int + /// The rate of increase of the reconnect delay. Allows reconnect attempts to back off when problems persist. + public var reconnectDecay: Double + /// The maximum number of reconnection attempts to make. Unlimited if zero. + public var maxReconnectAttempts: Int + + public init( + tlsConfiguration: TLSConfiguration? = nil, + maxFrameSize: Int = 1 << 14, + automaticOpen: Bool = true, + reconnectInterval: Int = 1000, + maxReconnectInterval: Int = 30000, + reconnectDecay: Double = 1.5, + maxReconnectAttempts: Int = 0 + + ) { + self.tlsConfiguration = tlsConfiguration + self.maxFrameSize = maxFrameSize + self.automaticOpen = automaticOpen + self.reconnectInterval = reconnectInterval + self.maxReconnectInterval = maxReconnectInterval + self.reconnectDecay = reconnectDecay + self.maxReconnectAttempts = maxReconnectAttempts + } + } + + public weak var delegate: EthereumWebSocketClientDelegate? + public var onReconnectCallback: (() -> Void)? + public let url: String + public let eventLoopGroup: EventLoopGroup + + private(set) var currentState: State = .closed + + public var network: EthereumNetwork? { + if let _ = self.retreivedNetwork { + return self.retreivedNetwork + } + + let group = DispatchGroup() + group.enter() + + var network: EthereumNetwork? + self.net_version { result in + switch result { + case .success(let data): + network = data + self.retreivedNetwork = network + case .failure(let error): + self.log.warning("Client has no network: \(error.localizedDescription)") + } + + group.leave() + } + + group.wait() + return network + } + + private let eventLoopGroupProvider: EventLoopGroupProvider + private let log: Logger + private let configuration: Configuration + private let resources = SharedResources() + + private let semaphore = DispatchSemaphore(value: 1) + private var reconnectAttempts = 0 + private var forcedClose = false + private var timedOut = false + + private var retreivedNetwork: EthereumNetwork? + private var webSocket: WebSocket? + + // won't ship with production code thanks to #if DEBUG + // WebSocket is need it for testing purposes +#if DEBUG + public func exposeWebSocket() -> WebSocket? { + return self.webSocket + } +#endif + + required public init(url: String, + eventLoopGroupProvider: EventLoopGroupProvider = .createNew, + configuration: Configuration = .init(), + logger: Logger? = nil) { + self.url = url + self.eventLoopGroupProvider = eventLoopGroupProvider + switch eventLoopGroupProvider { + case .shared(let group): + self.eventLoopGroup = group + case .createNew: + self.eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) + } + self.log = logger ?? Logger(label: "web3.swift.eth-websocket-client") + self.configuration = configuration + + // Whether or not to create a websocket upon instantiation + if configuration.automaticOpen { + connect(reconnectAttempt: false) + } + } + + deinit { + self.log.trace("Shutting down WebSocket") + disconnect() + + switch self.eventLoopGroupProvider { + case .shared: + self.log.trace("Running on shared EventLoopGroup. Not shutting down EventLoopGroup.") + case .createNew: + self.log.trace("Shutting down EventLoopGroup") + do { + try self.eventLoopGroup.syncShutdownGracefully() + } catch { + self.log.warning("Shutting down EventLoopGroup failed: \(error)") + } + } + } + + public func connect() { + connect(reconnectAttempt: false) + } + + public func disconnect(code: WebSocketErrorCode = .goingAway) { + do { + forcedClose = true + try webSocket?.close(code: code).wait() + } catch { + self.log.warning("Clossing WebSocket failed: \(error)") + } + } + + /// Additional public API method to refresh the connection if still open (close, re-open). + /// For example, if the app suspects bad data / missed heart beats, it can try to refresh. + public func refresh() { + do { + try webSocket?.close(code: .goingAway).wait() + } catch { + self.log.warning("Failed to Close WebSocket: \(error)") + } + } + + private func connect(reconnectAttempt: Bool) { + if let ws = webSocket, !ws.isClosed { + return + } + + if reconnectAttempt, configuration.maxReconnectAttempts > 0, self.reconnectAttempts > configuration.maxReconnectAttempts { + self.log.trace("WebSocket reached maxReconnectAttempts. Stop trying") + + for request in resources.requestQueue { + request.value.callback(.failure(.maxAttemptsReachedOnReconnecting)) + resources.removeRequest(request.key) + } + self.reconnectAttempts = 0 + return + } + + self.log.trace("Requesting WebSocket connection") + + do { + self.currentState = .connecting + + _ = try WebSocket.connect(to: url, + configuration: WebSocketClient.Configuration(tlsConfiguration: configuration.tlsConfiguration, + maxFrameSize: configuration.maxFrameSize), + on: eventLoopGroup) { ws in + self.log.trace("WebSocket connected") + + if reconnectAttempt { + self.delegate?.onWebSocketReconnect() + self.onReconnectCallback?() + } + + self.webSocket = ws + self.currentState = .open + self.reconnectAttempts = 0 + + // Send pending requests and delete + for request in self.resources.requestQueue { + ws.send(request.value.payload) + self.resources.removeRequest(request.key) + } + + ws.onText { [weak self] _, string in + guard let self = self else { return } + + if let data = string.data(using: .utf8), + let json = try? JSONDecoder().decode(JSON.self, from: data), + let subscriptionId = json["params"]?.objectValue?["subscription"]?.stringValue, + let subscription = self.resources.subscriptions.first(where: { $0.key.id == subscriptionId }) { + switch subscription.key.type { + case .newBlockHeaders: + if let data = string.data(using: .utf8), let response = try? JSONDecoder().decode(JSONRPCSubscriptionResponse.self, from: data) { + self.delegate?.onNewBlockHeader(subscription: subscription.key, header: response.params.result) + subscription.value(response.params.result) + } + case .pendingTransactions: + if let data = string.data(using: .utf8), let response = try? JSONDecoder().decode(JSONRPCSubscriptionResponse.self, from: data) { + self.delegate?.onNewPendingTransaction(subscription: subscription.key, txHash: response.params.result) + subscription.value(response.params.result) + } + case .syncing: + if let data = string.data(using: .utf8), let response = try? JSONDecoder().decode(JSONRPCSubscriptionResponse.self, from: data) { + self.delegate?.onSyncing(subscription: subscription.key, sync: response.params.result) + subscription.value(response.params.result) + } + } + } + + if let data = string.data(using: .utf8), + let json = try? JSONDecoder().decode(JSON.self, from: data), + let responseId = json["id"]?.doubleValue { + guard let response = self.resources.responseQueue.first(where: { $0.key == Int(responseId) }) else { return } + response.value.callback(.success(data)) + self.resources.removeResponse(response.key) + } + } + + ws.onClose.whenComplete { value in + if let code = ws.closeCode { + self.log.trace("WebSocket closed. Code: \(code)") + } else { + self.log.trace("WebSocket closed") + } + + for request in self.resources.requestQueue { + request.value.callback(.failure(.connectionNotOpen)) + self.resources.removeRequest(request.key) + } + + for response in self.resources.responseQueue { + response.value.callback(.failure(.invalidConnection)) + self.resources.removeResponse(response.key) + } + + self.resources.cleanSubscriptions() + + if self.forcedClose { + self.currentState = .closed + } else { + self.currentState = .connecting + + self.reconnect() + } + } + + }.wait() + } catch { + currentState = .closed + self.log.error("WebSocket connection failed: \(error)") + + for request in self.resources.requestQueue { + request.value.callback(.failure(.connectionNotOpen)) + self.resources.removeRequest(request.key) + } + + for response in self.resources.responseQueue { + response.value.callback(.failure(.connectionNotOpen)) + self.resources.removeResponse(response.key) + } + + self.resources.cleanSubscriptions() + + if case ChannelError.connectTimeout = error { + reconnect() + } + } + } + + private func encodeRequest(method: String, params: P, id: Int) throws -> String { + let rpcRequest = JSONRPCRequest(jsonrpc: "2.0", method: method, params: params, id: id) + log.trace("\(rpcRequest)") + let data = try JSONEncoder().encode(rpcRequest) + + guard let dataString = String(data: data, encoding: .utf8) else { + throw EthereumClientError.encodeIssue + } + + return dataString + } + + private func decoding(_ type: T.Type, then: @escaping (Result) -> Void) -> (Result) -> Void { + return { dataResult in + let decodedResult: Result = dataResult.tryMap { data in + if let result = try? JSONDecoder().decode(JSONRPCResult.self, from: data) { + return result.result + } else if let result = try? JSONDecoder().decode([JSONRPCResult].self, from: data) { + let resultObjects = result.map { return $0.result } + return resultObjects + } else if let errorResult = try? JSONDecoder().decode(JSONRPCErrorResult.self, from: data) { + throw EthereumClientError.executionError(errorResult.error) + } else { + throw EthereumClientError.unexpectedReturnValue + } + + } + then(decodedResult) + } + } + + private func send(method: String, params: P, resultType: U.Type, completionHandler: @escaping (Result) -> Void, resultDecodeHandler: @escaping (Result) -> Void) { + semaphore.wait() + + defer { + semaphore.signal() + } + resources.incrementCounter() + let id = resources.counter + + let requestString: String + + do { + requestString = try encodeRequest(method: method, params: params, id: id) + } catch { + completionHandler(.failure(.encodeIssue)) + return + } + + let wsRequest = WebSocketRequest(payload: requestString, callback: decoding(resultType.self) { result in + resultDecodeHandler(result) + }) + + // if socket is not connected yet or reconnecting + // add request to the queue + if currentState == .connecting { + resources.addRequest(id, request: wsRequest) + return + } + + // if socket is closed remove pending request + // and return failure + if currentState != .open { + resources.removeRequest(id) + completionHandler(.failure(.connectionNotOpen)) + return + } + + resources.addResponse(id, request: wsRequest) + resources.removeRequest(id) + + let sendPromise = self.eventLoopGroup.next().makePromise(of: Void.self) + sendPromise.futureResult.whenFailure({ error in + completionHandler(.failure(.webSocketError(EquatableError(base: error)))) + self.resources.removeResponse(id) + }) + webSocket?.send(requestString, promise: sendPromise) + } + + private func reconnect() { + for response in resources.responseQueue { + response.value.callback(.failure(.pendingRequestsOnReconnecting)) + resources.removeResponse(response.key) + } + + var delay = configuration.reconnectInterval * Int(pow(configuration.reconnectDecay, Double(reconnectAttempts))) + if delay > configuration.maxReconnectInterval { + delay = configuration.maxReconnectInterval + } + + self.log.trace("WebSocket reconnecting... Delay: \(delay) ms") + + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(delay), execute: { + self.reconnectAttempts += 1 + self.connect(reconnectAttempt: true) + }) + } +} + +extension EthereumWebSocketClient { + public func net_version(completionHandler: @escaping (Result) -> Void) { + send(method: "net_version", params: [Bool](), resultType: String.self, completionHandler: completionHandler) { result in + let newResult: Result = result.tryMap { data in + if let resString = data as? String { + let network = EthereumNetwork.fromString(resString) + return network + } else { + throw EthereumClientError.unexpectedReturnValue + } + } + completionHandler(newResult) + } + } + + public func eth_gasPrice(completionHandler: @escaping (Result) -> Void) { + send(method: "eth_gasPrice", params: [Bool](), resultType: String.self, completionHandler: completionHandler) { result in + switch result { + case .success(let data): + if let hexString = data as? String, let bigUInt = BigUInt(hex: hexString) { + completionHandler(.success(bigUInt)) + } else { + completionHandler(.failure(.unexpectedReturnValue)) + } + case .failure(let error): + completionHandler(.failure(error)) + } + } + } + + public func eth_blockNumber(completionHandler: @escaping (Result) -> Void) { + send(method: "eth_blockNumber", params: [Bool](), resultType: String.self, completionHandler: completionHandler) { result in + let newResult: Result = result.tryMap { data in + if let hexString = data as? String { + if let integerValue = Int(hex: hexString) { + return integerValue + } else { + throw EthereumClientError.decodeIssue + } + } else { + throw EthereumClientError.unexpectedReturnValue + } + } + completionHandler(newResult) + } + } + + public func eth_getBalance(address: EthereumAddress, block: EthereumBlock, completionHandler: @escaping (Result) -> Void) { + send(method: "eth_getBalance", params: [address.value, block.stringValue], resultType: String.self, completionHandler: completionHandler) { result in + let newResult: Result = result.tryMap { data in + if let resString = data as? String, let balanceInt = BigUInt(hex: resString.web3.noHexPrefix) { + return balanceInt + } else { + throw EthereumClientError.unexpectedReturnValue + } + } + completionHandler(newResult) + } + } + + public func eth_getCode(address: EthereumAddress, block: EthereumBlock, completionHandler: @escaping (Result) -> Void) { + send(method: "eth_getCode", params: [address.value, block.stringValue], resultType: String.self, completionHandler: completionHandler) { result in + let newResult: Result = result.tryMap { data in + if let resDataString = data as? String { + return resDataString + } else { + throw EthereumClientError.unexpectedReturnValue + } + } + completionHandler(newResult) + } + } + + public func eth_estimateGas(_ transaction: EthereumTransaction, completionHandler: @escaping (Result) -> Void) { + struct CallParams: Encodable { + let from: String? + let to: String + let gas: String? + let gasPrice: String? + let value: String? + let data: String? + + enum TransactionCodingKeys: String, CodingKey { + case from + case to + case 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, + gas: nil, + gasPrice: nil, + value: value?.web3.hexString, + data: transaction.data?.web3.hexString) + + send(method: "eth_estimateGas", params: params, resultType: String.self, completionHandler: completionHandler) { result in + let newResult: Result = result.tryMap { data in + if let gasHex = data as? String, let gas = BigUInt(hex: gasHex) { + return gas + } else { + throw EthereumClientError.unexpectedReturnValue + } + } + completionHandler(newResult) + } + } + + public func eth_sendRawTransaction(_ transaction: EthereumTransaction, withAccount account: EthereumAccountProtocol, completionHandler: @escaping (Result) -> Void) { + Task { + do { + 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 _ = transaction.chainId, let signedTx = (try? account.sign(transaction: transaction)), let transactionHex = signedTx.raw?.web3.hexString else { + completionHandler(.failure(.encodeIssue)) + return + } + + send(method: "eth_sendRawTransaction", params: [transactionHex], resultType: String.self, completionHandler: completionHandler) { result in + let newResult: Result = result.tryMap { data in + if let resDataString = data as? String { + return resDataString + } else { + throw EthereumClientError.unexpectedReturnValue + } + } + completionHandler(newResult) + } + } catch { + completionHandler(.failure(error as! EthereumClientError)) + } + } + } + + public func eth_getTransactionCount(address: EthereumAddress, block: EthereumBlock, completionHandler: @escaping (Result) -> Void) { + send(method: "eth_getTransactionCount", params: [address.value, block.stringValue], resultType: String.self, completionHandler: completionHandler) { result in + let newResult: Result = result.tryMap { data in + if let resString = data as? String, let count = Int(hex: resString) { + return count + } else { + throw EthereumClientError.unexpectedReturnValue + } + } + completionHandler(newResult) + } + } + + public func eth_getTransaction(byHash txHash: String, completionHandler: @escaping (Result) -> Void) { + send(method: "eth_getTransactionByHash", params: [txHash], resultType: EthereumTransaction.self, completionHandler: completionHandler) { result in + let newResult: Result = result.tryMap { data in + if let transaction = data as? EthereumTransaction { + return transaction + } else { + throw EthereumClientError.unexpectedReturnValue + } + } + completionHandler(newResult) + } + } + + public func eth_getTransactionReceipt(txHash: String, completionHandler: @escaping (Result) -> Void) { + send(method: "eth_getTransactionReceipt", params: [txHash], resultType: EthereumTransactionReceipt.self, completionHandler: completionHandler) { result in + let newResult: Result = result.tryMap { data in + if let receipt = data as? EthereumTransactionReceipt { + return receipt + } else { + throw EthereumClientError.noResultFound + } + } + completionHandler(newResult) + } + } + + + public func eth_call(_ transaction: EthereumTransaction, block: EthereumBlock = .Latest, completionHandler: @escaping (Result) -> Void) { + guard let transactionData = transaction.data else { + completionHandler(.failure(.noInputData)) + return + } + + struct CallParams: Encodable { + let from: String? + let to: String + let data: String + let block: String + + enum TransactionCodingKeys: String, CodingKey { + case from + case to + case data + } + + func encode(to encoder: Encoder) throws { + var container = encoder.unkeyedContainer() + var nested = container.nestedContainer(keyedBy: TransactionCodingKeys.self) + if let from = from { + try nested.encode(from, forKey: .from) + } + try nested.encode(to, forKey: .to) + try nested.encode(data, forKey: .data) + try container.encode(block) + } + } + + let params = CallParams( + from: transaction.from?.value, + to: transaction.to.value, + data: transactionData.web3.hexString, + block: block.stringValue + ) + send(method: "eth_call", params: params, resultType: String.self, completionHandler: completionHandler) { result in + let newResult: Result = result.tryMap { data in + if let resDataString = data as? String { + return resDataString + } else { + throw EthereumClientError.unexpectedReturnValue + } + } + + completionHandler(newResult) + } + } + + public func eth_call(_ transaction: EthereumTransaction, resolution: CallResolution = .noOffchain(failOnExecutionError: true), block: EthereumBlock = .Latest, completionHandler: @escaping (Result) -> Void) { + eth_call(transaction, completionHandler: completionHandler) + } + + public func eth_getLogs(addresses: [EthereumAddress]?, topics: [String?]?, fromBlock from: EthereumBlock = .Earliest, toBlock to: EthereumBlock = .Latest, completionHandler: @escaping (Result<[EthereumLog], EthereumClientError>) -> Void) { + eth_getLogs(addresses: addresses, topics: topics.map(Topics.plain), fromBlock: from, toBlock: to, completion: completionHandler) + } + + public func eth_getLogs(addresses: [EthereumAddress]?, orTopics topics: [[String]?]?, fromBlock from: EthereumBlock = .Earliest, toBlock to: EthereumBlock = .Latest, completionHandler: @escaping (Result<[EthereumLog], EthereumClientError>) -> Void) { + eth_getLogs(addresses: addresses, topics: topics.map(Topics.composed), fromBlock: from, toBlock: to, completion: completionHandler) + } + + public func eth_getBlockByNumber(_ block: EthereumBlock, completionHandler: @escaping (Result) -> Void) { + 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) + + send(method: "eth_getBlockByNumber", params: params, resultType: EthereumBlockInfo.self, completionHandler: completionHandler) { result in + let newResult: Result = result.tryMap { data in + if let blockData = data as? EthereumBlockInfo { + return blockData + } else { + throw EthereumClientError.unexpectedReturnValue + } + } + completionHandler(newResult) + } + } + + public func getLogs(addresses: [EthereumAddress]?, topics: Topics?, fromBlock: EthereumBlock, toBlock: EthereumBlock, completionHandler: @escaping ((Result<[EthereumLog], EthereumClientError>) -> Void)) { + 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) + send(method: "eth_getLogs", params: [params], resultType: [EthereumLog].self, completionHandler: completionHandler) { result in + var newResult: Result<[EthereumLog], EthereumClientError> = result.tryMap { data in + if let logs = data as? [EthereumLog] { + return logs + } else { + throw EthereumClientError.unexpectedReturnValue + } + } + + newResult = newResult.mapError { error in + if case let .executionError(innerError) = error, + innerError.code == JSONRPCErrorCode.tooManyResults { + return .tooManyResults + } else { + return .unexpectedReturnValue + } + } + + completionHandler(newResult) + } + } + + private func eth_getLogs(addresses: [EthereumAddress]?, topics: Topics?, fromBlock from: EthereumBlock, toBlock to: EthereumBlock, completion: @escaping((Result<[EthereumLog], EthereumClientError>) -> Void)) { + DispatchQueue.global(qos: .default) + .async { + let result = RecursiveLogCollector(ethClient: self) + .getAllLogs(addresses: addresses, topics: topics, from: from, to: to) + + completion(result) + } + } + + public func subscribe(type: EthereumSubscriptionType, completionHandler: @escaping (Result) -> Void) { + send(method: "eth_subscribe", params: [type.method, type.params].compactMap { $0 }, resultType: String.self, completionHandler: completionHandler) { result in + let newResult: Result = result.tryMap { data in + if let resDataString = data as? String { + let subscription = EthereumSubscription(type: type, id: resDataString) + self.resources.addSubscription(subscription, callback: { _ in }) + return subscription + } else { + throw EthereumClientError.unexpectedReturnValue + } + } + completionHandler(newResult) + } + } + + public func unsubscribe(_ subscription: EthereumSubscription, completionHandler: @escaping (Result) -> Void) { + send(method: "eth_unsubscribe", params: [subscription.id], resultType: Bool.self, completionHandler: completionHandler) { result in + let newResult: Result = result.tryMap { data in + if let resDataBool = data as? Bool { + self.resources.removeSubscription(subscription) + return resDataBool + } else { + throw EthereumClientError.unexpectedReturnValue + } + } + completionHandler(newResult) + } + } + + public func pendingTransactions(onSubscribe: @escaping (Result) -> Void, onData: @escaping (String) -> Void) { + send(method: "eth_subscribe", params: [EthereumSubscriptionType.pendingTransactions.method], resultType: String.self, completionHandler: onSubscribe) { result in + let newResult: Result = result.tryMap { data in + if let resDataString = data as? String { + let subscription = EthereumSubscription(type: .pendingTransactions, id: resDataString) + self.resources.addSubscription(subscription, callback: { object in + onData(object as! String) + }) + return subscription + } else { + throw EthereumClientError.unexpectedReturnValue + } + } + onSubscribe(newResult) + } + } + + public func newBlockHeaders(onSubscribe: @escaping (Result) -> Void, onData: @escaping (EthereumHeader) -> Void) { + send(method: "eth_subscribe", params: [EthereumSubscriptionType.newBlockHeaders.method], resultType: String.self, completionHandler: onSubscribe) { result in + let newResult: Result = result.tryMap { data in + if let resDataString = data as? String { + let subscription = EthereumSubscription(type: .newBlockHeaders, id: resDataString) + self.resources.addSubscription(subscription, callback: { object in + onData(object as! EthereumHeader) + }) + return subscription + } else { + throw EthereumClientError.unexpectedReturnValue + } + } + onSubscribe(newResult) + } + } + + public func syncing(onSubscribe: @escaping (Result) -> Void, onData: @escaping (EthereumSyncStatus) -> Void) { + send(method: "eth_subscribe", params: [EthereumSubscriptionType.syncing.method], resultType: String.self, completionHandler: onSubscribe) { result in + let newResult: Result = result.tryMap { data in + if let resDataString = data as? String { + let subscription = EthereumSubscription(type: .syncing, id: resDataString) + self.resources.addSubscription(subscription, callback: { object in + onData(object as! EthereumSyncStatus) + }) + return subscription + } else { + throw EthereumClientError.unexpectedReturnValue + } + } + onSubscribe(newResult) + } + } +} + +// MARK: - Async/Await +extension EthereumWebSocketClient { + public func net_version() async throws -> EthereumNetwork { + return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + net_version(completionHandler: continuation.resume) + } + } + + public func eth_gasPrice() async throws -> BigUInt { + return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + eth_gasPrice(completionHandler: continuation.resume) + } + } + + public func eth_blockNumber() async throws -> Int { + return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + eth_blockNumber(completionHandler: continuation.resume) + } + } + + public func eth_getBalance(address: EthereumAddress, block: EthereumBlock) async throws -> BigUInt { + return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + eth_getBalance(address: address, block: block, completionHandler: continuation.resume) + } + } + + public func eth_getCode(address: EthereumAddress, block: EthereumBlock = .Latest) async throws -> String { + return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + eth_getCode(address: address, block: block, completionHandler: continuation.resume) + } + } + + public func eth_estimateGas(_ transaction: EthereumTransaction) async throws -> BigUInt { + return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + eth_estimateGas(transaction, completionHandler: continuation.resume) + } + } + + public func eth_sendRawTransaction(_ transaction: EthereumTransaction, withAccount account: EthereumAccountProtocol) async throws -> String { + return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + eth_sendRawTransaction(transaction, withAccount: account, completionHandler: continuation.resume) + } + } + + public func eth_getTransactionCount(address: EthereumAddress, block: EthereumBlock) async throws -> Int { + return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + eth_getTransactionCount(address: address, block: block, completionHandler: continuation.resume) + } + } + + public func eth_getTransaction(byHash txHash: String) async throws -> EthereumTransaction { + return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + eth_getTransaction(byHash: txHash, completionHandler: continuation.resume) + } + } + + public func eth_getTransactionReceipt(txHash: String) async throws -> EthereumTransactionReceipt { + return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + eth_getTransactionReceipt(txHash: txHash, completionHandler: continuation.resume) + } + } + + public func eth_getLogs(addresses: [EthereumAddress]?, topics: [String?]?, fromBlock from: EthereumBlock = .Earliest, toBlock to: EthereumBlock = .Latest) async throws -> [EthereumLog] { + return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<[EthereumLog], Error>) in + eth_getLogs(addresses: addresses, topics: topics, fromBlock: from, toBlock: to, completionHandler: continuation.resume) + } + } + + public func eth_getLogs(addresses: [EthereumAddress]?, orTopics topics: [[String]?]?, fromBlock from: EthereumBlock = .Earliest, toBlock to: EthereumBlock = .Latest) async throws -> [EthereumLog] { + return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<[EthereumLog], Error>) in + eth_getLogs(addresses: addresses, orTopics: topics, fromBlock: from, toBlock: to, completionHandler: continuation.resume) + } + } + + public func eth_getBlockByNumber(_ block: EthereumBlock) async throws -> EthereumBlockInfo { + return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + eth_getBlockByNumber(block, completionHandler: continuation.resume) + } + } + + public func eth_call(_ transaction: EthereumTransaction, block: EthereumBlock = .Latest) async throws -> String { + return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + eth_call(transaction, block: block, completionHandler: continuation.resume) + } + } + + public func eth_call(_ transaction: EthereumTransaction, resolution: CallResolution = .noOffchain(failOnExecutionError: true), block: EthereumBlock = .Latest) async throws -> String { + return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + eth_call(transaction, resolution: resolution, block: block, completionHandler: continuation.resume) + } + } + + public func subscribe(type: EthereumSubscriptionType) async throws -> EthereumSubscription { + return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + subscribe(type: type, completionHandler: continuation.resume) + } + } + + public func unsubscribe(_ subscription: EthereumSubscription) async throws -> Bool { + return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + unsubscribe(subscription, completionHandler: continuation.resume) + } + } + + public func pendingTransactions(onData: @escaping (String) -> Void) async throws -> EthereumSubscription { + return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + pendingTransactions(onSubscribe: continuation.resume, onData: onData) + } + } + + public func newBlockHeaders(onData: @escaping (EthereumHeader) -> Void) async throws -> EthereumSubscription { + return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + newBlockHeaders(onSubscribe: continuation.resume, onData: onData) + } + } + + public func syncing(onData: @escaping (EthereumSyncStatus) -> Void) async throws -> EthereumSubscription { + return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + syncing(onSubscribe: continuation.resume, onData: onData) + } + } +} diff --git a/web3swift/src/Client/JSONRPC.swift b/web3swift/src/Client/JSONRPC.swift index 7956a397..8f044c8b 100644 --- a/web3swift/src/Client/JSONRPC.swift +++ b/web3swift/src/Client/JSONRPC.swift @@ -96,12 +96,12 @@ public class EthereumRPC { } request.httpBody = encoded - let task = session.dataTask(with: request) { (data, response, error) in + let task = session.dataTask(with: request) { (data, response, _) in if let data = data { if let result = try? JSONDecoder().decode(JSONRPCResult.self, from: data) { completionHandler(.success(result.result)) } else if let result = try? JSONDecoder().decode([JSONRPCResult].self, from: data) { - let resultObjects = result.map{ return $0.result } + let resultObjects = result.map { return $0.result } completionHandler(.success(resultObjects)) } else if let errorResult = try? JSONDecoder().decode(JSONRPCErrorResult.self, from: data) { completionHandler(.failure(JSONRPCError.executionError(errorResult))) @@ -127,18 +127,3 @@ extension EthereumRPC { } } } - -// MARK: - Deprecated -extension EthereumRPC { - @available(*, deprecated, renamed: "execute(session:url:method:params:receive:id:completionHandler:)") - public static func execute(session: URLSession, url: URL, method: String, params: T, receive: U.Type, id: Int = 1, completion: @escaping ((Error?, Any?) -> Void)) -> Void { - Self.execute(session: session, url: url, method: method, params: params, receive: receive, id: id) { result in - switch result { - case .success(let data): - completion(nil, data) - case .failure(let error): - completion(error, nil) - } - } - } -} diff --git a/web3swift/src/Client/Models/EthereumBlock.swift b/web3swift/src/Client/Models/EthereumBlock.swift index 5a6d04cc..6db3930a 100644 --- a/web3swift/src/Client/Models/EthereumBlock.swift +++ b/web3swift/src/Client/Models/EthereumBlock.swift @@ -13,7 +13,7 @@ public enum EthereumBlock: Hashable { case Earliest case Pending case Number(Int) - + public var stringValue: String { switch self { case .Latest: @@ -26,7 +26,7 @@ public enum EthereumBlock: Hashable { return int.web3.hexString } } - + public var intValue: Int? { switch self { case .Number(let int): @@ -35,11 +35,11 @@ public enum EthereumBlock: Hashable { return nil } } - + public init(rawValue: Int) { self = .Number(rawValue) } - + public init(rawValue: String) { if rawValue == "latest" { self = .Latest @@ -59,7 +59,7 @@ extension EthereumBlock: Codable { let strValue = try value.decode(String.self) self = EthereumBlock(rawValue: strValue) } - + public func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() try container.encode(self.stringValue) @@ -70,7 +70,7 @@ extension EthereumBlock: Comparable { static public func == (lhs: EthereumBlock, rhs: EthereumBlock) -> Bool { return lhs.stringValue == rhs.stringValue } - + static public func < (lhs: EthereumBlock, rhs: EthereumBlock) -> Bool { switch lhs { case .Earliest: @@ -91,6 +91,6 @@ extension EthereumBlock: Comparable { return lhsInt < rhsInt } } - + } } diff --git a/web3swift/src/Client/Models/EthereumBlockInfo.swift b/web3swift/src/Client/Models/EthereumBlockInfo.swift index 602a702b..6d3af2e1 100644 --- a/web3swift/src/Client/Models/EthereumBlockInfo.swift +++ b/web3swift/src/Client/Models/EthereumBlockInfo.swift @@ -22,31 +22,30 @@ extension EthereumBlockInfo: Codable { } public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) - + guard let number = try? container.decode(EthereumBlock.self, forKey: .number) else { throw JSONRPCError.decodingError } - + guard let timestampRaw = try? container.decode(String.self, forKey: .timestamp), let timestamp = TimeInterval(timestampRaw) else { throw JSONRPCError.decodingError } - + guard let transactions = try? container.decode([String].self, forKey: .transactions) else { throw JSONRPCError.decodingError } - + self.number = number self.timestamp = Date(timeIntervalSince1970: timestamp) self.transactions = transactions } - + public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) - + try container.encode(number, forKey: .number) try container.encode(Int(timestamp.timeIntervalSince1970).web3.hexString, forKey: .timestamp) try container.encode(transactions, forKey: .transactions) } } - diff --git a/web3swift/src/Client/Models/EthereumHeader.swift b/web3swift/src/Client/Models/EthereumHeader.swift new file mode 100644 index 00000000..0f84fe19 --- /dev/null +++ b/web3swift/src/Client/Models/EthereumHeader.swift @@ -0,0 +1,28 @@ +// +// EthereumHeader.swift +// web3swift +// +// Created by Dionisis Karatzas on 8/6/22. +// Copyright © 2018 Argent Labs Limited. All rights reserved. +// + +import Foundation + +public struct EthereumHeader: Codable { + public let parentHash: String + public let sha3Uncles: String + public let miner: String + public let stateRoot: String + public let transactionsRoot: String + public let receiptsRoot: String + public let logsBloom: String + public let difficulty: String + public let number: String + public let gasLimit: String + public let gasUsed: String + public let timestamp: String + public let extraData: String + public let mixHash: String + public let nonce: String + public let hash: String +} diff --git a/web3swift/src/Client/Models/EthereumLog.swift b/web3swift/src/Client/Models/EthereumLog.swift index 796213c0..9cbb4c5f 100644 --- a/web3swift/src/Client/Models/EthereumLog.swift +++ b/web3swift/src/Client/Models/EthereumLog.swift @@ -33,37 +33,37 @@ extension EthereumLog: Codable { case data // Data case topics // Array of Data } - + public init(from decoder: Decoder) throws { let values = try decoder.container(keyedBy: CodingKeys.self) - + self.removed = try values.decodeIfPresent(Bool.self, forKey: .removed) self.address = try values.decode(EthereumAddress.self, forKey: .address) self.data = try values.decode(String.self, forKey: .data) self.topics = try values.decode([String].self, forKey: .topics) - + if let logIndexString = try? values.decode(String.self, forKey: .logIndex), let logIndex = BigUInt(hex: logIndexString) { self.logIndex = logIndex } else { self.logIndex = nil } - + if let transactionIndexString = try? values.decode(String.self, forKey: .transactionIndex), let transactionIndex = BigUInt(hex: transactionIndexString) { self.transactionIndex = transactionIndex } else { self.transactionIndex = nil } - + self.transactionHash = try? values.decode(String.self, forKey: .transactionHash) self.blockHash = try? values.decode(String.self, forKey: .blockHash) - + if let blockNumberString = try? values.decode(String.self, forKey: .blockNumber) { self.blockNumber = EthereumBlock(rawValue: blockNumberString) } else { self.blockNumber = EthereumBlock.Earliest } } - + public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encodeIfPresent(self.removed, forKey: .removed) @@ -79,10 +79,8 @@ extension EthereumLog: Codable { try container.encode(self.address, forKey: .address) try container.encode(self.data, forKey: .data) try container.encode(self.topics, forKey: .topics) - - } - + } } extension EthereumLog: Comparable { @@ -92,9 +90,7 @@ extension EthereumLog: Comparable { let rhsIndex = rhs.logIndex { return lhsIndex < rhsIndex } - + return lhs.blockNumber < rhs.blockNumber } } - - diff --git a/web3swift/src/Client/Models/EthereumNetwork.swift b/web3swift/src/Client/Models/EthereumNetwork.swift index fe5b9556..9dfed1ed 100644 --- a/web3swift/src/Client/Models/EthereumNetwork.swift +++ b/web3swift/src/Client/Models/EthereumNetwork.swift @@ -8,13 +8,13 @@ import Foundation -public enum EthereumNetwork: Equatable { +public enum EthereumNetwork: Equatable, Decodable { case Mainnet case Ropsten case Rinkeby case Kovan case Custom(String) - + static func fromString(_ networkId: String) -> EthereumNetwork { switch networkId { case "1": @@ -29,7 +29,7 @@ public enum EthereumNetwork: Equatable { return .Custom(networkId) } } - + var stringValue: String { switch self { case .Mainnet: @@ -44,7 +44,7 @@ public enum EthereumNetwork: Equatable { return str } } - + var intValue: Int { switch self { case .Mainnet: @@ -63,5 +63,5 @@ public enum EthereumNetwork: Equatable { public func ==(lhs: EthereumNetwork, rhs: EthereumNetwork) -> Bool { return lhs.stringValue == rhs.stringValue - + } diff --git a/web3swift/src/Client/Models/EthereumSubscription.swift b/web3swift/src/Client/Models/EthereumSubscription.swift new file mode 100644 index 00000000..02f718ba --- /dev/null +++ b/web3swift/src/Client/Models/EthereumSubscription.swift @@ -0,0 +1,40 @@ +// +// EthereumNetwork.swift +// web3swift +// +// Created by Dionisis Karatzas on 8/6/22. +// Copyright © 2018 Argent Labs Limited. All rights reserved. +// + +import Foundation + +public enum EthereumSubscriptionType: Equatable, Hashable { + case newBlockHeaders + case pendingTransactions + case syncing + + var method: String { + switch self { + case .newBlockHeaders: + return "newHeads" + case .pendingTransactions: + return "newPendingTransactions" + case .syncing: + return "syncing" + } + } + + struct LogParams: Encodable { + let address: [EthereumAddress] + let topics: [String?] + } + + var params: String? { + return nil + } +} + +public struct EthereumSubscription: Hashable { + let type: EthereumSubscriptionType + let id: String +} diff --git a/web3swift/src/Client/Models/EthereumSyncStatus.swift b/web3swift/src/Client/Models/EthereumSyncStatus.swift new file mode 100644 index 00000000..6ed30fd1 --- /dev/null +++ b/web3swift/src/Client/Models/EthereumSyncStatus.swift @@ -0,0 +1,86 @@ +// +// EthereumSync.swift +// web3swift +// +// Created by Dionisis Karatzas on 8/6/22. +// Copyright © 2018 Argent Labs Limited. All rights reserved. +// + +public struct EthereumSyncStatus: Codable { + public let result: ResultUnion + + enum CodingKeys: String, CodingKey { + case result + } + + public init(result: ResultUnion) { + self.result = result + } +} + +public enum ResultUnion: Codable { + case bool(Bool) + case resultClass(ResultClass) + + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + if let x = try? container.decode(Bool.self) { + self = .bool(x) + return + } + if let x = try? container.decode(ResultClass.self) { + self = .resultClass(x) + return + } + throw DecodingError.typeMismatch(ResultUnion.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for ResultUnion")) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + switch self { + case .bool(let x): + try container.encode(x) + case .resultClass(let x): + try container.encode(x) + } + } +} + +public struct ResultClass: Codable { + public struct Status: Codable { + public let startingBlock: Int + public let currentBlock: Int + public let highestBlock: Int + public let pulledStates: Int + public let knownStates: Int + + enum CodingKeys: String, CodingKey { + case startingBlock + case currentBlock + case highestBlock + case pulledStates + case knownStates + } + + public init(startingBlock: Int, currentBlock: Int, highestBlock: Int, pulledStates: Int, knownStates: Int) { + self.startingBlock = startingBlock + self.currentBlock = currentBlock + self.highestBlock = highestBlock + self.pulledStates = pulledStates + self.knownStates = knownStates + } + } + + public let syncing: Bool + public let status: Status + + enum CodingKeys: String, CodingKey { + case syncing + case status + } + + public init(syncing: Bool, status: Status) { + self.syncing = syncing + self.status = status + } +} diff --git a/web3swift/src/Client/Models/EthereumTransaction.swift b/web3swift/src/Client/Models/EthereumTransaction.swift index d3c39641..4a392113 100644 --- a/web3swift/src/Client/Models/EthereumTransaction.swift +++ b/web3swift/src/Client/Models/EthereumTransaction.swift @@ -13,7 +13,7 @@ public protocol EthereumTransactionProtocol { init(from: EthereumAddress?, to: EthereumAddress, value: BigUInt?, data: Data?, nonce: Int?, gasPrice: BigUInt?, gasLimit: BigUInt?, chainId: Int?) init(from: EthereumAddress?, to: EthereumAddress, data: Data, gasPrice: BigUInt, gasLimit: BigUInt) init(to: EthereumAddress, data: Data) - + var raw: Data? { get } var hash: Data? { get } } @@ -28,13 +28,14 @@ public struct EthereumTransaction: EthereumTransactionProtocol, Equatable, Codab public let gasLimit: BigUInt? public let gas: BigUInt? public let blockNumber: EthereumBlock? + public let input: String? public private(set) var hash: Data? public var chainId: Int? { didSet { self.hash = self.raw?.web3.keccak256 } } - + public init(from: EthereumAddress?, to: EthereumAddress, value: BigUInt?, data: Data?, nonce: Int?, gasPrice: BigUInt?, gasLimit: BigUInt?, chainId: Int?) { self.from = from self.to = to @@ -48,6 +49,7 @@ public struct EthereumTransaction: EthereumTransactionProtocol, Equatable, Codab 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] self.hash = RLP.encode(txArray) + self.input = nil } public init( @@ -66,7 +68,7 @@ public struct EthereumTransaction: EthereumTransactionProtocol, Equatable, Codab gasLimit: gasLimit ) } - + public init( from: EthereumAddress?, to: EthereumAddress, @@ -84,8 +86,9 @@ public struct EthereumTransaction: EthereumTransactionProtocol, Equatable, Codab self.gas = nil self.blockNumber = nil self.hash = nil + self.input = nil } - + public init(to: EthereumAddress, data: Data) { self.from = nil self.to = to @@ -96,15 +99,16 @@ public struct EthereumTransaction: EthereumTransactionProtocol, Equatable, Codab self.gas = nil self.blockNumber = nil self.hash = nil + self.input = nil } - + public var raw: Data? { let txArray: [Any?] = [self.nonce, self.gasPrice, self.gasLimit, self.to.value.web3.noHexPrefix, self.value, self.data, self.chainId, 0, 0] return RLP.encode(txArray) } - - enum CodingKeys : String, CodingKey { + + enum CodingKeys: String, CodingKey { case from case to case value @@ -115,22 +119,23 @@ public struct EthereumTransaction: EthereumTransactionProtocol, Equatable, Codab case gasLimit case blockNumber case hash + case input } - + public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.to = try container.decode(EthereumAddress.self, forKey: .to) self.from = try? container.decode(EthereumAddress.self, forKey: .from) self.data = try? container.decode(Data.self, forKey: .data) - + let decodeHexUInt = { (key: CodingKeys) -> BigUInt? in return (try? container.decode(String.self, forKey: key)).flatMap { BigUInt(hex: $0)} } - + let decodeHexInt = { (key: CodingKeys) -> Int? in return (try? container.decode(String.self, forKey: key)).flatMap { Int(hex: $0)} } - + self.value = decodeHexUInt(.value) self.gasLimit = decodeHexUInt(.gasLimit) self.gasPrice = decodeHexUInt(.gasPrice) @@ -139,8 +144,9 @@ public struct EthereumTransaction: EthereumTransactionProtocol, Equatable, Codab self.blockNumber = try? container.decode(EthereumBlock.self, forKey: .blockNumber) self.hash = (try? container.decode(String.self, forKey: .hash))?.web3.hexData self.chainId = nil + self.input = try? container.decode(String.self, forKey: .input) } - + public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(to, forKey: .to) @@ -153,6 +159,7 @@ public struct EthereumTransaction: EthereumTransactionProtocol, Equatable, Codab try? container.encode(nonce?.web3.hexString, forKey: .nonce) try? container.encode(blockNumber, forKey: .blockNumber) try? container.encode(hash?.web3.hexString, forKey: .hash) + try? container.encode(input, forKey: .input) } } @@ -161,20 +168,20 @@ public struct SignedTransaction { let v: Int let r: Data let s: Data - + public init(transaction: EthereumTransaction, v: Int, r: Data, s: Data) { self.transaction = transaction self.v = v self.r = r.web3.strippingZeroesFromBytes self.s = s.web3.strippingZeroesFromBytes } - + public var raw: Data? { let txArray: [Any?] = [transaction.nonce, transaction.gasPrice, transaction.gasLimit, transaction.to.value.web3.noHexPrefix, transaction.value, transaction.data, self.v, self.r, self.s] return RLP.encode(txArray) } - + public var hash: Data? { return raw?.web3.keccak256 } diff --git a/web3swift/src/Client/Models/EthereumTransactionReceipt.swift b/web3swift/src/Client/Models/EthereumTransactionReceipt.swift index dd01bb61..cabdf2b7 100644 --- a/web3swift/src/Client/Models/EthereumTransactionReceipt.swift +++ b/web3swift/src/Client/Models/EthereumTransactionReceipt.swift @@ -22,10 +22,10 @@ public struct EthereumTransactionReceipt: Decodable { public var blockNumber: BigUInt public var gasUsed: BigUInt public var contractAddress: EthereumAddress? - public var logs: Array = [] + public var logs: [EthereumLog] = [] var logsBloom: Data? public var status: EthereumTransactionReceiptStatus - + enum CodingKeys: String, CodingKey { case transactionHash // Data case transactionIndex // Quantity @@ -38,31 +38,31 @@ public struct EthereumTransactionReceipt: Decodable { case logsBloom // Data case status // Quantity (success 1 or failure 0) } - + public init(from decoder: Decoder) throws { let values = try decoder.container(keyedBy: CodingKeys.self) - + self.transactionHash = try values.decode(String.self, forKey: .transactionHash) self.blockHash = try values.decode(String.self, forKey: .blockHash) self.contractAddress = try? values.decode(EthereumAddress.self, forKey: .contractAddress) - + let transactionIndexString = try values.decode(String.self, forKey: .transactionIndex) let blockNumberString = try values.decode(String.self, forKey: .blockNumber) let gasUsedString = try values.decode(String.self, forKey: .gasUsed) let logsBloomString = try values.decode(String.self, forKey: .logsBloom) let statusString = try values.decode(String.self, forKey: .status) - + guard let transactionIndex = BigUInt(hex: transactionIndexString), let blockNumber = BigUInt(hex: blockNumberString), let gasUsed = BigUInt(hex: gasUsedString), let statusCode = Int(hex: statusString) else { throw EthereumClientError.decodeIssue } - + self.transactionIndex = transactionIndex self.blockNumber = blockNumber self.gasUsed = gasUsed self.logsBloom = Data(hex: logsBloomString) ?? nil self.status = EthereumTransactionReceiptStatus(rawValue: statusCode) ?? .notProcessed - + self.logs = try values.decode([EthereumLog].self, forKey: .logs) } - + } diff --git a/web3swift/src/Client/RecursiveLogCollector.swift b/web3swift/src/Client/RecursiveLogCollector.swift index 9c5ed071..fc4b5609 100644 --- a/web3swift/src/Client/RecursiveLogCollector.swift +++ b/web3swift/src/Client/RecursiveLogCollector.swift @@ -8,11 +8,11 @@ import Foundation -enum Topics: Encodable { +public enum Topics: Encodable { case plain([String?]) case composed([[String]?]) - func encode(to encoder: Encoder) throws { + public func encode(to encoder: Encoder) throws { var container = encoder.unkeyedContainer() switch self { case .plain(let values): @@ -24,7 +24,7 @@ enum Topics: Encodable { } struct RecursiveLogCollector { - let ethClient: EthereumClient + let ethClient: EthereumClientProtocol func getAllLogs( addresses: [EthereumAddress]?, diff --git a/web3swift/src/Contract/Statically Typed/EthereumClient+Static.swift b/web3swift/src/Contract/Statically Typed/EthereumClient+Static.swift index 67f490fc..d64ea03a 100644 --- a/web3swift/src/Contract/Statically Typed/EthereumClient+Static.swift +++ b/web3swift/src/Contract/Statically Typed/EthereumClient+Static.swift @@ -289,170 +289,3 @@ public extension EthereumClientProtocol { } } } - -// MARK: - Deprecated -public extension ABIFunction { - @available(*, deprecated, renamed: "execute(withClient:account:completionHandler:)") - func execute(withClient client: EthereumClientProtocol, account: EthereumAccountProtocol, completion: @escaping((EthereumClientError?, String?) -> Void)) { - execute(withClient: client, account: account) { result in - switch result { - case .success(let value): - completion(nil, value) - case.failure(let error): - completion(error, nil) - } - } - } - - @available(*, deprecated, renamed: "call(withClient:responseType:block:resolution:completionHandler:)") - func call(withClient client: EthereumClientProtocol, - responseType: T.Type, - block: EthereumBlock = .Latest, - resolution: CallResolution = .noOffchain(failOnExecutionError: true), - completion: @escaping((EthereumClientError?, T?) -> Void)) { - call(withClient: client, responseType: responseType) { result in - switch result { - case .success(let value): - completion(nil, value) - case.failure(let error): - completion(error, nil) - } - } - } -} - -public extension EthereumClientProtocol { - @available(*, deprecated, renamed: "EventsCompletionHandler") - typealias EventsCompletion = (EthereumClientError?, [ABIEvent], [EthereumLog]) -> Void - - @available(*, deprecated, renamed: "getEvents(addresses:orTopics:fromBlock:toBlock:matching:completionHandler:)") - func getEvents(addresses: [EthereumAddress]?, - orTopics: [[String]?]?, - fromBlock: EthereumBlock, - toBlock: EthereumBlock, - matching matches: [EventFilter], - completion: @escaping EventsCompletion) { - self.eth_getLogs(addresses: addresses, orTopics: orTopics, fromBlock: fromBlock, toBlock: toBlock) { [weak self] (error, logs) in - self?.handleLogs(error, logs, matches, completion) - } - } - - @available(*, deprecated, renamed: "getEvents(addresses:orTopics:fromBlock:toBlock:eventTypes:completionHandler:)") - func getEvents(addresses: [EthereumAddress]?, - orTopics: [[String]?]?, - fromBlock: EthereumBlock, - toBlock: EthereumBlock, - eventTypes: [ABIEvent.Type], - completion: @escaping EventsCompletion) { - let unfiltered = eventTypes.map { EventFilter(type: $0, allowedSenders: []) } - self.eth_getLogs(addresses: addresses, orTopics: orTopics, fromBlock: fromBlock, toBlock: toBlock) { [weak self] (error, logs) in - self?.handleLogs(error, logs, unfiltered, completion) - } - } - - @available(*, deprecated, renamed: "getEvents(addresses:topics:fromBlock:toBlock:eventTypes:completionHandler:)") - func getEvents(addresses: [EthereumAddress]?, - topics: [String?]?, - fromBlock: EthereumBlock, - toBlock: EthereumBlock, - eventTypes: [ABIEvent.Type], - completion: @escaping EventsCompletion) { - let unfiltered = eventTypes.map { EventFilter(type: $0, allowedSenders: []) } - getEvents(addresses: addresses, - topics: topics, - fromBlock: fromBlock, - toBlock: toBlock, - matching: unfiltered, - completion: completion) - } - - @available(*, deprecated, renamed: "getEvents(addresses:topics:fromBlock:toBlock:matching:completionHandler:)") - func getEvents(addresses: [EthereumAddress]?, - topics: [String?]?, - fromBlock: EthereumBlock, - toBlock: EthereumBlock, - matching matches: [EventFilter], - completion: @escaping EventsCompletion) { - - self.eth_getLogs(addresses: addresses, topics: topics, fromBlock: fromBlock, toBlock: toBlock) { [weak self] (error, logs) in - self?.handleLogs(error, logs, matches, completion) - } - } - - @available(*, deprecated) - func handleLogs(_ error: EthereumClientError?, - _ logs: [EthereumLog]?, - _ matches: [EventFilter], - _ completion: EventsCompletion) { - if let error = error { - return completion(error, [], []) - } - - guard let logs = logs else { return completion(nil, [], []) } - - var events: [ABIEvent] = [] - var unprocessed: [EthereumLog] = [] - - var filtersBySignature: [String: [EventFilter]] = [:] - for filter in matches { - if let sig = try? filter.type.signature() { - var filters = filtersBySignature[sig, default: [EventFilter]()] - filters.append(filter) - filtersBySignature[sig] = filters - } - } - - let parseEvent: (EthereumLog, ABIEvent.Type) -> ABIEvent? = { log, eventType in - let topicTypes = eventType.types.enumerated() - .filter { eventType.typesIndexed[$0.offset] == true } - .compactMap { $0.element } - - let dataTypes = eventType.types.enumerated() - .filter { eventType.typesIndexed[$0.offset] == false } - .compactMap { $0.element } - - guard let data = try? ABIDecoder.decodeData(log.data, types: dataTypes, asArray: true) else { - return nil - } - - guard data.count == dataTypes.count else { - return nil - } - - let rawTopics = Array(log.topics.dropFirst()) - - guard let parsedTopics = (try? zip(rawTopics, topicTypes).map { pair in - try ABIDecoder.decodeData(pair.0, types: [pair.1]) - }) else { - return nil - } - - guard let eventOpt = ((try? eventType.init(topics: parsedTopics.flatMap { $0 }, data: data, log: log)) as ABIEvent??), let event = eventOpt else { - return nil - } - - return event - } - - for log in logs { - guard let signature = log.topics.first, - let filters = filtersBySignature[signature] else { - unprocessed.append(log) - continue - } - - for filter in filters { - let allowedSenders = Set(filter.allowedSenders) - if allowedSenders.count > 0 && !allowedSenders.contains(log.address) { - unprocessed.append(log) - } else if let event = parseEvent(log, filter.type) { - events.append(event) - } else { - unprocessed.append(log) - } - } - } - - return completion(error, events, unprocessed) - } -} diff --git a/web3swift/src/ENS/EthereumNameService.swift b/web3swift/src/ENS/EthereumNameService.swift index 2f517e39..0d5eb7ba 100644 --- a/web3swift/src/ENS/EthereumNameService.swift +++ b/web3swift/src/ENS/EthereumNameService.swift @@ -18,12 +18,12 @@ protocol EthereumNameServiceProtocol { func resolve( address: EthereumAddress, mode: ResolutionMode, - completion: @escaping((EthereumNameServiceError?, String?) -> Void) + completionHandler: @escaping(Result) -> Void ) -> Void func resolve( ens: String, mode: ResolutionMode, - completion: @escaping((EthereumNameServiceError?, EthereumAddress?) -> Void) + completionHandler: @escaping(Result) -> Void ) -> Void func resolve( @@ -245,36 +245,4 @@ extension EthereumNameService { throw error as? EthereumNameServiceError ?? .ensUnknown } } - -} - -// MARK: - Deprecated -extension EthereumNameService { - @available(*, deprecated, renamed: "resolve(address:mode:completionHandler:)") - public func resolve(address: EthereumAddress, - mode: ResolutionMode, - completion: @escaping ((EthereumNameServiceError?, String?) -> Void)) { - resolve(address: address, mode: mode) { result in - switch result { - case .success(let data): - completion(nil, data) - case .failure(let error): - completion(error, nil) - } - } - } - - @available(*, deprecated, renamed: "resolve(ens:mode:completionHandler:)") - public func resolve(ens: String, - mode: ResolutionMode, - completion: @escaping ((EthereumNameServiceError?, EthereumAddress?) -> Void)) { - resolve(ens: ens, mode: mode) { result in - switch result { - case .success(let data): - completion(nil, data) - case .failure(let error): - completion(error, nil) - } - } - } } diff --git a/web3swift/src/ERC165/ERC165.swift b/web3swift/src/ERC165/ERC165.swift index 14eb7067..2dee3e89 100644 --- a/web3swift/src/ERC165/ERC165.swift +++ b/web3swift/src/ERC165/ERC165.swift @@ -38,21 +38,6 @@ extension ERC165 { } } -// MARK: - Deprecated -extension ERC165 { - @available(*, deprecated, renamed: "supportsInterface(contract:id:completionHandler:)") - public func supportsInterface(contract: EthereumAddress, id: Data, completion: @escaping((Error?, Bool?) -> Void)) { - supportsInterface(contract: contract, id: id) { result in - switch result { - case .success(let data): - completion(nil, data) - case .failure(let error): - completion(error, nil) - } - } - } -} - public enum ERC165Functions { public static var interfaceId: Data { return "supportsInterface(bytes4)".web3.keccak256.web3.bytes4 diff --git a/web3swift/src/ERC20/ERC20.swift b/web3swift/src/ERC20/ERC20.swift index f901fd72..1334743f 100644 --- a/web3swift/src/ERC20/ERC20.swift +++ b/web3swift/src/ERC20/ERC20.swift @@ -10,7 +10,7 @@ import Foundation import BigInt public protocol ERC20Protocol { - init(client: EthereumClient) + init(client: EthereumClientProtocol) func name(tokenContract: EthereumAddress, completionHandler: @escaping(Result) -> Void) func symbol(tokenContract: EthereumAddress, completionHandler: @escaping(Result) -> Void) @@ -28,21 +28,12 @@ public protocol ERC20Protocol { func allowance(tokenContract: EthereumAddress, address: EthereumAddress, spender: EthereumAddress) async throws -> BigUInt func transferEventsTo(recipient: EthereumAddress, fromBlock: EthereumBlock, toBlock: EthereumBlock) async throws -> [ERC20Events.Transfer] func transferEventsFrom(sender: EthereumAddress, fromBlock: EthereumBlock, toBlock: EthereumBlock) async throws -> [ERC20Events.Transfer] - - // deprecated - func name(tokenContract: EthereumAddress, completion: @escaping((Error?, String?) -> Void)) - func symbol(tokenContract: EthereumAddress, completion: @escaping((Error?, String?) -> Void)) - func decimals(tokenContract: EthereumAddress, completion: @escaping((Error?, UInt8?) -> Void)) - func balanceOf(tokenContract: EthereumAddress, address: EthereumAddress, completion: @escaping((Error?, BigUInt?) -> Void)) - func allowance(tokenContract: EthereumAddress, address: EthereumAddress, spender: EthereumAddress, completion: @escaping((Error?, BigUInt?) -> Void)) - func transferEventsTo(recipient: EthereumAddress, fromBlock: EthereumBlock, toBlock: EthereumBlock, completion: @escaping((Error?, [ERC20Events.Transfer]?) -> Void)) - func transferEventsFrom(sender: EthereumAddress, fromBlock: EthereumBlock, toBlock: EthereumBlock, completion: @escaping((Error?, [ERC20Events.Transfer]?) -> Void)) } public class ERC20: ERC20Protocol { - let client: EthereumClient + let client: EthereumClientProtocol - required public init(client: EthereumClient) { + required public init(client: EthereumClientProtocol) { self.client = client } @@ -202,90 +193,3 @@ extension ERC20 { } } } - -// MARK: - Deprecated -extension ERC20 { - @available(*, deprecated, renamed: "name(tokenContract:completionHandler:)") - public func name(tokenContract: EthereumAddress, completion: @escaping((Error?, String?) -> Void)) { - name(tokenContract: tokenContract) { result in - switch result { - case .success(let data): - completion(nil, data) - case .failure(let error): - completion(error, nil) - } - } - } - - @available(*, deprecated, renamed: "symbol(tokenContract:completionHandler:)") - public func symbol(tokenContract: EthereumAddress, completion: @escaping((Error?, String?) -> Void)) { - symbol(tokenContract: tokenContract) { result in - switch result { - case .success(let data): - completion(nil, data) - case .failure(let error): - completion(error, nil) - } - } - } - - @available(*, deprecated, renamed: "decimals(tokenContract:completionHandler:)") - public func decimals(tokenContract: EthereumAddress, completion: @escaping((Error?, UInt8?) -> Void)) { - decimals(tokenContract: tokenContract) { result in - switch result { - case .success(let data): - completion(nil, data) - case .failure(let error): - completion(error, nil) - } - } - } - - @available(*, deprecated, renamed: "balanceOf(tokenContract:address:completionHandler:)") - public func balanceOf(tokenContract: EthereumAddress, address: EthereumAddress, completion: @escaping((Error?, BigUInt?) -> Void)) { - balanceOf(tokenContract: tokenContract, address: address) { result in - switch result { - case .success(let data): - completion(nil, data) - case .failure(let error): - completion(error, nil) - } - } - } - - @available(*, deprecated, renamed: "allowance(tokenContract:address:spender:completionHandler:)") - public func allowance(tokenContract: EthereumAddress, address: EthereumAddress, spender: EthereumAddress, completion: @escaping((Error?, BigUInt?) -> Void)) { - allowance(tokenContract: tokenContract, address: address, spender: spender) { result in - switch result { - case .success(let data): - completion(nil, data) - case .failure(let error): - completion(error, nil) - } - } - } - - @available(*, deprecated, renamed: "transferEventsTo(recipient:fromBlock:toBlock:completionHandler:)") - public func transferEventsTo(recipient: EthereumAddress, fromBlock: EthereumBlock, toBlock: EthereumBlock, completion: @escaping((Error?, [ERC20Events.Transfer]?) -> Void)) { - transferEventsTo(recipient: recipient, fromBlock: fromBlock, toBlock: toBlock) { result in - switch result { - case .success(let data): - completion(nil, data) - case .failure(let error): - completion(error, nil) - } - } - } - - @available(*, deprecated, renamed: "transferEventsFrom(sender:fromBlock:toBlock:completionHandler:)") - public func transferEventsFrom(sender: EthereumAddress, fromBlock: EthereumBlock, toBlock: EthereumBlock, completion: @escaping((Error?, [ERC20Events.Transfer]?) -> Void)) { - transferEventsFrom(sender: sender, fromBlock: fromBlock, toBlock: toBlock) { result in - switch result { - case .success(let data): - completion(nil, data) - case .failure(let error): - completion(error, nil) - } - } - } -} diff --git a/web3swift/src/ERC721/ERC721.swift b/web3swift/src/ERC721/ERC721.swift index 19cb2841..e9417e2c 100644 --- a/web3swift/src/ERC721/ERC721.swift +++ b/web3swift/src/ERC721/ERC721.swift @@ -367,164 +367,3 @@ extension ERC721Enumerable { } } } - -// MARK: - Deprecated -extension ERC721 { - @available(*, deprecated, renamed: "balanceOf(contract:address:completionHandler:)") - public func balanceOf(contract: EthereumAddress, - address: EthereumAddress, - completion: @escaping((Error?, BigUInt?) -> Void)) { - balanceOf(contract: contract, address: address) { result in - switch result { - case .success(let data): - completion(nil, data) - case .failure(let error): - completion(error, nil) - } - } - } - - @available(*, deprecated, renamed: "ownerOf(contract:tokenId:completionHandler:)") - public func ownerOf(contract: EthereumAddress, - tokenId: BigUInt, - completion: @escaping((Error?, EthereumAddress?) -> Void)) { - ownerOf(contract: contract, tokenId: tokenId) { result in - switch result { - case .success(let data): - completion(nil, data) - case .failure(let error): - completion(error, nil) - } - } - } - - @available(*, deprecated, renamed: "transferEventsTo(recipient:fromBlock:toBlock:completionHandler:)") - public func transferEventsTo(recipient: EthereumAddress, - fromBlock: EthereumBlock, - toBlock: EthereumBlock, - completion: @escaping((Error?, [ERC721Events.Transfer]?) -> Void)) { - transferEventsTo(recipient: recipient, fromBlock: fromBlock, toBlock: toBlock) { result in - switch result { - case .success(let data): - completion(nil, data) - case .failure(let error): - completion(error, nil) - } - } - } - - @available(*, deprecated, renamed: "transferEventsFrom(sender:fromBlock:toBlock:completionHandler:)") - public func transferEventsFrom(sender: EthereumAddress, - fromBlock: EthereumBlock, - toBlock: EthereumBlock, - completion: @escaping((Error?, [ERC721Events.Transfer]?) -> Void)) { - transferEventsFrom(sender: sender, fromBlock: fromBlock, toBlock: toBlock) { result in - switch result { - case .success(let data): - completion(nil, data) - case .failure(let error): - completion(error, nil) - } - } - } -} - -extension ERC721Metadata { - @available(*, deprecated, renamed: "name(contract:completionHandler:)") - public func name(contract: EthereumAddress, - completion: @escaping((Error?, String?) -> Void)) { - name(contract: contract) { result in - switch result { - case .success(let data): - completion(nil, data) - case .failure(let error): - completion(error, nil) - } - } - } - - @available(*, deprecated, renamed: "symbol(contract:completionHandler:)") - public func symbol(contract: EthereumAddress, - completion: @escaping((Error?, String?) -> Void)) { - symbol(contract: contract) { result in - switch result { - case .success(let data): - completion(nil, data) - case .failure(let error): - completion(error, nil) - } - } - } - - @available(*, deprecated, renamed: "tokenURI(contract:tokenID:completionHandler:)") - public func tokenURI(contract: EthereumAddress, - tokenID: BigUInt, - completion: @escaping((Error?, URL?) -> Void)) { - tokenURI(contract: contract, tokenID: tokenID) { result in - switch result { - case .success(let data): - completion(nil, data) - case .failure(let error): - completion(error, nil) - } - } - } - - @available(*, deprecated, renamed: "tokenMetadata(contract:tokenID:completionHandler:)") - public func tokenMetadata(contract: EthereumAddress, - tokenID: BigUInt, - completion: @escaping((Error?, Token?) -> Void)) { - tokenMetadata(contract: contract, tokenID: tokenID) { result in - switch result { - case .success(let data): - completion(nil, data) - case .failure(let error): - completion(error, nil) - } - } - } -} - -extension ERC721Enumerable { - @available(*, deprecated, renamed: "totalSupply(contract:completionHandler:)") - public func totalSupply(contract: EthereumAddress, - completion: @escaping((Error?, BigUInt?) -> Void)) { - totalSupply(contract: contract) { result in - switch result { - case .success(let data): - completion(nil, data) - case .failure(let error): - completion(error, nil) - } - } - } - - @available(*, deprecated, renamed: "tokenByIndex(contract:index:completionHandler:)") - public func tokenByIndex(contract: EthereumAddress, - index: BigUInt, - completion: @escaping((Error?, BigUInt?) -> Void)) { - tokenByIndex(contract: contract, index: index) { result in - switch result { - case .success(let data): - completion(nil, data) - case .failure(let error): - completion(error, nil) - } - } - } - - @available(*, deprecated, renamed: "tokenOfOwnerByIndex(contract:owner:index:completionHandler:)") - public func tokenOfOwnerByIndex(contract: EthereumAddress, - owner: EthereumAddress, - index: BigUInt, - completion: @escaping((Error?, BigUInt?) -> Void)) { - tokenOfOwnerByIndex(contract: contract, owner: owner, index: index) { result in - switch result { - case .success(let data): - completion(nil, data) - case .failure(let error): - completion(error, nil) - } - } - } -} diff --git a/web3swift/src/Extensions/ResultExtensions.swift b/web3swift/src/Extensions/ResultExtensions.swift new file mode 100644 index 00000000..acd55d64 --- /dev/null +++ b/web3swift/src/Extensions/ResultExtensions.swift @@ -0,0 +1,25 @@ +// +// ResultExtensions.swift +// web3swift +// +// Created by Dionisis Karatzas on 2/6/22. +// Copyright © 2018 Argent Labs Limited. All rights reserved. +// + +import Foundation + +extension Result where Failure == EthereumClientError { + init(catching body: () throws -> Success) { + do { + self = .success(try body()) + } catch { + self = .failure(error as! EthereumClientError) + } + } + + func tryMap(_ transform: (Success) throws -> NewSuccess) -> Result { + self.flatMap { value in + Result { try transform(value) } + } + } +} diff --git a/web3swift/src/Utils/KeyUtil.swift b/web3swift/src/Utils/KeyUtil.swift index 92798ef0..0e9fb2ec 100644 --- a/web3swift/src/Utils/KeyUtil.swift +++ b/web3swift/src/Utils/KeyUtil.swift @@ -8,6 +8,7 @@ import Foundation import secp256k1 +import Logging enum KeyUtilError: Error { case invalidContext @@ -19,13 +20,17 @@ enum KeyUtilError: Error { } class KeyUtil { + private static var log: Logger { + Logger(label: "web3.swift.key-util") + } + static func generatePrivateKeyData() -> Data? { return Data.randomOfLength(32) } static func generatePublicKey(from privateKey: Data) throws -> Data { guard let ctx = secp256k1_context_create(UInt32(SECP256K1_CONTEXT_SIGN | SECP256K1_CONTEXT_VERIFY)) else { - print("Failed to generate a public key: invalid context.") + log.warning("Failed to generate a public key: invalid context.") throw KeyUtilError.invalidContext } @@ -36,7 +41,7 @@ class KeyUtil { let privateKeyPtr = (privateKey as NSData).bytes.assumingMemoryBound(to: UInt8.self) guard secp256k1_ec_seckey_verify(ctx, privateKeyPtr) == 1 else { - print("Failed to generate a public key: private key is not valid.") + log.warning("Failed to generate a public key: private key is not valid.") throw KeyUtilError.privateKeyInvalid } @@ -45,7 +50,7 @@ class KeyUtil { publicKeyPtr.deallocate() } guard secp256k1_ec_pubkey_create(ctx, publicKeyPtr, privateKeyPtr) == 1 else { - print("Failed to generate a public key: public key could not be created.") + log.warning("Failed to generate a public key: public key could not be created.") throw KeyUtilError.unknownError } @@ -69,7 +74,7 @@ class KeyUtil { static func sign(message: Data, with privateKey: Data, hashing: Bool) throws -> Data { guard let ctx = secp256k1_context_create(UInt32(SECP256K1_CONTEXT_SIGN | SECP256K1_CONTEXT_VERIFY)) else { - print("Failed to sign message: invalid context.") + log.warning("Failed to sign message: invalid context.") throw KeyUtilError.invalidContext } @@ -85,7 +90,7 @@ class KeyUtil { signaturePtr.deallocate() } guard secp256k1_ecdsa_sign_recoverable(ctx, signaturePtr, msg, privateKeyPtr, nil, nil) == 1 else { - print("Failed to sign message: recoverable ECDSA signature creation failed.") + log.warning("Failed to sign message: recoverable ECDSA signature creation failed.") throw KeyUtilError.signatureFailure } @@ -114,7 +119,7 @@ class KeyUtil { } guard let ctx = secp256k1_context_create(UInt32(SECP256K1_CONTEXT_SIGN | SECP256K1_CONTEXT_VERIFY)) else { - print("Failed to sign message: invalid context.") + log.warning("Failed to sign message: invalid context.") throw KeyUtilError.invalidContext } defer { secp256k1_context_destroy(ctx) } @@ -135,7 +140,7 @@ class KeyUtil { try serializedSignature.withUnsafeBytes { guard secp256k1_ecdsa_recoverable_signature_parse_compact(ctx, signaturePtr, $0.bindMemory(to: UInt8.self).baseAddress!, v) == 1 else { - print("Failed to parse signature: recoverable ECDSA signature parse failed.") + log.warning("Failed to parse signature: recoverable ECDSA signature parse failed.") throw KeyUtilError.signatureParseFailure } }