Skip to content

Commit 197b5d7

Browse files
Revert "Revert "Implement Oracle class""
This reverts commit f48066b.
1 parent ad5d8dd commit 197b5d7

File tree

2 files changed

+190
-0
lines changed

2 files changed

+190
-0
lines changed
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
//
2+
// Web3+GasOracle.swift
3+
// web3swift
4+
//
5+
// Created by Yaroslav on 31.03.2022.
6+
// Copyright © 2022 web3swift. All rights reserved.
7+
//
8+
9+
import Foundation
10+
import BigInt
11+
12+
extension Web3 {
13+
/// Oracle is the class to of a transaction fee suggestion
14+
///
15+
/// It designed for EIP-1559 transactions only.
16+
final public class Oracle {
17+
private var latestBlock: Block?
18+
19+
/// Web3 provider by wich accessing to the blockchain
20+
private let web3Provider: web3
21+
22+
/// Ethereum scope shortcut
23+
private var eth: web3.Eth { web3Provider.eth }
24+
25+
/// Number of block to caltulate statistics
26+
public private(set) var blocksNumber: BigUInt
27+
28+
/// Number of transacrtions to filter block for tip calculation
29+
public private(set) var transactionsNumber: BigUInt
30+
31+
/// Oracle initializer
32+
/// - Parameters:
33+
/// - provider: Web3 Ethereum provider
34+
/// - blocksNumber: Number of block to caltulate statistics
35+
/// - transactionsNumber: Number of transacrtions to filter block for tip calculation
36+
public init(_ provider: web3, blocksNumber: BigUInt = 20, transactionsNumber: BigUInt = 50) {
37+
self.web3Provider = provider
38+
self.blocksNumber = blocksNumber
39+
self.transactionsNumber = transactionsNumber
40+
}
41+
42+
private func calcBaseFee(for block: Block?) -> BigUInt {
43+
guard let block = block else { return 0 }
44+
return Web3.calcBaseFee(block)
45+
}
46+
47+
private func calculateStatistic(_ data: [BigUInt], _ statistic: Statistic) throws -> BigUInt {
48+
let sortedData = data.sorted()
49+
let noAnomalyArray = sortedData.cropAnomalyValues()
50+
51+
guard !noAnomalyArray.isEmpty else { throw Web3Error.unknownError }
52+
53+
switch statistic {
54+
// Force unwrapping is ok, since array checked for epmtiness above
55+
case .minimum: return noAnomalyArray.min()!
56+
case .mean: return noAnomalyArray.mean()!
57+
case .median: return noAnomalyArray.median()!
58+
case .maximum:
59+
// Checking that suggestedBaseFee are not lower than will be in the next block
60+
// because in tne maximum statistic we should guarantee that transaction would be included in it.
61+
return max(calcBaseFee(for: latestBlock), noAnomalyArray.max()!)
62+
}
63+
}
64+
65+
private func suggestTipValue(_ statistic: Statistic) throws -> BigUInt {
66+
let latestBlockNumber = try eth.getBlockNumber()
67+
68+
var block: Block
69+
70+
// TODO: Make me work with cache
71+
repeat {
72+
block = try eth.getBlockByNumber(latestBlockNumber, fullTransactions: true)
73+
} while block.transactions.count < transactionsNumber
74+
75+
// Storing last block to calculate baseFee of the next block
76+
latestBlock = block
77+
78+
let transactionsTips = block.transactions
79+
.compactMap { t -> EthereumTransaction? in
80+
guard case let .transaction(transaction) = t else { return nil }
81+
return transaction
82+
}
83+
// TODO: Add filter for transaction types
84+
.map { $0.maxPriorityFeePerGas }
85+
86+
return try calculateStatistic(transactionsTips, statistic)
87+
}
88+
89+
private func suggestBaseFee(_ statistic: Statistic) throws -> BigUInt {
90+
let latestBlockNumber = try eth.getBlockNumber()
91+
92+
// Assigning last block to object var to predict baseFee of the next block
93+
latestBlock = try eth.getBlockByNumber(latestBlockNumber)
94+
// TODO: Make me work with cache
95+
let lastNthBlocksBaseFees = try (latestBlockNumber - blocksNumber ... latestBlockNumber)
96+
.map { try eth.getBlockByNumber($0) }
97+
.filter { !$0.transactions.isEmpty }
98+
.map { $0.baseFeePerGas }
99+
100+
return try calculateStatistic(lastNthBlocksBaseFees, statistic)
101+
}
102+
}
103+
}
104+
105+
public extension Web3.Oracle {
106+
// MARK: - Base Fee
107+
/// Base fee amount based on last Nth blocks
108+
///
109+
/// Normalized means that most high and most low value were droped from calculation.
110+
///
111+
/// Nth block may include empty ones.
112+
///
113+
/// - Parameter statistic: Statistic to apply for base fee calculation
114+
/// - Returns: Suggested base fee amount according to statistic, nil if failed to perdict
115+
func predictBaseFee(_ statistic: Statistic) -> BigUInt? {
116+
guard let value = try? suggestBaseFee(statistic) else { return nil }
117+
return value
118+
}
119+
120+
// MARK: - Tip
121+
/// Maximum tip amount based on last block tips
122+
///
123+
/// Normalized means that most high and most low value were droped from calculation.
124+
///
125+
/// Account first of the latest block that have more than `transactionsNumber` value.
126+
///
127+
/// - Parameter statistic: Statistic to apply for tip calculation
128+
/// - Returns: Suggested tip amount according to statistic, nil if failed to perdict
129+
func predictTip(_ statistic: Statistic) -> BigUInt? {
130+
guard let value = try? suggestTipValue(statistic) else { return nil }
131+
return value
132+
}
133+
134+
// MARK: - Summary fees
135+
/// Method to get summary fees
136+
/// - Parameters:
137+
/// - baseFee: Statistic to apply for baseFee
138+
/// - tip: Statistic to apply for tip
139+
/// - Returns: Touple where [0] — base fee, [1] — tip, nil if failed to predict
140+
func predictBothFees(baseFee: Statistic, tip: Statistic) -> (BigUInt, BigUInt)? {
141+
guard let baseFee = try? suggestBaseFee(baseFee) else { return nil }
142+
guard let tip = try? suggestTipValue(tip) else { return nil }
143+
144+
return (baseFee, tip)
145+
}
146+
}
147+
148+
public extension Web3.Oracle {
149+
// TODO: Make me struct and incapsulate math within to make me extendable
150+
enum Statistic {
151+
/// Mininum statistic
152+
case minimum
153+
/// Mean statistic
154+
case mean
155+
/// Median statistic
156+
case median
157+
/// Maximum statistic
158+
case maximum
159+
}
160+
}
161+
162+
extension Array {
163+
func cropAnomalyValues() -> Self {
164+
var tmpArr = self.dropFirst()
165+
tmpArr = self.dropLast()
166+
return Array(tmpArr)
167+
}
168+
}
169+
170+
extension Array where Element: BinaryInteger {
171+
func mean() -> BigUInt? {
172+
guard !self.isEmpty else { return nil }
173+
return BigUInt(self.reduce(0, +)) / BigUInt(self.count)
174+
}
175+
176+
func median() -> BigUInt? {
177+
guard !self.isEmpty else { return nil }
178+
179+
let sorted_data = self.sorted()
180+
if self.count % 2 == 1 {
181+
return BigUInt(sorted_data[Int(floor(Double(self.count) / 2))])
182+
} else {
183+
return BigUInt(sorted_data[self.count / 2] + sorted_data[(self.count / 2) - 1] / 2)
184+
}
185+
}
186+
}

