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();