diff --git a/CHANGELOG.md b/CHANGELOG.md index 6da5dbfb2..e59972867 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -344,7 +344,7 @@ **Closed issues:** -- Use custom JSONRPCmethod and Units [\#148](https://github.com/skywinder/web3swift/issues/148) +- Use custom JSONRPCMethod and Units [\#148](https://github.com/skywinder/web3swift/issues/148) - ERC20 some functions are not working [\#146](https://github.com/skywinder/web3swift/issues/146) - fix `pod install` absolute paths [\#97](https://github.com/skywinder/web3swift/issues/97) - Installing issue by pod [\#76](https://github.com/skywinder/web3swift/issues/76) diff --git a/Sources/Web3Core/EthereumNetwork/Request/APIRequest+Methods.swift b/Sources/Web3Core/EthereumNetwork/Request/APIRequest+Methods.swift index 463cd60d5..7cb93b1fc 100644 --- a/Sources/Web3Core/EthereumNetwork/Request/APIRequest+Methods.swift +++ b/Sources/Web3Core/EthereumNetwork/Request/APIRequest+Methods.swift @@ -1,6 +1,5 @@ // // APIRequest+Methods.swift -// // // Created by Yaroslav Yashin on 12.07.2022. // diff --git a/Sources/Web3Core/EthereumNetwork/RequestParameter/RequestParameter.swift b/Sources/Web3Core/EthereumNetwork/RequestParameter/RequestParameter.swift index 65c3059fa..edfdaa1e8 100644 --- a/Sources/Web3Core/EthereumNetwork/RequestParameter/RequestParameter.swift +++ b/Sources/Web3Core/EthereumNetwork/RequestParameter/RequestParameter.swift @@ -23,13 +23,13 @@ import Foundation Here's an example of using this enum in field. ```swift - let jsonRPCParams: [APIRequestParameterType] = [ + let JSONRPCParams: [APIRequestParameterType] = [ .init(rawValue: 12)!, .init(rawValue: "this")!, .init(rawValue: 12.2)!, .init(rawValue: [12.2, 12.4])! ] - let encoded = try JSONEncoder().encode(jsonRPCParams) + let encoded = try JSONEncoder().encode(JSONRPCParams) print(String(data: encoded, encoding: .utf8)!) //> [12,\"this\",12.2,[12.2,12.4]]` ``` diff --git a/Sources/Web3Core/KeystoreManager/BIP32Keystore.swift b/Sources/Web3Core/KeystoreManager/BIP32Keystore.swift index 67a95e3ef..b11b0ef5d 100755 --- a/Sources/Web3Core/KeystoreManager/BIP32Keystore.swift +++ b/Sources/Web3Core/KeystoreManager/BIP32Keystore.swift @@ -74,10 +74,10 @@ public class BIP32Keystore: AbstractKeystore { } public init?(_ jsonData: Data) { - guard var keystorePars = try? JSONDecoder().decode(KeystoreParamsBIP32.self, from: jsonData) else {return nil} - if keystorePars.version != Self.KeystoreParamsBIP32Version {return nil} - if keystorePars.crypto.version != nil && keystorePars.crypto.version != "1" {return nil} - if !keystorePars.isHDWallet {return nil} + guard var keystorePars = try? JSONDecoder().decode(KeystoreParamsBIP32.self, from: jsonData) else { return nil } + if keystorePars.version != Self.KeystoreParamsBIP32Version { return nil } + if keystorePars.crypto.version != nil && keystorePars.crypto.version != "1" { return nil } + if !keystorePars.isHDWallet { return nil } addressStorage = PathAddressStorage(pathAddressPairs: keystorePars.pathAddressPairs) @@ -88,8 +88,14 @@ public class BIP32Keystore: AbstractKeystore { rootPrefix = keystoreParams!.rootPath! } - public convenience init?(mnemonics: String, password: String, mnemonicsPassword: String = "", language: BIP39Language = BIP39Language.english, prefixPath: String = HDNode.defaultPathMetamaskPrefix, aesMode: String = "aes-128-cbc") throws { - guard var seed = BIP39.seedFromMmemonics(mnemonics, password: mnemonicsPassword, language: language) else { + public convenience init?(mnemonics: String, + password: String, + mnemonicsPassword: String = "", + language: BIP39Language = BIP39Language.english, + prefixPath: String = HDNode.defaultPathMetamaskPrefix, + aesMode: String = "aes-128-cbc") throws { + guard var seed = BIP39.seedFromMmemonics(mnemonics, password: mnemonicsPassword, language: language) + else { throw AbstractKeystoreError.noEntropyError } defer { @@ -98,9 +104,12 @@ public class BIP32Keystore: AbstractKeystore { try self.init(seed: seed, password: password, prefixPath: prefixPath, aesMode: aesMode) } - public init? (seed: Data, password: String, prefixPath: String = HDNode.defaultPathMetamaskPrefix, aesMode: String = "aes-128-cbc") throws { + public init?(seed: Data, + password: String, + prefixPath: String = HDNode.defaultPathMetamaskPrefix, + aesMode: String = "aes-128-cbc") throws { addressStorage = PathAddressStorage() - guard let rootNode = HDNode(seed: seed)?.derive(path: prefixPath, derivePrivateKey: true) else {return nil} + guard let rootNode = HDNode(seed: seed)?.derive(path: prefixPath, derivePrivateKey: true) else { return nil } self.rootPrefix = prefixPath try createNewAccount(parentNode: rootNode, password: password) guard let serializedRootNode = rootNode.serialize(serializePublic: false) else { @@ -127,7 +136,7 @@ public class BIP32Keystore: AbstractKeystore { try encryptDataToStorage(password, data: serializedRootNode, aesMode: self.keystoreParams!.crypto.cipher) } - func createNewAccount(parentNode: HDNode, password: String ) throws { + func createNewAccount(parentNode: HDNode, password: String) throws { var newIndex = UInt32(0) for p in addressStorage.paths { guard let idx = UInt32(p.components(separatedBy: "/").last!) else {continue} @@ -151,7 +160,8 @@ public class BIP32Keystore: AbstractKeystore { addressStorage.add(address: newAddress, for: newPath) } - public func createNewCustomChildAccount(password: String, path: String) throws {guard let decryptedRootNode = try? self.getPrefixNodeData(password) else { + public func createNewCustomChildAccount(password: String, path: String) throws { + guard let decryptedRootNode = try? self.getPrefixNodeData(password) else { throw AbstractKeystoreError.encryptionError("Failed to decrypt a keystore") } guard let rootNode = HDNode(decryptedRootNode) else { @@ -363,8 +373,7 @@ public class BIP32Keystore: AbstractKeystore { guard let params = self.keystoreParams else { return nil } - let data = try JSONEncoder().encode(params) - return data + return try JSONEncoder().encode(params) } public func serializeRootNodeToString(password: String ) throws -> String { diff --git a/Sources/Web3Core/Structure/Block/Block.swift b/Sources/Web3Core/Structure/Block/Block.swift index 1cb587d2e..6f567f458 100644 --- a/Sources/Web3Core/Structure/Block/Block.swift +++ b/Sources/Web3Core/Structure/Block/Block.swift @@ -11,7 +11,7 @@ import BigInt /// Ethereum Block /// /// Official specification: [](https://github.com/ethereum/execution-apis/blob/main/src/schemas/block.json) -public struct Block { +public struct Block: APIResultType { public var number: BigUInt // MARK: This is optional in web3js, but required in Ethereum JSON-RPC public var hash: Data // MARK: This is optional in web3js, but required in Ethereum JSON-RPC @@ -103,5 +103,3 @@ extension Block: Decodable { } } } - -extension Block: APIResultType { } diff --git a/Sources/Web3Core/Structure/Block/BlockHeader.swift b/Sources/Web3Core/Structure/Block/BlockHeader.swift new file mode 100644 index 000000000..692cabe28 --- /dev/null +++ b/Sources/Web3Core/Structure/Block/BlockHeader.swift @@ -0,0 +1,25 @@ +// +// BlockHeader.swift +// +// Created by JeneaVranceanu on 16.12.2022. +// + +import Foundation + +public struct BlockHeader: Decodable { + public let hash: String + public let difficulty: String + public let extraData: String + public let gasLimit: String + public let gasUsed: String + public let logsBloom: String + public let miner: String + public let nonce: String + public let number: String + public let parentHash: String + public let receiptsRoot: String + public let sha3Uncles: String + public let stateRoot: String + public let timestamp: String + public let transactionsRoot: String +} diff --git a/Sources/Web3Core/Structure/Block/SyncingInfo.swift b/Sources/Web3Core/Structure/Block/SyncingInfo.swift new file mode 100644 index 000000000..41db29131 --- /dev/null +++ b/Sources/Web3Core/Structure/Block/SyncingInfo.swift @@ -0,0 +1,21 @@ +// +// SyncingInfo.swift +// +// Created by JeneaVranceanu on 16.12.2022. +// + +import Foundation + +/// Returned to a WebSocket connections that subscribed on `"syncing"` event. +public struct SyncingInfo: Decodable { + public struct Status: Decodable { + public let startingBlock: Int + public let currentBlock: Int + public let highestBlock: Int + public let pulledStates: Int + public let knownStates: Int + } + + public let syncing: Bool + public let status: Status? +} diff --git a/Sources/Web3Core/Structure/JSONRPC.swift b/Sources/Web3Core/Structure/JSONRPC.swift new file mode 100755 index 000000000..8aaed5b76 --- /dev/null +++ b/Sources/Web3Core/Structure/JSONRPC.swift @@ -0,0 +1,397 @@ +// Package: web3swift +// Created by Alex Vlasov. +// Copyright © 2018 Alex Vlasov. All rights reserved. +// +// Additions to support new transaction types by Mark Loit March 2022 + +import Foundation +import BigInt + +public enum JSONRPCMethod: String, Encodable { + + // variable number of parameters in call + case newFilter = "eth_newFilter" + case getFilterLogs = "eth_getFilterLogs" + case subscribe = "eth_subscribe" + + // 0 parameter in call + case gasPrice = "eth_gasPrice" + case blockNumber = "eth_blockNumber" + case getNetwork = "net_version" + case getAccounts = "eth_accounts" + case getTxPoolStatus = "txpool_status" + case getTxPoolContent = "txpool_content" + case getTxPoolInspect = "txpool_inspect" + case estimateGas = "eth_estimateGas" + case newPendingTransactionFilter = "eth_newPendingTransactionFilter" + case newBlockFilter = "eth_newBlockFilter" + + // 1 parameter in call + case sendRawTransaction = "eth_sendRawTransaction" + case sendTransaction = "eth_sendTransaction" + case getTransactionByHash = "eth_getTransactionByHash" + case getTransactionReceipt = "eth_getTransactionReceipt" + case personalSign = "eth_sign" + case unlockAccount = "personal_unlockAccount" + case createAccount = "personal_createAccount" + case getLogs = "eth_getLogs" + case getFilterChanges = "eth_getFilterChanges" + case uninstallFilter = "eth_uninstallFilter" + case unsubscribe = "eth_unsubscribe" + + // 2 parameters in call + case call = "eth_call" + case getTransactionCount = "eth_getTransactionCount" + case getBalance = "eth_getBalance" + case getStorageAt = "eth_getStorageAt" + case getCode = "eth_getCode" + case getBlockByHash = "eth_getBlockByHash" + case getBlockByNumber = "eth_getBlockByNumber" + + // 3 parameters in call + case feeHistory = "eth_feeHistory" + + public var requiredNumOfParameters: Int? { + switch self { + case .newFilter, + .getFilterLogs, + .subscribe: + return nil + case .gasPrice, + .blockNumber, + .getNetwork, + .getAccounts, + .getTxPoolStatus, + .getTxPoolContent, + .getTxPoolInspect, + .newPendingTransactionFilter, + .newBlockFilter: + return 0 + case .sendRawTransaction, + .sendTransaction, + .getTransactionByHash, + .getTransactionReceipt, + .personalSign, + .unlockAccount, + .createAccount, + .getLogs, + .estimateGas, + .getFilterChanges, + .uninstallFilter, + .unsubscribe: + return 1 + case .call, + .getTransactionCount, + .getBalance, + .getStorageAt, + .getCode, + .getBlockByHash, + .getBlockByNumber: + return 2 + case .feeHistory: + return 3 + } + } +} + +public struct JSONRPCRequestFabric { + public static func prepareRequest(_ method: JSONRPCMethod, parameters: [Encodable]) -> JSONRPCRequest { + var request = JSONRPCRequest() + request.method = method + let pars = JSONRPCParams(params: parameters) + request.params = pars + return request + } +} + +/// JSON RPC request structure for serialization and deserialization purposes. +public struct JSONRPCRequest: Encodable { + public var jsonrpc: String = "2.0" + public var method: JSONRPCMethod? + public var params: JSONRPCParams? + public var id: UInt = Counter.increment() + + enum CodingKeys: String, CodingKey { + case jsonrpc + case method + case params + case id + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(jsonrpc, forKey: .jsonrpc) + try container.encode(method?.rawValue, forKey: .method) + try container.encode(params, forKey: .params) + try container.encode(id, forKey: .id) + } + + public var isValid: Bool { + get { + if self.method == nil { + return false + } + guard let method = self.method else {return false} + return method.requiredNumOfParameters == self.params?.params.count + } + } +} + +/// JSON RPC response structure for serialization and deserialization purposes. +public struct JSONRPCResponse: Decodable { + public var id: Int + public var jsonrpc = "2.0" + public var result: Result + public var error: ErrorMessage? + public var message: String? + + enum JSONRPCResponseKeys: String, CodingKey { + case id = "id" + case jsonrpc = "jsonrpc" + case result = "result" + case error = "error" + } + + public init(id: Int, jsonrpc: String, result: Result, error: ErrorMessage?) { + self.id = id + self.jsonrpc = jsonrpc + self.result = result + self.error = error + } + + public struct Result: Decodable { + private let value: Any? + + public init(value: Any?) { + self.value = value + } + + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + // TODO: refactor me + var value: Any? = nil + if let rawValue = try? container.decode(String.self) { + value = rawValue + } else if let rawValue = try? container.decode(Int.self) { + value = rawValue + } else if let rawValue = try? container.decode(Bool.self) { + value = rawValue + } else if let rawValue = try? container.decode(EventLog.self) { + value = rawValue + } else if let rawValue = try? container.decode(Block.self) { + value = rawValue + } else if let rawValue = try? container.decode(TransactionReceipt.self) { + value = rawValue + } else if let rawValue = try? container.decode(TransactionDetails.self) { + value = rawValue + } else if let rawValue = try? container.decode([EventLog].self) { + value = rawValue + } else if let rawValue = try? container.decode([Block].self) { + value = rawValue + } else if let rawValue = try? container.decode([TransactionReceipt].self) { + value = rawValue + } else if let rawValue = try? container.decode([TransactionDetails].self) { + value = rawValue + } else if let rawValue = try? container.decode(TxPoolStatus.self) { + value = rawValue + } else if let rawValue = try? container.decode(TxPoolContent.self) { + value = rawValue + } else if let rawValue = try? container.decode([Bool].self) { + value = rawValue + } else if let rawValue = try? container.decode([Int].self) { + value = rawValue + } else if let rawValue = try? container.decode([String].self) { + value = rawValue + } else if let rawValue = try? container.decode([String: String].self) { + value = rawValue + } else if let rawValue = try? container.decode([String: Int].self) { + value = rawValue + } else if let rawValue = try? container.decode([String: [String: [String: String]]].self) { + value = rawValue + } else if let rawValue = try? container.decode([String: [String: [String: [String: String?]]]].self) { + value = rawValue + } else if let rawValue = try? container.decode(Oracle.FeeHistory.self) { + value = rawValue + } else if let rawValue = try? container.decode(FilterChanges.self) { + value = rawValue + } + self.value = value + } + + public func getValue() -> T? { + let type = T.self + + if type == BigUInt.self { + guard let string = self.value as? String else { return nil } + guard let value = BigUInt(string.stripHexPrefix(), radix: 16) else { return nil } + return value as? T + } else if type == BigInt.self { + guard let string = self.value as? String else { return nil } + guard let value = BigInt(string.stripHexPrefix(), radix: 16) else { return nil } + return value as? T + } else if type == Data.self { + guard let string = self.value as? String else { return nil } + guard let value = Data.fromHex(string) else { return nil } + return value as? T + } else if type == EthereumAddress.self { + guard let string = self.value as? String else { return nil } + guard let value = EthereumAddress(string, ignoreChecksum: true) else { return nil } + return value as? T + } else if type == [BigUInt].self { + guard let string = self.value as? [String] else { return nil } + let values = string.compactMap { (str) -> BigUInt? in + return BigUInt(str.stripHexPrefix(), radix: 16) + } + return values as? T + } else if type == [BigInt].self { + guard let string = self.value as? [String] else { return nil } + let values = string.compactMap { (str) -> BigInt? in + return BigInt(str.stripHexPrefix(), radix: 16) + } + return values as? T + } else if type == [Data].self { + guard let string = self.value as? [String] else { return nil } + let values = string.compactMap { (str) -> Data? in + return Data.fromHex(str) + } + return values as? T + } else if type == [EthereumAddress].self { + guard let string = self.value as? [String] else { return nil } + let values = string.compactMap { (str) -> EthereumAddress? in + return EthereumAddress(str, ignoreChecksum: true) + } + return values as? T + } + return self.value as? T + } + } + + public struct ErrorMessage: Decodable { + public var code: Int + public var message: String + + public init(code: Int, message: String) { + self.code = code + self.message = message + } + } + + internal var decodableTypes: [Decodable.Type] = [ + [EventLog].self, + [TransactionDetails].self, + [TransactionReceipt].self, + [Block].self, + [String].self, + [Int].self, + [Bool].self, + EventLog.self, + TransactionDetails.self, + TransactionReceipt.self, + Block.self, + String.self, + Int.self, + Bool.self, + [String: String].self, + [String: Int].self, + [String: [String: [String: [String]]]].self, + Oracle.FeeHistory.self + ] + + // FIXME: Make me a real generic + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: JSONRPCResponseKeys.self) + let id: Int = try container.decode(Int.self, forKey: .id) + let jsonrpc: String = try container.decode(String.self, forKey: .jsonrpc) + let errorMessage = try container.decodeIfPresent(ErrorMessage.self, forKey: .error) + if errorMessage != nil { + self.init(id: id, jsonrpc: jsonrpc, result: Result(value: nil), error: errorMessage) + return + } + let result = try container.decode(Result.self, forKey: .result) + self.init(id: id, jsonrpc: jsonrpc, result: result, error: nil) + } + + // FIXME: Make me a real generic + /// Get the JSON RCP reponse value by deserializing it into some native class. + /// + /// Returns nil if serialization fails + public func getValue() -> T? { + result.getValue() + } +} + +/// Transaction parameters JSON structure for interaction with Ethereum node. +public struct TransactionParameters: Codable { + /// accessList parameter JSON structure + public struct AccessListEntry: Codable { + public var address: String + public var storageKeys: [String] + } + + public var type: String? // must be set for new EIP-2718 transaction types + public var chainID: String? + public var data: String? + public var from: String? + public var gas: String? + public var gasPrice: String? // Legacy & EIP-2930 + public var maxFeePerGas: String? // EIP-1559 + public var maxPriorityFeePerGas: String? // EIP-1559 + public var accessList: [AccessListEntry]? // EIP-1559 & EIP-2930 + public var to: String? + public var value: String? = "0x0" + + public init(from _from: String?, to _to: String?) { + from = _from + to = _to + } +} + + +/// Raw JSON RCP 2.0 internal flattening wrapper. +public struct JSONRPCParams: Encodable{ + // TODO: Rewrite me to generic + public var params = [Any]() + + public func encode(to encoder: Encoder) throws { + var container = encoder.unkeyedContainer() + for par in params { + if let p = par as? TransactionParameters { + try container.encode(p) + } else if let p = par as? String { + try container.encode(p) + } else if let p = par as? Bool { + try container.encode(p) + } else if let p = par as? EventFilterParameters { + try container.encode(p) + } else if let p = par as? [Double] { + try container.encode(p) + } else if let p = par as? SubscribeOnLogsParams { + try container.encode(p) + } + } + } +} + +public struct SubscribeOnLogsParams: Encodable { + public let address: [String]? + public let topics: [String]? + + public init(address: [String]?, topics: [String]?) { + self.address = address + self.topics = topics + } +} + +public enum FilterChanges: Decodable { + case hashes([String]) + case logs([EventLog]) + + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + if let hashes = try? container.decode([String].self) { + self = .hashes(hashes) + } else { + self = .logs(try container.decode([EventLog].self)) + } + } +} diff --git a/Sources/Web3Core/Structure/SubscriptionProviderModels.swift b/Sources/Web3Core/Structure/SubscriptionProviderModels.swift new file mode 100644 index 000000000..812073cd5 --- /dev/null +++ b/Sources/Web3Core/Structure/SubscriptionProviderModels.swift @@ -0,0 +1,36 @@ +// +// SubscriptionProviderModels.swift +// +// Created by Ostap Danylovych on 29.12.2021. +// + +import Foundation + +public enum SubscribeEventFilter { + case newHeads + case logs(params: Encodable) + case newPendingTransactions + case syncing +} + +extension SubscribeEventFilter { + public var params: [Encodable] { + switch self { + case .newHeads: return ["newHeads"] + case .logs(let logsParam): return ["logs", logsParam] + case .newPendingTransactions: return ["newPendingTransactions"] + case .syncing: return ["syncing"] + } + } +} + +public protocol Subscription { + func unsubscribe() +} + +public typealias Web3SubscriptionListener = (Result) -> Void + +public protocol Web3SubscriptionProvider: Web3Provider { + func subscribe(filter: SubscribeEventFilter, + listener: @escaping Web3SubscriptionListener) -> Subscription +} diff --git a/Sources/Web3Core/Structure/Web3ProviderProtocol.swift b/Sources/Web3Core/Structure/Web3ProviderProtocol.swift index 6af95e217..b1c729ef3 100644 --- a/Sources/Web3Core/Structure/Web3ProviderProtocol.swift +++ b/Sources/Web3Core/Structure/Web3ProviderProtocol.swift @@ -1,6 +1,5 @@ // // Web3ProviderProtocol.swift -// // // Created by Yaroslav Yashin on 11.07.2022. // @@ -8,9 +7,17 @@ import Foundation public protocol Web3Provider { - var network: Networks? {get set} - var attachedKeystoreManager: KeystoreManager? {get set} - var policies: Policies {get set} - var url: URL {get} - var session: URLSession {get} + var network: Networks? { get } + var keystoreManager: KeystoreManager? { get set } + var policies: Policies { get set } + var url: URL { get } + var session: URLSession { get } + + func sendAsync(_ request: JSONRPCRequest, callback: @escaping (JSONRPCResponse) -> Void) +} + +public extension Web3Provider { + func sendAsync(_ request: JSONRPCRequest, callback: @escaping (JSONRPCResponse) -> Void) { + fatalError("Must be implemented by a provider that supports JSONRPCRequest, e.g. WebSocket provider.") + } } diff --git a/Sources/Web3Core/Utility/CryptoExtension.swift b/Sources/Web3Core/Utility/CryptoExtension.swift index 6c270ee1b..a3e073341 100755 --- a/Sources/Web3Core/Utility/CryptoExtension.swift +++ b/Sources/Web3Core/Utility/CryptoExtension.swift @@ -11,7 +11,7 @@ func toByteArray(_ value: T) -> [UInt8] { return withUnsafeBytes(of: &value) { Array($0) } } -func scrypt (password: String, salt: Data, length: Int, N: Int, R: Int, P: Int) -> Data? { +func scrypt(password: String, salt: Data, length: Int, N: Int, R: Int, P: Int) -> Data? { guard let passwordData = password.data(using: .utf8) else {return nil} guard let deriver = try? Scrypt(password: passwordData.bytes, salt: salt.bytes, dkLen: length, N: N, r: R, p: P) else {return nil} guard let result = try? deriver.calculate() else {return nil} diff --git a/Sources/web3swift/EthereumAPICalls/Ethereum/IEth+Defaults.swift b/Sources/web3swift/EthereumAPICalls/Ethereum/IEth+Defaults.swift index 6c5424689..70940f173 100644 --- a/Sources/web3swift/EthereumAPICalls/Ethereum/IEth+Defaults.swift +++ b/Sources/web3swift/EthereumAPICalls/Ethereum/IEth+Defaults.swift @@ -116,7 +116,7 @@ public extension IEth { public extension IEth { func ownedAccounts() async throws -> [EthereumAddress] { - if let addresses = provider.attachedKeystoreManager?.addresses { + if let addresses = provider.keystoreManager?.addresses { return addresses } return try await APIRequest.sendRequest(with: provider, for: .getAccounts).result diff --git a/Sources/web3swift/EthereumAPICalls/Personal/Personal+CreateAccount.swift b/Sources/web3swift/EthereumAPICalls/Personal/Personal+CreateAccount.swift index 746db1789..8d57f8d6a 100755 --- a/Sources/web3swift/EthereumAPICalls/Personal/Personal+CreateAccount.swift +++ b/Sources/web3swift/EthereumAPICalls/Personal/Personal+CreateAccount.swift @@ -9,7 +9,7 @@ import Web3Core extension Web3.Personal { public func createAccount(password: String ) async throws -> EthereumAddress { - guard self.web3.provider.attachedKeystoreManager == nil else { + guard self.eth.provider.keystoreManager == nil else { throw Web3Error.inputError(desc: "Creating account in a local keystore with this method is not supported") } diff --git a/Sources/web3swift/EthereumAPICalls/Personal/Personal+Sign.swift b/Sources/web3swift/EthereumAPICalls/Personal/Personal+Sign.swift index c410b4381..2f2badec7 100755 --- a/Sources/web3swift/EthereumAPICalls/Personal/Personal+Sign.swift +++ b/Sources/web3swift/EthereumAPICalls/Personal/Personal+Sign.swift @@ -10,7 +10,7 @@ import Web3Core extension Web3.Personal { public func signPersonal(message: Data, from: EthereumAddress, password: String) async throws -> Data { - guard let attachedKeystoreManager = self.web3.provider.attachedKeystoreManager else { + guard let attachedKeystoreManager = self.eth.provider.keystoreManager else { let hexData = message.toHexString().addHexPrefix() let request: APIRequest = .personalSign(from.address.lowercased(), hexData) let response: APIResponse = try await APIRequest.sendRequest(with: provider, for: request) diff --git a/Sources/web3swift/EthereumAPICalls/Personal/Personal+UnlockAccount.swift b/Sources/web3swift/EthereumAPICalls/Personal/Personal+UnlockAccount.swift index d1ffe5444..944513259 100755 --- a/Sources/web3swift/EthereumAPICalls/Personal/Personal+UnlockAccount.swift +++ b/Sources/web3swift/EthereumAPICalls/Personal/Personal+UnlockAccount.swift @@ -13,7 +13,7 @@ extension Web3.Personal { } public func unlock(account: Address, password: String, seconds: UInt = 300) async throws -> Bool { - guard self.web3.provider.attachedKeystoreManager == nil else { + guard eth.provider.keystoreManager == nil else { throw Web3Error.inputError(desc: "Can not unlock a local keystore") } diff --git a/Sources/web3swift/HookedFunctions/Web3+BrowserFunctions.swift b/Sources/web3swift/HookedFunctions/Web3+BrowserFunctions.swift index 46b64c006..5d2a5c79e 100755 --- a/Sources/web3swift/HookedFunctions/Web3+BrowserFunctions.swift +++ b/Sources/web3swift/HookedFunctions/Web3+BrowserFunctions.swift @@ -11,7 +11,7 @@ extension Web3.BrowserFunctions { public func getAccounts() async -> [String]? { do { - let accounts = try await self.web3.eth.ownedAccounts() + let accounts = try await eth.ownedAccounts() return accounts.compactMap({$0.address}) } catch { return [String]() @@ -19,8 +19,8 @@ extension Web3.BrowserFunctions { } public func getCoinbase() async -> String? { - guard let addresses = await self.getAccounts() else {return nil} - guard addresses.count > 0 else {return nil} + guard let addresses = await self.getAccounts() else { return nil } + guard addresses.count > 0 else { return nil } return addresses[0] } @@ -29,15 +29,15 @@ extension Web3.BrowserFunctions { } public func sign(_ personalMessage: String, account: String, password: String ) -> String? { - guard let data = Data.fromHex(personalMessage) else {return nil} + guard let data = Data.fromHex(personalMessage) else { return nil } return self.sign(data, account: account, password: password) } public func sign(_ personalMessage: Data, account: String, password: String ) -> String? { do { - guard let keystoreManager = self.web3.provider.attachedKeystoreManager else {return nil} - guard let from = EthereumAddress(account, ignoreChecksum: true) else {return nil} - guard let signature = try Web3Signer.signPersonalMessage(personalMessage, keystore: keystoreManager, account: from, password: password) else {return nil} + guard let keystoreManager = eth.provider.keystoreManager else { return nil } + guard let from = EthereumAddress(account, ignoreChecksum: true) else { return nil } + guard let signature = try Web3Signer.signPersonalMessage(personalMessage, keystore: keystoreManager, account: from, password: password) else { return nil } return signature.toHexString().addHexPrefix() } catch { print(error) @@ -46,13 +46,13 @@ extension Web3.BrowserFunctions { } public func personalECRecover(_ personalMessage: String, signature: String) -> String? { - guard let data = Data.fromHex(personalMessage) else {return nil} - guard let sig = Data.fromHex(signature) else {return nil} - return self.personalECRecover(data, signature: sig) + guard let data = Data.fromHex(personalMessage), + let sig = Data.fromHex(signature) else { return nil } + return personalECRecover(data, signature: sig) } public func personalECRecover(_ personalMessage: Data, signature: Data) -> String? { - if signature.count != 65 { return nil} + if signature.count != 65 { return nil } let rData = signature[0..<32].bytes let sData = signature[32..<64].bytes var vData = signature[64] @@ -63,9 +63,9 @@ extension Web3.BrowserFunctions { } else if vData >= 35 && vData <= 38 { vData -= 35 } - guard let signatureData = SECP256K1.marshalSignature(v: vData, r: rData, s: sData) else {return nil} - guard let hash = Utilities.hashPersonalMessage(personalMessage) else {return nil} - guard let publicKey = SECP256K1.recoverPublicKey(hash: hash, signature: signatureData) else {return nil} + guard let signatureData = SECP256K1.marshalSignature(v: vData, r: rData, s: sData) else { return nil } + guard let hash = Utilities.hashPersonalMessage(personalMessage) else { return nil } + guard let publicKey = SECP256K1.recoverPublicKey(hash: hash, signature: signatureData) else { return nil } return Utilities.publicToAddressString(publicKey) } @@ -103,7 +103,7 @@ extension Web3.BrowserFunctions { // FIXME: Rewrite this to CodableTransaction public func estimateGas(_ transaction: CodableTransaction) async -> BigUInt? { do { - let result = try await self.web3.eth.estimateGas(for: transaction) + let result = try await self.eth.estimateGas(for: transaction) return result } catch { return nil @@ -162,16 +162,15 @@ extension Web3.BrowserFunctions { // return await self.signTransaction(transaction , password: password) // } catch { return nil } // } - // FIXME: Rewrite this to EthereumTransaction // public func signTransaction(_ trans: EthereumTransaction, transaction: TransactionOptions, password: String ) async -> String? { // do { // var transaction = trans -// guard let from = transaction.from else {return nil} -// guard let keystoreManager = self.web3.provider.attachedKeystoreManager else {return nil} -// guard let gasPricePolicy = transaction.gasPrice else {return nil} -// guard let gasLimitPolicy = transaction.gasLimit else {return nil} -// guard let noncePolicy = transaction.nonce else {return nil} +// guard let from = transaction.from else { return nil } +// guard let keystoreManager = self.web3.provider.keystoreManager else { return nil } +// guard let gasPricePolicy = transaction.gasPrice else { return nil } +// guard let gasLimitPolicy = transaction.gasLimit else { return nil } +// guard let noncePolicy = transaction.nonce else { return nil } // switch gasPricePolicy { // case .manual(let gasPrice): // transaction.parameters.gasPrice = gasPrice @@ -200,7 +199,7 @@ extension Web3.BrowserFunctions { // transaction.chainID = self.web3.provider.network?.chainID // } // -// guard let keystore = keystoreManager.walletForAddress(from) else {return nil} +// guard let keystore = keystoreManager.walletForAddress(from) else { return nil } // try Web3Signer.signTX(transaction: &transaction, keystore: keystore, account: from, password: password) // print(transaction) // let signedData = transaction.encode(for: .transaction)?.toHexString().addHexPrefix() diff --git a/Sources/web3swift/HookedFunctions/Web3+Wallet.swift b/Sources/web3swift/HookedFunctions/Web3+Wallet.swift index 7623f698f..e3e08a67b 100755 --- a/Sources/web3swift/HookedFunctions/Web3+Wallet.swift +++ b/Sources/web3swift/HookedFunctions/Web3+Wallet.swift @@ -10,7 +10,7 @@ import Web3Core extension Web3.Web3Wallet { public func getAccounts() throws -> [EthereumAddress] { - guard let keystoreManager = self.web3.provider.attachedKeystoreManager else { + guard let keystoreManager = eth.provider.keystoreManager else { throw Web3Error.walletError } guard let ethAddresses = keystoreManager.addresses else { @@ -29,14 +29,14 @@ extension Web3.Web3Wallet { public func signTX(transaction: inout CodableTransaction, account: EthereumAddress, password: String ) throws -> Bool { do { - guard let keystoreManager = self.web3.provider.attachedKeystoreManager else { + guard let keystoreManager = eth.provider.keystoreManager else { throw Web3Error.walletError } try Web3Signer.signTX(transaction: &transaction, keystore: keystoreManager, account: account, password: password) return true } catch { - if error is AbstractKeystoreError { - throw Web3Error.keystoreError(err: error as! AbstractKeystoreError) + if let error = error as? AbstractKeystoreError { + throw Web3Error.keystoreError(err: error) } throw Web3Error.generalError(err: error) } @@ -46,12 +46,12 @@ extension Web3.Web3Wallet { guard let data = Data.fromHex(personalMessage) else { throw Web3Error.dataError } - return try self.signPersonalMessage(data, account: account, password: password) + return try signPersonalMessage(data, account: account, password: password) } public func signPersonalMessage(_ personalMessage: Data, account: EthereumAddress, password: String ) throws -> Data { do { - guard let keystoreManager = self.web3.provider.attachedKeystoreManager else { + guard let keystoreManager = eth.provider.keystoreManager else { throw Web3Error.walletError } guard let data = try Web3Signer.signPersonalMessage(personalMessage, keystore: keystoreManager, account: account, password: password) else { @@ -59,11 +59,10 @@ extension Web3.Web3Wallet { } return data } catch { - if error is AbstractKeystoreError { - throw Web3Error.keystoreError(err: error as! AbstractKeystoreError) + if let error = error as? AbstractKeystoreError { + throw Web3Error.keystoreError(err: error) } throw Web3Error.generalError(err: error) } } - } diff --git a/Sources/web3swift/Operations/WriteOperation.swift b/Sources/web3swift/Operations/WriteOperation.swift index f1fa366aa..00ec2460f 100755 --- a/Sources/web3swift/Operations/WriteOperation.swift +++ b/Sources/web3swift/Operations/WriteOperation.swift @@ -27,7 +27,7 @@ public class WriteOperation: ReadOperation { return try await web3.eth.send(transaction) } - guard let attachedKeystoreManager = web3.provider.attachedKeystoreManager else { + guard let attachedKeystoreManager = web3.provider.keystoreManager else { throw Web3Error.inputError(desc: "Failed to locally sign a transaction. Web3 provider doesn't have keystore attached.") } diff --git a/Sources/web3swift/Web3/DefaultWeb3SocketClient.swift b/Sources/web3swift/Web3/DefaultWeb3SocketClient.swift new file mode 100644 index 000000000..3646db1e6 --- /dev/null +++ b/Sources/web3swift/Web3/DefaultWeb3SocketClient.swift @@ -0,0 +1,69 @@ +// +// DefaultWeb3SocketClient.swift +// +// Created by JeneaVranceanu on 14.12.2022. +// + +import Foundation + +#if !os(Linux) +public class DefaultWeb3SocketClient: Web3SocketClient { + + public let session = URLSession(configuration: .default) + private let webSocketTask: URLSessionWebSocketTask + private weak var delegate: Web3SocketDelegate? + + public private(set) var url: URL + + public init(url: URL) { + self.url = url + webSocketTask = session.webSocketTask(with: url) + webSocketTask.receive { [weak self] result in + switch result { + case .failure(let error): + self?.delegate?.received(error) + case .success(let message): + switch message { + case .string(let text): + self?.delegate?.received(text) + case .data(let data): + self?.delegate?.received(data) + @unknown default: + fatalError("New type of message was added by Apple into URLSessionWebSocketTask. Please, file an issue on https://github.com/web3swift-team/web3swift/issues. \(String(describing: message))") + } + } + } + } + + public func setDelegate(_ delegate: Web3SocketDelegate) { + self.delegate = delegate + } + + public func send(_ message: String) { + webSocketTask.send(.string(message)) { error in + if let error = error { + self.delegate?.received(error) + } + } + } + + public func send(_ message: Data) { + webSocketTask.send(.data(message)) { error in + if let error = error { + self.delegate?.received(error) + } + } + } + + public func resume() { + if webSocketTask.state == .canceling || + webSocketTask.state == .completed || + webSocketTask.closeCode != .invalid { return } + webSocketTask.resume() + } + + public func cancel() { + webSocketTask.cancel() + } +} +#endif diff --git a/Sources/web3swift/Web3/Web3+EIP1559.swift b/Sources/web3swift/Web3/Web3+EIP1559.swift index 9e0e9de8b..85f9fd6ed 100644 --- a/Sources/web3swift/Web3/Web3+EIP1559.swift +++ b/Sources/web3swift/Web3/Web3+EIP1559.swift @@ -176,7 +176,7 @@ public extension Web3 { extension Web3.MainChainVersion: Comparable { public static func < (lhs: Web3.MainChainVersion, rhs: Web3.MainChainVersion) -> Bool { return lhs.mainNetFisrtBlockNumber < rhs.mainNetFisrtBlockNumber } - } +} extension Block { /// Returns chain version of mainnet block with such number diff --git a/Sources/web3swift/Web3/Web3+Eth+Websocket.swift b/Sources/web3swift/Web3/Web3+Eth+Websocket.swift new file mode 100644 index 000000000..80fcdb67d --- /dev/null +++ b/Sources/web3swift/Web3/Web3+Eth+Websocket.swift @@ -0,0 +1,43 @@ +// +// Web3+Eth+Websocket.swift +// web3swift +// +// Created by Anton on 03/04/2019. +// Copyright © 2019 The Matter Inc. All rights reserved. +// + +import Foundation +import BigInt +import Core + +extension Web3.Eth { + private func _subscribe(filter: SubscribeEventFilter, + listener: @escaping Web3SubscriptionListener) throws -> Subscription { + guard let provider = provider as? Web3SubscriptionProvider else { + throw Web3Error.processingError(desc: "Provider is not subscribable") + } + return provider.subscribe(filter: filter, listener: listener) + } + + public func subscribeOnNewHeads(listener: @escaping Web3SubscriptionListener) throws -> Subscription { + try _subscribe(filter: .newHeads, listener: listener) + } + + public func subscribeOnLogs(addresses: [EthereumAddress]? = nil, + topics: [String]? = nil, + listener: @escaping Web3SubscriptionListener) throws -> Subscription { + let params = SubscribeOnLogsParams(address: addresses?.map { $0.address }, topics: topics) + return try _subscribe(filter: .logs(params: params), listener: listener) + } + + public func subscribeOnNewPendingTransactions(listener: @escaping Web3SubscriptionListener) throws -> Subscription { + try _subscribe(filter: .newPendingTransactions, listener: listener) + } + + public func subscribeOnSyncing(listener: @escaping Web3SubscriptionListener) throws -> Subscription { + guard provider.network != Networks.Kovan else { + throw Web3Error.inputError(desc: "Can't sync on Kovan") + } + return try _subscribe(filter: .syncing, listener: listener) + } +} diff --git a/Sources/web3swift/Web3/Web3+Eventloop.swift b/Sources/web3swift/Web3/Web3+Eventloop.swift index 258dc97b7..541916d8b 100755 --- a/Sources/web3swift/Web3/Web3+Eventloop.swift +++ b/Sources/web3swift/Web3/Web3+Eventloop.swift @@ -7,7 +7,6 @@ import Foundation extension Web3.Eventloop { - // @available(iOS 10.0, *) public func start(_ timeInterval: TimeInterval) { if self.timer != nil { self.timer!.suspend() @@ -32,7 +31,7 @@ extension Web3.Eventloop { let function = prop.calledFunction Task { - await function(self.web3) + await function(eth) } } diff --git a/Sources/web3swift/Web3/Web3+HttpProvider.swift b/Sources/web3swift/Web3/Web3+HttpProvider.swift index 3f271a3e2..bcc90d9fd 100755 --- a/Sources/web3swift/Web3/Web3+HttpProvider.swift +++ b/Sources/web3swift/Web3/Web3+HttpProvider.swift @@ -12,15 +12,20 @@ public class Web3HttpProvider: Web3Provider { public var url: URL public var network: Networks? public var policies: Policies = .auto - public var attachedKeystoreManager: KeystoreManager? + public var keystoreManager: KeystoreManager? + public var session: URLSession = {() -> URLSession in let config = URLSessionConfiguration.default let urlSession = URLSession(configuration: config) return urlSession }() - public init?(_ httpProviderURL: URL, network net: Networks?, keystoreManager manager: KeystoreManager? = nil) async { + + public init?(_ httpProviderURL: URL, + network net: Networks? = nil, + keystoreManager: KeystoreManager? = nil) async { guard httpProviderURL.scheme == "http" || httpProviderURL.scheme == "https" else { return nil } url = httpProviderURL + self.keystoreManager = keystoreManager if let net = net { network = net } else { @@ -37,6 +42,5 @@ public class Web3HttpProvider: Web3Provider { return nil } } - attachedKeystoreManager = manager } } diff --git a/Sources/web3swift/Web3/Web3+InfuraProviders.swift b/Sources/web3swift/Web3/Web3+InfuraProviders.swift index 493d6bc65..25b799f25 100755 --- a/Sources/web3swift/Web3/Web3+InfuraProviders.swift +++ b/Sources/web3swift/Web3/Web3+InfuraProviders.swift @@ -2,16 +2,54 @@ // Created by Alex Vlasov. // Copyright © 2018 Alex Vlasov. All rights reserved. // + import Foundation import BigInt import Web3Core /// Custom Web3 HTTP provider of Infura nodes. public final class InfuraProvider: Web3HttpProvider { - public init?(_ net: Networks, accessToken token: String? = nil, keystoreManager manager: KeystoreManager? = nil) async { - var requestURLstring = "https://" + net.name + Constants.infuraHttpScheme - requestURLstring += token ?? Constants.infuraToken - let providerURL = URL(string: requestURLstring) - await super.init(providerURL!, network: net, keystoreManager: manager) + public init?(_ net: Networks, + accessToken token: String? = nil, + keystoreManager: KeystoreManager? = nil) async { + let rawUrl = "https://" + net.name + Constants.infuraHttpScheme + guard let url = URL(string: rawUrl) else { return nil } + await super.init(url.appendingPathComponent(token ?? ""), + network: net, + keystoreManager: keystoreManager) } } + +/// Custom Websocket provider of Infura nodes. +public final class InfuraWebsocketProvider: Web3SocketProvider { + public convenience init?(_ network: Networks, + forwarder: Web3SocketMessageForwarder? = nil, + token: String? = nil, + keystoreManager: KeystoreManager? = nil) { + guard let url = URL(string: "wss://" + network.name + Constants.infuraWsScheme) else { return nil } + self.init(url.appendingPathComponent(token ?? ""), + forwarder: forwarder, + keystoreManager: keystoreManager) + + } + + public convenience init?(_ endpoint: String, + forwarder: Web3SocketMessageForwarder? = nil, + keystoreManager: KeystoreManager? = nil) { + guard let endpoint = URL(string: endpoint) else { return nil } + self.init(endpoint, + forwarder: forwarder, + keystoreManager: keystoreManager) + } + + public convenience init?(_ endpoint: URL, + forwarder: Web3SocketMessageForwarder? = nil, + keystoreManager: KeystoreManager? = nil) { + guard ["wss", "ws"].contains(endpoint.scheme) else { return nil } + self.init(DefaultWeb3SocketClient(url: endpoint), + forwarder: forwarder, + keystoreManager: keystoreManager) + } +} + + diff --git a/Sources/web3swift/Web3/Web3+Instance.swift b/Sources/web3swift/Web3/Web3+Instance.swift index 5fdd99217..cd802ed4c 100755 --- a/Sources/web3swift/Web3/Web3+Instance.swift +++ b/Sources/web3swift/Web3/Web3+Instance.swift @@ -10,141 +10,63 @@ import Web3Core // FIXME: Rewrite this to CodableTransaction /// A web3 instance bound to provider. All further functionality is provided under web.*. namespaces. public class Web3 { - public var provider: Web3Provider - - /// Raw initializer using a Web3Provider protocol object, dispatch queue and request dispatcher. - public init(provider prov: Web3Provider) { - provider = prov - } - - /// Keystore manager can be bound to Web3 instance. If some manager is bound all further account related functions, such - /// as account listing, transaction signing, etc. are done locally using private keys and accounts found in a manager. - public func addKeystoreManager(_ manager: KeystoreManager?) { - provider.attachedKeystoreManager = manager - } - - var ethInstance: IEth? - - /// Public web3.eth.* namespace. - public var eth: IEth { - if ethInstance != nil { - return ethInstance! - } - ethInstance = Web3.Eth(provider: provider) - return ethInstance! - } + // MARK: - Type definitions // FIXME: Rewrite this to CodableTransaction public class Eth: IEth { public var provider: Web3Provider - public init(provider prov: Web3Provider) { provider = prov } } - var personalInstance: Web3.Personal? - - /// Public web3.personal.* namespace. - public var personal: Web3.Personal { - if self.personalInstance != nil { - return self.personalInstance! - } - self.personalInstance = Web3.Personal(provider: self.provider, web3: self) - return self.personalInstance! - } - // FIXME: Rewrite this to CodableTransaction public class Personal { var provider: Web3Provider - // FIXME: remove dependency on web3 instance!! - var web3: Web3 - public init(provider prov: Web3Provider, web3 web3instance: Web3) { + public lazy var eth: IEth = { + Web3.Eth(provider: provider) + }() + public init(provider prov: Web3Provider) { provider = prov - web3 = web3instance } } - var txPoolInstance: Web3.TxPool? - - /// Public web3.personal.* namespace. - public var txPool: Web3.TxPool { - if self.txPoolInstance != nil { - return self.txPoolInstance! - } - self.txPoolInstance = Web3.TxPool(provider: self.provider, web3: self) - return self.txPoolInstance! - } - // FIXME: Rewrite this to CodableTransaction public class TxPool { var provider: Web3Provider - // FIXME: remove dependency on web3 instance!! - var web3: Web3 - public init(provider prov: Web3Provider, web3 web3instance: Web3) { + public lazy var eth: IEth = { + Web3.Eth(provider: provider) + }() + public init(provider prov: Web3Provider) { provider = prov - web3 = web3instance - } - } - - var walletInstance: Web3.Web3Wallet? - - /// Public web3.wallet.* namespace. - public var wallet: Web3.Web3Wallet { - if self.walletInstance != nil { - return self.walletInstance! } - self.walletInstance = Web3.Web3Wallet(provider: self.provider, web3: self) - return self.walletInstance! } public class Web3Wallet { var provider: Web3Provider - // FIXME: remove dependency on web3 instance!! - var web3: Web3 - public init(provider prov: Web3Provider, web3 web3instance: Web3) { + public lazy var eth: IEth = { + Web3.Eth(provider: provider) + }() + public init(provider prov: Web3Provider) { provider = prov - web3 = web3instance } } - var browserFunctionsInstance: Web3.BrowserFunctions? - - /// Public web3.browserFunctions.* namespace. - public var browserFunctions: Web3.BrowserFunctions { - if self.browserFunctionsInstance != nil { - return self.browserFunctionsInstance! - } - self.browserFunctionsInstance = Web3.BrowserFunctions(provider: self.provider, web3: self) - return self.browserFunctionsInstance! - } - // FIXME: Rewrite this to CodableTransaction public class BrowserFunctions { var provider: Web3Provider - // FIXME: remove dependency on web3 instance!! - public var web3: Web3 - public init(provider prov: Web3Provider, web3 web3instance: Web3) { + public lazy var eth: IEth = { + Web3.Eth(provider: provider) + }() + public init(provider prov: Web3Provider) { provider = prov - web3 = web3instance } } - var eventLoopInstance: Web3.Eventloop? - - /// Public web3.browserFunctions.* namespace. - public var eventLoop: Web3.Eventloop { - if self.eventLoopInstance != nil { - return self.eventLoopInstance! - } - self.eventLoopInstance = Web3.Eventloop(provider: self.provider, web3: self) - return self.eventLoopInstance! - } - // FIXME: Rewrite this to CodableTransaction public class Eventloop { - public typealias EventLoopCall = (Web3) async -> Void + public typealias EventLoopCall = (IEth) async -> Void public typealias EventLoopContractCall = (Contract) -> Void public struct MonitoredProperty { @@ -153,41 +75,122 @@ public class Web3 { } var provider: Web3Provider - // FIXME: remove dependency on web3 instance!! - var web3: Web3 var timer: RepeatingTimer? public var monitoredProperties: [MonitoredProperty] = [MonitoredProperty]() // public var monitoredContracts: [MonitoredContract] = [MonitoredContract]() public var monitoredUserFunctions: [EventLoopRunnableProtocol] = [EventLoopRunnableProtocol]() - public init(provider prov: Web3Provider, web3 web3instance: Web3) { + + + public lazy var eth: IEth = { + Web3.Eth(provider: provider) + }() + + public init(provider prov: Web3Provider) { provider = prov - web3 = web3instance } } -// public typealias AssemblyHookFunction = ((inout CodableTransaction, EthereumContract)) -> Bool -// -// public typealias SubmissionHookFunction = (inout CodableTransaction) -> Bool + // MARK: - Variables - public typealias SubmissionResultHookFunction = (TransactionSendingResult) -> Void + public var provider: Web3Provider + + var ethInstance: IEth? + + /// Public web3.eth.* namespace. + public var eth: IEth { + if ethInstance != nil { + return ethInstance! + } + ethInstance = Web3.Eth(provider: provider) + return ethInstance! + } -// public struct AssemblyHook { -// public var function: AssemblyHookFunction -// } + var personalInstance: Web3.Personal? + + /// Public web3.personal.* namespace. + public var personal: Web3.Personal { + if let personalInstance = personalInstance { + return personalInstance + } + personalInstance = Web3.Personal(provider: provider) + return personalInstance! + } + + var txPoolInstance: Web3.TxPool? -// public struct SubmissionHook { -// public var function: SubmissionHookFunction -// } + /// Public web3.personal.* namespace. + public var txPool: Web3.TxPool { + if let txPoolInstance = txPoolInstance { + return txPoolInstance + } + txPoolInstance = Web3.TxPool(provider: provider) + return txPoolInstance! + } + var walletInstance: Web3.Web3Wallet? + + /// Public web3.wallet.* namespace. + public var wallet: Web3.Web3Wallet { + if let walletInstance = walletInstance{ + return walletInstance + } + walletInstance = Web3.Web3Wallet(provider: provider) + return walletInstance! + } + + var browserFunctionsInstance: Web3.BrowserFunctions? + + /// Public web3.browserFunctions.* namespace. + public var browserFunctions: Web3.BrowserFunctions { + if let browserFunctionsInstance = browserFunctionsInstance { + return browserFunctionsInstance + } + browserFunctionsInstance = Web3.BrowserFunctions(provider: provider) + return browserFunctionsInstance! + } + + var eventLoopInstance: Web3.Eventloop? + + /// Public web3.browserFunctions.* namespace. + public var eventLoop: Web3.Eventloop { + if let eventLoopInstance = eventLoopInstance { + return eventLoopInstance + } + eventLoopInstance = Web3.Eventloop(provider: provider) + return eventLoopInstance! + } + + + /// Raw initializer using a Web3Provider protocol object, dispatch queue and request dispatcher. + public init(provider prov: Web3Provider) { + provider = prov + } + + /// Keystore manager can be bound to Web3 instance. If some manager is bound all further account related functions, such + /// as account listing, transaction signing, etc. are done locally using private keys and accounts found in a manager. + public func setKeystoreManager(_ manager: KeystoreManager?) { + provider.keystoreManager = manager + } + + // public typealias AssemblyHookFunction = ((inout CodableTransaction, EthereumContract)) -> Bool + // + // public typealias SubmissionHookFunction = (inout CodableTransaction) -> Bool + public typealias SubmissionResultHookFunction = (TransactionSendingResult) -> Void + + // public struct AssemblyHook { + // public var function: AssemblyHookFunction + // } + // public struct SubmissionHook { + // public var function: SubmissionHookFunction + // } public struct SubmissionResultHook { public var function: SubmissionResultHookFunction } -// public var preAssemblyHooks: [AssemblyHook] = [AssemblyHook]() -// public var postAssemblyHooks: [AssemblyHook] = [AssemblyHook]() -// -// public var preSubmissionHooks: [SubmissionHook] = [SubmissionHook]() + // public var preAssemblyHooks: [AssemblyHook] = [AssemblyHook]() + // public var postAssemblyHooks: [AssemblyHook] = [AssemblyHook]() + // + // public var preSubmissionHooks: [SubmissionHook] = [SubmissionHook]() public var postSubmissionHooks: [SubmissionResultHook] = [SubmissionResultHook]() - } diff --git a/Sources/web3swift/Web3/Web3+Personal.swift b/Sources/web3swift/Web3/Web3+Personal.swift index 3949fcd7f..8edc79da3 100755 --- a/Sources/web3swift/Web3/Web3+Personal.swift +++ b/Sources/web3swift/Web3/Web3+Personal.swift @@ -9,55 +9,36 @@ import Web3Core extension Web3.Personal { - /** - *Locally or remotely sign a message (arbitrary data) with the private key. To avoid potential signing of a transaction the message is first prepended by a special header and then hashed.* - - - parameters: - - message: Message Data - - from: Use a private key that corresponds to this account - - password: Password for account if signing locally - - - returns: - - Result object - - - important: This call is synchronous - - */ + /// Locally or remotely sign a message (arbitrary data) with the private key. + /// To avoid potential signing of a transaction the message is first prepended by a special header + /// and then hashed.* + /// - Parameters: + /// - message: Message to sign + /// - from: Use a private key that corresponds to this account + /// - password: Password for account if signing locally + /// - Returns: signature public func signPersonalMessage(message: Data, from: EthereumAddress, password: String) async throws -> Data { let result = try await self.signPersonal(message: message, from: from, password: password) return result } - /** - *Unlock an account on the remote node to be able to send transactions and sign messages.* - - - parameters: - - account: EthereumAddress of the account to unlock - - password: Password to use for the account - - seconds: Time inteval before automatic account lock by Ethereum node - - - returns: - - Result object - - - important: This call is synchronous. Does nothing if private keys are stored locally. - - */ + /// Unlock an account on the remote node to be able to send transactions and sign messages. + /// - Parameters: + /// - account: EthereumAddress of the account to unlock + /// - password: Password to use for the account + /// - seconds: Time inteval before automatic account lock by Ethereum node + /// - Returns: `true` if account was unlocked. public func unlockAccount(account: EthereumAddress, password: String, seconds: UInt = 300) async throws -> Bool { let result = try await self.unlock(account: account, password: password) return result } - /** - *Recovers a signer of some message. Message is first prepended by special prefix (check the "signPersonalMessage" method description) and then hashed.* - - - parameters: - - personalMessage: Message Data - - signature: Serialized signature, 65 bytes - - - returns: - - Result object - - */ + /// Recovers a signer of some message. Message is first prepended by special prefix + /// (check the "signPersonalMessage" method description) and then hashed. + /// - Parameters: + /// - personalMessage: Original message + /// - signature: Serialized signature, 65 bytes + /// - Returns: address recovered from the signature public func ecrecover(personalMessage: Data, signature: Data) throws -> EthereumAddress { guard let recovered = Utilities.personalECRecover(personalMessage, signature: signature) else { throw Web3Error.dataError @@ -65,17 +46,11 @@ extension Web3.Personal { return recovered } - /** - *Recovers a signer of some hash. Checking what is under this hash is on behalf of the user.* - - - parameters: - - hash: Signed hash - - signature: Serialized signature, 65 bytes - - - returns: - - Result object - - */ + /// Recovers a signer of some hash. Checking what is under this hash is on behalf of the user. + /// - Parameters: + /// - hash: Hash of the original message + /// - signature: Serialized signature, 65 bytes + /// - Returns: address recovered from the signature public func ecrecover(hash: Data, signature: Data) throws -> EthereumAddress { guard let recovered = Utilities.hashECRecover(hash: hash, signature: signature) else { throw Web3Error.dataError diff --git a/Sources/web3swift/Web3/Web3+WebsocketProvider.swift b/Sources/web3swift/Web3/Web3+WebsocketProvider.swift new file mode 100644 index 000000000..43c919de5 --- /dev/null +++ b/Sources/web3swift/Web3/Web3+WebsocketProvider.swift @@ -0,0 +1,280 @@ +// +// Web3+WebsocketProvider.swift +// +// Created by JeneaVranceanu on 14.12.2022. +// + +import Core +import BigInt +import Foundation + +/// A protocol for forwarding websocket messages. +public protocol Web3SocketMessageForwarder: AnyObject { + /// Called when a websocket message is received. Potentially could be converted to `String` using + /// `String(bytes: message, encoding: .utf8)`. + /// - Parameter message: The message that was received. + func received(_ message: Data) + + /// Called when an error occurs while receiving a websocket message. + /// - Parameter error: The error that occurred. + func received(_ error: Error) +} + +/// A protocol for receiving websocket events from a Web3 client. +public protocol Web3SocketDelegate: AnyObject { + /// Called when the websocket connection is established. + func connected() + + /// Called when the websocket connection is closed. + func disconnected() + + /// Called when a text message is received over the websocket connection. + /// - Parameter message: The text message that was received. + func received(_ message: String) + + /// Called when a binary message is received over the websocket connection. + /// - Parameter message: The binary message that was received. + func received(_ message: Data) + + /// Called when an error occurs while receiving a websocket message. + /// - Parameter error: The error that occurred. + func received(_ error: Error) +} + +public class WebsocketSubscription: Subscription { + public var id: String? = nil + private let unsubscribeCallback: (WebsocketSubscription) -> Void + + public init(unsubscribeCallback: @escaping (WebsocketSubscription) -> Void) { + self.unsubscribeCallback = unsubscribeCallback + } + + public func unsubscribe() { + unsubscribeCallback(self) + } +} + +public struct JSONRPCSubscriptionEvent: Decodable { + public struct Params: Decodable { + public let result: R + public let subscription: String + } + + public let method: String + public let params: Params +} + +/// A protocol to implement by the WebSocket client of your choice. +public protocol Web3SocketClient { + /// URL of the WebSocket server + var url: URL { get } + /// Internal session used by this WebSocket client + var session: URLSession { get } + /// Send a message to the WebSocket server + func send(_ message: String) + /// Send a message to the WebSocket server + func send(_ message: Data) + /// Sets a delegate that send received responses back to the WebSocketProvider + func setDelegate(_ delegate: Web3SocketDelegate) + /// Resumes or starts the WebSocket connection + func resume() + /// Closes the WebSocket connection. Calling `resume` will have no effect after this call. + func cancel() +} + +/// The default websocket provider. +public class Web3SocketProvider: Web3SubscriptionProvider, Web3SocketDelegate { + + public var url: URL { + web3SocketClient.url + } + public private(set) var network: Networks? + public var policies: Policies = .auto + public var keystoreManager: KeystoreManager? + // TODO: Consider removing `public var session: URLSession` completely + public var session: URLSession { + web3SocketClient.session + } + + public var web3SocketClient: Web3SocketClient + public var forwarder: Web3SocketMessageForwarder? + /// A flag that is true if socket connected or false if socket doesn't connected. + public var websocketConnected: Bool = false + /// Maintains a strong reference to subscriptions so that they could be reinitialized if a socket connection fails. + /// Only the subscriptions previously successfully connected are added to the dictionary. + /// The key is the subscription ID returned as the response from the server. + private var subscriptions = [String: (sub: WebsocketSubscription, callback: (Swift.Result) -> Void)]() + /// One time requests where key is the ``JSONRPCRequest/id``. + private var requests = [UInt: (Swift.Result) -> Void]() + private var pendingRequests = [() -> Void]() + /// Used to sync requests that are being tracked. + private let internalQueue = DispatchQueue(label: "web3swift.websocketProvider.internalQueue", + target: .global()) + + public init?(_ web3SocketClient: Web3SocketClient, + network: Networks, + forwarder: Web3SocketMessageForwarder? = nil, + keystoreManager: KeystoreManager? = nil) { + self.web3SocketClient = web3SocketClient + self.network = network + self.keystoreManager = keystoreManager + web3SocketClient.setDelegate(self) + } + + public convenience init?(_ endpoint: String, + forwarder: Web3SocketMessageForwarder? = nil) { + guard let url = URL(string: endpoint) else { return nil } + self.init(DefaultWeb3SocketClient(url: url), forwarder: forwarder) + } + + public func subscribe(filter: SubscribeEventFilter, + listener: @escaping Web3SubscriptionListener) -> Subscription { + internalQueue.sync { + let subscription = WebsocketSubscription() { subscription in + guard let id = subscription.id else { + return + } + let request = JSONRPCRequestFabric.prepareRequest(.unsubscribe, parameters: [id]) + self.sendAsync(request) { result in + switch result { + case .success(let response): + guard let unsubscribed: Bool = response.getValue() else { + listener(.failure(Web3Error.processingError(desc: "Wrong result in response: \(response)"))) + return + } + if unsubscribed { + self.subscriptions.removeValue(forKey: id) + listener(.failure(Web3Error.processingError(desc: "Subscribtion with ID \(id) was cancelled (unsubscribed)"))) + } else { + listener(.failure(Web3Error.processingError(desc: "Can\'t unsubscribe \(id)"))) + } + case .failure(let error): + listener(.failure(error)) + } + } + } + + let request = JSONRPCRequestFabric.prepareRequest(JSONRPCMethod.subscribe, parameters: filter.params) + sendAsync(request) { result in + switch result { + case .success(let response): + guard let subscriptionID: String = response.getValue() else { + listener(.failure(Web3Error.processingError(desc: "Wrong result in response: \(response)"))) + return + } + subscription.id = subscriptionID + self.subscriptions[subscriptionID] = (subscription, { result in + listener(result.flatMap { eventData in + Swift.Result { + try JSONDecoder().decode(JSONRPCSubscriptionEvent.self, from: eventData) + } + }.map { $0.params.result }) + }) + case .failure(let error): + listener(.failure(error)) + } + } + return subscription + } + } + + public func sendAsync(_ request: JSONRPCRequest, + _ callback: @escaping (Result) -> Void) { + guard let method = request.method else { + callback(.failure(Web3Error.inputError(desc: "No method in request: \(request)"))) + return + } + guard [.subscribe, .unsubscribe].contains(method) else { + callback(.failure(Web3Error.inputError(desc: "Unsupported method: \(method)"))) + return + } + + let requestData: Data + do { + requestData = try JSONEncoder().encode(request) + } catch { + callback(.failure(error)) + return + } + + internalQueue.sync { + self.requests[request.id] = { result in + callback(result) + } + let writeRequest = { self.web3SocketClient.send(requestData) } + if self.websocketConnected { + writeRequest() + } else { + self.pendingRequests.append(writeRequest) + } + } + } + + public func disconnected() { + #warning("NOT IMPLEMENTED!!!") + fatalError("NOT IMPLEMENTED!!!") + } + + public func connectSocket() { + web3SocketClient.resume() + } + + public func disconnectSocket() { + web3SocketClient.cancel() + } + + public func isConnect() -> Bool { + return websocketConnected + } + + public func connected() { + internalQueue.sync { + pendingRequests.forEach { $0() } + pendingRequests.removeAll() + } + } + + public func received(_ message: String) { + guard let message = message.data(using: .utf8) else { return } + received(message) + } + + public func received(_ message: Data) { + guard let dictionary = try? JSONSerialization.jsonObject(with: message, options: []) as? [String: Any] + else { return } + let messageAsString = String(describing: String(bytes: message, encoding: .utf8)) + if let _ = dictionary["id"] as? UInt64 { + let response: JSONRPCResponse + do { + response = try JSONDecoder().decode(JSONRPCResponse.self, from: message) + } catch { + forwarder?.received(Web3Error.processingError(desc: "Cannot parse JSON-RPC response. Error: \(String(describing: error)). Response: \(messageAsString)")) + return + } + internalQueue.sync { + if let request = requests.removeValue(forKey: UInt(response.id)) { + if let error = response.error { + request(.failure(Web3Error.nodeError(desc: "Received an error message\n" + String(describing: error)))) + } else { + request(.success(response)) + } + } else { + forwarder?.received(Web3Error.processingError(desc: "Unknown response id. Message is: \(messageAsString)")) + } + } + } else if let params = dictionary["params"] as? [String: Any], + let subscriptionID = params["subscription"] as? String { + guard let subscription = subscriptions[subscriptionID] else { + forwarder?.received(Web3Error.processingError(desc: "Unknown subscription id: \(subscriptionID)")) + return + } + subscription.callback(.success(message)) + } else { + forwarder?.received(Web3Error.processingError(desc: "Can\'t get known result. Message is: \(messageAsString)")) + } + } + + public func received(_ error: Error) { + forwarder?.received(Web3Error.processingError(desc: error.localizedDescription)) + } +} diff --git a/Tests/web3swiftTests/localTests/EthereumContractTest.swift b/Tests/web3swiftTests/localTests/EthereumContractTest.swift index 6031e0e84..380f17329 100644 --- a/Tests/web3swiftTests/localTests/EthereumContractTest.swift +++ b/Tests/web3swiftTests/localTests/EthereumContractTest.swift @@ -47,6 +47,18 @@ class EthereumContractTest: LocalTestCase { XCTAssertTrue(contract.contract.methods[getFuncSignature("setData(bytes32,bytes)")]?.count == 1) XCTAssertTrue(contract.contract.methods[getFuncSignature("setData(bytes32[],bytes[])")]?.count == 1) + + contract.transaction.value = 10000 + let writeOperation = contract.createWriteOperation("setData(bytes32,bytes)")! + NSLog(writeOperation.transaction.value.description) // Will hold 10000 + //writeOperation.writeToChain(password: ....) + + // .. somewhere down the road + + let writeOperation2 = contract.createWriteOperation("setData(bytes32,bytes)")! + NSLog(writeOperation2.transaction.value.description) // Will also hold 10000!!!! MUST NOT HAPPEN! + //writeOperation.writeToChain(password: ....) + XCTAssertTrue(contract.contract.methods["noInputFunction"]?.count == 1) XCTAssertTrue(contract.contract.methods["noInputFunction()"]?.count == 1) XCTAssertTrue(contract.contract.methods[getFuncSignature("noInputFunction()")]?.count == 1) diff --git a/Tests/web3swiftTests/localTests/EventloopTests.swift b/Tests/web3swiftTests/localTests/EventloopTests.swift index 923aaf774..301e8b0a2 100755 --- a/Tests/web3swiftTests/localTests/EventloopTests.swift +++ b/Tests/web3swiftTests/localTests/EventloopTests.swift @@ -13,15 +13,15 @@ class EventloopTests: XCTestCase { func testBasicEventLoop() async throws { var ticksToWait = 5 let expectation = self.expectation(description: "Waiting") - func getBlockNumber(_ web3: Web3) async { + func getBlockNumber(_ eth: IEth) async { do { - let blockNumber = try await web3.eth.blockNumber() + let blockNumber = try await eth.blockNumber() ticksToWait = ticksToWait - 1 if ticksToWait == 0 { expectation.fulfill() } } catch { - + XCTFail("Failed to get block number: \(error.localizedDescription)") } } diff --git a/Tests/web3swiftTests/localTests/PersonalSignatureTests.swift b/Tests/web3swiftTests/localTests/PersonalSignatureTests.swift index 141b23b0d..c8271a824 100755 --- a/Tests/web3swiftTests/localTests/PersonalSignatureTests.swift +++ b/Tests/web3swiftTests/localTests/PersonalSignatureTests.swift @@ -16,7 +16,7 @@ class PersonalSignatureTests: XCTestCase { let web3 = try await Web3.new(LocalTestCase.url) let tempKeystore = try! EthereumKeystoreV3(password: "") let keystoreManager = KeystoreManager([tempKeystore!]) - web3.addKeystoreManager(keystoreManager) + web3.setKeystoreManager(keystoreManager) let message = "Hello World" let expectedAddress = keystoreManager.addresses![0] @@ -56,7 +56,7 @@ class PersonalSignatureTests: XCTestCase { // Signing let tempKeystore = try! EthereumKeystoreV3(password: "") let keystoreManager = KeystoreManager([tempKeystore!]) - web3.addKeystoreManager(keystoreManager) + web3.setKeystoreManager(keystoreManager) let message = "Hello World" let expectedAddress = keystoreManager.addresses![0] diff --git a/Tests/web3swiftTests/localTests/UncategorizedTests.swift b/Tests/web3swiftTests/localTests/UncategorizedTests.swift index dc5600da9..9aabe948c 100755 --- a/Tests/web3swiftTests/localTests/UncategorizedTests.swift +++ b/Tests/web3swiftTests/localTests/UncategorizedTests.swift @@ -101,12 +101,13 @@ class UncategorizedTests: XCTestCase { XCTAssert(ibn == "XE83FUTTUNPK7WZJSGGCWVEBARQWQ8YML4") } -// func testGenericRPCresponse() throws { -// let hex = "0x1" -// let rpcResponse = JSONRPCresponse(id: 1, jsonrpc: "2.0", result: hex, error: nil) -// let value: BigUInt? = rpcResponse.getValue() -// XCTAssert(value == 1) -// } + func testGenericRPCresponse() throws { + let hex = "0x1" + let rpcResponse = JSONRPCResponse(id: 1, jsonrpc: "2.0", + result: .init(value: hex), error: nil) + let value: BigUInt? = rpcResponse.getValue() + XCTAssertEqual(value, 1) + } func testPublicMappingsAccess() async throws { let jsonString = "[{\"constant\":true,\"inputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"name\":\"users\",\"outputs\":[{\"name\":\"name\",\"type\":\"uint256\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"\",\"type\":\"address\"}],\"name\":\"userDeviceCount\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"totalUsers\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"}]"