web3swift.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@
169169
5C03EAB4274405D20052C66D /* EIP712.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB50A52927060C5300D7E39B /* EIP712.swift */; };
170170
5C26D89E27F3724600431EB0 /* Web3+EIP1559.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C26D89D27F3724600431EB0 /* Web3+EIP1559.swift */; };
171171
5C26D8A027F3725500431EB0 /* EIP1559BlockTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C26D89F27F3725500431EB0 /* EIP1559BlockTests.swift */; };
172+
5C26D8A227F5AC0300431EB0 /* Web3+GasOracle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C26D8A127F5AC0300431EB0 /* Web3+GasOracle.swift */; };
172173
5CF7E8A2276B79290009900F /* web3swiftEIP681Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF7E891276B79270009900F /* web3swiftEIP681Tests.swift */; };
173174
5CF7E8A3276B792A0009900F /* web3swiftPersonalSignatureTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF7E892276B79270009900F /* web3swiftPersonalSignatureTests.swift */; };
174175
5CF7E8A4276B792A0009900F /* web3swiftTransactionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF7E893276B79270009900F /* web3swiftTransactionsTests.swift */; };
@@ -386,6 +387,7 @@
386387
4E2DFEF325485B53001AF561 /* KeystoreParams.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeystoreParams.swift; sourceTree = "<group>"; };
387388
5C26D89D27F3724600431EB0 /* Web3+EIP1559.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Web3+EIP1559.swift"; sourceTree = "<group>"; };
388389
5C26D89F27F3725500431EB0 /* EIP1559BlockTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EIP1559BlockTests.swift; sourceTree = "<group>"; };
390+
5C26D8A127F5AC0300431EB0 /* Web3+GasOracle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Web3+GasOracle.swift"; sourceTree = "<group>"; };
389391
5CDEF972275A74590004A2F2 /* web3swift.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; name = web3swift.xctestplan; path = Tests/web3swiftTests/web3swift.xctestplan; sourceTree = SOURCE_ROOT; };
390392
5CDEF973275A74670004A2F2 /* LocalTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = LocalTests.xctestplan; sourceTree = "<group>"; };
391393
5CDEF974275A747B0004A2F2 /* RemoteTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = RemoteTests.xctestplan; sourceTree = "<group>"; };
@@ -731,6 +733,7 @@
731733
isa = PBXGroup;
732734
children = (
733735
5C26D89D27F3724600431EB0 /* Web3+EIP1559.swift */,
736+
5C26D8A127F5AC0300431EB0 /* Web3+GasOracle.swift */,
734737
3AA8154E2276E44100F5DB52 /* Web3+HttpProvider.swift */,
735738
3AA8154F2276E44100F5DB52 /* Web3.swift */,
736739
3AA815502276E44100F5DB52 /* Web3+InfuraProviders.swift */,
@@ -1320,6 +1323,7 @@
13201323
3AA815AD2276E44100F5DB52 /* ENSRegistry.swift in Sources */,
13211324
3AA815CD2276E44100F5DB52 /* Web3+Personal.swift in Sources */,
13221325
3AA8151E2276E42F00F5DB52 /* EthereumFilterEncodingExtensions.swift in Sources */,
1326+
5C26D8A227F5AC0300431EB0 /* Web3+GasOracle.swift in Sources */,
13231327
3AA8151D2276E42F00F5DB52 /* ComparisonExtensions.swift in Sources */,
13241328
3AA815AF2276E44100F5DB52 /* NonceMiddleware.swift in Sources */,
13251329
3AA815E92276E44100F5DB52 /* EthereumAddress.swift in Sources */,

0 commit comments

Comments
 (0)