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 <omarh.ismail1@gmail.com>
This commit is contained in:
David Adegoke
2025-10-25 13:38:32 +02:00
committed by GitHub
co-authored by Omar Hatem
parent c457a7fea6
commit 226addcc3d
2 changed files with 166 additions and 85 deletions
@@ -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<String, String> 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<String, String> Function(Match) extractor;
final Map<String, String> Function(Map<String, String>) calculator;
ErrorPattern({
required this.name,
required this.regex,
required this.extractor,
required this.calculator,
});
}
+7
View File
@@ -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'''