Skip to content

Commit d040e49

Browse files
Move GasOracle to Core,
to make transaction resolve its gasWhatever.
1 parent 7b5284c commit d040e49

File tree

8 files changed

+321
-339
lines changed

8 files changed

+321
-339
lines changed

Sources/Core/Oracle/GasOracle.swift

Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
//
2+
// GasOracle.swift
3+
//
4+
// Created by Yaroslav on 31.03.2022.
5+
// Copyright © 2022 web3swift. All rights reserved.
6+
//
7+
8+
import Foundation
9+
import BigInt
10+
11+
/// Oracle is the class to do a transaction fee suggestion
12+
final public class Oracle {
13+
14+
/// Web3 provider by which accessing to the blockchain
15+
private let web3Provider: Web3Provider
16+
17+
private var feeHistory: FeeHistory?
18+
19+
/// Block to start getting history backward
20+
var block: BlockNumber
21+
22+
/// Count of blocks to include in dataset
23+
var blockCount: BigUInt
24+
25+
/// Percentiles
26+
///
27+
/// This property set values by which dataset would be sliced.
28+
///
29+
/// If you set it to `[25.0, 50.0, 75.0]` on any prediction property read you'll get
30+
/// `[71456911562, 92735433497, 105739785122]` which means that first item in array is more
31+
/// than 25% of the whole dataset, second one more than 50% of the dataset and third one
32+
/// more than 75% of the dataset.
33+
///
34+
/// Another example: If you set it [100.0] you'll get the very highest value of a dataset e.g. max Tip amount.
35+
var percentiles: [Double]
36+
37+
var forceDropCache = false
38+
39+
var cacheTimeout: Double
40+
41+
/// Oracle initializer
42+
/// - Parameters:
43+
/// - provider: Web3 Ethereum provider
44+
/// - block: Number of block from which counts starts backward
45+
/// - blockCount: Count of block to calculate statistics
46+
/// - percentiles: Percentiles of fees to which result of predictions will be split in
47+
public init(_ provider: Web3Provider, block: BlockNumber = .latest, blockCount: BigUInt = 20, percentiles: [Double] = [25, 50, 75], cacheTimeout: Double = 10) {
48+
self.web3Provider = provider
49+
self.block = block
50+
self.blockCount = blockCount
51+
self.percentiles = percentiles
52+
self.cacheTimeout = cacheTimeout
53+
}
54+
55+
56+
/// Returning one dimensional array from two dimensional array
57+
///
58+
/// We've got `[[min],[middle],[max]]` 2 dimensional array
59+
/// we're getting `[min, middle, max].count == self.percentiles.count`,
60+
/// where each value are mean from the input percentile arrays
61+
///
62+
/// - Parameter array: `[[min], [middle], [max]]` 2 dimensional array
63+
/// - Returns: `[min, middle, max].count == self.percentiles.count`
64+
private func soft(twoDimentsion array: [[BigUInt]]) -> [BigUInt] {
65+
array.compactMap { percentileArray -> [BigUInt]? in
66+
guard !percentileArray.isEmpty else { return nil }
67+
// swiftlint:disable force_unwrapping
68+
return [percentileArray.mean()!]
69+
// swiftlint:enable force_unwrapping
70+
}
71+
.flatMap { $0 }
72+
}
73+
74+
/// Method calculates percentiles array based on `self.percetniles` value
75+
/// - Parameter data: Integer data from which percentiles should be calculated
76+
/// - Returns: Array of values which is in positions in dataset to given percentiles
77+
private func calculatePercentiles(for data: [BigUInt]) -> [BigUInt] {
78+
percentiles.compactMap { percentile in
79+
data.percentile(of: percentile)
80+
}
81+
}
82+
83+
private func suggestGasValues() async throws -> FeeHistory {
84+
/// This is some kind of cache.
85+
/// It stores about 10 seconds, than it rewrites it with newer data.
86+
87+
/// We're explicitly checking that feeHistory is not nil before force unwrapping it.
88+
guard let feeHistory = feeHistory, !forceDropCache, feeHistory.timestamp.distance(to: Date()) < cacheTimeout else {
89+
// swiftlint: disable force_unwrapping
90+
let result: FeeHistory = try await combineRequest(request: .feeHistory(blockCount, block, percentiles))
91+
feeHistory = result
92+
return feeHistory!
93+
// swiftlint: enable force_unwrapping
94+
}
95+
96+
return feeHistory
97+
}
98+
99+
/// Suggesting tip values
100+
/// - Returns: `[percentile_1, percentile_2, percentile_3, ...].count == self.percentile.count`
101+
/// by default there's 3 percentile.
102+
private func suggestTipValue() async throws -> [BigUInt] {
103+
var rearrengedArray: [[BigUInt]] = []
104+
105+
/// reaarange `[[min, middle, max]]` to `[[min], [middle], [max]]`
106+
try await suggestGasValues().reward
107+
.forEach { percentiles in
108+
percentiles.enumerated().forEach { (index, percentile) in
109+
/// if `rearrengedArray` have not that enough items
110+
/// as `percentiles` current item index
111+
if rearrengedArray.endIndex <= index {
112+
/// append its as an array
113+
rearrengedArray.append([percentile])
114+
} else {
115+
/// append `percentile` value to appropriate `percentiles` array.
116+
rearrengedArray[index].append(percentile)
117+
}
118+
}
119+
}
120+
return soft(twoDimentsion: rearrengedArray)
121+
}
122+
123+
private func suggestBaseFee() async throws -> [BigUInt] {
124+
self.feeHistory = try await suggestGasValues()
125+
return calculatePercentiles(for: feeHistory!.baseFeePerGas)
126+
}
127+
128+
private func combineRequest<Result>(request: APIRequest) async throws-> Result where Result: APIResultType {
129+
let response: APIResponse<Result> = try await APIRequest.sendRequest(with: self.web3Provider, for: request)
130+
return response.result
131+
}
132+
133+
private func suggestGasFeeLegacy() async throws -> [BigUInt] {
134+
var latestBlockNumber: BigUInt = 0
135+
switch block {
136+
case .latest:
137+
let block: BigUInt = try await combineRequest(request: .blockNumber)
138+
latestBlockNumber = block
139+
case let .exact(number): latestBlockNumber = number
140+
// Error throws since pending and erliest are unable to be used in this method.
141+
default: throw Web3Error.valueError
142+
}
143+
144+
/// checking if latest block number is greather than number of blocks to take in account
145+
/// we're ignoring case when `latestBlockNumber` == `blockCount` since it's unlikely case
146+
/// which we could neglect
147+
guard latestBlockNumber > blockCount else { return [] }
148+
149+
// TODO: Make me work with cache
150+
let blocks = try await withThrowingTaskGroup(of: Block.self, returning: [Block].self) { group in
151+
(latestBlockNumber - blockCount ... latestBlockNumber)
152+
.forEach { block in
153+
group.addTask {
154+
let result: Block = try await self.combineRequest(request: .getBlockByNumber(.exact(block), true))
155+
return result
156+
}
157+
}
158+
159+
var collected = [Block]()
160+
161+
for try await value in group {
162+
collected.append(value)
163+
}
164+
165+
return collected
166+
}
167+
168+
let lastNthBlockGasPrice = blocks.flatMap { b -> [CodableTransaction] in
169+
b.transactions.compactMap { t -> CodableTransaction? in
170+
guard case let .transaction(transaction) = t else { return nil }
171+
return transaction
172+
}
173+
}
174+
.compactMap { $0.meta?.gasPrice ?? 0 }
175+
176+
return calculatePercentiles(for: lastNthBlockGasPrice)
177+
}
178+
}
179+
180+
181+
public extension Oracle {
182+
// MARK: - Base Fee
183+
/// Soften baseFee amount
184+
///
185+
/// - Returns: `[percentile_1, percentile_2, percentile_3, ...].count == self.percentile.count`
186+
/// empty array if failed to predict. By default there's 3 percentile.
187+
func baseFeePercentiles() async -> [BigUInt] {
188+
guard let value = try? await suggestBaseFee() else { return [] }
189+
return value
190+
}
191+
192+
// MARK: - Tip
193+
/// Tip amount
194+
///
195+
/// - Returns: `[percentile_1, percentile_2, percentile_3, ...].count == self.percentile.count`
196+
/// empty array if failed to predict. By default there's 3 percentile.
197+
func tipFeePercentiles() async -> [BigUInt] {
198+
guard let value = try? await suggestTipValue() else { return [] }
199+
return value
200+
}
201+
202+
// MARK: - Summary fees
203+
/// Summary fees amount
204+
///
205+
/// - Returns: `[percentile_1, percentile_2, percentile_3, ...].count == self.percentile.count`
206+
/// nil if failed to predict. By default there's 3 percentile.
207+
func bothFeesPercentiles() async -> (baseFee: [BigUInt], tip: [BigUInt])? {
208+
var baseFeeArr: [BigUInt] = []
209+
var tipArr: [BigUInt] = []
210+
if let baseFee = try? await suggestBaseFee() {
211+
baseFeeArr = baseFee
212+
}
213+
if let tip = try? await suggestTipValue() {
214+
tipArr = tip
215+
}
216+
return (baseFee: baseFeeArr, tip: tipArr)
217+
}
218+
219+
// MARK: - Legacy GasPrice
220+
/// Legacy gasPrice amount
221+
///
222+
/// - Returns: `[percentile_1, percentile_2, percentile_3, ...].count == self.percentile.count`
223+
/// empty array if failed to predict. By default there's 3 percentile.
224+
func gasPriceLegacyPercentiles() async -> [BigUInt] {
225+
guard let value = try? await suggestGasFeeLegacy() else { return [] }
226+
return value
227+
}
228+
}
229+
230+
extension Oracle {
231+
public struct FeeHistory {
232+
let timestamp = Date()
233+
let baseFeePerGas: [BigUInt]
234+
let gasUsedRatio: [Double]
235+
let oldestBlock: BigUInt
236+
let reward: [[BigUInt]]
237+
}
238+
}
239+
240+
extension Oracle.FeeHistory: Decodable {
241+
enum CodingKeys: String, CodingKey {
242+
case baseFeePerGas
243+
case gasUsedRatio
244+
case oldestBlock
245+
case reward
246+
}
247+
248+
public init(from decoder: Decoder) throws {
249+
let values = try decoder.container(keyedBy: CodingKeys.self)
250+
251+
self.baseFeePerGas = try values.decodeHex([BigUInt].self, forKey: .baseFeePerGas)
252+
self.gasUsedRatio = try values.decode([Double].self, forKey: .gasUsedRatio)
253+
self.oldestBlock = try values.decodeHex(BigUInt.self, forKey: .oldestBlock)
254+
self.reward = try values.decodeHex([[BigUInt]].self, forKey: .reward)
255+
}
256+
}
257+
258+
extension Oracle.FeeHistory: APIResultType { }

