Skip to content

Commit 0d2db9d

Browse files
Merge pull request #547 from mloit/feature/automatic-gas-1559
2 parents c1c44fc + 171db00 commit 0d2db9d

29 files changed

+517
-441
lines changed

Documentation/Usage.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -359,8 +359,8 @@ let result = try! transaction.call()
359359
##### Other Transaction Types
360360

361361
By default a `legacy` transaction will be created which is compatible across all chains, regardless of which fork.
362-
To create one of the new transaction types introduced with the `london` fork you will need to set some additonal parameters
363-
in the `TransactionOptions` object. Note you should only try to send one of tehse new types of transactions if you are on a chain
362+
To create one of the new transaction types introduced with the `london` fork you will need to set some additional parameters
363+
in the `TransactionOptions` object. Note you should only try to send one of these new types of transactions if you are on a chain
364364
that supports them.
365365

366366
To send an EIP-2930 style transacton with an access list you need to set the transaction type, and the access list,
@@ -385,10 +385,10 @@ To send an EIP-1559 style transaction you set the transaction type, and the new
385385
(you may also send an AccessList with an EIP-1559 transaction) When sending an EIP-1559 transaction, the older `gasPrice` parameter is ignored.
386386
```swift
387387
options.type = .eip1559
388-
options.maxFeePerGas = .manual(...) // the maximum price per unit of gas, inclusive of baseFee and tip
389-
options.maxPriorityFeePerGas = .manual(...) // the tip to be paid to the miner, per unit of gas
388+
options.maxFeePerGas = .automatic // the maximum price per unit of gas, inclusive of baseFee and tip
389+
options.maxPriorityFeePerGas = .automatic // the 'tip' to be paid to the miner, per unit of gas
390390
```
391-
Note there is a new `Oracle` object available that can be used to assist with estimating the new gas fees
391+
Note: There is a new `Oracle` object available that can be used to assist with estimating the new gas fees if you wish to set them manually.
392392

393393
### Chain state
394394

Sources/web3swift/Web3/Web3+MutatingTransaction.swift

