diff --git a/cw_evm/lib/evm_chain_wallet.dart b/cw_evm/lib/evm_chain_wallet.dart index c0453d817..991fd0718 100644 --- a/cw_evm/lib/evm_chain_wallet.dart +++ b/cw_evm/lib/evm_chain_wallet.dart @@ -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.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 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 changePassword(String password) { throw UnimplementedError("changePassword"); @@ -1048,15 +1068,14 @@ abstract class EVMChainWalletBase } Future 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; +} diff --git a/cw_evm/lib/utils/evm_chain_utils.dart b/cw_evm/lib/utils/evm_chain_utils.dart index 59b6f6560..60355cabf 100644 --- a/cw_evm/lib/utils/evm_chain_utils.dart +++ b/cw_evm/lib/utils/evm_chain_utils.dart @@ -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(" ", "_"); diff --git a/lib/evm/cw_evm.dart b/lib/evm/cw_evm.dart index 723310c54..ec27c930b 100644 --- a/lib/evm/cw_evm.dart +++ b/lib/evm/cw_evm.dart @@ -239,10 +239,7 @@ class CWEVM extends EVM { (wallet as EVMChainWallet).isApprovalRequired(tokenContract, spender, requiredAmount); @override - Future getAllowance( - WalletBase wallet, - String tokenContract, - String spender) => + Future 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 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, diff --git a/lib/src/screens/wallet_connect/services/chain_service/eth/evm_chain_service.dart b/lib/src/screens/wallet_connect/services/chain_service/eth/evm_chain_service.dart index c65d84c6b..15d2bdc00 100644 --- a/lib/src/screens/wallet_connect/services/chain_service/eth/evm_chain_service.dart +++ b/lib/src/screens/wallet_connect/services/chain_service/eth/evm_chain_service.dart @@ -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 _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 _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()}'); diff --git a/tool/configure.dart b/tool/configure.dart index f003d9f33..8e8da7183 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -1555,6 +1555,12 @@ abstract class EVM { bool hasPriorityFee(int chainId); + + Future getWCBufferedFeeQuote( + WalletBase wallet, + TransactionPriority priority, + ); + Future 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';