Skip to content

Commit ba1a60d

Browse files
Implement feeHistory usage for Gas cost prediction
- Add method `decodeHex` for `Arrays<T> where T: DecodableFromHex`. - Add feeHistory request. - Add public feeHistory API call - Move GasOracle from `Web3` scope to `web3.Eth` scope (watch out case of first "w") - Change Oracle struct to work with feeHistory API call. - Refactor `JSONRPCmethod` to make it more explicit. - Refactor `InfuraWebsockerMethod` with a styling.
1 parent 812bd13 commit ba1a60d

File tree

8 files changed

+182
-94
lines changed

8 files changed

+182
-94
lines changed

Sources/web3swift/Convenience/Decodable+Extensions.swift

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,20 +95,33 @@ extension KeyedDecodingContainer {
9595
return try decode(type, forKey: key)
9696
}
9797

98-
/// Decodes a value of the given key from Hex to BigUInt
98+
/// Decodes a value of the given key from Hex to `DecodableFromHex`
9999
///
100100
/// Currently this method supports only `Data.Type`, `BigUInt.Type`, `Date.Type`
101101
///
102102
/// - Parameter type: Generic type `T` wich conforms to `DecodableFromHex` protocol
103103
/// - Parameter key: The key that the decoded value is associated with.
104104
/// - Returns: A decoded value of type `BigUInt`
105-
/// - throws: `Web3Error.dataError` if value associated with key are unable
106-
/// to be initialized as `BigUInt`.
105+
/// - throws: `Web3Error.dataError` if value associated with key are unable to be initialized as `DecodableFromHex`.
107106
public func decodeHex<T: DecodableFromHex>(to type: T.Type, key: KeyedDecodingContainer<K>.Key) throws -> T {
108107
let string = try self.decode(String.self, forKey: key)
109108
guard let number = T(fromHex: string) else { throw Web3Error.dataError }
110109
return number
111110
}
111+
112+
/// Decodes a value of the given key from Hex to `[DecodableFromHex]`
113+
///
114+
/// Currently this method supports only `Data.Type`, `BigUInt.Type`, `Date.Type`
115+
///
116+
/// - Parameter type: Array of a generic type `T` wich conforms to `DecodableFromHex` protocol
117+
/// - Parameter key: The key that the decoded value is associated with.
118+
/// - Returns: A decoded value of type `BigUInt`
119+
/// - throws: `Web3Error.dataError` if value associated with key are unable to be initialized as `[DecodableFromHex]`.
120+
public func decodeHex<T: DecodableFromHex>(to type: Array<T>.Type, key: KeyedDecodingContainer<K>.Key) throws -> Array<T> {
121+
var container = try self.nestedUnkeyedContainer(forKey: key)
122+
guard let array = try? container.decode(type) else { throw Web3Error.dataError }
123+
return array
124+
}
112125
}
113126

114127
public protocol DecodableFromHex: Decodable {
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
//
2+
// Promise+Web3+Eth+FeeHistory.swift
3+
// web3swift
4+
//
5+
// Created by Yaroslav on 11.04.2022.
6+
// Copyright © 2022 web3swift. All rights reserved.
7+
//
8+
9+
import Foundation
10+
import PromiseKit
11+
import BigInt
12+
13+
extension web3.Eth {
14+
// public func feeHistory(blockCount: Int = 10, onBlock: String = "latest", percentiles:[Double] = [25, 50, 75]) -> Promise<FeeHistory> {
15+
// return feeHistory(address: addr, onBlock: onBlock)
16+
// }
17+
public func feeHistory(blockCount: Int, block: String, percentiles:[Double]) throws -> Promise<Oracle.FeeHistory> {
18+
let request = JSONRPCRequestFabric.prepareRequest(.feeHistory, parameters: [blockCount.description.addHexPrefix(), block, percentiles])
19+
let rp = web3.dispatch(request)
20+
let queue = web3.requestDispatcher.queue
21+
return rp.map(on: queue) { response in
22+
guard let value: Oracle.FeeHistory = response.getValue() else {
23+
if response.error != nil {
24+
throw Web3Error.nodeError(desc: response.error!.message)
25+
}
26+
throw Web3Error.nodeError(desc: "Invalid value from Ethereum node")
27+
}
28+
return value
29+
}
30+
}
31+
}

Sources/web3swift/Web3/Web3+Eth.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,4 +352,9 @@ extension web3.Eth {
352352
return sendERC20tokensWithKnownDecimals(tokenAddress: tokenAddress, from: from, to: to, amount: value, transactionOptions: mergedOptions)
353353
}
354354

355+
public func feeHistory(blockCount: Int = 10, block: String = "latest", percentiles:[Double] = [25, 50, 75]) throws -> Oracle.FeeHistory {
356+
let result = try self.feeHistory(blockCount: blockCount, block: block, percentiles: percentiles).wait()
357+
return result
358+
}
359+
355360
}