Sources/Core/Transaction/CodableTransaction.swift

Lines changed: 30 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -183,19 +183,16 @@ public struct CodableTransaction {
183183
return self.envelope.encode(for: type)
184184
}
185185

186-
// public mutating func resolve() {
187-
// // type cannot be changed here, and is ignored
188-
// // FIXME: Add appropiate values of resolveAny
189-
// self.nonce = options.resolveNonce(nonce)
190-
// self.gasPrice = options.resolveGasPrice(gasPrice ?? 0)
191-
// self.gasLimit = options.resolveGasLimit(gasLimit)
192-
// self.maxFeePerGas = options.resolveMaxFeePerGas(maxFeePerGas ?? 0)
193-
// self.maxPriorityFeePerGas = options.resolveMaxPriorityFeePerGas(maxPriorityFeePerGas ?? 0)
194-
// self.value = options.value ?? value
195-
// self.from = options.from
196-
// self.to = options.to ?? to
197-
// self.accessList = options.accessList
198-
// }
186+
public mutating func resolve(provider: Web3Provider) async {
187+
// type cannot be changed here, and is ignored
188+
// FIXME: This is resolves nothing yet.
189+
// FIXME: Delete force try
190+
self.nonce = try! await self.resolveNonce(provider: provider)
191+
self.gasPrice = try! await self.resolveGasPrice(provider: provider)
192+
self.gasLimit = try! await self.resolveGasLimit(provider: provider)
193+
self.maxFeePerGas = try! await self.resolveMaxFeePerGas(provider: provider)
194+
self.maxPriorityFeePerGas = try! await self.resolveMaxPriorityFeePerGas(provider: provider)
195+
}
199196

