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