Sources/web3swift/Web3/Web3+GasOracle.swift

Lines changed: 55 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,40 +9,42 @@
99
import Foundation
1010
import BigInt
1111

12-
extension Web3 {
12+
extension web3.Eth {
1313
/// Oracle is the class to do a transaction fee suggestion
14-
///
15-
/// Designed for EIP-1559 transactions only.
1614
final public class Oracle {
17-
private var latestBlock: Block?
1815

1916
/// Web3 provider by which accessing to the blockchain
2017
private let web3Provider: web3
2118

2219
/// Ethereum scope shortcut
2320
private var eth: web3.Eth { web3Provider.eth }
2421

22+
/// Block to start getting history
23+
var block: String
24+
2525
/// Count of blocks to calculate statistics
26-
public private(set) var blockCount: BigUInt
26+
var blockCount: BigUInt
2727

2828
/// Count of transactions to filter block for tip calculation
29-
public private(set) var transactionCount: BigUInt
29+
var percentiles: [Double]
3030

3131
/// Oracle initializer
3232
/// - Parameters:
3333
/// - provider: Web3 Ethereum provider
34+
/// - block: Number of block from which counts starts
3435
/// - blockCount: Count of block to calculate statistics
35-
/// - transactionCount: Count of transaction to filter block for tip calculation
36-
public init(_ provider: web3, blockCount: BigUInt = 20, transactionCount: BigUInt = 50) {
36+
/// - percentiles: Percentiles of fees which will split in fees history
37+
public init(_ provider: web3, block: String = "latest", blockCount: BigUInt = 20, percentiles: [Double] = [25, 50, 75]) {
3738
self.web3Provider = provider
39+
self.block = block
3840
self.blockCount = blockCount
39-
self.transactionCount = transactionCount
41+
self.percentiles = percentiles
4042
}
4143

42-
private func calcBaseFee(for block: Block?) -> BigUInt {
43-
guard let block = block else { return 0 }
44-
return Web3.calcBaseFee(block) ?? 0
45-
}
44+
// private func calcBaseFee(for block: Block?) -> BigUInt {
45+
// guard let block = block else { return 0 }
46+
// return Web3.calcBaseFee(block) ?? 0
47+
// }
4648

4749
private func calculateStatistic(for statistic: Statistic, data: [BigUInt]) throws -> BigUInt {
4850
let noAnomalyArray = data.cropAnomalyValues()
@@ -59,23 +61,29 @@ extension Web3 {
5961
case .maximum:
6062
// Checking that suggestedBaseFee is not lower than it will be in the next block
6163
// because in the maximum statistic we should guarantee that transaction would be included in it.
62-
return max(calcBaseFee(for: latestBlock), unwrappedArray.max()!)
64+
// return max(calcBaseFee(for: latestBlock), unwrappedArray.max()!)
65+
return unwrappedArray.max()!
6366
}
6467
// swiftlint:enable force_unwrapping
6568
}
6669

70+
// private func suggestGasValues( statistic: Statistic) throws -> FeeHistory {
71+
//
72+
// }
73+
6774
private func suggestTipValue(_ statistic: Statistic) throws -> BigUInt {
75+
6876
let latestBlockNumber = try eth.getBlockNumber()
6977

7078
var block: Block
7179

7280
// TODO: Make me work with cache
7381
repeat {
7482
block = try eth.getBlockByNumber(latestBlockNumber, fullTransactions: true)
75-
} while block.transactions.count < transactionCount
83+
} while block.transactions.count < 20
7684

7785
// Storing last block to calculate baseFee of the next block
78-
latestBlock = block
86+
// latestBlock = block
7987

8088
let transactionsTips = block.transactions
8189
.compactMap { t -> EthereumTransaction? in
@@ -92,7 +100,7 @@ extension Web3 {
92100
let latestBlockNumber = try eth.getBlockNumber()
93101

94102
// Assigning last block to object var to predict baseFee of the next block
95-
latestBlock = try eth.getBlockByNumber(latestBlockNumber)
103+
// latestBlock = try eth.getBlockByNumber(latestBlockNumber)
96104
// TODO: Make me work with cache
97105
let lastNthBlocksBaseFees = try (latestBlockNumber - blockCount ... latestBlockNumber)
98106
.map { try eth.getBlockByNumber($0) }
@@ -106,7 +114,7 @@ extension Web3 {
106114
let latestBlockNumber = try eth.getBlockNumber()
107115

108116
// Assigning last block to object var to predict baseFee of the next block
109-
latestBlock = try eth.getBlockByNumber(latestBlockNumber)
117+
// latestBlock = try eth.getBlockByNumber(latestBlockNumber)
110118
// TODO: Make me work with cache
111119
let lastNthBlockGasPrice = try (latestBlockNumber - blockCount ... latestBlockNumber)
112120
.map { try eth.getBlockByNumber($0, fullTransactions: true) }
@@ -123,7 +131,7 @@ extension Web3 {
123131
}
124132
}
125133

126-
public extension Web3.Oracle {
134+
public extension web3.Eth.Oracle {
127135
// MARK: - Base Fee
128136
/// Base fee amount based on last Nth blocks
129137
///
@@ -175,7 +183,7 @@ public extension Web3.Oracle {
175183
}
176184
}
177185

178-
public extension Web3.Oracle {
186+
public extension web3.Eth.Oracle {
179187
// TODO: Make me struct and encapsulate math within to make me extendable
180188
enum Statistic {
181189
/// Mininum statistic
@@ -189,6 +197,33 @@ public extension Web3.Oracle {
189197
}
190198
}
191199

200+
public extension web3.Eth.Oracle {
201+
struct FeeHistory {
202+
let baseFeePerGas: [BigUInt]
203+
let gasUsedRatio: [Double]
204+
let oldestBlock: BigUInt
205+
let reward: [[BigUInt]]
206+
}
207+
}
208+
209+
extension web3.Eth.Oracle.FeeHistory: Decodable {
210+
enum CodingKeys: String, CodingKey {
211+
case baseFeePerGas
212+
case gasUsedRatio
213+
case oldestBlock
214+
case reward
215+
}
216+
217+
public init(from decoder: Decoder) throws {
218+
let values = try decoder.container(keyedBy: CodingKeys.self)
219+
220+
self.baseFeePerGas = try values.decodeHex(to: [BigUInt].self, key: .baseFeePerGas)
221+
self.gasUsedRatio = try values.decodeHex(to: [Double].self, key: .gasUsedRatio)
222+
self.oldestBlock = try values.decodeHex(to: BigUInt.self, key: .oldestBlock)
223+
self.reward = try values.decodeHex(to: [[BigUInt]].self, key: .reward)
224+
}
225+
}
226+
192227
extension Array where Element: Comparable {
193228

194229
/// Sorts array and drops most and least values.

Sources/web3swift/Web3/Web3+Methods.swift

Lines changed: 49 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -8,64 +8,68 @@ import Foundation
88

99
public enum JSONRPCmethod: String, Encodable {
1010

11+
// 0 parameter in call
1112
case gasPrice = "eth_gasPrice"
1213
case blockNumber = "eth_blockNumber"
1314
case getNetwork = "net_version"
15+
case getAccounts = "eth_accounts"
16+
case getTxPoolStatus = "txpool_status"
17+
case getTxPoolContent = "txpool_content"
18+
case getTxPoolInspect = "txpool_inspect"
19+
case estimateGas = "eth_estimateGas"
20+
21+
// 1 parameter in call
1422
case sendRawTransaction = "eth_sendRawTransaction"
1523
case sendTransaction = "eth_sendTransaction"
16-
case estimateGas = "eth_estimateGas"
17-
case call = "eth_call"
18-
case getTransactionCount = "eth_getTransactionCount"
19-
case getBalance = "eth_getBalance"
20-
case getCode = "eth_getCode"
21-
case getStorageAt = "eth_getStorageAt"
2224
case getTransactionByHash = "eth_getTransactionByHash"
2325
case getTransactionReceipt = "eth_getTransactionReceipt"
24-
case getAccounts = "eth_accounts"
25-
case getBlockByHash = "eth_getBlockByHash"
26-
case getBlockByNumber = "eth_getBlockByNumber"
2726
case personalSign = "eth_sign"
2827
case unlockAccount = "personal_unlockAccount"
2928
case createAccount = "personal_createAccount"
3029
case getLogs = "eth_getLogs"
31-
case getTxPoolInspect = "txpool_inspect"
32-
case getTxPoolStatus = "txpool_status"
33-
case getTxPoolContent = "txpool_content"
30+
31+
// 2 parameters in call
32+
case call = "eth_call"
33+
case getTransactionCount = "eth_getTransactionCount"
34+
case getBalance = "eth_getBalance"
35+
case getStorageAt = "eth_getStorageAt"
36+
case getCode = "eth_getCode"
37+
case getBlockByHash = "eth_getBlockByHash"
38+
case getBlockByNumber = "eth_getBlockByNumber"
39+
40+
// 3 parameters in call
41+
case feeHistory = "eth_feeHistory"
3442

3543
public var requiredNumOfParameters: Int {
36-
get {
37-
switch self {
38-
case .call:
39-
return 2
40-
case .getTransactionCount:
41-
return 2
42-
case .getBalance:
43-
return 2
44-
case .getStorageAt:
45-
return 2
46-
case .getCode:
47-
return 2
48-
case .getBlockByHash:
49-
return 2
50-
case .getBlockByNumber:
51-
return 2
52-
case .gasPrice:
53-
return 0
54-
case .blockNumber:
55-
return 0
56-
case .getNetwork:
57-
return 0
58-
case .getAccounts:
59-
return 0
60-
case .getTxPoolStatus:
61-
return 0
62-
case .getTxPoolContent:
63-
return 0
64-
case .getTxPoolInspect:
65-
return 0
66-
default:
67-
return 1
68-
}
44+
switch self {
45+
case .gasPrice,
46+
.blockNumber,
47+
.getNetwork,
48+
.getAccounts,
49+
.getTxPoolStatus,
50+
.getTxPoolContent,
51+
.getTxPoolInspect,
52+
.estimateGas:
53+
return 0
54+
case .sendRawTransaction,
55+
.sendTransaction,
56+
.getTransactionByHash,
57+
.getTransactionReceipt,
58+
.personalSign,
59+
.unlockAccount,
60+
.createAccount,
61+
.getLogs:
62+
return 1
63+
case .call,
64+
.getTransactionCount,
65+
.getBalance,
66+
.getStorageAt,
67+
.getCode,
68+
.getBlockByHash,
69+
.getBlockByNumber:
70+
return 2
71+
case .feeHistory:
72+
return 3
6973
}
7074
}
7175
}

0 commit comments

Comments
 (0)