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';