fix: walletconnect fee refresh (#3153)

* fix: refresh EIP-1559 fees before sign for walletconnect transactions

* fix: add missing data to configure file

---------

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>
This commit is contained in:
David Adegoke
2026-04-09 10:45:14 +02:00
committed by GitHub
co-authored by Omar Hatem
parent fd62cd5b52
commit ef752c747e
5 changed files with 209 additions and 80 deletions
+65 -39
View File
@@ -99,7 +99,8 @@ abstract class EVMChainWalletBase
_isTransactionUpdating = false,
_client = client,
selectedChainId = initialChainId ?? _getInitialChainId(walletInfo.type),
walletAddresses = EVMChainWalletAddresses(walletInfo, initialChainId ?? _getInitialChainId(walletInfo.type)),
walletAddresses = EVMChainWalletAddresses(
walletInfo, initialChainId ?? _getInitialChainId(walletInfo.type)),
balance = ObservableMap<CryptoCurrency, EVMChainERC20Balance>.of(
{
nativeCurrency: initialBalance ?? EVMChainERC20Balance(BigInt.zero),
@@ -779,27 +780,13 @@ abstract class EVMChainWalletBase
throw EVMChainTransactionFeesException('Failed to retrieve gas price from node');
}
int maxFeePerGas;
int adjustedGasPrice;
if (gasBaseFee != null && gasBaseFee > 0) {
// For chains with base fee, add priority fee (if supported) and a buffer to account for base fee increases
// Base fee can increase between estimation and transaction submission
final baseFeeWithPriority = gasBaseFee + priorityFee;
// For chains without priority fees (e.g., Arbitrum), use a 5% buffer
// For chains with priority fees (e.g., Ethereum), use a 15% buffer to account for base fee volatility
// Base fee can increase significantly during high network activity
final bufferMultiplier = hasPriorityFee ? 115 : 105;
final bufferPercent = (baseFeeWithPriority * bufferMultiplier) ~/ 100;
final bufferMin = baseFeeWithPriority + (baseFeeWithPriority ~/ 100);
maxFeePerGas = bufferPercent > bufferMin ? bufferPercent : bufferMin;
} else {
// Fallback to gasPrice if baseFee is not available
maxFeePerGas = gasPrice + priorityFee;
}
adjustedGasPrice = maxFeePerGas;
final maxFeePerGas = EVMChainUtils.computeBufferedMaxFeePerGasWei(
gasBaseFee: gasBaseFee,
gasPrice: gasPrice,
priorityFeeWei: priorityFee,
chainHasPriorityFee: hasPriorityFee,
);
final adjustedGasPrice = maxFeePerGas;
final estimatedGas = await _client.getEstimatedGasUnitsForTransaction(
contractAddress: contractAddress,
@@ -829,6 +816,39 @@ abstract class EVMChainWalletBase
}
}
Future<WalletConnectBufferedFeeData?> getWCBufferedFeeQuote(TransactionPriority priority) async {
try {
final gasBaseFee = await _client.getGasBaseFee();
final gasPrice = await _client.getGasUnitPrice();
if (gasPrice <= 0) {
printV('WC fee quote: invalid gas price $gasPrice');
return null;
}
int priorityFee = 0;
if (hasPriorityFee && priority is EVMChainTransactionPriority) {
priorityFee = getTotalPriorityFee(priority);
}
final maxFee = EVMChainUtils.computeBufferedMaxFeePerGasWei(
gasBaseFee: gasBaseFee,
gasPrice: gasPrice,
priorityFeeWei: priorityFee,
chainHasPriorityFee: hasPriorityFee,
);
return WalletConnectBufferedFeeData(
maxFeePerGasWei: maxFee,
maxPriorityFeePerGasWei: priorityFee,
latestBaseFeeWei: gasBaseFee,
);
} catch (e, s) {
printV('getWalletConnectBufferedFeeQuote: $e\n$s');
return null;
}
}
@override
Future<void> changePassword(String password) {
throw UnimplementedError("changePassword");
@@ -1048,15 +1068,14 @@ abstract class EVMChainWalletBase
}
Future<PendingTransaction> createCallDataTransaction(
String to,
String dataHex,
BigInt valueWei,
EVMChainTransactionPriority? priority,
String? sourceTokenAddress,
BigInt? sourceTokenAmount, {
bool useBlinkProtection = true,
}) async {
String to,
String dataHex,
BigInt valueWei,
EVMChainTransactionPriority? priority,
String? sourceTokenAddress,
BigInt? sourceTokenAmount, {
bool useBlinkProtection = true,
}) async {
// Define Native Currency
final nativeCurrency = switch (selectedChainId) {
137 => CryptoCurrency.maticpoly,
@@ -1099,12 +1118,9 @@ abstract class EVMChainWalletBase
cleanAddress == '0x0000000000000000000000000000000000000000';
if (!isNativeSource && sourceTokenAmount != null && sourceTokenAmount > BigInt.zero) {
// Filter list to find match.
final matchingTokens = balance.keys.where((k) =>
k is Erc20Token &&
k.contractAddress.toLowerCase() == cleanAddress
);
final matchingTokens = balance.keys
.where((k) => k is Erc20Token && k.contractAddress.toLowerCase() == cleanAddress);
if (matchingTokens.isEmpty) {
// Token is not in the wallet balance map -> Balance is 0
@@ -1262,9 +1278,7 @@ abstract class EVMChainWalletBase
if (existingTxInfo == null) {
result[transactionModel.hash] = newTxInfo;
}
else if (newTxInfo.direction == TransactionDirection.incoming &&
} else if (newTxInfo.direction == TransactionDirection.incoming &&
existingTxInfo.direction == TransactionDirection.outgoing) {
result[transactionModel.hash] = newTxInfo;
}
@@ -1760,3 +1774,15 @@ class MoralisDiscoveryResult {
static const MoralisDiscoveryResult empty = MoralisDiscoveryResult(newTokens: []);
}
class WalletConnectBufferedFeeData {
const WalletConnectBufferedFeeData({
required this.maxFeePerGasWei,
required this.maxPriorityFeePerGasWei,
this.latestBaseFeeWei,
});
final int maxFeePerGasWei;
final int maxPriorityFeePerGasWei;
final int? latestBaseFeeWei;
}
+16
View File
@@ -22,6 +22,22 @@ class EVMChainUtils {
};
}
static int computeBufferedMaxFeePerGasWei({
required int? gasBaseFee,
required int gasPrice,
required int priorityFeeWei,
required bool chainHasPriorityFee,
}) {
if (gasBaseFee != null && gasBaseFee > 0) {
final baseFeeWithPriority = gasBaseFee + priorityFeeWei;
final bufferMultiplier = chainHasPriorityFee ? 115 : 105;
final bufferPercent = (baseFeeWithPriority * bufferMultiplier) ~/ 100;
final bufferMin = baseFeeWithPriority + (baseFeeWithPriority ~/ 100);
return bufferPercent > bufferMin ? bufferPercent : bufferMin;
}
return gasPrice + priorityFeeWei;
}
static String getErc20TokensBoxName(String walletName, int chainId) {
final sanitizedName = walletName.replaceAll(" ", "_");
+20 -6
View File
@@ -239,10 +239,7 @@ class CWEVM extends EVM {
(wallet as EVMChainWallet).isApprovalRequired(tokenContract, spender, requiredAmount);
@override
Future<BigInt?> getAllowance(
WalletBase wallet,
String tokenContract,
String spender) =>
Future<BigInt?> getAllowance(WalletBase wallet, String tokenContract, String spender) =>
(wallet as EVMChainWallet).getAllowance(tokenContract, spender);
@override
@@ -272,7 +269,7 @@ class CWEVM extends EVM {
String to,
String dataHex,
BigInt valueWei,
TransactionPriority? priority,{
TransactionPriority? priority, {
bool useBlinkProtection = true,
String? sourceTokenAddress,
BigInt? sourceTokenAmount,
@@ -481,7 +478,7 @@ class CWEVM extends EVM {
@override
BigInt? getERC20AvailableBalance(Object balance) {
if(balance is EVMChainERC20Balance) {
if (balance is EVMChainERC20Balance) {
return balance.balance;
}
return null;
@@ -544,6 +541,23 @@ class CWEVM extends EVM {
@override
bool hasPriorityFee(int chainId) => EVMChainUtils.hasPriorityFee(chainId);
@override
Future<EvmWalletConnectFeeQuote?> getWCBufferedFeeQuote(
WalletBase wallet,
TransactionPriority priority,
) async {
if (wallet is! EVMChainWallet) return null;
final data = await wallet.getWCBufferedFeeQuote(priority);
if (data == null) return null;
return EvmWalletConnectFeeQuote(
maxFeePerGasWei: data.maxFeePerGasWei,
maxPriorityFeePerGasWei: data.maxPriorityFeePerGasWei,
latestBaseFeeWei: data.latestBaseFeeWei,
);
}
Future<({double usdValue, bool hasValidFiatPrice})> _getTokenUsdValueAndFiatCheck(
Erc20Token token,
BigInt balanceWei,
@@ -432,43 +432,14 @@ class EvmChainServiceImpl {
}
}
// we need to check if dApp provides the gas values and if not, we need to estimate them
final hasGasLimit = transaction.maxGas != null && transaction.maxGas! > 0;
final hasGasPrice = transaction.gasPrice != null;
final hasMaxFeePerGas = transaction.maxFeePerGas != null;
final hasMaxPriorityFeePerGas = transaction.maxPriorityFeePerGas != null;
final needsGasEstimation = !hasGasLimit || (!hasGasPrice && !hasMaxFeePerGas);
if (needsGasEstimation) {
try {
final gasPrice = hasGasPrice ? transaction.gasPrice! : await ethClient.getGasPrice();
if (!hasGasLimit) {
final gasLimit = await ethClient.estimateGas(
sender: transaction.from,
to: transaction.to,
value: transaction.value,
data: transaction.data,
gasPrice: gasPrice,
);
if (hasMaxFeePerGas || hasMaxPriorityFeePerGas) {
transaction = transaction.copyWith(maxGas: gasLimit.toInt());
} else {
transaction = transaction.copyWith(
gasPrice: hasGasPrice ? transaction.gasPrice : gasPrice,
maxGas: gasLimit.toInt(),
);
}
} else if (!hasGasPrice && !hasMaxFeePerGas) {
transaction = transaction.copyWith(gasPrice: gasPrice);
}
} on RPCError catch (e) {
return JsonRpcError(code: e.errorCode, message: e.message);
}
try {
transaction = await _ensureWCTransactionHasGasLimit(transaction);
} on RPCError catch (e) {
return JsonRpcError(code: e.errorCode, message: e.message);
}
transaction = await _applyWCBufferedFees(transaction);
final gweiGasPrice =
(transaction.gasPrice?.getInWei ?? transaction.maxFeePerGas?.getInWei ?? BigInt.zero) /
BigInt.from(1000000000);
@@ -500,6 +471,90 @@ class EvmChainServiceImpl {
return JsonRpcError(code: 5002, message: S.current.user_rejected_method);
}
Future<Transaction> _ensureWCTransactionHasGasLimit(Transaction transaction) async {
final hasGasLimit = transaction.maxGas != null && transaction.maxGas! > 0;
if (hasGasLimit) return transaction;
final hint = transaction.gasPrice ?? transaction.maxFeePerGas ?? await ethClient.getGasPrice();
final gasLimit = await ethClient.estimateGas(
sender: transaction.from,
to: transaction.to,
value: transaction.value,
data: transaction.data,
gasPrice: hint,
);
if (transaction.isEIP1559) {
return transaction.copyWith(maxGas: gasLimit.toInt());
}
return transaction.copyWith(
maxGas: gasLimit.toInt(),
gasPrice: transaction.gasPrice ?? hint,
);
}
Future<Transaction> _applyWCBufferedFees(Transaction transaction) async {
try {
final storedPriority =
appStore.settingsStore.getPriority(appStore.wallet!.type, chainId: reference.chainId);
final priority = storedPriority ?? evm!.getDefaultTransactionPriority();
final quote = await evm!.getWCBufferedFeeQuote(appStore.wallet!, priority);
if (quote != null) {
return _mergeWCBufferedFees(transaction, quote);
}
} catch (e) {
debugPrint('WalletConnect fee refresh failed: $e');
}
if (!transaction.isEIP1559 && transaction.gasPrice == null) {
return transaction.copyWith(gasPrice: await ethClient.getGasPrice());
}
return transaction;
}
Transaction _mergeWCBufferedFees(Transaction transaction, EvmWalletConnectFeeQuote quote) {
if (transaction.isEIP1559) {
// the fees coming from the dApp
final dAppMax = transaction.maxFeePerGas?.getInWei ?? BigInt.zero;
final dAppPri = transaction.maxPriorityFeePerGas?.getInWei ?? BigInt.zero;
// the updated fees coming from the wallet, handles buffered fees
final quoteMax = BigInt.from(quote.maxFeePerGasWei);
final quotePri = BigInt.from(quote.maxPriorityFeePerGasWei);
// we'll just use the higher of the two
var newMaxFeePerGasWei = dAppMax > quoteMax ? dAppMax : quoteMax;
var newPriorityFeePerGasWei = dAppPri > quotePri ? dAppPri : quotePri;
final base = quote.latestBaseFeeWei;
if (base != null) {
final baseB = BigInt.from(base);
final maxPriAllowed = newMaxFeePerGasWei - baseB;
if (newPriorityFeePerGasWei > maxPriAllowed) {
if (maxPriAllowed > BigInt.zero) {
newPriorityFeePerGasWei = maxPriAllowed;
} else {
newMaxFeePerGasWei = baseB + newPriorityFeePerGasWei;
}
}
}
return transaction.copyWith(
maxFeePerGas: EtherAmount.inWei(newMaxFeePerGasWei),
maxPriorityFeePerGas: EtherAmount.inWei(newPriorityFeePerGasWei),
);
}
final dPrice = transaction.gasPrice?.getInWei ?? BigInt.zero;
final floor = BigInt.from(quote.maxFeePerGasWei);
final newPriceWei = dPrice > floor ? dPrice : floor;
return transaction.copyWith(gasPrice: EtherAmount.inWei(newPriceWei));
}
void _onSessionRequest(SessionRequestEvent? args) async {
if (args != null && args.chainId == getChainId()) {
debugPrint('_onSessionRequest ${args.toString()}');
+18
View File
@@ -1555,6 +1555,12 @@ abstract class EVM {
bool hasPriorityFee(int chainId);
Future<EvmWalletConnectFeeQuote?> getWCBufferedFeeQuote(
WalletBase wallet,
TransactionPriority priority,
);
Future<void> discoverAndAddWalletTokens(WalletBase wallet);
}
@@ -1577,6 +1583,18 @@ class ChainInfo {
@override
int get hashCode => chainId.hashCode;
}
class EvmWalletConnectFeeQuote {
const EvmWalletConnectFeeQuote({
required this.maxFeePerGasWei,
required this.maxPriorityFeePerGasWei,
this.latestBaseFeeWei,
});
final int maxFeePerGasWei;
final int maxPriorityFeePerGasWei;
final int? latestBaseFeeWei;
}
""";
const evmEmptyDefinition = 'EVM? evm;\n';