Skip to content

Commit 73985d7

Browse files
Rewrite GasOracle
- Implement generic decodeHex methods for T: DecodableFromHex, [T], [[T]]. - Make RPC API `feeHistory` call internal. - Reimplement `tipFeePercentiles` to return array of percentiles of last tips. - Reimplement `baseFeePercentiles` to return array of percentiles of last baseFee. - Rename `bothFeesPercentiles` to return tuple of arrays of percentiles of last both tips and baseFee. - `JSONRPCresponse` now can decode `Web3.Oracle.FeeHistory` - `JSONRPCparams` now can encode `[Double]` - Add `OracleTests` - `testPretictBaseFee` - `testPredictTip`
1 parent 4329f90 commit 73985d7

File tree

6 files changed

+166
-86
lines changed

6 files changed

+166
-86
lines changed

Sources/web3swift/Convenience/Array+Extension.swift

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
//
66

77
import Foundation
8+
import BigInt
89

910
extension Array {
1011
public func split(intoChunksOf chunkSize: Int) -> [[Element]] {
@@ -14,3 +15,38 @@ extension Array {
1415
}
1516
}
1617
}
18+
19+
extension Array where Element: Comparable {
20+
21+
/// Sorts array and drops most and least values.
22+
/// - Returns: Sorted array without most and least values, nil if `array.count` <= 2
23+
func cropAnomalyValues() -> Self? {
24+
var sortedArray = self.sorted()
25+
// Array should at least counts two to pass that formations.
26+
guard sortedArray.count > 1 else { return nil }
27+
sortedArray.removeLast()
28+
sortedArray.removeFirst()
29+
return sortedArray
30+
}
31+
}
32+
33+
extension Array where Element: BinaryInteger {
34+
// TODO: Make me generic
35+
// FIXME: Add doc comment
36+
func mean() -> BigUInt? {
37+
guard !self.isEmpty else { return nil }
38+
return BigUInt(self.reduce(0, +)) / BigUInt(self.count)
39+
}
40+
41+
// FIXME: Add doc comment
42+
func percentile(of value: Double) -> Element? {
43+
guard !self.isEmpty else { return nil }
44+
45+
let normalizedValue = value / 100 * Double(self.count)
46+
let index = Int(ceil(normalizedValue))
47+
guard index < self.count else { return nil }
48+
49+
let sorted_data = self.sorted()
50+
return sorted_data[index]
51+
}
52+
}

Sources/web3swift/Convenience/Decodable+Extensions.swift