200197
public var noncePolicy: NoncePolicy
201198
public var maxFeePerGasPolicy: FeePerGasPolicy
@@ -304,52 +301,60 @@ extension CodableTransaction {
304301
case manual(BigUInt)
305302
}
306303

307-
public func resolveNonce(_ suggestedByNode: BigUInt) -> BigUInt {
304+
public func resolveNonce(provider: Web3Provider) async throws -> BigUInt {
308305
switch noncePolicy {
309306
case .pending, .latest, .earliest:
310-
return suggestedByNode
307+
guard let address = from ?? sender else { throw Web3Error.valueError }
308+
let request: APIRequest = .getTransactionCount(address.address , callOnBlock ?? .latest)
309+
let response: APIResponse<BigUInt> = try await APIRequest.sendRequest(with: provider, for: request)
310+
return response.result
311311
case .exact(let value):
312312
return value
313313
}
314314
}
315315

316-
public func resolveGasPrice(_ suggestedByNode: BigUInt) -> BigUInt {
316+
public func resolveGasPrice(provider: Web3Provider) async throws -> BigUInt {
317+
let oracle = Oracle(provider)
317318
switch gasPricePolicy {
318319
case .automatic, .withMargin:
319-
return suggestedByNode
320+
return await oracle.gasPriceLegacyPercentiles().max()!
320321
case .manual(let value):
321322
return value
322323
}
323324
}
324325

325-
public func resolveGasLimit(_ suggestedByNode: BigUInt) -> BigUInt {
326+
public func resolveGasLimit(provider: Web3Provider) async throws -> BigUInt {
327+
let request: APIRequest = .estimateGas(self, self.callOnBlock ?? .latest)
328+
let response: APIResponse<BigUInt> = try await APIRequest.sendRequest(with: provider, for: request)
326329
switch gasLimitPolicy {
327330
case .automatic, .withMargin:
328-
return suggestedByNode
331+
return response.result
329332
case .manual(let value):
330333
return value
331334
case .limited(let limit):
332-
if limit <= suggestedByNode {
333-
return suggestedByNode
335+
if limit <= response.result {
336+
return response.result
334337
} else {
335338
return limit
336339
}
337340
}
338341
}
339342

340-
public func resolveMaxFeePerGas(_ suggestedByNode: BigUInt) -> BigUInt {
343+
public func resolveMaxFeePerGas(provider: Web3Provider) async throws -> BigUInt {
344+
let oracle = Oracle(provider)
341345
switch maxFeePerGasPolicy {
342346
case .automatic:
343-
return suggestedByNode
347+
return await oracle.baseFeePercentiles().max()!
344348
case .manual(let value):
345349
return value
346350
}
347351
}
348352

349-
public func resolveMaxPriorityFeePerGas(_ suggestedByNode: BigUInt) -> BigUInt {
353+
public func resolveMaxPriorityFeePerGas(provider: Web3Provider) async throws -> BigUInt {
354+
let oracle = Oracle(provider)
350355
switch maxPriorityFeePerGasPolicy {
351356
case .automatic:
352-
return suggestedByNode
357+
return await oracle.tipFeePercentiles().max()!
353358
case .manual(let value):
354359
return value
355360
}
@@ -406,12 +411,3 @@ extension CodableTransaction {
406411
}
407412

408413
extension CodableTransaction: APIRequestParameterType { }
409-
410-
private func mergeIfNotNil<T>(first: T?, second: T?) -> T? {
411-
if second != nil {
412-
return second
413-
} else if first != nil {
414-
return first
415-
}
416-
return nil
417-
}

0 commit comments

Comments
 (0)