CW-672: Enhance ETH Transaction Fee Calculation (#1545)

* fix: Eth transaction fees WIP

* Revert "fix: Eth transaction fees WIP"

This reverts commit b9a469bc7e.

* 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>
This commit is contained in:
Adegoke David 2024-07-21 00:04:22 +01:00 committed by GitHub
parent 4410101672
commit 415d2a3573
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 186 additions and 45 deletions

View file

@ -2,7 +2,7 @@ import 'dart:typed_data';
import 'package:web3dart/web3dart.dart' as web3; 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"}]', '[{"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'); 'Erc20');
@ -13,7 +13,7 @@ class ERC20 extends web3.GeneratedContract {
required web3.EthereumAddress address, required web3.EthereumAddress address,
required web3.Web3Client client, required web3.Web3Client client,
int? chainId, 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. /// 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.
/// ///

View file

@ -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/evm_erc20_balance.dart';
import 'package:cw_evm/pending_evm_chain_transaction.dart'; import 'package:cw_evm/pending_evm_chain_transaction.dart';
import 'package:cw_evm/.secrets.g.dart' as secrets; 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:hex/hex.dart' as hex;
import 'package:http/http.dart'; import 'package:http/http.dart';
import 'package:web3dart/web3dart.dart'; import 'package:web3dart/web3dart.dart';
@ -65,16 +65,65 @@ abstract class EVMChainClient {
Future<int> getGasUnitPrice() async { Future<int> getGasUnitPrice() async {
try { try {
final gasPrice = await _client!.getGasPrice(); final gasPrice = await _client!.getGasPrice();
return gasPrice.getInWei.toInt(); return gasPrice.getInWei.toInt();
} catch (_) { } catch (_) {
return 0; return 0;
} }
} }
Future<int> getEstimatedGas() async { Future<int> getGasBaseFee() async {
try { try {
final estimatedGas = await _client!.estimateGas(); final blockInfo = await _client!.getBlockInformation(isContainFullObj: false);
return estimatedGas.toInt(); final baseFee = blockInfo.baseFeePerGas;
return baseFee!.getInWei.toInt();
} catch (_) {
return 0;
}
}
Future<int> 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 (_) { } catch (_) {
return 0; return 0;
} }
@ -84,7 +133,7 @@ abstract class EVMChainClient {
required Credentials privateKey, required Credentials privateKey,
required String toAddress, required String toAddress,
required BigInt amount, required BigInt amount,
required int gas, required BigInt gas,
required EVMChainTransactionPriority priority, required EVMChainTransactionPriority priority,
required CryptoCurrency currency, required CryptoCurrency currency,
required int exponent, required int exponent,
@ -97,8 +146,6 @@ abstract class EVMChainClient {
bool isNativeToken = currency == CryptoCurrency.eth || currency == CryptoCurrency.maticpoly; bool isNativeToken = currency == CryptoCurrency.eth || currency == CryptoCurrency.maticpoly;
final price = _client!.getGasPrice();
final Transaction transaction = createTransaction( final Transaction transaction = createTransaction(
from: privateKey.address, from: privateKey.address,
to: EthereumAddress.fromHex(toAddress), to: EthereumAddress.fromHex(toAddress),
@ -130,11 +177,10 @@ abstract class EVMChainClient {
_sendTransaction = () async => await sendTransaction(signedTransaction); _sendTransaction = () async => await sendTransaction(signedTransaction);
return PendingEVMChainTransaction( return PendingEVMChainTransaction(
signedTransaction: signedTransaction, signedTransaction: signedTransaction,
amount: amount.toString(), amount: amount.toString(),
fee: BigInt.from(gas) * (await price).getInWei, fee: gas,
sendTransaction: _sendTransaction, sendTransaction: _sendTransaction,
exponent: exponent, exponent: exponent,
); );
@ -233,7 +279,6 @@ abstract class EVMChainClient {
final decodedResponse = jsonDecode(response.body)[0] as Map<String, dynamic>; final decodedResponse = jsonDecode(response.body)[0] as Map<String, dynamic>;
final symbol = (decodedResponse['symbol'] ?? '') as String; final symbol = (decodedResponse['symbol'] ?? '') as String;
String filteredSymbol = symbol.replaceFirst(RegExp('^\\\$'), ''); String filteredSymbol = symbol.replaceFirst(RegExp('^\\\$'), '');

View file

@ -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_chain_wallet_addresses.dart';
import 'package:cw_evm/evm_ledger_credentials.dart'; import 'package:cw_evm/evm_ledger_credentials.dart';
import 'package:cw_evm/file.dart'; import 'package:cw_evm/file.dart';
import 'package:flutter/foundation.dart';
import 'package:hex/hex.dart'; import 'package:hex/hex.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:mobx/mobx.dart'; import 'package:mobx/mobx.dart';
@ -102,10 +103,12 @@ abstract class EVMChainWalletBase
Credentials get evmChainPrivateKey => _evmChainPrivateKey; 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; bool _isTransactionUpdating;
// TODO: remove after integrating our own node and having eth_newPendingTransactionFilter // TODO: remove after integrating our own node and having eth_newPendingTransactionFilter
@ -173,12 +176,70 @@ abstract class EVMChainWalletBase
@override @override
int calculateEstimatedFee(TransactionPriority priority, int? amount) { 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<int> calculateActualEstimatedFeeForCreateTransaction({
required amount,
required String? contractAddress,
required String receivingAddressHex,
required TransactionPriority priority,
}) async {
try { try {
if (priority is EVMChainTransactionPriority) { if (priority is EVMChainTransactionPriority) {
final priorityFee = EtherAmount.fromInt(EtherUnit.gwei, priority.tip).getInWei.toInt(); 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; return 0;
} catch (e) { } catch (e) {
return 0; return 0;
@ -225,13 +286,12 @@ abstract class EVMChainWalletBase
syncStatus = AttemptingSyncStatus(); syncStatus = AttemptingSyncStatus();
await _updateBalance(); await _updateBalance();
await _updateTransactions(); await _updateTransactions();
_gasPrice = await _client.getGasUnitPrice();
_estimatedGas = await _client.getEstimatedGas();
Timer.periodic( await _updateEstimatedGasFeeParams();
const Duration(minutes: 1), (timer) async => _gasPrice = await _client.getGasUnitPrice());
Timer.periodic(const Duration(seconds: 10), Timer.periodic(const Duration(seconds: 10), (timer) async {
(timer) async => _estimatedGas = await _client.getEstimatedGas()); await _updateEstimatedGasFeeParams();
});
syncStatus = SyncedSyncStatus(); syncStatus = SyncedSyncStatus();
} catch (e) { } catch (e) {
@ -239,6 +299,19 @@ abstract class EVMChainWalletBase
} }
} }
Future<void> _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 @override
Future<PendingTransaction> createTransaction(Object credentials) async { Future<PendingTransaction> createTransaction(Object credentials) async {
final _credentials = credentials as EVMChainTransactionCredentials; final _credentials = credentials as EVMChainTransactionCredentials;
@ -258,8 +331,17 @@ abstract class EVMChainWalletBase
final erc20Balance = balance[transactionCurrency]!; final erc20Balance = balance[transactionCurrency]!;
BigInt totalAmount = BigInt.zero; BigInt totalAmount = BigInt.zero;
BigInt estimatedFeesForTransaction = BigInt.zero;
int exponent = transactionCurrency is Erc20Token ? transactionCurrency.decimal : 18; int exponent = transactionCurrency is Erc20Token ? transactionCurrency.decimal : 18;
num amountToEVMChainMultiplier = pow(10, exponent); 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 // so far this can not be made with Ethereum as Ethereum does not support multiple recipients
if (hasMultiDestination) { if (hasMultiDestination) {
@ -271,35 +353,50 @@ abstract class EVMChainWalletBase
outputs.fold(0, (acc, value) => acc + (value.formattedCryptoAmount ?? 0))); outputs.fold(0, (acc, value) => acc + (value.formattedCryptoAmount ?? 0)));
totalAmount = BigInt.from(totalOriginalAmount * amountToEVMChainMultiplier); 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) { if (erc20Balance.balance < totalAmount) {
throw EVMChainTransactionCreationException(transactionCurrency); throw EVMChainTransactionCreationException(transactionCurrency);
} }
} else { } else {
final output = outputs.first; final output = outputs.first;
// since the fees are taken from Ethereum if (!output.sendAll) {
// 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 {
final totalOriginalAmount = final totalOriginalAmount =
EVMChainFormatter.parseEVMChainAmountToDouble(output.formattedCryptoAmount ?? 0); EVMChainFormatter.parseEVMChainAmountToDouble(output.formattedCryptoAmount ?? 0);
totalAmount = BigInt.from(totalOriginalAmount * amountToEVMChainMultiplier); 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) { if (erc20Balance.balance < totalAmount) {
throw EVMChainTransactionCreationException(transactionCurrency); throw EVMChainTransactionCreationException(transactionCurrency);
} }
@ -312,11 +409,9 @@ abstract class EVMChainWalletBase
final pendingEVMChainTransaction = await _client.signTransaction( final pendingEVMChainTransaction = await _client.signTransaction(
privateKey: _evmChainPrivateKey, privateKey: _evmChainPrivateKey,
toAddress: _credentials.outputs.first.isParsedAddress toAddress: toAddress,
? _credentials.outputs.first.extractedAddress!
: _credentials.outputs.first.address,
amount: totalAmount, amount: totalAmount,
gas: _estimatedGas!, gas: estimatedFeesForTransaction,
priority: _credentials.priority!, priority: _credentials.priority!,
currency: transactionCurrency, currency: transactionCurrency,
exponent: exponent, exponent: exponent,
@ -483,6 +578,7 @@ abstract class EVMChainWalletBase
return EthPrivateKey.fromHex(HEX.encode(addressAtIndex.privateKey as List<int>)); return EthPrivateKey.fromHex(HEX.encode(addressAtIndex.privateKey as List<int>));
} }
@override
Future<void>? updateBalance() async => await _updateBalance(); Future<void>? updateBalance() async => await _updateBalance();
List<Erc20Token> get erc20Currencies => evmChainErc20TokensBox.values.toList(); List<Erc20Token> get erc20Currencies => evmChainErc20TokensBox.values.toList();