|
| 1 | +// |
| 2 | +// Web3+APIMethod.swift |
| 3 | +// Web3swift |
| 4 | +// |
| 5 | +// Created by Yaroslav on 24.05.2022. |
| 6 | +// |
| 7 | + |
| 8 | +import Foundation |
| 9 | +import BigInt |
| 10 | + |
| 11 | +public typealias Hash = String // 32 bytes hash of block (64 chars length without 0x) |
| 12 | +public typealias Receipt = Hash |
| 13 | +public typealias Address = Hash // 20 bytes (40 chars length without 0x) |
| 14 | +public typealias TransactionHash = Hash // 64 chars length without 0x |
| 15 | + |
| 16 | +// FIXME: Add documentation to each method. |
| 17 | +/// Ethereum JSON RPC API Calls |
| 18 | +public enum APIRequest { |
| 19 | + // MARK: - Official API |
| 20 | + // 0 parameter in call |
| 21 | + case gasPrice |
| 22 | + case blockNumber |
| 23 | + case getNetwork |
| 24 | + case getAccounts |
| 25 | + // ?? |
| 26 | + case estimateGas(TransactionParameters, BlockNumber) |
| 27 | + case sendRawTransaction(Hash) |
| 28 | + case sendTransaction(TransactionParameters) |
| 29 | + case getTransactionByHash(Hash) |
| 30 | + case getTransactionReceipt(Hash) |
| 31 | + case getLogs(EventFilterParameters) |
| 32 | + case personalSign(Address, String) |
| 33 | + case call(TransactionParameters, BlockNumber) |
| 34 | + case getTransactionCount(Address, BlockNumber) |
| 35 | + case getBalance(Address, BlockNumber) |
| 36 | + |
| 37 | + /// Returns the value from a storage position at a given address. |
| 38 | + /// |
| 39 | + /// - Parameters: |
| 40 | + /// - Address: Address |
| 41 | + /// - Storage: slot |
| 42 | + /// - BlockNumber: sd |
| 43 | + case getStorageAt(Address, Hash, BlockNumber) |
| 44 | + |
| 45 | + case getCode(Address, BlockNumber) |
| 46 | + case getBlockByHash(Hash, Bool) |
| 47 | + case getBlockByNumber(BlockNumber, Bool) |
| 48 | + |
| 49 | + /// Returns fee history with a respect to given setup |
| 50 | + /// |
| 51 | + /// Generates and returns an estimate of how much gas is necessary to allow the transaction to complete. |
| 52 | + /// The transaction will not be added to the blockchain. Note that the estimate may be significantly more |
| 53 | + /// than the amount of gas actually used by the transaction, for a variety of reasons including EVM mechanics and node performance. |
| 54 | + /// |
| 55 | + /// - Parameters: |
| 56 | + /// - UInt: Requested range of blocks. Clients will return less than the requested range if not all blocks are available. |
| 57 | + /// - BlockNumber: Highest block of the requested range. |
| 58 | + /// - [Double]: A monotonically increasing list of percentile values. |
| 59 | + /// For each block in the requested range, the transactions will be sorted in ascending order |
| 60 | + /// by effective tip per gas and the coresponding effective tip for the percentile will be determined, accounting for gas consumed." |
| 61 | + case feeHistory(BigUInt, BlockNumber, [Double]) |
| 62 | + |
| 63 | + // MARK: - Additional API |
| 64 | + /// Creates new account. |
| 65 | + /// |
| 66 | + /// Note: it becomes the new current unlocked account. There can only be one unlocked account at a time. |
| 67 | + /// |
| 68 | + /// - Parameters: |
| 69 | + /// - String: Password for the new account. |
| 70 | + case createAccount(String) // No in Eth API |
| 71 | + |
| 72 | + /// Unlocks specified account for use. |
| 73 | + /// |
| 74 | + /// If permanent unlocking is disabled (the default) then the duration argument will be ignored, |
| 75 | + /// and the account will be unlocked for a single signing. |
| 76 | + /// With permanent locking enabled, the duration sets the number of seconds to hold the account open for. |
| 77 | + /// It will default to 300 seconds. Passing 0 unlocks the account indefinitely. |
| 78 | + /// |
| 79 | + /// There can only be one unlocked account at a time. |
| 80 | + /// |
| 81 | + /// - Parameters: |
| 82 | + /// - Address: The address of the account to unlock. |
| 83 | + /// - String: Passphrase to unlock the account. |
| 84 | + /// - UInt?: Duration in seconds how long the account should remain unlocked for. |
| 85 | + case unlockAccount(Address, String, UInt?) |
| 86 | + case getTxPoolStatus // No in Eth API |
| 87 | + case getTxPoolContent // No in Eth API |
| 88 | + case getTxPoolInspect // No in Eth API |
| 89 | +} |
| 90 | + |
| 91 | +// FIXME: This conformance should be changed to `LiteralInitiableFromString` |
| 92 | +extension Data: APIResultType { } |
| 93 | + |
| 94 | +extension APIRequest { |
| 95 | + var method: REST { |
| 96 | + switch self { |
| 97 | + default: return .POST |
| 98 | + } |
| 99 | + } |
| 100 | + |
| 101 | + public var responseType: APIResultType.Type { |
| 102 | + switch self { |
| 103 | + case .blockNumber: return BigUInt.self |
| 104 | + case .getAccounts: return [EthereumAddress].self |
| 105 | + case .getBalance: return BigUInt.self |
| 106 | + case .getBlockByHash: return Block.self |
| 107 | + case .getBlockByNumber: return Block.self |
| 108 | + case .gasPrice: return BigUInt.self |
| 109 | + case .feeHistory: return Web3.Oracle.FeeHistory.self |
| 110 | + case .getTransactionCount: return BigUInt.self |
| 111 | + case .getCode: return Hash.self |
| 112 | + case .getTransactionReceipt: return TransactionReceipt.self |
| 113 | + case .createAccount: return EthereumAddress.self |
| 114 | + case .unlockAccount: return Bool.self |
| 115 | + case .getTransactionByHash: return TransactionDetails.self |
| 116 | + case .sendTransaction: return Hash.self |
| 117 | + case .sendRawTransaction: return Hash.self |
| 118 | + case .estimateGas: return BigUInt.self |
| 119 | + case .call: return Data.self |
| 120 | + // FIXME: Not checked |
| 121 | + case .getNetwork: return Int.self |
| 122 | + case .personalSign: return Data.self |
| 123 | + case .getTxPoolStatus: return TxPoolStatus.self |
| 124 | + case .getTxPoolContent: return TxPoolContent.self |
| 125 | + case .getLogs: return [EventLog].self |
| 126 | + |
| 127 | + // FIXME: Not implemented |
| 128 | + case .getStorageAt: return String.self |
| 129 | + case .getTxPoolInspect: return String.self |
| 130 | + } |
| 131 | + } |
| 132 | + |
| 133 | + var encodedBody: Data { |
| 134 | + let request = RequestBody(method: self.call, params: self.parameters) |
| 135 | + // this is safe to force try this here |
| 136 | + // Because request must failed to compile if it not conformable with `Encodable` protocol |
| 137 | + return try! JSONEncoder().encode(request) |
| 138 | + } |
| 139 | + |
| 140 | + var parameters: [RequestParameter] { |
| 141 | + switch self { |
| 142 | + case .gasPrice, .blockNumber, .getNetwork, .getAccounts, .getTxPoolStatus, .getTxPoolContent, .getTxPoolInspect: |
| 143 | + return [RequestParameter]() |
| 144 | + |
| 145 | + case .estimateGas(let transactionParameters, let blockNumber): |
| 146 | + return [RequestParameter.transaction(transactionParameters), RequestParameter.string(blockNumber.stringValue)] |
| 147 | + |
| 148 | + case let .sendRawTransaction(hash): |
| 149 | + return [RequestParameter.string(hash)] |
| 150 | + |
| 151 | + case let .sendTransaction(transactionParameters): |
| 152 | + return [RequestParameter.transaction(transactionParameters)] |
| 153 | + |
| 154 | + case .getTransactionByHash(let hash): |
| 155 | + return [RequestParameter.string(hash)] |
| 156 | + |
| 157 | + case .getTransactionReceipt(let receipt): |
| 158 | + return [RequestParameter.string(receipt)] |
| 159 | + |
| 160 | + case .getLogs(let eventFilterParameters): |
| 161 | + return [RequestParameter.eventFilter(eventFilterParameters)] |
| 162 | + |
| 163 | + case .personalSign(let address, let string): |
| 164 | + // FIXME: Add second parameter |
| 165 | + return [RequestParameter.string(address), RequestParameter.string(string)] |
| 166 | + |
| 167 | + case .call(let transactionParameters, let blockNumber): |
| 168 | + return [RequestParameter.transaction(transactionParameters), RequestParameter.string(blockNumber.stringValue)] |
| 169 | + |
| 170 | + case .getTransactionCount(let address, let blockNumber): |
| 171 | + return [RequestParameter.string(address), RequestParameter.string(blockNumber.stringValue)] |
| 172 | + |
| 173 | + case .getBalance(let address, let blockNumber): |
| 174 | + return [RequestParameter.string(address), RequestParameter.string(blockNumber.stringValue)] |
| 175 | + |
| 176 | + case .getStorageAt(let address, let hash, let blockNumber): |
| 177 | + return [RequestParameter.string(address), RequestParameter.string(hash), RequestParameter.string(blockNumber.stringValue)] |
| 178 | + |
| 179 | + case .getCode(let address, let blockNumber): |
| 180 | + return [RequestParameter.string(address), RequestParameter.string(blockNumber.stringValue)] |
| 181 | + |
| 182 | + case .getBlockByHash(let hash, let bool): |
| 183 | + return [RequestParameter.string(hash), RequestParameter.bool(bool)] |
| 184 | + |
| 185 | + case .getBlockByNumber(let block, let bool): |
| 186 | + return [RequestParameter.string(block.stringValue), RequestParameter.bool(bool)] |
| 187 | + |
| 188 | + case .feeHistory(let uInt, let blockNumber, let array): |
| 189 | + return [RequestParameter.string(uInt.hexString), RequestParameter.string(blockNumber.stringValue), RequestParameter.doubleArray(array)] |
| 190 | + |
| 191 | + case .createAccount(let string): |
| 192 | + return [RequestParameter.string(string)] |
| 193 | + |
| 194 | + case .unlockAccount(let address, let string, let uInt): |
| 195 | + return [RequestParameter.string(address), RequestParameter.string(string), RequestParameter.uint(uInt ?? 0)] |
| 196 | + } |
| 197 | + } |
| 198 | + |
| 199 | + var call: String { |
| 200 | + switch self { |
| 201 | + case .gasPrice: return "eth_gasPrice" |
| 202 | + case .blockNumber: return "eth_blockNumber" |
| 203 | + case .getNetwork: return "net_version" |
| 204 | + case .getAccounts: return "eth_accounts" |
| 205 | + case .sendRawTransaction: return "eth_sendRawTransaction" |
| 206 | + case .sendTransaction: return "eth_sendTransaction" |
| 207 | + case .getTransactionByHash: return "eth_getTransactionByHash" |
| 208 | + case .getTransactionReceipt: return "eth_getTransactionReceipt" |
| 209 | + case .personalSign: return "eth_sign" |
| 210 | + case .getLogs: return "eth_getLogs" |
| 211 | + case .call: return "eth_call" |
| 212 | + case .estimateGas: return "eth_estimateGas" |
| 213 | + case .getTransactionCount: return "eth_getTransactionCount" |
| 214 | + case .getBalance: return "eth_getBalance" |
| 215 | + case .getStorageAt: return "eth_getStorageAt" |
| 216 | + case .getCode: return "eth_getCode" |
| 217 | + case .getBlockByHash: return "eth_getBlockByHash" |
| 218 | + case .getBlockByNumber: return "eth_getBlockByNumber" |
| 219 | + case .feeHistory: return "eth_feeHistory" |
| 220 | + |
| 221 | + case .unlockAccount: return "personal_unlockAccount" |
| 222 | + case .createAccount: return "personal_createAccount" |
| 223 | + case .getTxPoolStatus: return "txpool_status" |
| 224 | + case .getTxPoolContent: return "txpool_content" |
| 225 | + case .getTxPoolInspect: return "txpool_inspect" |
| 226 | + } |
| 227 | + } |
| 228 | +} |
| 229 | + |
| 230 | +extension APIRequest { |
| 231 | + public static func sendRequest<Result>(with provider: Web3Provider, for call: APIRequest) async throws -> APIResponse<Result> { |
| 232 | + /// Don't even try to make network request if the `Result` type dosen't equal to supposed by API |
| 233 | + // FIXME: Add appropriate error thrown |
| 234 | + guard Result.self == call.responseType else { throw Web3Error.unknownError } |
| 235 | + let request = setupRequest(for: call, with: provider) |
| 236 | + return try await APIRequest.send(uRLRequest: request, with: provider.session) |
| 237 | + } |
| 238 | + |
| 239 | + static func setupRequest(for call: APIRequest, with provider: Web3Provider) -> URLRequest { |
| 240 | + var urlRequest = URLRequest(url: provider.url, cachePolicy: .reloadIgnoringCacheData) |
| 241 | + urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type") |
| 242 | + urlRequest.setValue("application/json", forHTTPHeaderField: "Accept") |
| 243 | + urlRequest.httpMethod = call.method.rawValue |
| 244 | + urlRequest.httpBody = call.encodedBody |
| 245 | + return urlRequest |
| 246 | + } |
| 247 | + |
| 248 | + static func send<Result>(uRLRequest: URLRequest, with session: URLSession) async throws -> APIResponse<Result> { |
| 249 | + let (data, response) = try await session.data(for: uRLRequest) |
| 250 | + |
| 251 | + // FIXME: Add appropriate error thrown |
| 252 | + guard let httpResponse = response as? HTTPURLResponse, |
| 253 | + 200 ..< 400 ~= httpResponse.statusCode else { throw Web3Error.connectionError } |
| 254 | + |
| 255 | + // FIXME: Add throwing an error from is server fails. |
| 256 | + /// This bit of code is purposed to work with literal types that comes in Response in hexString type. |
| 257 | + /// Currently it's just any kind of Integers like `(U)Int`, `Big(U)Int`. |
| 258 | + if Result.self == Data.self || Result.self == UInt.self || Result.self == Int.self || Result.self == BigInt.self || Result.self == BigUInt.self { |
| 259 | + /// This types for sure conformed with `LiteralInitiableFromString` |
| 260 | + // FIXME: Make appropriate error |
| 261 | + guard let U = Result.self as? LiteralInitiableFromString.Type else { throw Web3Error.unknownError } |
| 262 | + let responseAsString = try! JSONDecoder().decode(APIResponse<String>.self, from: data) |
| 263 | + // FIXME: Add appropriate error thrown. |
| 264 | + guard let literalValue = U.init(from: responseAsString.result) else { throw Web3Error.unknownError } |
| 265 | + /// `U` is a APIResponseType type, which `LiteralInitiableFromString` conforms to, so it is safe to cast that. |
| 266 | + // FIXME: Make appropriate error |
| 267 | + guard let asT = literalValue as? Result else { throw Web3Error.unknownError } |
| 268 | + return APIResponse(id: responseAsString.id, jsonrpc: responseAsString.jsonrpc, result: asT) |
| 269 | + } |
| 270 | + return try JSONDecoder().decode(APIResponse<Result>.self, from: data) |
| 271 | + } |
| 272 | +} |
| 273 | + |
| 274 | +enum REST: String { |
| 275 | + case POST |
| 276 | + case GET |
| 277 | +} |
| 278 | + |
| 279 | +struct RequestBody: Encodable { |
| 280 | + var jsonrpc = "2.0" |
| 281 | + var id = Counter.increment() |
| 282 | + |
| 283 | + var method: String |
| 284 | + var params: [RequestParameter] |
| 285 | +} |
| 286 | + |
| 287 | +/// JSON RPC response structure for serialization and deserialization purposes. |
| 288 | +public struct APIResponse<Result>: Decodable where Result: APIResultType { |
| 289 | + public var id: Int |
| 290 | + public var jsonrpc = "2.0" |
| 291 | + public var result: Result |
| 292 | +} |
0 commit comments