Skip to content

Commit 92f288a

Browse files
Add comments to GasOracle & common methods
- Add `BlockNumber` enum to enable strong typing in block number management. - Full test coverage of GasOracle public methods. - Clean GasOracle class.
1 parent 93f66ca commit 92f288a

File tree

3 files changed

+125
-86
lines changed

3 files changed

+125
-86
lines changed

Sources/web3swift/Convenience/Array+Extension.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,17 @@ extension Array where Element: Comparable {
3232

3333
extension Array where Element: BinaryInteger {
3434
// TODO: Make me generic
35-
// FIXME: Add doc comment
35+
/// Calculates mean value of a dataset
36+
/// - Returns: Mean value of a dataset, nil if dataset is empty
3637
func mean() -> BigUInt? {
3738
guard !self.isEmpty else { return nil }
3839
return BigUInt(self.reduce(0, +)) / BigUInt(self.count)
3940
}
4041

41-
// FIXME: Add doc comment
42+
43+
/// Calculates percentile of dataset on which get called.
44+
/// - Parameter value: Percentile value.
45+
/// - Returns: Item from dataset that is belongs to given percentile, nil if dataset is empty.
4246
func percentile(of value: Double) -> Element? {
4347
guard !self.isEmpty else { return nil }
4448

Sources/web3swift/Web3/Web3+GasOracle.swift

Lines changed: 86 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -21,58 +21,50 @@ extension Web3 {
2121
/// Ethereum scope shortcut
2222
private var eth: web3.Eth { web3Provider.eth }
2323

24-
/// Block to start getting history
25-
var block: String
24+
/// Block to start getting history backward
25+
var block: BlockNumber
2626

27-
/// Count of blocks to calculate statistics
27+
/// Count of blocks to include in dataset
2828
var blockCount: BigUInt
2929

30-
/// Count of transactions to filter block for tip calculation
30+
/// Percentiles
31+
///
32+
/// This property set values by which dataset would be sliced.
33+
///
34+
/// If you set it to `[25.0, 50.0, 75.0]` on any prediction property read you'll get
35+
/// `[71456911562, 92735433497, 105739785122]` which means that first item in array is more
36+
/// than 25% of the whole dataset, second one more than 50% of the dataset and third one
37+
/// more than 75% of the dataset.
38+
///
39+
/// Another example: If you set it [100.0] you'll get the very highest value of a dataset e.g. max Tip amount.
3140
var percentiles: [Double]
3241

42+
// TODO: Disabled until 3.0 version, coz will be enabled from 3.0.0.
43+
// var forceDropCache = false
44+
3345
/// Oracle initializer
3446
/// - Parameters:
3547
/// - provider: Web3 Ethereum provider
3648
/// - block: Number of block from which counts starts backward
3749
/// - blockCount: Count of block to calculate statistics
38-
/// - percentiles: Percentiles of fees which will split in fees history
39-
public init(_ provider: web3, block: String = "latest", blockCount: BigUInt = 20, percentiles: [Double] = [25, 50, 75]) {
50+
/// - percentiles: Percentiles of fees to which result of predictions will be split in
51+
public init(_ provider: web3, block: BlockNumber = .latest, blockCount: BigUInt = 20, percentiles: [Double] = [25, 50, 75]) {
4052
self.web3Provider = provider
4153
self.block = block
4254
self.blockCount = blockCount
4355
self.percentiles = percentiles
4456
}
4557

46-
// private func calcBaseFee(for block: Block?) -> BigUInt {
47-
// guard let block = block else { return 0 }
48-
// return Web3.calcBaseFee(block) ?? 0
49-
// }
50-
51-
private func calculateStatistic(for statistic: Statistic, data: [BigUInt]) throws -> BigUInt {
52-
let noAnomalyArray = data.cropAnomalyValues()
53-
54-
// FIXME: Set appropriate error thrown.
55-
guard let unwrappedArray = noAnomalyArray, !unwrappedArray.isEmpty else { throw Web3Error.unknownError }
56-
57-
switch statistic {
58-
// Force unwrapping is ok, since array checked for epmtiness above
59-
// swiftlint:disable force_unwrapping
60-
case .minimum: return unwrappedArray.min()!
61-
case .mean: return unwrappedArray.mean()!
62-
case .median: return unwrappedArray.mean()!
63-
case .maximum:
64-
// Checking that suggestedBaseFee is not lower than it will be in the next block
65-
// because in the maximum statistic we should guarantee that transaction would be included in it.
66-
// return max(calcBaseFee(for: latestBlock), unwrappedArray.max()!)
67-
return unwrappedArray.max()!
68-
}
69-
// swiftlint:enable force_unwrapping
70-
}
7158

59+
/// Returning one dimensional array from two dimensional array
60+
///
61+
/// We've got `[[min],[middle],[max]]` 2 dimensional array
62+
/// we're getting `[min, middle, max].count == self.percentiles.count`,
63+
/// where each value are mean from the input percentile arrays
64+
///
65+
/// - Parameter array: `[[min], [middle], [max]]` 2 dimensional array
66+
/// - Returns: `[min, middle, max].count == self.percentiles.count`
7267
private func soft(twoDimentsion array: [[BigUInt]]) -> [BigUInt] {
73-
/// We've got `[[min],[middle],[max]]` 2 dimensional array
74-
/// we're getting `[min, middle, max].count == self.percentiles.count`,
75-
/// where each value are mean from the input percentile arrays
7668
array.compactMap { percentileArray -> [BigUInt]? in
7769
guard !percentileArray.isEmpty else { return nil }
7870
// swiftlint:disable force_unwrapping
@@ -82,6 +74,9 @@ extension Web3 {
8274
.flatMap { $0 }
8375
}
8476

77+
/// Method calculates percentiles array based on `self.percetniles` value
78+
/// - Parameter data: Integer data from which percentiles should be calculated
79+
/// - Returns: Array of values which is in positions in dataset to given percentiles
8580
private func calculatePercentiles(for data: [BigUInt]) -> [BigUInt] {
8681
percentiles.compactMap { percentile in
8782
data.percentile(of: percentile)
@@ -92,9 +87,9 @@ extension Web3 {
9287
// This is some kind of cache.
9388
// It stores about 9 seconds, than it rewrites it with newer data.
9489
// TODO: Disabled until 3.0 version, coz `distance` available from iOS 13.
95-
// guard feeHistory != nil, feeHistory!.timestamp.distance(to: Date()) < cacheTimeout else { return feeHistory! }
90+
// guard feeHistory == nil, forceDropCache, feeHistory!.timestamp.distance(to: Date()) > cacheTimeout else { return feeHistory! }
9691

97-
return try eth.feeHistory(blockCount: blockCount, block: block, percentiles: percentiles)
92+
return try eth.feeHistory(blockCount: blockCount, block: block.hexValue, percentiles: percentiles)
9893
}
9994

10095
/// Suggesting tip values
@@ -126,8 +121,14 @@ extension Web3 {
126121
return calculatePercentiles(for: feeHistory!.baseFeePerGas)
127122
}
128123

129-
private func suggestGasFeeLegacy(_ statistic: Statistic) throws -> BigUInt {
130-
let latestBlockNumber = try eth.getBlockNumber()
124+
private func suggestGasFeeLegacy() throws -> [BigUInt] {
125+
var latestBlockNumber: BigUInt = 0
126+
switch block {
127+
case .latest: latestBlockNumber = try eth.getBlockNumber()
128+
case let .exact(number): latestBlockNumber = number
129+
}
130+
131+
guard latestBlockNumber != 0 else { return [] }
131132

132133
// TODO: Make me work with cache
133134
let lastNthBlockGasPrice = try (latestBlockNumber - blockCount ... latestBlockNumber)
@@ -140,7 +141,7 @@ extension Web3 {
140141
}
141142
.map { $0.gasPrice }
142143

143-
return try calculateStatistic(for: statistic, data: lastNthBlockGasPrice)
144+
return calculatePercentiles(for: lastNthBlockGasPrice)
144145
}
145146
}
146147
}
@@ -149,62 +150,48 @@ public extension Web3.Oracle {
149150
// MARK: - Base Fee
150151
/// Softed baseFee amount
151152
///
152-
/// Normalized means that most high and most low value were droped from calculation.
153-
///
154-
/// - Returns: Suggested base fee amount according to statistic, nil if failed to perdict
153+
/// - Returns: `[percentile_1, percentile_2, percentile_3, ...].count == self.percentile.count`
154+
/// empty array if failed to perdict. By default there's 3 percentile.
155155
var baseFeePercentiles: [BigUInt] {
156156
guard let value = try? suggestBaseFee() else { return [] }
157157
return value
158158
}
159159

160160
// MARK: - Tip
161-
/// Maximum tip amount based on last block tips
161+
/// Tip amount
162162
///
163-
/// Normalized means that most high and most low value were droped from calculation.
164-
///
165-
/// Method calculates the suggested tip based on the most recent block that contains more than transactionsCount transactions
166-
///
167-
/// - Parameter statistic: Statistic to apply for tip calculation
168-
/// - Returns: Suggested tip amount according to statistic, nil if failed to perdict
163+
/// - Returns: `[percentile_1, percentile_2, percentile_3, ...].count == self.percentile.count`
164+
/// empty array if failed to perdict. By default there's 3 percentile.
169165
var tipFeePercentiles: [BigUInt] {
170166
guard let value = try? suggestTipValue() else { return [] }
171167
return value
172168
}
173169

174170
// MARK: - Summary fees
175-
/// Method to get summary fees
176-
/// - Parameters:
177-
/// - baseFee: Statistic to apply for baseFee
178-
/// - tip: Statistic to apply for tip
179-
/// - Returns: Tuple where `baseFee` — base fee, `tip` — tip, nil if failed to predict
171+
/// Summary fees amount
172+
///
173+
/// - Returns: `[percentile_1, percentile_2, percentile_3, ...].count == self.percentile.count`
174+
/// nil if failed to perdict. By default there's 3 percentile.
180175
var bothFeesPercentiles: (baseFee: [BigUInt], tip: [BigUInt])? {
181-
guard let baseFee = try? suggestBaseFee() else { return nil }
182-
guard let tip = try? suggestTipValue() else { return nil }
183-
184-
return (baseFee: baseFee, tip: tip)
176+
var baseFeeArr: [BigUInt] = []
177+
var tipArr: [BigUInt] = []
178+
if let baseFee = try? suggestBaseFee() {
179+
baseFeeArr = baseFee
180+
}
181+
if let tip = try? suggestTipValue() {
182+
tipArr = tip
183+
}
184+
return (baseFee: baseFeeArr, tip: tipArr)
185185
}
186186

187187
// MARK: - Legacy GasPrice
188-
/// Method to get legacy gas price
189-
/// - Parameter statistic: Statistic to apply for gas price
190-
/// - Returns: Suggested gas price amount according to statistic, nil if failed to predict
191-
// func predictGasPriceLegacy() -> BigUInt? {
192-
// guard let value = try? suggestGasFeeLegacy() else { return nil}
193-
// return value
194-
// }
195-
}
196-
197-
public extension Web3.Oracle {
198-
// TODO: Make me struct and encapsulate math within to make me extendable
199-
enum Statistic {
200-
/// Mininum statistic
201-
case minimum
202-
/// Mean statistic
203-
case mean
204-
/// Median statistic
205-
case median
206-
/// Maximum statistic
207-
case maximum
188+
/// Legacy gasPrice amount
189+
///
190+
/// - Returns: `[percentile_1, percentile_2, percentile_3, ...].count == self.percentile.count`
191+
/// empty array if failed to perdict. By default there's 3 percentile.
192+
var gasPriceLegacyPercentiles: [BigUInt] {
193+
guard let value = try? suggestGasFeeLegacy() else { return [] }
194+
return value
208195
}
209196
}
210197

@@ -235,3 +222,24 @@ extension Web3.Oracle.FeeHistory: Decodable {
235222
self.reward = try values.decodeHex([[BigUInt]].self, forKey: .reward)
236223
}
237224
}
225+
226+
227+
public extension Web3 {
228+
/// Enum for convenient type safe work with block number
229+
enum BlockNumber {
230+
/// Latest block of a chain
231+
case latest
232+
/// Exact block number
233+
case exact(BigUInt)
234+
235+
/// Block number as a string
236+
///
237+
/// Could be `hexString` either `latest`
238+
internal var hexValue: String {
239+
switch self {
240+
case .latest: return "latest"
241+
case let .exact(number): return String(number, radix: 16).addHexPrefix()
242+
}
243+
}
244+
}
245+
}

Tests/web3swiftTests/remoteTests/GasOracleTests.swift

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ class OracleTests: XCTestCase {
1515

1616
let web3 = Web3.InfuraMainnetWeb3(accessToken: Constants.infuraToken)
1717

18-
lazy var oracle: Web3.Oracle = .init(web3, block: "0xde5910", blockCount: 20, percentiles: [10, 40, 60, 90])
18+
lazy var oracle: Web3.Oracle = .init(web3, block: .exact(14571792), blockCount: 20, percentiles: [10, 40, 60, 90])
1919

2020
func testPretictBaseFee() throws {
2121
let etalonPercentiles: [BigUInt] = [
@@ -26,7 +26,6 @@ class OracleTests: XCTestCase {
2626
]
2727

2828
let baseFeePercentiles = oracle.baseFeePercentiles
29-
print("baseFeePercentiles: \(baseFeePercentiles)")
3029
XCTAssertEqual(baseFeePercentiles, etalonPercentiles, "Arrays should be equal")
3130
}
3231

@@ -38,12 +37,40 @@ class OracleTests: XCTestCase {
3837
11394017894 // 90 percentile
3938
]
4039

41-
let predictTip = oracle.tipFeePercentiles
42-
print("predictTip: \(predictTip)")
43-
XCTAssertEqual(predictTip, etalonPercentiles, "Arrays should be equal")
40+
let tipFeePercentiles = oracle.tipFeePercentiles
41+
XCTAssertEqual(tipFeePercentiles, etalonPercentiles, "Arrays should be equal")
4442
}
4543

46-
func testPredictGasPrice() throws {
44+
func testPredictBothFee() throws {
45+
let etalonPercentiles: ([BigUInt], [BigUInt]) = (
46+
baseFee: [
47+
71456911562, // 10 percentile
48+
92735433497, // 40 percentile
49+
105739785122, // 60 percentile
50+
118929912191 // 90 percentile
51+
],
52+
tip: [
53+
1251559157, // 10 percentile
54+
1594062500, // 40 percentile
55+
2268157275, // 60 percentile
56+
11394017894 // 90 percentile
57+
]
58+
)
59+
60+
let bothFeesPercentiles = oracle.bothFeesPercentiles
61+
XCTAssertEqual(bothFeesPercentiles?.baseFee, etalonPercentiles.0, "Arrays should be equal")
62+
XCTAssertEqual(bothFeesPercentiles?.tip, etalonPercentiles.1, "Arrays should be equal")
63+
}
64+
65+
func testPredictLegacyGasPrice() throws {
66+
let etalonPercentiles: [BigUInt] = [
67+
100474449775, // 10 percentile
68+
114000000000, // 40 percentile
69+
125178275323, // 60 percentile
70+
146197706224 // 90 percentile
71+
]
4772

73+
let gasPriceLegacyPercentiles = oracle.gasPriceLegacyPercentiles
74+
XCTAssertEqual(gasPriceLegacyPercentiles, etalonPercentiles, "Arrays should be equal")
4875
}
4976
}

0 commit comments

Comments
 (0)