Lines changed: 86 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ public class WriteTransaction: ReadTransaction {
7373
optionsForGasEstimation.value = mergedOptions.value
7474
optionsForGasEstimation.gasLimit = mergedOptions.gasLimit
7575
optionsForGasEstimation.callOnBlock = mergedOptions.callOnBlock
76+
optionsForGasEstimation.type = mergedOptions.type
77+
optionsForGasEstimation.accessList = mergedOptions.accessList
7678

7779
// assemble promise for gasLimit
7880
var gasEstimatePromise: Promise<BigUInt>? = nil
@@ -102,20 +104,75 @@ public class WriteTransaction: ReadTransaction {
102104
getNoncePromise = Promise<BigUInt>.value(nonce)
103105
}
104106

105-
// assemble promise for gasPrice
106-
var gasPricePromise: Promise<BigUInt>? = nil
107-
guard let gasPricePolicy = mergedOptions.gasPrice else {
108-
seal.reject(Web3Error.inputError(desc: "No gasPrice policy provided"))
109-
return
110-
}
111-
switch gasPricePolicy {
112-
case .automatic, .withMargin:
113-
gasPricePromise = self.web3.eth.getGasPricePromise()
114-
case .manual(let gasPrice):
115-
gasPricePromise = Promise<BigUInt>.value(gasPrice)
107+
// determine gas costing, taking transaction type into account
108+
let oracle = Web3.Oracle(self.web3, percentiles: [75])
109+
let finalGasPrice: BigUInt? // legacy gas model
110+
let finalGasFee: BigUInt? // EIP-1559 gas model
111+
let finalTipFee: BigUInt? // EIP-1559 gas model
112+
113+
if mergedOptions.type == nil || mergedOptions.type != .eip1559 { // legacy Gas
114+
// set unused gas parameters to nil
115+
finalGasFee = nil
116+
finalTipFee = nil
117+
118+
// determine the (legacy) gas price
119+
guard let gasPricePolicy = mergedOptions.gasPrice else {
120+
seal.reject(Web3Error.inputError(desc: "No gasPrice policy provided"))
121+
return
122+
}
123+
switch gasPricePolicy {
124+
case .automatic, .withMargin:
125+
let percentiles = oracle.gasPriceLegacyPercentiles
126+
guard !percentiles.isEmpty else {
127+
throw Web3Error.processingError(desc: "Failed to fetch gas price")
128+
}
129+
finalGasPrice = percentiles[0]
130+
case .manual(let gasPrice):
131+
finalGasPrice = gasPrice
132+
}
133+
} else { // else new gas fees (EIP-1559)
134+
// set unused gas parametes to nil
135+
finalGasPrice = nil
136+
137+
// determine the tip
138+
guard let maxPriorityFeePerGasPolicy = mergedOptions.maxPriorityFeePerGas else {
139+
seal.reject(Web3Error.inputError(desc: "No maxPriorityFeePerGas policy provided"))
140+
return
141+
}
142+
switch maxPriorityFeePerGasPolicy {
143+
case .automatic:
144+
let percentiles = oracle.tipFeePercentiles
145+
guard !percentiles.isEmpty else {
146+
throw Web3Error.processingError(desc: "Failed to fetch maxPriorityFeePerGas data")
147+
}
148+
finalTipFee = percentiles[0]
149+
case .manual(let maxPriorityFeePerGas):
150+
finalTipFee = maxPriorityFeePerGas
151+
}
152+
153+
// determine the baseFee, and calculate the maxFeePerGas
154+
guard let maxFeePerGasPolicy = mergedOptions.maxFeePerGas else {
155+
seal.reject(Web3Error.inputError(desc: "No maxFeePerGas policy provided"))
156+
return
157+
}
158+
switch maxFeePerGasPolicy {
159+
case .automatic:
160+
let percentiles = oracle.baseFeePercentiles
161+
guard !percentiles.isEmpty else {
162+
throw Web3Error.processingError(desc: "Failed to fetch baseFee data")
163+
}
164+
guard let tipFee = finalTipFee else {
165+
throw Web3Error.processingError(desc: "Missing tip value")
166+
}
167+
finalGasFee = percentiles[0] + tipFee
168+
case .manual(let maxFeePerGas):
169+
finalGasFee = maxFeePerGas
170+
}
116171
}
117-
var promisesToFulfill: [Promise<BigUInt>] = [getNoncePromise!, gasPricePromise!, gasEstimatePromise!]
118-
when(resolved: getNoncePromise!, gasEstimatePromise!, gasPricePromise!).map(on: queue, { (results: [PromiseResult<BigUInt>]) throws -> EthereumTransaction in
172+
173+
// wait for promises to resolve
174+
var promisesToFulfill: [Promise<BigUInt>] = [getNoncePromise!, gasEstimatePromise!]
175+
when(resolved: getNoncePromise!, gasEstimatePromise!).map(on: queue, { (results: [PromiseResult<BigUInt>]) throws -> EthereumTransaction in
119176

120177
promisesToFulfill.removeAll()
121178
guard case .fulfilled(let nonce) = results[0] else {
@@ -124,17 +181,25 @@ public class WriteTransaction: ReadTransaction {
124181
guard case .fulfilled(let gasEstimate) = results[1] else {
125182
throw Web3Error.processingError(desc: "Failed to fetch gas estimate")
126183
}
127-
guard case .fulfilled(let gasPrice) = results[2] else {
128-
throw Web3Error.processingError(desc: "Failed to fetch gas price")
129-
}
130-
131-
let estimate = mergedOptions.resolveGasLimit(gasEstimate)
132-
let finalGasPrice = mergedOptions.resolveGasPrice(gasPrice)
133184

134185
var finalOptions = TransactionOptions()
186+
finalOptions.type = mergedOptions.type
135187
finalOptions.nonce = .manual(nonce)
136-
finalOptions.gasLimit = .manual(estimate)
137-
finalOptions.gasPrice = .manual(finalGasPrice)
188+
finalOptions.gasLimit = .manual(mergedOptions.resolveGasLimit(gasEstimate))
189+
finalOptions.accessList = mergedOptions.accessList
190+
191+
// set the finalized gas parameters
192+
if let gasPrice = finalGasPrice {
193+
finalOptions.gasPrice = .manual(mergedOptions.resolveGasPrice(gasPrice))
194+
}
195+
196+
if let tipFee = finalTipFee {
197+
finalOptions.maxPriorityFeePerGas = .manual(mergedOptions.resolveMaxPriorityFeePerGas(tipFee))
198+
}
199+
200+
if let gasFee = finalGasFee {
201+
finalOptions.maxFeePerGas = .manual(mergedOptions.resolveMaxFeePerGas(gasFee))
202+
}
138203

139204
assembledTransaction.applyOptions(finalOptions)
140205

Tests/web3swiftTests/localTests/ABIEncoderSoliditySha3Test.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import Foundation
1010
import XCTest
1111
@testable import web3swift
1212

13-
class ABIEncoderSoliditySha3Test: XCTestCase {
13+
class ABIEncoderSoliditySha3Test: LocalTestCase {
1414

1515
func test_soliditySha3() throws {
1616
var hex = try ABIEncoder.soliditySha3(true).toHexString().addHexPrefix()

Tests/web3swiftTests/localTests/DataConversionTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import XCTest
1616

1717
// Some base58 test vectors pulled from: https://tools.ietf.org/id/draft-msporny-base58-01.html
1818
// note that one of the return values is incorrect in the reference above, it is corrected here
19-
class DataConversionTests: XCTestCase {
19+
class DataConversionTests: LocalTestCase {
2020
// test an empty input for the base58 decoder & decoder
2121
func testBase58() throws {
2222
let vector = ""

Tests/web3swiftTests/localTests/EIP1559BlockTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import BigInt
44
@testable
55
import web3swift
66

7-
class EIP1559BlockTests: XCTestCase {
7+
class EIP1559BlockTests: LocalTestCase {
88
let uselessBlockPart = (
99
number: BigUInt(12_965_000),
1010
hash: Data(fromHex: "0xef95f2f1ed3ca60b048b4bf67cde2195961e0bba6f70bcbea9a2c4e133e34b46")!, // "hash":

Tests/web3swiftTests/localTests/EIP712Tests.swift

Lines changed: 31 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
import XCTest
22
@testable import web3swift
33

4-
class EIP712Tests: XCTestCase {
4+
class EIP712Tests: LocalTestCase {
55
func testWithoutChainId() throws {
6-
6+
77
let to = EthereumAddress("0x3F06bAAdA68bB997daB03d91DBD0B73e196c5A4d")!
8-
8+
99
let value = EIP712.UInt256(0)
10-
10+
1111
let amountLinen = EIP712.UInt256("0001000000000000000")//
12-
12+
1313
let function = ABI.Element.Function(
1414
name: "approveAndMint",
1515
inputs: [
@@ -18,22 +18,22 @@ class EIP712Tests: XCTestCase {
1818
outputs: [.init(name: "", type: .bool)],
1919
constant: false,
2020
payable: false)
21-
21+
2222
let object = ABI.Element.function(function)
23-
23+
2424
let safeTxData = object.encodeParameters([
2525
EthereumAddress("0x41B5844f4680a8C38fBb695b7F9CFd1F64474a72")! as AnyObject,
2626
amountLinen as AnyObject
2727
])!
28-
28+
2929
let operation: EIP712.UInt8 = 1
30-
30+
3131
let safeTxGas = EIP712.UInt256(250000)
32-
32+
3333
let baseGas = EIP712.UInt256(60000)
34-
34+
3535
let gasPrice = EIP712.UInt256("20000000000")
36-
36+
3737
let gasToken = EthereumAddress("0x0000000000000000000000000000000000000000")!
3838
print(1)
3939
let refundReceiver = EthereumAddress("0x7c07D32e18D6495eFDC487A32F8D20daFBa53A5e")!
@@ -76,13 +76,13 @@ class EIP712Tests: XCTestCase {
7676
}
7777

7878
func testWithChainId() throws {
79-
79+
8080
let to = EthereumAddress("0x3F06bAAdA68bB997daB03d91DBD0B73e196c5A4d")!
81-
81+
8282
let value = EIP712.UInt256(0)
83-
83+
8484
let amount = EIP712.UInt256("0001000000000000000")
85-
85+
8686
let function = ABI.Element.Function(
8787
name: "approveAndMint",
8888
inputs: [
@@ -91,28 +91,28 @@ class EIP712Tests: XCTestCase {
9191
outputs: [.init(name: "", type: .bool)],
9292
constant: false,
9393
payable: false)
94-
94+
9595
let object = ABI.Element.function(function)
96-
96+
9797
let safeTxData = object.encodeParameters([
9898
EthereumAddress("0x41B5844f4680a8C38fBb695b7F9CFd1F64474a72")! as AnyObject,
9999
amount as AnyObject
100100
])!
101-
101+
102102
let operation: EIP712.UInt8 = 1
103-
103+
104104
let safeTxGas = EIP712.UInt256(250000)
105-
105+
106106
let baseGas = EIP712.UInt256(60000)
107-
107+
108108
let gasPrice = EIP712.UInt256("20000000000")
109-
109+
110110
let gasToken = EthereumAddress("0x0000000000000000000000000000000000000000")!
111-
111+
112112
let refundReceiver = EthereumAddress("0x7c07D32e18D6495eFDC487A32F8D20daFBa53A5e")!
113-
113+
114114
let nonce: EIP712.UInt256 = .init(0)
115-
115+
116116
let safeTX = SafeTx(
117117
to: to,
118118
value: value,
@@ -124,25 +124,24 @@ class EIP712Tests: XCTestCase {
124124
gasToken: gasToken,
125125
refundReceiver: refundReceiver,
126126
nonce: nonce)
127-
127+
128128
let mnemonic = "normal dune pole key case cradle unfold require tornado mercy hospital buyer"
129129
let keystore = try! BIP32Keystore(mnemonics: mnemonic, password: "", mnemonicsPassword: "")!
130-
130+
131131
let verifyingContract = EthereumAddress("0x76106814dc6150b0fe510fbda4d2d877ac221270")!
132-
132+
133133
let account = keystore.addresses?[0]
134134
let password = ""
135135
let chainId: EIP712.UInt256? = EIP712.UInt256(42)
136-
136+
137137
let signature = try Web3Signer.signEIP712(
138138
safeTx: safeTX,
139139
keystore: keystore,
140140
verifyingContract: verifyingContract,
141141
account: account!,
142142
password: password,
143143
chainId: chainId)
144-
144+
145145
XCTAssertEqual(signature.toHexString(), "f1f423cb23efad5035d4fb95c19cfcd46d4091f2bd924680b88c4f9edfa1fb3a4ce5fc5d169f354e3b464f45a425ed3f6203af06afbacdc5c8224a300ce9e6b21b")
146146
}
147147
}
148-
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import Foundation
2+
import XCTest
3+
import BigInt
4+
5+
import web3swift
6+
7+
// SuperClass that all local tests should be derived from
8+
// while this class does show up in the navigator, it has no associated tests
9+
class LocalTestCase: XCTestCase {
10+
static let url = URL.init(string: "http://127.0.0.1:8545")!
11+
let ganache = try! Web3.new(LocalTestCase.url)
12+
static var isSetUp = false
13+
14+
override class func setUp() {
15+
super.setUp()
16+
17+
// check to see if we need to run the one-time setup
18+
if isSetUp { return }
19+
isSetUp = true
20+
21+
let web3 = try! Web3.new(LocalTestCase.url)
22+
23+
let block = try! web3.eth.getBlockNumber()
24+
if block >= 25 { return }
25+
26+
print("\n ****** Preloading Ganache (\(25 - block) blocks) *****\n")
27+
28+
let allAddresses = try! web3.eth.getAccounts()
29+
let sendToAddress = allAddresses[0]
30+
let contract = web3.contract(Web3.Utils.coldWalletABI, at: sendToAddress, abiVersion: 2)
31+
let value = Web3.Utils.parseToBigUInt("1.0", units: .eth)
32+
33+
let from = allAddresses[0]
34+
let writeTX = contract!.write("fallback")!
35+
writeTX.transactionOptions.from = from
36+
writeTX.transactionOptions.value = value
37+
writeTX.transactionOptions.gasLimit = .manual(78423)
38+
writeTX.transactionOptions.gasPrice = .manual(20000000000)
39+
40+
for _ in block..<25 {
41+
let _ = try! writeTX.sendPromise(password: "").wait()
42+
}
43+
}
44+
}

Tests/web3swiftTests/localTests/LocalTests.xctestplan

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@
2121
"RemoteParsingTests",
2222
"RemoteTests",
2323
"ST20AndSecurityTokenTests",
24-
"WebsocketTests"
24+
"WebsocketTests",
25+
"LocalTestCase"
2526
],
2627
"target" : {
2728
"containerPath" : "container:web3swift.xcodeproj",

0 commit comments

Comments
 (0)