Skip to content

Commit 5db2272

Browse files
Implement most of required protocols to make decoding responses to generic implementation.
There's follow types presented: - enum `APIRequest` - main type that user should be working with. - `APIResponse<T>` - type that returns network request method on successfully API call, where generic `T` type is the any given value of `result`. - protocol `APIResponseType` - protocol that must conforms any possible `result` type. - protocol `LiteralInitableFromString` - protocol that conforms any `Numeric` literals that `result` property hols as `String`. - protocol `IntegerInitableWithRadix` - utility protocol that conforms any `Integer` literals that have follow initializee: `init?(from hexString: String)`
1 parent 9e0343c commit 5db2272

File tree

5 files changed

+305
-254
lines changed

5 files changed

+305
-254
lines changed
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
//
2+
// HexDecodableProtocols.swift
3+
// Web3swift
4+
//
5+
// Created by Yaroslav on 25.05.2022.
6+
//
7+
8+
import BigInt
9+
10+
/// This is utility protocol for decoding API Responses
11+
///
12+
/// You better not use it in any other part of a bit of code except `APIResponse<T>` decoding.
13+
///
14+
/// This protocols intention is to work around that Ethereum API cases, when almost all numbers are comming as strings.
15+
/// More than that their notation (e.g. 0x12d) are don't fit with the default Numeric decoders behaviours.
16+
/// So to work around that for generic cases we're going to force decode `APIResponse.result` field as `String`
17+
/// and then initiate it
18+
public protocol LiteralInitableFromString: APIResponseType {
19+
init?(from hexString: String)
20+
}
21+
22+
extension LiteralInitableFromString where Self: IntegerInitableWithRadix {
23+
/// This initializer is intended to init `(U)Int` from hex string with `0x` prefix.
24+
public init?(from hexString: String) {
25+
guard hexString.hasPrefix("0x") else { return nil }
26+
let tmpString = String(hexString.dropFirst(2))
27+
guard let value = Self(tmpString, radix: 16) else { return nil }
28+
self = value
29+
}
30+
}
31+
32+
extension Int: LiteralInitableFromString { }
33+
34+
extension UInt: LiteralInitableFromString { }
35+
36+
extension BigInt: LiteralInitableFromString { }
37+
38+
extension BigUInt: LiteralInitableFromString { }
39+
40+
extension String: LiteralInitableFromString {
41+
public init?(from hexString: String) {
42+
self = hexString
43+
}
44+
}
45+
46+
47+
// ------
48+
public protocol IntegerInitableWithRadix {
49+
init?<S: StringProtocol>(_ text: S, radix: Int)
50+
}
51+
52+
extension Int: IntegerInitableWithRadix { }
53+
54+
extension UInt: IntegerInitableWithRadix { }
55+
56+
extension BigInt: IntegerInitableWithRadix { }
57+
58+
extension BigUInt: IntegerInitableWithRadix { }
59+
// ------
Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
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+
}

Sources/web3swift/Web3/Web3+APIMethod.swift

Lines changed: 0 additions & 8 deletions
This file was deleted.

0 commit comments

Comments
 (0)