-
Notifications
You must be signed in to change notification settings - Fork 483
Generic network layer #577
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
c53c4e4
e4012bf
d86583b
24dc3a8
cd751ee
fb4c10d
88c84bb
72e7ad9
9f79702
d7953dc
e439b04
1a7734e
8398c5d
8c0e119
dfe4f72
e4fcc1d
f1ab07d
9e0343c
5db2272
245cc0d
2a54eec
4e69483
635009a
27a28e2
351b671
7c5126a
412972b
2de0d21
8207727
216686e
dddf1f2
65c5a53
b24948f
0bc450b
983d084
bbe64ef
16d6f1c
6611999
fec0bc0
78301bd
41ac98f
23ef94a
fa80d43
90e2061
55732ab
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,292 @@ | ||
// | ||
// Web3+APIMethod.swift | ||
// Web3swift | ||
// | ||
// Created by Yaroslav on 24.05.2022. | ||
// | ||
|
||
import Foundation | ||
import BigInt | ||
|
||
public typealias Hash = String // 32 bytes hash of block (64 chars length without 0x) | ||
public typealias Receipt = Hash | ||
public typealias Address = Hash // 20 bytes (40 chars length without 0x) | ||
public typealias TransactionHash = Hash // 64 chars length without 0x | ||
|
||
// FIXME: Add documentation to each method. | ||
/// Ethereum JSON RPC API Calls | ||
public enum APIRequest { | ||
// MARK: - Official API | ||
// 0 parameter in call | ||
case gasPrice | ||
case blockNumber | ||
case getNetwork | ||
case getAccounts | ||
// ?? | ||
case estimateGas(TransactionParameters, BlockNumber) | ||
case sendRawTransaction(Hash) | ||
case sendTransaction(TransactionParameters) | ||
case getTransactionByHash(Hash) | ||
case getTransactionReceipt(Hash) | ||
case getLogs(EventFilterParameters) | ||
case personalSign(Address, String) | ||
case call(TransactionParameters, BlockNumber) | ||
case getTransactionCount(Address, BlockNumber) | ||
case getBalance(Address, BlockNumber) | ||
|
||
/// Returns the value from a storage position at a given address. | ||
/// | ||
/// - Parameters: | ||
/// - Address: Address | ||
/// - Storage: slot | ||
/// - BlockNumber: sd | ||
case getStorageAt(Address, Hash, BlockNumber) | ||
|
||
case getCode(Address, BlockNumber) | ||
case getBlockByHash(Hash, Bool) | ||
case getBlockByNumber(BlockNumber, Bool) | ||
|
||
/// Returns fee history with a respect to given setup | ||
/// | ||
/// Generates and returns an estimate of how much gas is necessary to allow the transaction to complete. | ||
/// The transaction will not be added to the blockchain. Note that the estimate may be significantly more | ||
/// than the amount of gas actually used by the transaction, for a variety of reasons including EVM mechanics and node performance. | ||
/// | ||
/// - Parameters: | ||
/// - UInt: Requested range of blocks. Clients will return less than the requested range if not all blocks are available. | ||
/// - BlockNumber: Highest block of the requested range. | ||
/// - [Double]: A monotonically increasing list of percentile values. | ||
/// For each block in the requested range, the transactions will be sorted in ascending order | ||
/// by effective tip per gas and the coresponding effective tip for the percentile will be determined, accounting for gas consumed." | ||
case feeHistory(BigUInt, BlockNumber, [Double]) | ||
|
||
// MARK: - Additional API | ||
/// Creates new account. | ||
/// | ||
/// Note: it becomes the new current unlocked account. There can only be one unlocked account at a time. | ||
/// | ||
/// - Parameters: | ||
/// - String: Password for the new account. | ||
case createAccount(String) // No in Eth API | ||
|
||
/// Unlocks specified account for use. | ||
/// | ||
/// If permanent unlocking is disabled (the default) then the duration argument will be ignored, | ||
/// and the account will be unlocked for a single signing. | ||
/// With permanent locking enabled, the duration sets the number of seconds to hold the account open for. | ||
/// It will default to 300 seconds. Passing 0 unlocks the account indefinitely. | ||
/// | ||
/// There can only be one unlocked account at a time. | ||
/// | ||
/// - Parameters: | ||
/// - Address: The address of the account to unlock. | ||
/// - String: Passphrase to unlock the account. | ||
/// - UInt?: Duration in seconds how long the account should remain unlocked for. | ||
case unlockAccount(Address, String, UInt?) | ||
case getTxPoolStatus // No in Eth API | ||
case getTxPoolContent // No in Eth API | ||
case getTxPoolInspect // No in Eth API | ||
} | ||
|
||
// FIXME: This conformance should be changed to `LiteralInitiableFromString` | ||
extension Data: APIResultType { } | ||
|
||
extension APIRequest { | ||
var method: REST { | ||
switch self { | ||
default: return .POST | ||
} | ||
} | ||
|
||
public var responseType: APIResultType.Type { | ||
switch self { | ||
case .blockNumber: return BigUInt.self | ||
case .getAccounts: return [EthereumAddress].self | ||
case .getBalance: return BigUInt.self | ||
case .getBlockByHash: return Block.self | ||
case .getBlockByNumber: return Block.self | ||
case .gasPrice: return BigUInt.self | ||
case .feeHistory: return Web3.Oracle.FeeHistory.self | ||
case .getTransactionCount: return BigUInt.self | ||
case .getCode: return Hash.self | ||
case .getTransactionReceipt: return TransactionReceipt.self | ||
case .createAccount: return EthereumAddress.self | ||
case .unlockAccount: return Bool.self | ||
case .getTransactionByHash: return TransactionDetails.self | ||
case .sendTransaction: return Hash.self | ||
case .sendRawTransaction: return Hash.self | ||
case .estimateGas: return BigUInt.self | ||
case .call: return Data.self | ||
// FIXME: Not checked | ||
case .getNetwork: return Int.self | ||
case .personalSign: return Data.self | ||
case .getTxPoolStatus: return TxPoolStatus.self | ||
case .getTxPoolContent: return TxPoolContent.self | ||
case .getLogs: return [EventLog].self | ||
|
||
// FIXME: Not implemented | ||
case .getStorageAt: return String.self | ||
case .getTxPoolInspect: return String.self | ||
} | ||
} | ||
|
||
var encodedBody: Data { | ||
let request = RequestBody(method: self.call, params: self.parameters) | ||
// this is safe to force try this here | ||
// Because request must failed to compile if it not conformable with `Encodable` protocol | ||
return try! JSONEncoder().encode(request) | ||
} | ||
|
||
var parameters: [RequestParameter] { | ||
switch self { | ||
case .gasPrice, .blockNumber, .getNetwork, .getAccounts, .getTxPoolStatus, .getTxPoolContent, .getTxPoolInspect: | ||
return [RequestParameter]() | ||
|
||
case .estimateGas(let transactionParameters, let blockNumber): | ||
return [RequestParameter.transaction(transactionParameters), RequestParameter.string(blockNumber.stringValue)] | ||
|
||
case let .sendRawTransaction(hash): | ||
return [RequestParameter.string(hash)] | ||
|
||
case let .sendTransaction(transactionParameters): | ||
return [RequestParameter.transaction(transactionParameters)] | ||
|
||
case .getTransactionByHash(let hash): | ||
return [RequestParameter.string(hash)] | ||
|
||
case .getTransactionReceipt(let receipt): | ||
return [RequestParameter.string(receipt)] | ||
|
||
case .getLogs(let eventFilterParameters): | ||
return [RequestParameter.eventFilter(eventFilterParameters)] | ||
|
||
case .personalSign(let address, let string): | ||
// FIXME: Add second parameter | ||
return [RequestParameter.string(address), RequestParameter.string(string)] | ||
|
||
case .call(let transactionParameters, let blockNumber): | ||
return [RequestParameter.transaction(transactionParameters), RequestParameter.string(blockNumber.stringValue)] | ||
|
||
case .getTransactionCount(let address, let blockNumber): | ||
return [RequestParameter.string(address), RequestParameter.string(blockNumber.stringValue)] | ||
|
||
case .getBalance(let address, let blockNumber): | ||
return [RequestParameter.string(address), RequestParameter.string(blockNumber.stringValue)] | ||
|
||
case .getStorageAt(let address, let hash, let blockNumber): | ||
return [RequestParameter.string(address), RequestParameter.string(hash), RequestParameter.string(blockNumber.stringValue)] | ||
|
||
case .getCode(let address, let blockNumber): | ||
return [RequestParameter.string(address), RequestParameter.string(blockNumber.stringValue)] | ||
|
||
case .getBlockByHash(let hash, let bool): | ||
return [RequestParameter.string(hash), RequestParameter.bool(bool)] | ||
|
||
case .getBlockByNumber(let block, let bool): | ||
return [RequestParameter.string(block.stringValue), RequestParameter.bool(bool)] | ||
|
||
case .feeHistory(let uInt, let blockNumber, let array): | ||
return [RequestParameter.string(uInt.hexString), RequestParameter.string(blockNumber.stringValue), RequestParameter.doubleArray(array)] | ||
|
||
case .createAccount(let string): | ||
return [RequestParameter.string(string)] | ||
|
||
case .unlockAccount(let address, let string, let uInt): | ||
return [RequestParameter.string(address), RequestParameter.string(string), RequestParameter.uint(uInt ?? 0)] | ||
} | ||
} | ||
|
||
var call: String { | ||
switch self { | ||
case .gasPrice: return "eth_gasPrice" | ||
case .blockNumber: return "eth_blockNumber" | ||
case .getNetwork: return "net_version" | ||
case .getAccounts: return "eth_accounts" | ||
case .sendRawTransaction: return "eth_sendRawTransaction" | ||
case .sendTransaction: return "eth_sendTransaction" | ||
case .getTransactionByHash: return "eth_getTransactionByHash" | ||
case .getTransactionReceipt: return "eth_getTransactionReceipt" | ||
case .personalSign: return "eth_sign" | ||
case .getLogs: return "eth_getLogs" | ||
case .call: return "eth_call" | ||
case .estimateGas: return "eth_estimateGas" | ||
case .getTransactionCount: return "eth_getTransactionCount" | ||
case .getBalance: return "eth_getBalance" | ||
case .getStorageAt: return "eth_getStorageAt" | ||
case .getCode: return "eth_getCode" | ||
case .getBlockByHash: return "eth_getBlockByHash" | ||
case .getBlockByNumber: return "eth_getBlockByNumber" | ||
case .feeHistory: return "eth_feeHistory" | ||
|
||
case .unlockAccount: return "personal_unlockAccount" | ||
case .createAccount: return "personal_createAccount" | ||
case .getTxPoolStatus: return "txpool_status" | ||
case .getTxPoolContent: return "txpool_content" | ||
case .getTxPoolInspect: return "txpool_inspect" | ||
} | ||
} | ||
} | ||
|
||
extension APIRequest { | ||
public static func sendRequest<Result>(with provider: Web3Provider, for call: APIRequest) async throws -> APIResponse<Result> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can we have this not be a static There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I doubt it. I've considered to move this method to Making this method non static within this enum makes API kinda wired. It'll be looks like this: let request: APIRequest = .gasPrice
let response: APIResponse<BigUInt> = request.send(with: web3.provider) So i've refused that either. |
||
/// Don't even try to make network request if the `Result` type dosen't equal to supposed by API | ||
// FIXME: Add appropriate error thrown | ||
guard Result.self == call.responseType else { throw Web3Error.unknownError } | ||
let request = setupRequest(for: call, with: provider) | ||
return try await APIRequest.send(uRLRequest: request, with: provider.session) | ||
} | ||
|
||
static func setupRequest(for call: APIRequest, with provider: Web3Provider) -> URLRequest { | ||
var urlRequest = URLRequest(url: provider.url, cachePolicy: .reloadIgnoringCacheData) | ||
urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type") | ||
urlRequest.setValue("application/json", forHTTPHeaderField: "Accept") | ||
urlRequest.httpMethod = call.method.rawValue | ||
urlRequest.httpBody = call.encodedBody | ||
return urlRequest | ||
} | ||
|
||
static func send<Result>(uRLRequest: URLRequest, with session: URLSession) async throws -> APIResponse<Result> { | ||
let (data, response) = try await session.data(for: uRLRequest) | ||
|
||
// FIXME: Add appropriate error thrown | ||
guard let httpResponse = response as? HTTPURLResponse, | ||
200 ..< 400 ~= httpResponse.statusCode else { throw Web3Error.connectionError } | ||
|
||
// FIXME: Add throwing an error from is server fails. | ||
/// This bit of code is purposed to work with literal types that comes in Response in hexString type. | ||
/// Currently it's just any kind of Integers like `(U)Int`, `Big(U)Int`. | ||
if Result.self == Data.self || Result.self == UInt.self || Result.self == Int.self || Result.self == BigInt.self || Result.self == BigUInt.self { | ||
/// This types for sure conformed with `LiteralInitiableFromString` | ||
// FIXME: Make appropriate error | ||
guard let U = Result.self as? LiteralInitiableFromString.Type else { throw Web3Error.unknownError } | ||
let responseAsString = try! JSONDecoder().decode(APIResponse<String>.self, from: data) | ||
// FIXME: Add appropriate error thrown. | ||
guard let literalValue = U.init(from: responseAsString.result) else { throw Web3Error.unknownError } | ||
/// `U` is a APIResponseType type, which `LiteralInitiableFromString` conforms to, so it is safe to cast that. | ||
// FIXME: Make appropriate error | ||
guard let asT = literalValue as? Result else { throw Web3Error.unknownError } | ||
return APIResponse(id: responseAsString.id, jsonrpc: responseAsString.jsonrpc, result: asT) | ||
} | ||
return try JSONDecoder().decode(APIResponse<Result>.self, from: data) | ||
} | ||
} | ||
|
||
enum REST: String { | ||
case POST | ||
case GET | ||
} | ||
|
||
struct RequestBody: Encodable { | ||
var jsonrpc = "2.0" | ||
var id = Counter.increment() | ||
|
||
var method: String | ||
var params: [RequestParameter] | ||
} | ||
|
||
/// JSON RPC response structure for serialization and deserialization purposes. | ||
public struct APIResponse<Result>: Decodable where Result: APIResultType { | ||
public var id: Int | ||
public var jsonrpc = "2.0" | ||
public var result: Result | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suggest removing force try
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
in this PR and new code for 3.0 I suggest we don't open PR's with force unwraps/for try etc
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Generally i agreed with you, but in some case force try is reasonable, like that one which were used in gas prediction PR, so this rule should not be carved in stone.
But here i'm agreed with you, it's unnecessary here, and i'll fix it soon.