mirror of
https://github.com/cake-tech/cake_wallet.git
synced 2024-11-16 17:27:37 +00:00
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:
parent
4410101672
commit
415d2a3573
3 changed files with 186 additions and 45 deletions
|
@ -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.
|
||||
///
|
||||
|
|
|
@ -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<int> getGasUnitPrice() async {
|
||||
try {
|
||||
final gasPrice = await _client!.getGasPrice();
|
||||
|
||||
return gasPrice.getInWei.toInt();
|
||||
} catch (_) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
Future<int> getEstimatedGas() async {
|
||||
Future<int> 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<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 (_) {
|
||||
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<String, dynamic>;
|
||||
|
||||
|
||||
final symbol = (decodedResponse['symbol'] ?? '') as String;
|
||||
String filteredSymbol = symbol.replaceFirst(RegExp('^\\\$'), '');
|
||||
|
||||
|
|
|
@ -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<int> 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<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
|
||||
Future<PendingTransaction> 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<int>));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void>? updateBalance() async => await _updateBalance();
|
||||
|
||||
List<Erc20Token> get erc20Currencies => evmChainErc20TokensBox.values.toList();
|
||||
|
|
Loading…
Reference in a new issue