From 415d2a35736a13fce200517c3f0e603573cf41c0 Mon Sep 17 00:00:00 2001 From: Adegoke David <64401859+Blazebrain@users.noreply.github.com> Date: Sun, 21 Jul 2024 00:04:22 +0100 Subject: [PATCH] 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 --- cw_evm/lib/contract/erc20.dart | 4 +- cw_evm/lib/evm_chain_client.dart | 65 +++++++++++-- cw_evm/lib/evm_chain_wallet.dart | 162 ++++++++++++++++++++++++------- 3 files changed, 186 insertions(+), 45 deletions(-) diff --git a/cw_evm/lib/contract/erc20.dart b/cw_evm/lib/contract/erc20.dart index 297b77e71..76d45064f 100644 --- a/cw_evm/lib/contract/erc20.dart +++ b/cw_evm/lib/contract/erc20.dart @@ -2,7 +2,7 @@ import 'dart:typed_data'; import 'package:web3dart/web3dart.dart' as web3; -final _contractAbi = web3.ContractAbi.fromJson( +final ethereumContractAbi = web3.ContractAbi.fromJson( '[{"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"}]', 'Erc20'); @@ -13,7 +13,7 @@ class ERC20 extends web3.GeneratedContract { required web3.EthereumAddress address, required web3.Web3Client client, int? chainId, - }) : super(web3.DeployedContract(_contractAbi, address), client, chainId); + }) : super(web3.DeployedContract(ethereumContractAbi, address), client, chainId); /// 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. /// diff --git a/cw_evm/lib/evm_chain_client.dart b/cw_evm/lib/evm_chain_client.dart index 2185936ea..033dc7143 100644 --- a/cw_evm/lib/evm_chain_client.dart +++ b/cw_evm/lib/evm_chain_client.dart @@ -10,7 +10,7 @@ import 'package:cw_evm/evm_chain_transaction_priority.dart'; import 'package:cw_evm/evm_erc20_balance.dart'; import 'package:cw_evm/pending_evm_chain_transaction.dart'; import 'package:cw_evm/.secrets.g.dart' as secrets; -import 'package:flutter/services.dart'; +import 'package:flutter/foundation.dart'; import 'package:hex/hex.dart' as hex; import 'package:http/http.dart'; import 'package:web3dart/web3dart.dart'; @@ -65,16 +65,65 @@ abstract class EVMChainClient { Future getGasUnitPrice() async { try { final gasPrice = await _client!.getGasPrice(); + return gasPrice.getInWei.toInt(); } catch (_) { return 0; } } - Future getEstimatedGas() async { + Future getGasBaseFee() async { try { - final estimatedGas = await _client!.estimateGas(); - return estimatedGas.toInt(); + final blockInfo = await _client!.getBlockInformation(isContainFullObj: false); + final baseFee = blockInfo.baseFeePerGas; + + return baseFee!.getInWei.toInt(); + } catch (_) { + return 0; + } + } + + Future getEstimatedGas({ + String? contractAddress, + required EthereumAddress toAddress, + required EthereumAddress senderAddress, + required EtherAmount value, + EtherAmount? gasPrice, + // EtherAmount? maxFeePerGas, + // EtherAmount? maxPriorityFeePerGas, + }) async { + try { + if (contractAddress == null) { + final estimatedGas = await _client!.estimateGas( + sender: senderAddress, + gasPrice: gasPrice, + to: toAddress, + value: value, + // maxPriorityFeePerGas: maxPriorityFeePerGas, + // maxFeePerGas: maxFeePerGas, + ); + + return estimatedGas.toInt(); + } else { + final contract = DeployedContract( + ethereumContractAbi, + EthereumAddress.fromHex(contractAddress), + ); + + final transferFunction = contract.function('transferFrom'); + + final estimatedGas = await _client!.estimateGas( + sender: senderAddress, + to: toAddress, + value: value, + data: transferFunction.encodeCall([ + senderAddress, + toAddress, + value.getInWei, + ]), + ); + return estimatedGas.toInt(); + } } catch (_) { return 0; } @@ -84,7 +133,7 @@ abstract class EVMChainClient { required Credentials privateKey, required String toAddress, required BigInt amount, - required int gas, + required BigInt gas, required EVMChainTransactionPriority priority, required CryptoCurrency currency, required int exponent, @@ -97,8 +146,6 @@ abstract class EVMChainClient { bool isNativeToken = currency == CryptoCurrency.eth || currency == CryptoCurrency.maticpoly; - final price = _client!.getGasPrice(); - final Transaction transaction = createTransaction( from: privateKey.address, to: EthereumAddress.fromHex(toAddress), @@ -130,11 +177,10 @@ abstract class EVMChainClient { _sendTransaction = () async => await sendTransaction(signedTransaction); - return PendingEVMChainTransaction( signedTransaction: signedTransaction, amount: amount.toString(), - fee: BigInt.from(gas) * (await price).getInWei, + fee: gas, sendTransaction: _sendTransaction, exponent: exponent, ); @@ -233,7 +279,6 @@ abstract class EVMChainClient { final decodedResponse = jsonDecode(response.body)[0] as Map; - final symbol = (decodedResponse['symbol'] ?? '') as String; String filteredSymbol = symbol.replaceFirst(RegExp('^\\\$'), ''); diff --git a/cw_evm/lib/evm_chain_wallet.dart b/cw_evm/lib/evm_chain_wallet.dart index 2adb54746..2ab1c17a0 100644 --- a/cw_evm/lib/evm_chain_wallet.dart +++ b/cw_evm/lib/evm_chain_wallet.dart @@ -27,6 +27,7 @@ import 'package:cw_evm/evm_chain_transaction_priority.dart'; import 'package:cw_evm/evm_chain_wallet_addresses.dart'; import 'package:cw_evm/evm_ledger_credentials.dart'; import 'package:cw_evm/file.dart'; +import 'package:flutter/foundation.dart'; import 'package:hex/hex.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; @@ -102,10 +103,12 @@ abstract class EVMChainWalletBase Credentials get evmChainPrivateKey => _evmChainPrivateKey; - late EVMChainClient _client; + late final EVMChainClient _client; + + int gasPrice = 0; + int? gasBaseFee = 0; + int estimatedGasUnits = 0; - int? _gasPrice; - int? _estimatedGas; bool _isTransactionUpdating; // TODO: remove after integrating our own node and having eth_newPendingTransactionFilter @@ -173,12 +176,70 @@ abstract class EVMChainWalletBase @override int calculateEstimatedFee(TransactionPriority priority, int? amount) { + { + try { + if (priority is EVMChainTransactionPriority) { + final priorityFee = EtherAmount.fromInt(EtherUnit.gwei, priority.tip).getInWei.toInt(); + + int maxFeePerGas; + if (gasBaseFee != null) { + // MaxFeePerGas with EIP1559; + maxFeePerGas = gasBaseFee! + priorityFee; + } else { + // MaxFeePerGas with gasPrice; + maxFeePerGas = gasPrice; + debugPrint('MaxFeePerGas with gasPrice: $maxFeePerGas'); + } + + final totalGasFee = estimatedGasUnits * maxFeePerGas; + return totalGasFee; + } + + return 0; + } catch (e) { + return 0; + } + } + } + + /// Allows more customization to the fetch estimatedFees flow. + /// + /// We are able to pass in: + /// - The exact amount the user wants to send, + /// - The addressHex for the receiving wallet, + /// - A contract address which would be essential in determining if to calcualate the estimate for ERC20 or native ETH + Future calculateActualEstimatedFeeForCreateTransaction({ + required amount, + required String? contractAddress, + required String receivingAddressHex, + required TransactionPriority priority, + }) async { try { if (priority is EVMChainTransactionPriority) { final priorityFee = EtherAmount.fromInt(EtherUnit.gwei, priority.tip).getInWei.toInt(); - return (_gasPrice! + priorityFee) * (_estimatedGas ?? 0); - } + int maxFeePerGas; + if (gasBaseFee != null) { + // MaxFeePerGas with EIP1559; + maxFeePerGas = gasBaseFee! + priorityFee; + } else { + // MaxFeePerGas with gasPrice + maxFeePerGas = gasPrice; + } + + final estimatedGas = await _client.getEstimatedGas( + contractAddress: contractAddress, + senderAddress: _evmChainPrivateKey.address, + value: EtherAmount.fromBigInt(EtherUnit.wei, amount!), + gasPrice: EtherAmount.fromInt(EtherUnit.wei, gasPrice), + toAddress: EthereumAddress.fromHex(receivingAddressHex), + // maxFeePerGas: EtherAmount.fromInt(EtherUnit.wei, maxFeePerGas), + // maxPriorityFeePerGas: EtherAmount.fromInt(EtherUnit.gwei, priority.tip), + ); + + final totalGasFee = estimatedGas * maxFeePerGas; + return totalGasFee; + } return 0; } catch (e) { return 0; @@ -225,13 +286,12 @@ abstract class EVMChainWalletBase syncStatus = AttemptingSyncStatus(); await _updateBalance(); await _updateTransactions(); - _gasPrice = await _client.getGasUnitPrice(); - _estimatedGas = await _client.getEstimatedGas(); - Timer.periodic( - const Duration(minutes: 1), (timer) async => _gasPrice = await _client.getGasUnitPrice()); - Timer.periodic(const Duration(seconds: 10), - (timer) async => _estimatedGas = await _client.getEstimatedGas()); + await _updateEstimatedGasFeeParams(); + + Timer.periodic(const Duration(seconds: 10), (timer) async { + await _updateEstimatedGasFeeParams(); + }); syncStatus = SyncedSyncStatus(); } catch (e) { @@ -239,6 +299,19 @@ abstract class EVMChainWalletBase } } + Future _updateEstimatedGasFeeParams() async { + gasBaseFee = await _client.getGasBaseFee(); + + gasPrice = await _client.getGasUnitPrice(); + + estimatedGasUnits = await _client.getEstimatedGas( + senderAddress: _evmChainPrivateKey.address, + toAddress: _evmChainPrivateKey.address, + gasPrice: EtherAmount.fromInt(EtherUnit.wei, gasPrice), + value: EtherAmount.fromBigInt(EtherUnit.wei, BigInt.one), + ); + } + @override Future createTransaction(Object credentials) async { final _credentials = credentials as EVMChainTransactionCredentials; @@ -258,8 +331,17 @@ abstract class EVMChainWalletBase final erc20Balance = balance[transactionCurrency]!; BigInt totalAmount = BigInt.zero; + BigInt estimatedFeesForTransaction = BigInt.zero; int exponent = transactionCurrency is Erc20Token ? transactionCurrency.decimal : 18; num amountToEVMChainMultiplier = pow(10, exponent); + String? contractAddress; + String toAddress = _credentials.outputs.first.isParsedAddress + ? _credentials.outputs.first.extractedAddress! + : _credentials.outputs.first.address; + + if (transactionCurrency is Erc20Token) { + contractAddress = transactionCurrency.contractAddress; + } // so far this can not be made with Ethereum as Ethereum does not support multiple recipients if (hasMultiDestination) { @@ -271,35 +353,50 @@ abstract class EVMChainWalletBase outputs.fold(0, (acc, value) => acc + (value.formattedCryptoAmount ?? 0))); totalAmount = BigInt.from(totalOriginalAmount * amountToEVMChainMultiplier); + final estimateFees = await calculateActualEstimatedFeeForCreateTransaction( + amount: totalAmount, + receivingAddressHex: toAddress, + priority: _credentials.priority!, + contractAddress: contractAddress, + ); + + estimatedFeesForTransaction = BigInt.from(estimateFees); + if (erc20Balance.balance < totalAmount) { throw EVMChainTransactionCreationException(transactionCurrency); } } else { final output = outputs.first; - // since the fees are taken from Ethereum - // then no need to subtract the fees from the amount if send all - final BigInt allAmount; - if (transactionCurrency is Erc20Token) { - allAmount = erc20Balance.balance; - } else { - final estimatedFee = BigInt.from(calculateEstimatedFee(_credentials.priority!, null)); - - if (estimatedFee > erc20Balance.balance) { - throw EVMChainTransactionFeesException(); - } - - allAmount = erc20Balance.balance - estimatedFee; - } - - if (output.sendAll) { - totalAmount = allAmount; - } else { + if (!output.sendAll) { final totalOriginalAmount = EVMChainFormatter.parseEVMChainAmountToDouble(output.formattedCryptoAmount ?? 0); totalAmount = BigInt.from(totalOriginalAmount * amountToEVMChainMultiplier); } + if (output.sendAll && transactionCurrency is Erc20Token) { + totalAmount = erc20Balance.balance; + } + + final estimateFees = await calculateActualEstimatedFeeForCreateTransaction( + amount: totalAmount, + receivingAddressHex: toAddress, + priority: _credentials.priority!, + contractAddress: contractAddress, + ); + + estimatedFeesForTransaction = BigInt.from(estimateFees); + + debugPrint('Estimated Fees for Transaction: $estimatedFeesForTransaction'); + + if (output.sendAll && transactionCurrency is! Erc20Token) { + totalAmount = (erc20Balance.balance - estimatedFeesForTransaction); + + if (estimatedFeesForTransaction > erc20Balance.balance) { + throw EVMChainTransactionFeesException(); + } + } + if (erc20Balance.balance < totalAmount) { throw EVMChainTransactionCreationException(transactionCurrency); } @@ -312,11 +409,9 @@ abstract class EVMChainWalletBase final pendingEVMChainTransaction = await _client.signTransaction( privateKey: _evmChainPrivateKey, - toAddress: _credentials.outputs.first.isParsedAddress - ? _credentials.outputs.first.extractedAddress! - : _credentials.outputs.first.address, + toAddress: toAddress, amount: totalAmount, - gas: _estimatedGas!, + gas: estimatedFeesForTransaction, priority: _credentials.priority!, currency: transactionCurrency, exponent: exponent, @@ -483,6 +578,7 @@ abstract class EVMChainWalletBase return EthPrivateKey.fromHex(HEX.encode(addressAtIndex.privateKey as List)); } + @override Future? updateBalance() async => await _updateBalance(); List get erc20Currencies => evmChainErc20TokensBox.values.toList();