From 226addcc3d5044f69c760deaedfbadbb432a2017 Mon Sep 17 00:00:00 2001 From: David Adegoke <64401859+Blazebrain@users.noreply.github.com> Date: Sat, 25 Oct 2025 12:38:32 +0100 Subject: [PATCH] Enhance EVM Fees Error Handling (#2610) * fix: Add more patterns to the EVM fees error handler * refactor: Enhance EVM transaction error handling with multiple parsing patterns --------- Co-authored-by: Omar Hatem --- .../evm_transaction_error_fees_handler.dart | 244 ++++++++++++------ lib/view_model/send/send_view_model.dart | 7 + 2 files changed, 166 insertions(+), 85 deletions(-) diff --git a/lib/entities/evm_transaction_error_fees_handler.dart b/lib/entities/evm_transaction_error_fees_handler.dart index 85b40be65..9db07e1bd 100644 --- a/lib/entities/evm_transaction_error_fees_handler.dart +++ b/lib/entities/evm_transaction_error_fees_handler.dart @@ -32,100 +32,174 @@ class EVMTransactionErrorFeesHandler { String errorMessage, double assetPriceUsd, ) { - // Pattern: "insufficient funds for gas * price + value: have 4728796358953246 want 4728796842182575" - RegExp insufficientFundsRegExp = RegExp(r'have (\d+) want (\d+)'); - Match? insufficientFundsMatch = insufficientFundsRegExp.firstMatch(errorMessage); + // Allows us define multiple patterns to parse the error message + // Order matters: more specific patterns first, generic patterns last + final patterns = [ + // Pattern 1: "have X want Y" format + ErrorPattern( + name: 'have_want', + regex: RegExp(r'have\s+(\d+)\s+want\s+(\d+)', caseSensitive: false), + extractor: (match) => { + 'balance': match.group(1)!, + 'required': match.group(2)!, + }, + calculator: (values) => { + 'balanceWei': values['balance']!, + 'txCostWei': values['required']!, + 'overshotWei': + (BigInt.parse(values['required']!) - BigInt.parse(values['balance']!)).toString(), + }, + ), - if (insufficientFundsMatch != null) { - try { - // Extract the numerical strings from the new format - String balanceStr = insufficientFundsMatch.group(1)!; - String requiredStr = insufficientFundsMatch.group(2)!; + // Pattern 2: "balance X, queued cost Y, tx cost Z, overshot W" format (most specific) + ErrorPattern( + name: 'balance_queued_txcost_overshot', + regex: RegExp(r'balance\s+(\d+),\s*queued\s+cost\s+(\d+),\s*tx\s+cost\s+(\d+),\s*overshot\s+(\d+)', caseSensitive: false), + extractor: (match) => { + 'balance': match.group(1)!, + 'txCost': match.group(3)!, // Skip queued cost, use tx cost + 'overshot': match.group(4)!, + }, + calculator: (values) => { + 'balanceWei': values['balance']!, + 'txCostWei': values['txCost']!, + 'overshotWei': values['overshot']!, + }, + ), - // Parse the numerical strings to BigInt - BigInt balanceWei = BigInt.parse(balanceStr); - BigInt requiredWei = BigInt.parse(requiredStr); + // Pattern 3: "balance X, tx cost Y, overshot Z" format + ErrorPattern( + name: 'balance_txcost_overshot', + regex: RegExp(r'balance\s+(\d+),\s*tx\s+cost\s+(\d+),\s*overshot\s+(\d+)', caseSensitive: false), + extractor: (match) => { + 'balance': match.group(1)!, + 'txCost': match.group(2)!, + 'overshot': match.group(3)!, + }, + calculator: (values) => { + 'balanceWei': values['balance']!, + 'txCostWei': values['txCost']!, + 'overshotWei': values['overshot']!, + }, + ), - // Calculate overshot (how much more is needed) - BigInt overshotWei = requiredWei - balanceWei; + // Pattern 4: Individual field matching (legacy fallback) + ErrorPattern( + name: 'individual_fields', + regex: RegExp(r'balance\s+(\d+).*tx\s+cost\s+(\d+).*overshot\s+(\d+)', caseSensitive: false), + extractor: (match) => { + 'balance': match.group(1)!, + 'txCost': match.group(2)!, + 'overshot': match.group(3)!, + }, + calculator: (values) => { + 'balanceWei': values['balance']!, + 'txCostWei': values['txCost']!, + 'overshotWei': values['overshot']!, + }, + ), - // The transaction cost is the required amount - BigInt txCostWei = requiredWei; + // Pattern 5: Generic "insufficient funds for gas * price + value" (least specific - must be last) + ErrorPattern( + name: 'generic_insufficient_funds', + regex: RegExp(r'insufficient\s+funds\s+for\s+gas\s*\*\s*price\s*\+\s*value', caseSensitive: false), + extractor: (match) => { + 'balance': '0', // We don't have specific values, so use 0 + 'txCost': '0', + 'overshot': '0', + }, + calculator: (values) => { + 'balanceWei': values['balance']!, + 'txCostWei': values['txCost']!, + 'overshotWei': values['overshot']!, + }, + ), + ]; - // Convert wei to ETH (1 ETH = 1e18 wei) - double balanceEth = balanceWei.toDouble() / 1e18; - double txCostEth = txCostWei.toDouble() / 1e18; - double overshotEth = overshotWei.toDouble() / 1e18; + for (final pattern in patterns) { + final match = pattern.regex.firstMatch(errorMessage); + if (match != null) { + try { + final extractedValues = pattern.extractor(match); + final calculatedValues = pattern.calculator(extractedValues); - // Calculate the USD values - double balanceUsd = balanceEth * assetPriceUsd; - double txCostUsd = txCostEth * assetPriceUsd; - double overshotUsd = overshotEth * assetPriceUsd; - - return EVMTransactionErrorFeesHandler( - balanceWei: balanceWei.toString(), - balanceEth: balanceEth.toString().safeSubString(0, 12), - balanceUsd: balanceUsd.toString().safeSubString(0, 4), - txCostWei: txCostWei.toString(), - txCostEth: txCostEth.toString().safeSubString(0, 12), - txCostUsd: txCostUsd.toString().safeSubString(0, 4), - overshotWei: overshotWei.toString(), - overshotEth: overshotEth.toString().safeSubString(0, 12), - overshotUsd: overshotUsd.toString().safeSubString(0, 4), - ); - } catch (e) {} - } - - // Define Regular Expressions to extract the numerical values - RegExp balanceRegExp = RegExp(r'balance (\d+)'); - RegExp txCostRegExp = RegExp(r'tx cost (\d+)'); - RegExp overshotRegExp = RegExp(r'overshot (\d+)'); - - // Match the patterns in the error message - Match? balanceMatch = balanceRegExp.firstMatch(errorMessage); - Match? txCostMatch = txCostRegExp.firstMatch(errorMessage); - Match? overshotMatch = overshotRegExp.firstMatch(errorMessage); - - // Check if all required values are found - if (balanceMatch != null && txCostMatch != null && overshotMatch != null) { - try { - // Extract the numerical strings - String balanceStr = balanceMatch.group(1)!; - String txCostStr = txCostMatch.group(1)!; - String overshotStr = overshotMatch.group(1)!; - - // Parse the numerical strings to BigInt - BigInt balanceWei = BigInt.parse(balanceStr); - BigInt txCostWei = BigInt.parse(txCostStr); - BigInt overshotWei = BigInt.parse(overshotStr); - - // Convert wei to ETH (1 ETH = 1e18 wei) - double balanceEth = balanceWei.toDouble() / 1e18; - double txCostEth = txCostWei.toDouble() / 1e18; - double overshotEth = overshotWei.toDouble() / 1e18; - - // Calculate the USD values - double balanceUsd = balanceEth * assetPriceUsd; - double txCostUsd = txCostEth * assetPriceUsd; - double overshotUsd = overshotEth * assetPriceUsd; - - return EVMTransactionErrorFeesHandler( - balanceWei: balanceWei.toString(), - balanceEth: balanceEth.toString().safeSubString(0, 12), - balanceUsd: balanceUsd.toString().safeSubString(0, 4), - txCostWei: txCostWei.toString(), - txCostEth: txCostEth.toString().safeSubString(0, 12), - txCostUsd: txCostUsd.toString().safeSubString(0, 4), - overshotWei: overshotWei.toString(), - overshotEth: overshotEth.toString().safeSubString(0, 12), - overshotUsd: overshotUsd.toString().safeSubString(0, 4), - ); - } catch (e) { - // If parsing fails, continue to error case + return _createHandlerFromValues(calculatedValues, assetPriceUsd); + } catch (e) { + continue; + } } } - // If both parsing attempts fail, return an error message return EVMTransactionErrorFeesHandler(error: 'Could not parse the error message.'); } + + /// Creates a handler instance from parsed values + static EVMTransactionErrorFeesHandler _createHandlerFromValues( + Map values, + double assetPriceUsd, + ) { + final balanceWei = BigInt.parse(values['balanceWei']!); + final txCostWei = BigInt.parse(values['txCostWei']!); + final overshotWei = BigInt.parse(values['overshotWei']!); + + final isGenericError = balanceWei == BigInt.zero && + txCostWei == BigInt.zero && + overshotWei == BigInt.zero; + + if (isGenericError) { + return genericInsufficientFunds(); + } + + // Convert wei to ETH (1 ETH = 1e18 wei) + final balanceEth = balanceWei.toDouble() / 1e18; + final txCostEth = txCostWei.toDouble() / 1e18; + final overshotEth = overshotWei.toDouble() / 1e18; + + // Calculate USD values + final balanceUsd = balanceEth * assetPriceUsd; + final txCostUsd = txCostEth * assetPriceUsd; + final overshotUsd = overshotEth * assetPriceUsd; + + return EVMTransactionErrorFeesHandler( + balanceWei: balanceWei.toString(), + balanceEth: balanceEth.toString().safeSubString(0, 12), + balanceUsd: balanceUsd.toString().safeSubString(0, 4), + txCostWei: txCostWei.toString(), + txCostEth: txCostEth.toString().safeSubString(0, 12), + txCostUsd: txCostUsd.toString().safeSubString(0, 4), + overshotWei: overshotWei.toString(), + overshotEth: overshotEth.toString().safeSubString(0, 12), + overshotUsd: overshotUsd.toString().safeSubString(0, 4), + ); + } + + static EVMTransactionErrorFeesHandler genericInsufficientFunds() { + return EVMTransactionErrorFeesHandler( + balanceWei: '0', + balanceEth: '0', + balanceUsd: '0', + txCostWei: '0', + txCostEth: '0', + txCostUsd: '0', + overshotWei: '0', + overshotEth: '0', + overshotUsd: '0', + error: 'generic_insufficient_funds', + ); + } +} + +/// Represents an error parsing pattern +class ErrorPattern { + final String name; + final RegExp regex; + final Map Function(Match) extractor; + final Map Function(Map) calculator; + + ErrorPattern({ + required this.name, + required this.regex, + required this.extractor, + required this.calculator, + }); } diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index 83480cfed..cc9da2809 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -1031,10 +1031,17 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor _fiatConversationStore.prices[currency] ?? 0.0, ); + // Handle generic insufficient funds error (no specific values available) + if (parsedErrorMessageResult.error == 'generic_insufficient_funds') { + return S.current.insufficient_funds_for_tx; + } + + // Handle parsing errors (couldn't parse the error message) if (parsedErrorMessageResult.error != null) { return S.current.insufficient_funds_for_tx; } + // Handle successfully parsed errors with specific values return '''${S.current.insufficient_funds_for_tx} \n\n''' '''${S.current.balance}: ${parsedErrorMessageResult.balanceEth} ${walletType == WalletType.polygon ? "POL" : "ETH"} (${parsedErrorMessageResult.balanceUsd} ${fiatFromSettings.name})\n\n''' '''${S.current.transaction_cost}: ${parsedErrorMessageResult.txCostEth} ${walletType == WalletType.polygon ? "POL" : "ETH"} (${parsedErrorMessageResult.txCostUsd} ${fiatFromSettings.name})\n\n'''