Lines changed: 49 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ extension KeyedDecodingContainer {
1818
/// - Returns: A decoded value of type `BigUInt`
1919
/// - throws: `Web3Error.dataError` if value associated with key are unable to be initialized as `DecodableFromHex`.
2020
public func decodeHex<T: DecodableFromHex>(_ type: T.Type, forKey: KeyedDecodingContainer<K>.Key) throws -> T {
21-
let string = try self.decode(String.self, forKey: forKey)
22-
guard let number = T(fromHex: string) else { throw Web3Error.dataError }
21+
let hexString = try self.decode(String.self, forKey: forKey)
22+
guard let number = T(fromHex: hexString) else { throw Web3Error.dataError }
2323
return number
2424
}
2525

@@ -30,24 +30,63 @@ extension KeyedDecodingContainer {
3030
/// - Parameter type: Array of a generic type `T` wich conforms to `DecodableFromHex` protocol
3131
/// - Parameter key: The key that the decoded value is associated with.
3232
/// - Returns: A decoded value of type `BigUInt`
33-
/// - throws: `Web3Error.dataError` if value associated with key are unable to be initialized as `[DecodableFromHex]`.
34-
public func decodeHex<T: DecodableFromHex>(_ type: Array<T>.Type, forKey: KeyedDecodingContainer<K>.Key) throws -> Array<T> {
35-
var container = try self.nestedUnkeyedContainer(forKey: forKey)
36-
guard let array = try? container.decode(type) else { throw Web3Error.dataError }
33+
/// - throws: `Web3Error.dataError` if value associated with key are unable to be initialized as `[[DecodableFromHex]]`.
34+
public func decodeHex<T: DecodableFromHex>(_ type: [T].Type, forKey: KeyedDecodingContainer<K>.Key) throws -> [T] {
35+
var container = try nestedUnkeyedContainer(forKey: forKey)
36+
guard let array = try? container.decodeHex(type) else { throw Web3Error.dataError }
3737
return array
3838
}
3939

4040
/// Decodes a value of the given key from Hex to `[DecodableFromHex]`
4141
///
42-
/// Currently this method supports only `Data.Type`, `BigUInt.Type`, `Date.Type`
42+
/// Currently this method supports only `Data.Type`, `BigUInt.Type`, `Date.Type`, `EthereumAddress`
4343
///
4444
/// - Parameter type: Array of a generic type `T` wich conforms to `DecodableFromHex` protocol
4545
/// - Parameter key: The key that the decoded value is associated with.
4646
/// - Returns: A decoded value of type `BigUInt`
47+
/// - throws: `Web3Error.dataError` if value associated with key are unable to be initialized as `[[DecodableFromHex]]`.
48+
public func decodeHex<T: DecodableFromHex>(_ type: [[T]].Type, forKey: KeyedDecodingContainer<K>.Key) throws -> [[T]] {
49+
var container = try nestedUnkeyedContainer(forKey: forKey)
50+
guard let array = try? container.decodeHex(type) else { throw Web3Error.dataError }
51+
return array
52+
}
53+
}
54+
55+
public extension UnkeyedDecodingContainer {
56+
/// Decodes a unkeyed value from hex to `[DecodableFromHex]`
57+
///
58+
/// Currently this method supports only `Data.Type`, `BigUInt.Type`, `Date.Type`, `EthereumAddress`
59+
///
60+
/// - Parameter type: Generic type `T` wich conforms to `DecodableFromHex` protocol
61+
/// - Parameter key: The key that the decoded value is associated with.
62+
/// - Returns: A decoded value of type `BigUInt`
4763
/// - throws: `Web3Error.dataError` if value associated with key are unable to be initialized as `[DecodableFromHex]`.
48-
public func decodeHex<T: DecodableFromHex>(_ type: Array<Array<T>>.Type, forKey: KeyedDecodingContainer<K>.Key) throws -> Array<Array<T>> {
49-
var container = try self.nestedUnkeyedContainer(forKey: forKey)
50-
guard let array = try? container.decode(type) else { throw Web3Error.dataError }
64+
mutating func decodeHex<T: DecodableFromHex>(_ type: [T].Type) throws -> [T] {
65+
var array: [T] = []
66+
while !isAtEnd {
67+
let hexString = try decode(String.self)
68+
guard let item = T(fromHex: hexString) else { continue }
69+
array.append(item)
70+
}
71+
return array
72+
}
73+
74+
75+
/// Decodes a unkeyed value from Hex to `DecodableFromHex`
76+
///
77+
/// Currently this method supports only `Data.Type`, `BigUInt.Type`, `Date.Type`, `EthereumAddress`
78+
///
79+
/// - Parameter type: Generic type `T` wich conforms to `DecodableFromHex` protocol
80+
/// - Parameter key: The key that the decoded value is associated with.
81+
/// - Returns: A decoded value of type `BigUInt`
82+
/// - throws: `Web3Error.dataError` if value associated with key are unable to be initialized as `[[DecodableFromHex]]`.
83+
mutating func decodeHex<T: DecodableFromHex>(_ type: [[T]].Type) throws -> [[T]] {
84+
var array: [[T]] = []
85+
while !isAtEnd {
86+
var container = try nestedUnkeyedContainer()
87+
let intArr = try container.decodeHex([T].self)
88+
array.append(intArr)
89+
}
5190
return array
5291
}
5392
}

Sources/web3swift/Promises/Promise+Web3+Eth+FeeHistory.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@ extension web3.Eth {
1515
// return feeHistory(address: addr, onBlock: onBlock)
1616
// }
1717

18-
public func feeHistory(blockCount: BigUInt, block: String, percentiles:[Double]) throws -> Oracle.FeeHistory {
18+
func feeHistory(blockCount: BigUInt, block: String, percentiles:[Double]) throws -> Web3.Oracle.FeeHistory {
1919
let request = JSONRPCRequestFabric.prepareRequest(.feeHistory, parameters: [blockCount.description.addHexPrefix(), block, percentiles])
2020
let rp = web3.dispatch(request)
2121
let queue = web3.requestDispatcher.queue
2222
return try rp.map(on: queue) { response in
23-
guard let value: Oracle.FeeHistory = response.getValue() else {
23+
guard let value: Web3.Oracle.FeeHistory = response.getValue() else {
2424
if response.error != nil {
2525
throw Web3Error.nodeError(desc: response.error!.message)
2626
}

Sources/web3swift/Web3/Web3+GasOracle.swift

Lines changed: 30 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import Foundation
1010
import BigInt
1111

12-
extension web3.Eth {
12+
extension Web3 {
1313
/// Oracle is the class to do a transaction fee suggestion
1414
final public class Oracle {
1515

@@ -33,10 +33,10 @@ extension web3.Eth {
3333
/// Oracle initializer
3434
/// - Parameters:
3535
/// - provider: Web3 Ethereum provider
36-
/// - block: Number of block from which counts starts
36+
/// - block: Number of block from which counts starts backward
3737
/// - blockCount: Count of block to calculate statistics
3838
/// - percentiles: Percentiles of fees which will split in fees history
39-
public init(_ provider: web3, block: String = "latest", blockCount: BigUInt = 20, percentiles: [Double] = [10, 50, 90]) {
39+
public init(_ provider: web3, block: String = "latest", blockCount: BigUInt = 20, percentiles: [Double] = [25, 50, 75]) {
4040
self.web3Provider = provider
4141
self.block = block
4242
self.blockCount = blockCount
@@ -72,7 +72,7 @@ extension web3.Eth {
7272
private func soft(twoDimentsion array: [[BigUInt]]) -> [BigUInt] {
7373
/// We've got `[[min],[middle],[max]]` 2 dimensional array
7474
/// we're getting `[min, middle, max].count == self.percentiles.count`,
75-
/// where each value are mean from the input percentile array
75+
/// where each value are mean from the input percentile arrays
7676
array.compactMap { percentileArray -> [BigUInt]? in
7777
guard !percentileArray.isEmpty else { return nil }
7878
// swiftlint:disable force_unwrapping
@@ -98,28 +98,37 @@ extension web3.Eth {
9898
}
9999

100100
/// Suggesting tip values
101-
/// - Returns: `[percentile 1, percentile 2, percentile 3].count == self.percentile.count`
101+
/// - Returns: `[percentile_1, percentile_2, percentile_3, ...].count == self.percentile.count`
102102
/// by default there's 3 percentile.
103103
private func suggestTipValue() throws -> [BigUInt] {
104+
var rearrengedArray: [[BigUInt]] = []
105+
104106
/// reaarange `[[min, middle, max]]` to `[[min], [middle], [max]]`
105-
let rearrengedArray = try suggestGasValues().reward
106-
.map { internalArray -> [BigUInt] in
107-
var newArray = [BigUInt]()
108-
internalArray.enumerated().forEach { newArray[$0] = $1 }
109-
return newArray
107+
try suggestGasValues().reward
108+
.forEach { percentiles in
109+
percentiles.enumerated().forEach { (index, percentile) in
110+
/// if `rearrengedArray` have not that enough items
111+
/// as `percentiles` current item index
112+
if rearrengedArray.endIndex <= index {
113+
/// append its as an array
114+
rearrengedArray.append([percentile])
115+
} else {
116+
/// append `percentile` value to appropriate `percentiles` array.
117+
rearrengedArray[index].append(percentile)
118+
}
119+
}
110120
}
111121
return soft(twoDimentsion: rearrengedArray)
112122
}
113123

114124
private func suggestBaseFee() throws -> [BigUInt] {
115-
calculatePercentiles(for: try suggestGasValues().baseFeePerGas)
125+
self.feeHistory = try suggestGasValues()
126+
return calculatePercentiles(for: feeHistory!.baseFeePerGas)
116127
}
117128

118129
private func suggestGasFeeLegacy(_ statistic: Statistic) throws -> BigUInt {
119130
let latestBlockNumber = try eth.getBlockNumber()
120131

121-
// Assigning last block to object var to predict baseFee of the next block
122-
// latestBlock = try eth.getBlockByNumber(latestBlockNumber)
123132
// TODO: Make me work with cache
124133
let lastNthBlockGasPrice = try (latestBlockNumber - blockCount ... latestBlockNumber)
125134
.map { try eth.getBlockByNumber($0, fullTransactions: true) }
@@ -136,17 +145,14 @@ extension web3.Eth {
136145
}
137146
}
138147

139-
public extension web3.Eth.Oracle {
148+
public extension Web3.Oracle {
140149
// MARK: - Base Fee
141-
/// Base fee amount based on last Nth blocks
150+
/// Softed baseFee amount
142151
///
143152
/// Normalized means that most high and most low value were droped from calculation.
144153
///
145-
/// Nth block may include empty ones.
146-
///
147-
/// - Parameter statistic: Statistic to apply for base fee calculation
148154
/// - Returns: Suggested base fee amount according to statistic, nil if failed to perdict
149-
func predictBaseFee() -> [BigUInt] {
155+
var baseFeePercentiles: [BigUInt] {
150156
guard let value = try? suggestBaseFee() else { return [] }
151157
return value
152158
}
@@ -160,7 +166,7 @@ public extension web3.Eth.Oracle {
160166
///
161167
/// - Parameter statistic: Statistic to apply for tip calculation
162168
/// - Returns: Suggested tip amount according to statistic, nil if failed to perdict
163-
func predictTip() -> [BigUInt] {
169+
var tipFeePercentiles: [BigUInt] {
164170
guard let value = try? suggestTipValue() else { return [] }
165171
return value
166172
}
@@ -171,7 +177,7 @@ public extension web3.Eth.Oracle {
171177
/// - baseFee: Statistic to apply for baseFee
172178
/// - tip: Statistic to apply for tip
173179
/// - Returns: Tuple where `baseFee` — base fee, `tip` — tip, nil if failed to predict
174-
func predictBothFees() -> (baseFee: [BigUInt], tip: [BigUInt])? {
180+
var bothFeesPercentiles: (baseFee: [BigUInt], tip: [BigUInt])? {
175181
guard let baseFee = try? suggestBaseFee() else { return nil }
176182
guard let tip = try? suggestTipValue() else { return nil }
177183

@@ -188,7 +194,7 @@ public extension web3.Eth.Oracle {
188194
// }
189195
}
190196

191-
public extension web3.Eth.Oracle {
197+
public extension Web3.Oracle {
192198
// TODO: Make me struct and encapsulate math within to make me extendable
193199
enum Statistic {
194200
/// Mininum statistic
@@ -202,7 +208,7 @@ public extension web3.Eth.Oracle {
202208
}
203209
}
204210

205-
public extension web3.Eth.Oracle {
211+
extension Web3.Oracle {
206212
struct FeeHistory {
207213
let timestamp = Date()
208214
let baseFeePerGas: [BigUInt]
@@ -212,7 +218,7 @@ public extension web3.Eth.Oracle {
212218
}
213219
}
214220

215-
extension web3.Eth.Oracle.FeeHistory: Decodable {
221+
extension Web3.Oracle.FeeHistory: Decodable {
216222
enum CodingKeys: String, CodingKey {
217223
case baseFeePerGas
218224
case gasUsedRatio
@@ -229,35 +235,3 @@ extension web3.Eth.Oracle.FeeHistory: Decodable {
229235
self.reward = try values.decodeHex([[BigUInt]].self, forKey: .reward)
230236
}
231237
}
232-
233-
extension Array where Element: Comparable {
234-
235-
/// Sorts array and drops most and least values.
236-
/// - Returns: Sorted array without most and least values, nil if `array.count` <= 2
237-
func cropAnomalyValues() -> Self? {
238-
var sortedArray = self.sorted()
239-
// Array should at least counts two to pass that formations.
240-
guard sortedArray.count > 1 else { return nil }
241-
sortedArray.removeLast()
242-
sortedArray.removeFirst()
243-
return sortedArray
244-
}
245-
}
246-
247-
extension Array where Element: BinaryInteger {
248-
func mean() -> BigUInt? {
249-
guard !self.isEmpty else { return nil }
250-
return BigUInt(self.reduce(0, +)) / BigUInt(self.count)
251-
}
252-
253-
func percentile(of value: Double) -> BigUInt? {
254-
guard !self.isEmpty else { return nil }
255-
256-
let sorted_data = self.sorted()
257-
// if self.count % 2 == 1 {
258-
return BigUInt(sorted_data[Int(floor(Double(self.count) / value / 10))])
259-
// } else {
260-
// return BigUInt(sorted_data[self.count / Int(value) / 10] + sorted_data[(self.count / Int(value) / 10) - 1] /( value) / 10)
261-
// }
262-
}
263-
}

Sources/web3swift/Web3/Web3+JSONRPC.swift

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -91,23 +91,26 @@ public struct JSONRPCresponse: Decodable{
9191
public var message: String
9292
}
9393

94-
internal var decodableTypes: [Decodable.Type] = [[EventLog].self,
95-
[TransactionDetails].self,
96-
[TransactionReceipt].self,
97-
[Block].self,
98-
[String].self,
99-
[Int].self,
100-
[Bool].self,
101-
EventLog.self,
102-
TransactionDetails.self,
103-
TransactionReceipt.self,
104-
Block.self,
105-
String.self,
106-
Int.self,
107-
Bool.self,
108-
[String: String].self,
109-
[String: Int].self,
110-
[String: [String: [String: [String]]]].self]
94+
internal var decodableTypes: [Decodable.Type] = [
95+
[EventLog].self,
96+
[TransactionDetails].self,
97+
[TransactionReceipt].self,
98+
[Block].self,
99+
[String].self,
100+
[Int].self,
101+
[Bool].self,
102+
EventLog.self,
103+
TransactionDetails.self,
104+
TransactionReceipt.self,
105+
Block.self,
106+
String.self,
107+
Int.self,
108+
Bool.self,
109+
[String: String].self,
110+
[String: Int].self,
111+
[String: [String: [String: [String]]]].self,
112+
Web3.Oracle.FeeHistory.self
113+
]
111114

112115
// FIXME: Make me a real generic
113116
public init(from decoder: Decoder) throws {
@@ -160,6 +163,8 @@ public struct JSONRPCresponse: Decodable{
160163
result = rawValue
161164
} else if let rawValue = try? container.decodeIfPresent([String: [String: [String: [String: String?]]]].self, forKey: .result) {
162165
result = rawValue
166+
} else if let rawValue = try? container.decodeIfPresent(Web3.Oracle.FeeHistory.self, forKey: .result) {
167+
result = rawValue
163168
}
164169
self.init(id: id, jsonrpc: jsonrpc, result: result, error: nil)
165170
}
@@ -261,6 +266,7 @@ public struct EventFilterParameters: Codable {
261266

262267
/// Raw JSON RCP 2.0 internal flattening wrapper.
263268
public struct JSONRPCparams: Encodable{
269+
// TODO: Rewrite me to generic
264270
public var params = [Any]()
265271

266272
public func encode(to encoder: Encoder) throws {
@@ -274,6 +280,8 @@ public struct JSONRPCparams: Encodable{
274280
try container.encode(p)
275281
} else if let p = par as? EventFilterParameters {
276282
try container.encode(p)
283+
} else if let p = par as? [Double] {
284+
try container.encode(p)
277285
}
278286
}
279287
}

0 commit comments

Comments
 (0)