CW-672: Enhance ETH Transaction Fee Calculation (#1545)
* fix: Eth transaction fees WIP * Revert "fix: Eth transaction fees WIP" This reverts commit b9a469bc7e22134d78bf0cc4c00485e1d4515ebd. * fix: Modifying fee WIP * fix: Enhance ETH Wallet fee calculation WIP * feat: Enhance Transaction fees for ETH Transactions, Native transactions done, left with ERC20 transactions * fix: Pre PR cleanups * minor things [skip ci] --------- Co-authored-by: OmarHatem <omarh.ismail1@gmail.com>
Adegoke David committed
Jul 21, 2024 at 00:04 UTC
415d2a35736a13fce200517c3f0e603573cf41c0
3 files changed
+186
-45
cw_evm/lib/contract/erc20.dart
+2
-2
@@ -2,7 +2,7 @@ import 'dart:typed_data';
2
3
import 'package:web3dart/web3dart.dart' as web3;
4
5
-final _contractAbi = web3.ContractAbi.fromJson(
5
+final ethereumContractAbi = web3.ContractAbi.fromJson(
6
'[{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"spender","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Transfer","type":"event"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"spender","type":"address"}],"name":"allowance","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"approve","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"decimals","outputs":[{"internalType":"uint8","name":"","type":"uint8"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"name","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"symbol","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"transfer","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"sender","type":"address"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"transferFrom","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"}]',
7
'Erc20');
8
@@ -13,7 +13,7 @@ class ERC20 extends web3.GeneratedContract {
13
required web3.EthereumAddress address,
14
required web3.Web3Client client,
15
int? chainId,
16
- }) : super(web3.DeployedContract(_contractAbi, address), client, chainId);
16
+ }) : super(web3.DeployedContract(ethereumContractAbi, address), client, chainId);
17
18
/// Returns the remaining number of tokens that [spender] will be allowed to spend on behalf of [owner] through [transferFrom]. This is zero by default. This value changes when [approve] or [transferFrom] are called.
19
///
cw_evm/lib/evm_chain_client.dart
+55
-10
@@ -10,7 +10,7 @@ import 'package:cw_evm/evm_chain_transaction_priority.dart';
10
import 'package:cw_evm/evm_erc20_balance.dart';
11
import 'package:cw_evm/pending_evm_chain_transaction.dart';
12
import 'package:cw_evm/.secrets.g.dart' as secrets;
13
-import 'package:flutter/services.dart';
13
+import 'package:flutter/foundation.dart';
14
import 'package:hex/hex.dart' as hex;
15
import 'package:http/http.dart';
16
import 'package:web3dart/web3dart.dart';
@@ -65,16 +65,65 @@ abstract class EVMChainClient {
65
Future<int> getGasUnitPrice() async {
66
try {
67
final gasPrice = await _client!.getGasPrice();
68
+
69
return gasPrice.getInWei.toInt();
70
} catch (_) {
71
return 0;
72
}
73
}
74
74
- Future<int> getEstimatedGas() async {
75
+ Future<int> getGasBaseFee() async {
76
try {
76
- final estimatedGas = await _client!.estimateGas();
77
- return estimatedGas.toInt();
77
+ final blockInfo = await _client!.getBlockInformation(isContainFullObj: false);
78
+ final baseFee = blockInfo.baseFeePerGas;
79
+
80
+ return baseFee!.getInWei.toInt();
81
+ } catch (_) {
82
+ return 0;
83
+ }
84
+ }
85
+
86
+ Future<int> getEstimatedGas({
87
+ String? contractAddress,
88
+ required EthereumAddress toAddress,
89
+ required EthereumAddress senderAddress,
90
+ required EtherAmount value,
91
+ EtherAmount? gasPrice,
92
+ // EtherAmount? maxFeePerGas,
93
+ // EtherAmount? maxPriorityFeePerGas,
94
+ }) async {
95
+ try {
96
+ if (contractAddress == null) {
97
+ final estimatedGas = await _client!.estimateGas(
98
+ sender: senderAddress,
99
+ gasPrice: gasPrice,
100
+ to: toAddress,
101
+ value: value,
102
+ // maxPriorityFeePerGas: maxPriorityFeePerGas,
103
+ // maxFeePerGas: maxFeePerGas,
104
+ );
105
+
106
+ return estimatedGas.toInt();
107
+ } else {
108
+ final contract = DeployedContract(
109
+ ethereumContractAbi,
110
+ EthereumAddress.fromHex(contractAddress),
111
+ );
112
+
113
+ final transferFunction = contract.function('transferFrom');
114
+
115
+ final estimatedGas = await _client!.estimateGas(
116
+ sender: senderAddress,
117
+ to: toAddress,
118
+ value: value,
119
+ data: transferFunction.encodeCall([
120
+ senderAddress,
121
+ toAddress,
122
+ value.getInWei,
123
+ ]),
124
+ );
125
+ return estimatedGas.toInt();
126
+ }
127
} catch (_) {
128
return 0;
129
}
@@ -84,7 +133,7 @@ abstract class EVMChainClient {
133
required Credentials privateKey,
134
required String toAddress,
135
required BigInt amount,
87
- required int gas,
136
+ required BigInt gas,
137
required EVMChainTransactionPriority priority,
138
required CryptoCurrency currency,
139
required int exponent,
@@ -97,8 +146,6 @@ abstract class EVMChainClient {
146
147
bool isNativeToken = currency == CryptoCurrency.eth || currency == CryptoCurrency.maticpoly;
148
100
- final price = _client!.getGasPrice();
101
-
149
final Transaction transaction = createTransaction(
150
from: privateKey.address,
151
to: EthereumAddress.fromHex(toAddress),
@@ -130,11 +177,10 @@ abstract class EVMChainClient {
177
178
_sendTransaction = () async => await sendTransaction(signedTransaction);
179
133
-
180
return PendingEVMChainTransaction(
181
signedTransaction: signedTransaction,
182
amount: amount.toString(),
137
- fee: BigInt.from(gas) * (await price).getInWei,
183
+ fee: gas,
184
sendTransaction: _sendTransaction,
185
exponent: exponent,
186
);
@@ -233,7 +279,6 @@ abstract class EVMChainClient {
279
280
final decodedResponse = jsonDecode(response.body)[0] as Map<String, dynamic>;
281
236
-
282
final symbol = (decodedResponse['symbol'] ?? '') as String;
283
String filteredSymbol = symbol.replaceFirst(RegExp('^\\\$'), '');
284
cw_evm/lib/evm_chain_wallet.dart
+129
-33
@@ -27,6 +27,7 @@ import 'package:cw_evm/evm_chain_transaction_priority.dart';
27
import 'package:cw_evm/evm_chain_wallet_addresses.dart';
28
import 'package:cw_evm/evm_ledger_credentials.dart';
29
import 'package:cw_evm/file.dart';
30
+import 'package:flutter/foundation.dart';
31
import 'package:hex/hex.dart';
32
import 'package:hive/hive.dart';
33
import 'package:mobx/mobx.dart';
@@ -102,10 +103,12 @@ abstract class EVMChainWalletBase
103
104
Credentials get evmChainPrivateKey => _evmChainPrivateKey;
105
105
- late EVMChainClient _client;
106
+ late final EVMChainClient _client;
107
+
108
+ int gasPrice = 0;
109
+ int? gasBaseFee = 0;
110
+ int estimatedGasUnits = 0;
111
107
- int? _gasPrice;
108
- int? _estimatedGas;
112
bool _isTransactionUpdating;
113
114
// TODO: remove after integrating our own node and having eth_newPendingTransactionFilter
@@ -173,12 +176,70 @@ abstract class EVMChainWalletBase
176
177
@override
178
int calculateEstimatedFee(TransactionPriority priority, int? amount) {
179
+ {
180
+ try {
181
+ if (priority is EVMChainTransactionPriority) {
182
+ final priorityFee = EtherAmount.fromInt(EtherUnit.gwei, priority.tip).getInWei.toInt();
183
+
184
+ int maxFeePerGas;
185
+ if (gasBaseFee != null) {
186
+ // MaxFeePerGas with EIP1559;
187
+ maxFeePerGas = gasBaseFee! + priorityFee;
188
+ } else {
189
+ // MaxFeePerGas with gasPrice;
190
+ maxFeePerGas = gasPrice;
191
+ debugPrint('MaxFeePerGas with gasPrice: $maxFeePerGas');
192
+ }
193
+
194
+ final totalGasFee = estimatedGasUnits * maxFeePerGas;
195
+ return totalGasFee;
196
+ }
197
+
198
+ return 0;
199
+ } catch (e) {
200
+ return 0;
201
+ }
202
+ }
203
+ }
204
+
205
+ /// Allows more customization to the fetch estimatedFees flow.
206
+ ///
207
+ /// We are able to pass in:
208
+ /// - The exact amount the user wants to send,
209
+ /// - The addressHex for the receiving wallet,
210
+ /// - A contract address which would be essential in determining if to calcualate the estimate for ERC20 or native ETH
211
+ Future<int> calculateActualEstimatedFeeForCreateTransaction({
212
+ required amount,
213
+ required String? contractAddress,
214
+ required String receivingAddressHex,
215
+ required TransactionPriority priority,
216
+ }) async {
217
try {
218
if (priority is EVMChainTransactionPriority) {
219
final priorityFee = EtherAmount.fromInt(EtherUnit.gwei, priority.tip).getInWei.toInt();
179
- return (_gasPrice! + priorityFee) * (_estimatedGas ?? 0);
180
- }
220
221
+ int maxFeePerGas;
222
+ if (gasBaseFee != null) {
223
+ // MaxFeePerGas with EIP1559;
224
+ maxFeePerGas = gasBaseFee! + priorityFee;
225
+ } else {
226
+ // MaxFeePerGas with gasPrice
227
+ maxFeePerGas = gasPrice;
228
+ }
229
+
230
+ final estimatedGas = await _client.getEstimatedGas(
231
+ contractAddress: contractAddress,
232
+ senderAddress: _evmChainPrivateKey.address,
233
+ value: EtherAmount.fromBigInt(EtherUnit.wei, amount!),
234
+ gasPrice: EtherAmount.fromInt(EtherUnit.wei, gasPrice),
235
+ toAddress: EthereumAddress.fromHex(receivingAddressHex),
236
+ // maxFeePerGas: EtherAmount.fromInt(EtherUnit.wei, maxFeePerGas),
237
+ // maxPriorityFeePerGas: EtherAmount.fromInt(EtherUnit.gwei, priority.tip),
238
+ );
239
+
240
+ final totalGasFee = estimatedGas * maxFeePerGas;
241
+ return totalGasFee;
242
+ }
243
return 0;
244
} catch (e) {
245
return 0;
@@ -225,13 +286,12 @@ abstract class EVMChainWalletBase
286
syncStatus = AttemptingSyncStatus();
287
await _updateBalance();
288
await _updateTransactions();
228
- _gasPrice = await _client.getGasUnitPrice();
229
- _estimatedGas = await _client.getEstimatedGas();
289
231
- Timer.periodic(
232
- const Duration(minutes: 1), (timer) async => _gasPrice = await _client.getGasUnitPrice());
233
- Timer.periodic(const Duration(seconds: 10),
234
- (timer) async => _estimatedGas = await _client.getEstimatedGas());
290
+ await _updateEstimatedGasFeeParams();
291
+
292
+ Timer.periodic(const Duration(seconds: 10), (timer) async {
293
+ await _updateEstimatedGasFeeParams();
294
+ });
295
296
syncStatus = SyncedSyncStatus();
297
} catch (e) {
@@ -239,6 +299,19 @@ abstract class EVMChainWalletBase
299
}
300
}
301
302
+ Future<void> _updateEstimatedGasFeeParams() async {
303
+ gasBaseFee = await _client.getGasBaseFee();
304
+
305
+ gasPrice = await _client.getGasUnitPrice();
306
+
307
+ estimatedGasUnits = await _client.getEstimatedGas(
308
+ senderAddress: _evmChainPrivateKey.address,
309
+ toAddress: _evmChainPrivateKey.address,
310
+ gasPrice: EtherAmount.fromInt(EtherUnit.wei, gasPrice),
311
+ value: EtherAmount.fromBigInt(EtherUnit.wei, BigInt.one),
312
+ );
313
+ }
314
+
315
@override
316
Future<PendingTransaction> createTransaction(Object credentials) async {
317
final _credentials = credentials as EVMChainTransactionCredentials;
@@ -258,8 +331,17 @@ abstract class EVMChainWalletBase
331
332
final erc20Balance = balance[transactionCurrency]!;
333
BigInt totalAmount = BigInt.zero;
334
+ BigInt estimatedFeesForTransaction = BigInt.zero;
335
int exponent = transactionCurrency is Erc20Token ? transactionCurrency.decimal : 18;
336
num amountToEVMChainMultiplier = pow(10, exponent);
337
+ String? contractAddress;
338
+ String toAddress = _credentials.outputs.first.isParsedAddress
339
+ ? _credentials.outputs.first.extractedAddress!
340
+ : _credentials.outputs.first.address;
341
+
342
+ if (transactionCurrency is Erc20Token) {
343
+ contractAddress = transactionCurrency.contractAddress;
344
+ }
345
346
// so far this can not be made with Ethereum as Ethereum does not support multiple recipients
347
if (hasMultiDestination) {
@@ -271,35 +353,50 @@ abstract class EVMChainWalletBase
353
outputs.fold(0, (acc, value) => acc + (value.formattedCryptoAmount ?? 0)));
354
totalAmount = BigInt.from(totalOriginalAmount * amountToEVMChainMultiplier);
355
356
+ final estimateFees = await calculateActualEstimatedFeeForCreateTransaction(
357
+ amount: totalAmount,
358
+ receivingAddressHex: toAddress,
359
+ priority: _credentials.priority!,
360
+ contractAddress: contractAddress,
361
+ );
362
+
363
+ estimatedFeesForTransaction = BigInt.from(estimateFees);
364
+
365
if (erc20Balance.balance < totalAmount) {
366
throw EVMChainTransactionCreationException(transactionCurrency);
367
}
368
} else {
369
final output = outputs.first;
279
- // since the fees are taken from Ethereum
280
- // then no need to subtract the fees from the amount if send all
281
- final BigInt allAmount;
282
- if (transactionCurrency is Erc20Token) {
283
- allAmount = erc20Balance.balance;
284
- } else {
285
- final estimatedFee = BigInt.from(calculateEstimatedFee(_credentials.priority!, null));
286
-
287
- if (estimatedFee > erc20Balance.balance) {
288
- throw EVMChainTransactionFeesException();
289
- }
290
-
291
- allAmount = erc20Balance.balance - estimatedFee;
292
- }
293
-
294
- if (output.sendAll) {
295
- totalAmount = allAmount;
296
- } else {
370
+ if (!output.sendAll) {
371
final totalOriginalAmount =
372
EVMChainFormatter.parseEVMChainAmountToDouble(output.formattedCryptoAmount ?? 0);
373
374
totalAmount = BigInt.from(totalOriginalAmount * amountToEVMChainMultiplier);
375
}
376
377
+ if (output.sendAll && transactionCurrency is Erc20Token) {
378
+ totalAmount = erc20Balance.balance;
379
+ }
380
+
381
+ final estimateFees = await calculateActualEstimatedFeeForCreateTransaction(
382
+ amount: totalAmount,
383
+ receivingAddressHex: toAddress,
384
+ priority: _credentials.priority!,
385
+ contractAddress: contractAddress,
386
+ );
387
+
388
+ estimatedFeesForTransaction = BigInt.from(estimateFees);
389
+
390
+ debugPrint('Estimated Fees for Transaction: $estimatedFeesForTransaction');
391
+
392
+ if (output.sendAll && transactionCurrency is! Erc20Token) {
393
+ totalAmount = (erc20Balance.balance - estimatedFeesForTransaction);
394
+
395
+ if (estimatedFeesForTransaction > erc20Balance.balance) {
396
+ throw EVMChainTransactionFeesException();
397
+ }
398
+ }
399
+
400
if (erc20Balance.balance < totalAmount) {
401
throw EVMChainTransactionCreationException(transactionCurrency);
402
}
@@ -312,11 +409,9 @@ abstract class EVMChainWalletBase
409
410
final pendingEVMChainTransaction = await _client.signTransaction(
411
privateKey: _evmChainPrivateKey,
315
- toAddress: _credentials.outputs.first.isParsedAddress
316
- ? _credentials.outputs.first.extractedAddress!
317
- : _credentials.outputs.first.address,
412
+ toAddress: toAddress,
413
amount: totalAmount,
319
- gas: _estimatedGas!,
414
+ gas: estimatedFeesForTransaction,
415
priority: _credentials.priority!,
416
currency: transactionCurrency,
417
exponent: exponent,
@@ -483,6 +578,7 @@ abstract class EVMChainWalletBase
578
return EthPrivateKey.fromHex(HEX.encode(addressAtIndex.privateKey as List<int>));
579
}
580
581
+ @override
582
Future<void>? updateBalance() async => await _updateBalance();
583
584
List<Erc20Token> get erc20Currencies => evmChainErc20TokensBox.values.toList();