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>

David Adegoke committed Apr 9, 2026 at 09:45 UTC ef752c747e32038877793cfdda5039252e6ca7cb
5 files changed +209 -80
cw_evm/lib/evm_chain_wallet.dart
+65 -39
@@ -99,7 +99,8 @@ abstract class EVMChainWalletBase
99 _isTransactionUpdating = false,
100 _client = client,
101 selectedChainId = initialChainId ?? _getInitialChainId(walletInfo.type),
102 - walletAddresses = EVMChainWalletAddresses(walletInfo, initialChainId ?? _getInitialChainId(walletInfo.type)),
102 + walletAddresses = EVMChainWalletAddresses(
103 + walletInfo, initialChainId ?? _getInitialChainId(walletInfo.type)),
104 balance = ObservableMap<CryptoCurrency, EVMChainERC20Balance>.of(
105 {
106 nativeCurrency: initialBalance ?? EVMChainERC20Balance(BigInt.zero),
@@ -779,27 +780,13 @@ abstract class EVMChainWalletBase
780 throw EVMChainTransactionFeesException('Failed to retrieve gas price from node');
781 }
782
782 - int maxFeePerGas;
783 - int adjustedGasPrice;
784 -
785 - if (gasBaseFee != null && gasBaseFee > 0) {
786 - // For chains with base fee, add priority fee (if supported) and a buffer to account for base fee increases
787 - // Base fee can increase between estimation and transaction submission
788 - final baseFeeWithPriority = gasBaseFee + priorityFee;
789 -
790 - // For chains without priority fees (e.g., Arbitrum), use a 5% buffer
791 - // For chains with priority fees (e.g., Ethereum), use a 15% buffer to account for base fee volatility
792 - // Base fee can increase significantly during high network activity
793 - final bufferMultiplier = hasPriorityFee ? 115 : 105;
794 - final bufferPercent = (baseFeeWithPriority * bufferMultiplier) ~/ 100;
795 - final bufferMin = baseFeeWithPriority + (baseFeeWithPriority ~/ 100);
796 - maxFeePerGas = bufferPercent > bufferMin ? bufferPercent : bufferMin;
797 - } else {
798 - // Fallback to gasPrice if baseFee is not available
799 - maxFeePerGas = gasPrice + priorityFee;
800 - }
801 -
802 - adjustedGasPrice = maxFeePerGas;
783 + final maxFeePerGas = EVMChainUtils.computeBufferedMaxFeePerGasWei(
784 + gasBaseFee: gasBaseFee,
785 + gasPrice: gasPrice,
786 + priorityFeeWei: priorityFee,
787 + chainHasPriorityFee: hasPriorityFee,
788 + );
789 + final adjustedGasPrice = maxFeePerGas;
790
791 final estimatedGas = await _client.getEstimatedGasUnitsForTransaction(
792 contractAddress: contractAddress,
@@ -829,6 +816,39 @@ abstract class EVMChainWalletBase
816 }
817 }
818
819 + Future<WalletConnectBufferedFeeData?> getWCBufferedFeeQuote(TransactionPriority priority) async {
820 + try {
821 + final gasBaseFee = await _client.getGasBaseFee();
822 + final gasPrice = await _client.getGasUnitPrice();
823 +
824 + if (gasPrice <= 0) {
825 + printV('WC fee quote: invalid gas price $gasPrice');
826 + return null;
827 + }
828 +
829 + int priorityFee = 0;
830 + if (hasPriorityFee && priority is EVMChainTransactionPriority) {
831 + priorityFee = getTotalPriorityFee(priority);
832 + }
833 +
834 + final maxFee = EVMChainUtils.computeBufferedMaxFeePerGasWei(
835 + gasBaseFee: gasBaseFee,
836 + gasPrice: gasPrice,
837 + priorityFeeWei: priorityFee,
838 + chainHasPriorityFee: hasPriorityFee,
839 + );
840 +
841 + return WalletConnectBufferedFeeData(
842 + maxFeePerGasWei: maxFee,
843 + maxPriorityFeePerGasWei: priorityFee,
844 + latestBaseFeeWei: gasBaseFee,
845 + );
846 + } catch (e, s) {
847 + printV('getWalletConnectBufferedFeeQuote: $e\n$s');
848 + return null;
849 + }
850 + }
851 +
852 @override
853 Future<void> changePassword(String password) {
854 throw UnimplementedError("changePassword");
@@ -1048,15 +1068,14 @@ abstract class EVMChainWalletBase
1068 }
1069
1070 Future<PendingTransaction> createCallDataTransaction(
1051 - String to,
1052 - String dataHex,
1053 - BigInt valueWei,
1054 - EVMChainTransactionPriority? priority,
1055 - String? sourceTokenAddress,
1056 - BigInt? sourceTokenAmount, {
1057 - bool useBlinkProtection = true,
1058 - }) async {
1059 -
1071 + String to,
1072 + String dataHex,
1073 + BigInt valueWei,
1074 + EVMChainTransactionPriority? priority,
1075 + String? sourceTokenAddress,
1076 + BigInt? sourceTokenAmount, {
1077 + bool useBlinkProtection = true,
1078 + }) async {
1079 // Define Native Currency
1080 final nativeCurrency = switch (selectedChainId) {
1081 137 => CryptoCurrency.maticpoly,
@@ -1099,12 +1118,9 @@ abstract class EVMChainWalletBase
1118 cleanAddress == '0x0000000000000000000000000000000000000000';
1119
1120 if (!isNativeSource && sourceTokenAmount != null && sourceTokenAmount > BigInt.zero) {
1102 -
1121 // Filter list to find match.
1104 - final matchingTokens = balance.keys.where((k) =>
1105 - k is Erc20Token &&
1106 - k.contractAddress.toLowerCase() == cleanAddress
1107 - );
1122 + final matchingTokens = balance.keys
1123 + .where((k) => k is Erc20Token && k.contractAddress.toLowerCase() == cleanAddress);
1124
1125 if (matchingTokens.isEmpty) {
1126 // Token is not in the wallet balance map -> Balance is 0
@@ -1262,9 +1278,7 @@ abstract class EVMChainWalletBase
1278
1279 if (existingTxInfo == null) {
1280 result[transactionModel.hash] = newTxInfo;
1265 - }
1266 -
1267 - else if (newTxInfo.direction == TransactionDirection.incoming &&
1281 + } else if (newTxInfo.direction == TransactionDirection.incoming &&
1282 existingTxInfo.direction == TransactionDirection.outgoing) {
1283 result[transactionModel.hash] = newTxInfo;
1284 }
@@ -1760,3 +1774,15 @@ class MoralisDiscoveryResult {
1774
1775 static const MoralisDiscoveryResult empty = MoralisDiscoveryResult(newTokens: []);
1776 }
1777 +
1778 +class WalletConnectBufferedFeeData {
1779 + const WalletConnectBufferedFeeData({
1780 + required this.maxFeePerGasWei,
1781 + required this.maxPriorityFeePerGasWei,
1782 + this.latestBaseFeeWei,
1783 + });
1784 +
1785 + final int maxFeePerGasWei;
1786 + final int maxPriorityFeePerGasWei;
1787 + final int? latestBaseFeeWei;
1788 +}
cw_evm/lib/utils/evm_chain_utils.dart
+16
@@ -22,6 +22,22 @@ class EVMChainUtils {
22 };
23 }
24
25 + static int computeBufferedMaxFeePerGasWei({
26 + required int? gasBaseFee,
27 + required int gasPrice,
28 + required int priorityFeeWei,
29 + required bool chainHasPriorityFee,
30 + }) {
31 + if (gasBaseFee != null && gasBaseFee > 0) {
32 + final baseFeeWithPriority = gasBaseFee + priorityFeeWei;
33 + final bufferMultiplier = chainHasPriorityFee ? 115 : 105;
34 + final bufferPercent = (baseFeeWithPriority * bufferMultiplier) ~/ 100;
35 + final bufferMin = baseFeeWithPriority + (baseFeeWithPriority ~/ 100);
36 + return bufferPercent > bufferMin ? bufferPercent : bufferMin;
37 + }
38 + return gasPrice + priorityFeeWei;
39 + }
40 +
41 static String getErc20TokensBoxName(String walletName, int chainId) {
42 final sanitizedName = walletName.replaceAll(" ", "_");
43
lib/evm/cw_evm.dart
+20 -6
@@ -239,10 +239,7 @@ class CWEVM extends EVM {
239 (wallet as EVMChainWallet).isApprovalRequired(tokenContract, spender, requiredAmount);
240
241 @override
242 - Future<BigInt?> getAllowance(
243 - WalletBase wallet,
244 - String tokenContract,
245 - String spender) =>
242 + Future<BigInt?> getAllowance(WalletBase wallet, String tokenContract, String spender) =>
243 (wallet as EVMChainWallet).getAllowance(tokenContract, spender);
244
245 @override
@@ -272,7 +269,7 @@ class CWEVM extends EVM {
269 String to,
270 String dataHex,
271 BigInt valueWei,
275 - TransactionPriority? priority,{
272 + TransactionPriority? priority, {
273 bool useBlinkProtection = true,
274 String? sourceTokenAddress,
275 BigInt? sourceTokenAmount,
@@ -481,7 +478,7 @@ class CWEVM extends EVM {
478
479 @override
480 BigInt? getERC20AvailableBalance(Object balance) {
484 - if(balance is EVMChainERC20Balance) {
481 + if (balance is EVMChainERC20Balance) {
482 return balance.balance;
483 }
484 return null;
@@ -544,6 +541,23 @@ class CWEVM extends EVM {
541 @override
542 bool hasPriorityFee(int chainId) => EVMChainUtils.hasPriorityFee(chainId);
543
544 + @override
545 + Future<EvmWalletConnectFeeQuote?> getWCBufferedFeeQuote(
546 + WalletBase wallet,
547 + TransactionPriority priority,
548 + ) async {
549 + if (wallet is! EVMChainWallet) return null;
550 +
551 + final data = await wallet.getWCBufferedFeeQuote(priority);
552 + if (data == null) return null;
553 +
554 + return EvmWalletConnectFeeQuote(
555 + maxFeePerGasWei: data.maxFeePerGasWei,
556 + maxPriorityFeePerGasWei: data.maxPriorityFeePerGasWei,
557 + latestBaseFeeWei: data.latestBaseFeeWei,
558 + );
559 + }
560 +
561 Future<({double usdValue, bool hasValidFiatPrice})> _getTokenUsdValueAndFiatCheck(
562 Erc20Token token,
563 BigInt balanceWei,
lib/src/screens/wallet_connect/services/chain_service/eth/evm_chain_service.dart
+90 -35
@@ -432,43 +432,14 @@ class EvmChainServiceImpl {
432 }
433 }
434
435 - // we need to check if dApp provides the gas values and if not, we need to estimate them
436 - final hasGasLimit = transaction.maxGas != null && transaction.maxGas! > 0;
437 - final hasGasPrice = transaction.gasPrice != null;
438 - final hasMaxFeePerGas = transaction.maxFeePerGas != null;
439 - final hasMaxPriorityFeePerGas = transaction.maxPriorityFeePerGas != null;
440 -
441 - final needsGasEstimation = !hasGasLimit || (!hasGasPrice && !hasMaxFeePerGas);
442 -
443 - if (needsGasEstimation) {
444 - try {
445 - final gasPrice = hasGasPrice ? transaction.gasPrice! : await ethClient.getGasPrice();
446 -
447 - if (!hasGasLimit) {
448 - final gasLimit = await ethClient.estimateGas(
449 - sender: transaction.from,
450 - to: transaction.to,
451 - value: transaction.value,
452 - data: transaction.data,
453 - gasPrice: gasPrice,
454 - );
455 -
456 - if (hasMaxFeePerGas || hasMaxPriorityFeePerGas) {
457 - transaction = transaction.copyWith(maxGas: gasLimit.toInt());
458 - } else {
459 - transaction = transaction.copyWith(
460 - gasPrice: hasGasPrice ? transaction.gasPrice : gasPrice,
461 - maxGas: gasLimit.toInt(),
462 - );
463 - }
464 - } else if (!hasGasPrice && !hasMaxFeePerGas) {
465 - transaction = transaction.copyWith(gasPrice: gasPrice);
466 - }
467 - } on RPCError catch (e) {
468 - return JsonRpcError(code: e.errorCode, message: e.message);
469 - }
435 + try {
436 + transaction = await _ensureWCTransactionHasGasLimit(transaction);
437 + } on RPCError catch (e) {
438 + return JsonRpcError(code: e.errorCode, message: e.message);
439 }
440
441 + transaction = await _applyWCBufferedFees(transaction);
442 +
443 final gweiGasPrice =
444 (transaction.gasPrice?.getInWei ?? transaction.maxFeePerGas?.getInWei ?? BigInt.zero) /
445 BigInt.from(1000000000);
@@ -500,6 +471,90 @@ class EvmChainServiceImpl {
471 return JsonRpcError(code: 5002, message: S.current.user_rejected_method);
472 }
473
474 + Future<Transaction> _ensureWCTransactionHasGasLimit(Transaction transaction) async {
475 + final hasGasLimit = transaction.maxGas != null && transaction.maxGas! > 0;
476 + if (hasGasLimit) return transaction;
477 +
478 + final hint = transaction.gasPrice ?? transaction.maxFeePerGas ?? await ethClient.getGasPrice();
479 +
480 + final gasLimit = await ethClient.estimateGas(
481 + sender: transaction.from,
482 + to: transaction.to,
483 + value: transaction.value,
484 + data: transaction.data,
485 + gasPrice: hint,
486 + );
487 +
488 + if (transaction.isEIP1559) {
489 + return transaction.copyWith(maxGas: gasLimit.toInt());
490 + }
491 +
492 + return transaction.copyWith(
493 + maxGas: gasLimit.toInt(),
494 + gasPrice: transaction.gasPrice ?? hint,
495 + );
496 + }
497 +
498 + Future<Transaction> _applyWCBufferedFees(Transaction transaction) async {
499 + try {
500 + final storedPriority =
501 + appStore.settingsStore.getPriority(appStore.wallet!.type, chainId: reference.chainId);
502 + final priority = storedPriority ?? evm!.getDefaultTransactionPriority();
503 +
504 + final quote = await evm!.getWCBufferedFeeQuote(appStore.wallet!, priority);
505 + if (quote != null) {
506 + return _mergeWCBufferedFees(transaction, quote);
507 + }
508 + } catch (e) {
509 + debugPrint('WalletConnect fee refresh failed: $e');
510 + }
511 +
512 + if (!transaction.isEIP1559 && transaction.gasPrice == null) {
513 + return transaction.copyWith(gasPrice: await ethClient.getGasPrice());
514 + }
515 +
516 + return transaction;
517 + }
518 +
519 + Transaction _mergeWCBufferedFees(Transaction transaction, EvmWalletConnectFeeQuote quote) {
520 + if (transaction.isEIP1559) {
521 + // the fees coming from the dApp
522 + final dAppMax = transaction.maxFeePerGas?.getInWei ?? BigInt.zero;
523 + final dAppPri = transaction.maxPriorityFeePerGas?.getInWei ?? BigInt.zero;
524 +
525 + // the updated fees coming from the wallet, handles buffered fees
526 + final quoteMax = BigInt.from(quote.maxFeePerGasWei);
527 + final quotePri = BigInt.from(quote.maxPriorityFeePerGasWei);
528 +
529 + // we'll just use the higher of the two
530 + var newMaxFeePerGasWei = dAppMax > quoteMax ? dAppMax : quoteMax;
531 + var newPriorityFeePerGasWei = dAppPri > quotePri ? dAppPri : quotePri;
532 +
533 + final base = quote.latestBaseFeeWei;
534 + if (base != null) {
535 + final baseB = BigInt.from(base);
536 + final maxPriAllowed = newMaxFeePerGasWei - baseB;
537 + if (newPriorityFeePerGasWei > maxPriAllowed) {
538 + if (maxPriAllowed > BigInt.zero) {
539 + newPriorityFeePerGasWei = maxPriAllowed;
540 + } else {
541 + newMaxFeePerGasWei = baseB + newPriorityFeePerGasWei;
542 + }
543 + }
544 + }
545 +
546 + return transaction.copyWith(
547 + maxFeePerGas: EtherAmount.inWei(newMaxFeePerGasWei),
548 + maxPriorityFeePerGas: EtherAmount.inWei(newPriorityFeePerGasWei),
549 + );
550 + }
551 +
552 + final dPrice = transaction.gasPrice?.getInWei ?? BigInt.zero;
553 + final floor = BigInt.from(quote.maxFeePerGasWei);
554 + final newPriceWei = dPrice > floor ? dPrice : floor;
555 + return transaction.copyWith(gasPrice: EtherAmount.inWei(newPriceWei));
556 + }
557 +
558 void _onSessionRequest(SessionRequestEvent? args) async {
559 if (args != null && args.chainId == getChainId()) {
560 debugPrint('_onSessionRequest ${args.toString()}');
tool/configure.dart
+18
@@ -1555,6 +1555,12 @@ abstract class EVM {
1555
1556 bool hasPriorityFee(int chainId);
1557
1558 +
1559 + Future<EvmWalletConnectFeeQuote?> getWCBufferedFeeQuote(
1560 + WalletBase wallet,
1561 + TransactionPriority priority,
1562 + );
1563 +
1564 Future<void> discoverAndAddWalletTokens(WalletBase wallet);
1565 }
1566
@@ -1577,6 +1583,18 @@ class ChainInfo {
1583 @override
1584 int get hashCode => chainId.hashCode;
1585 }
1586 +
1587 +class EvmWalletConnectFeeQuote {
1588 + const EvmWalletConnectFeeQuote({
1589 + required this.maxFeePerGasWei,
1590 + required this.maxPriorityFeePerGasWei,
1591 + this.latestBaseFeeWei,
1592 + });
1593 +
1594 + final int maxFeePerGasWei;
1595 + final int maxPriorityFeePerGasWei;
1596 + final int? latestBaseFeeWei;
1597 +}
1598 """;
1599
1600 const evmEmptyDefinition = 'EVM? evm;\n';