Skip to content

Commit 03f55e2

Browse files
Implement Oracle class
And object that predicts required transactions gas fee and tip amounts to proceed success. - Change all Block side gas methods to static since they don't need any Web3 scope values.
1 parent 4a3fd36 commit 03f55e2

File tree

4 files changed

+193
-3
lines changed

4 files changed

+193
-3
lines changed

Sources/web3swift/Transaction/EthereumTransaction.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import Foundation
88
import BigInt
99

1010
public struct EthereumTransaction: CustomStringConvertible {
11+
// FIXME: Add Type value https://blog.mycrypto.com/new-transaction-types-on-ethereum
1112
public var nonce: BigUInt
1213
public var gasPrice: BigUInt = 0
1314
public var gasLimit: BigUInt = 0

Sources/web3swift/Web3/Web3+EIP1559.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import BigInt
1515
///
1616
/// Additional info about base fee options: https://ethereum.org/en/developers/docs/gas/#post-london
1717
public extension Web3 {
18-
private func verifyGasLimit(parentGasLimit: BigUInt, currentGasLimit: BigUInt) -> Bool {
18+
private static func verifyGasLimit(parentGasLimit: BigUInt, currentGasLimit: BigUInt) -> Bool {
1919
var diff = BigInt(parentGasLimit) - BigInt(currentGasLimit)
2020

2121
// make diff positive number
@@ -43,7 +43,7 @@ public extension Web3 {
4343
/// - parent: Previous Block
4444
/// - current: Current block
4545
/// - Returns: True or false if block is EIP-1559 or not
46-
func isEip1559Block(parent: Block, current: Block) -> Bool {
46+
static func isEip1559Block(parent: Block, current: Block) -> Bool {
4747
let parentGasLimit = parent.chainVersion >= .London ? parent.gasLimit : parent.gasLimit * Web3.ElasticityMultiplier
4848

4949
guard verifyGasLimit(parentGasLimit: parentGasLimit, currentGasLimit: current.gasLimit) else { return false }
@@ -64,7 +64,7 @@ public extension Web3 {
6464
///
6565
/// - Parameter parent: Parent `Block`
6666
/// - Returns: Amount of expected base fee for current `Block`
67-
func calcBaseFee(_ parent: Block) -> BigUInt {
67+
static func calcBaseFee(_ parent: Block) -> BigUInt {
6868
// If given blocks ChainVersion is lower than London — always returns InitialBaseFee
6969
guard parent.chainVersion >= .London else { return Web3.InitialBaseFee }
7070

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

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 /* EIP1559Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C26D89F27F3725500431EB0 /* EIP1559Tests.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 */; };
@@ -384,6 +385,7 @@
384385
4E2DFEF325485B53001AF561 /* KeystoreParams.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeystoreParams.swift; sourceTree = "<group>"; };
385386
5C26D89D27F3724600431EB0 /* Web3+EIP1559.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Web3+EIP1559.swift"; sourceTree = "<group>"; };
386387
5C26D89F27F3725500431EB0 /* EIP1559Tests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EIP1559Tests.swift; sourceTree = "<group>"; };
388+
5C26D8A127F5AC0300431EB0 /* Web3+GasOracle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Web3+GasOracle.swift"; sourceTree = "<group>"; };
387389
5CDEF972275A74590004A2F2 /* web3swift.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; name = web3swift.xctestplan; path = Tests/web3swiftTests/web3swift.xctestplan; sourceTree = SOURCE_ROOT; };
388390
5CDEF973275A74670004A2F2 /* LocalTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = LocalTests.xctestplan; sourceTree = "<group>"; };
389391
5CDEF974275A747B0004A2F2 /* RemoteTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = RemoteTests.xctestplan; sourceTree = "<group>"; };
@@ -727,6 +729,7 @@
727729
isa = PBXGroup;
728730
children = (
729731
5C26D89D27F3724600431EB0 /* Web3+EIP1559.swift */,
732+
5C26D8A127F5AC0300431EB0 /* Web3+GasOracle.swift */,
730733
3AA8154E2276E44100F5DB52 /* Web3+HttpProvider.swift */,
731734
3AA8154F2276E44100F5DB52 /* Web3.swift */,
732735
3AA815502276E44100F5DB52 /* Web3+InfuraProviders.swift */,
@@ -1314,6 +1317,7 @@
13141317
3AA815AD2276E44100F5DB52 /* ENSRegistry.swift in Sources */,
13151318
3AA815CD2276E44100F5DB52 /* Web3+Personal.swift in Sources */,
13161319
3AA8151E2276E42F00F5DB52 /* EthereumFilterEncodingExtensions.swift in Sources */,
1320+
5C26D8A227F5AC0300431EB0 /* Web3+GasOracle.swift in Sources */,
13171321
3AA8151D2276E42F00F5DB52 /* ComparisonExtensions.swift in Sources */,
13181322
3AA815AF2276E44100F5DB52 /* NonceMiddleware.swift in Sources */,
13191323
3AA815E92276E44100F5DB52 /* EthereumAddress.swift in Sources */,

0 commit comments

Comments
 (0)