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>

David Adegoke committed Oct 25, 2025 at 12:38 UTC 226addcc3d5044f69c760deaedfbadbb432a2017
2 files changed +172 -91
lib/entities/evm_transaction_error_fees_handler.dart
+165 -91
@@ -32,100 +32,174 @@ class EVMTransactionErrorFeesHandler {
32 String errorMessage,
33 double assetPriceUsd,
34 ) {
35 - // Pattern: "insufficient funds for gas * price + value: have 4728796358953246 want 4728796842182575"
36 - RegExp insufficientFundsRegExp = RegExp(r'have (\d+) want (\d+)');
37 - Match? insufficientFundsMatch = insufficientFundsRegExp.firstMatch(errorMessage);
38 -
39 - if (insufficientFundsMatch != null) {
40 - try {
41 - // Extract the numerical strings from the new format
42 - String balanceStr = insufficientFundsMatch.group(1)!;
43 - String requiredStr = insufficientFundsMatch.group(2)!;
44 -
45 - // Parse the numerical strings to BigInt
46 - BigInt balanceWei = BigInt.parse(balanceStr);
47 - BigInt requiredWei = BigInt.parse(requiredStr);
48 -
49 - // Calculate overshot (how much more is needed)
50 - BigInt overshotWei = requiredWei - balanceWei;
51 -
52 - // The transaction cost is the required amount
53 - BigInt txCostWei = requiredWei;
54 -
55 - // Convert wei to ETH (1 ETH = 1e18 wei)
56 - double balanceEth = balanceWei.toDouble() / 1e18;
57 - double txCostEth = txCostWei.toDouble() / 1e18;
58 - double overshotEth = overshotWei.toDouble() / 1e18;
59 -
60 - // Calculate the USD values
61 - double balanceUsd = balanceEth * assetPriceUsd;
62 - double txCostUsd = txCostEth * assetPriceUsd;
63 - double overshotUsd = overshotEth * assetPriceUsd;
64 -
65 - return EVMTransactionErrorFeesHandler(
66 - balanceWei: balanceWei.toString(),
67 - balanceEth: balanceEth.toString().safeSubString(0, 12),
68 - balanceUsd: balanceUsd.toString().safeSubString(0, 4),
69 - txCostWei: txCostWei.toString(),
70 - txCostEth: txCostEth.toString().safeSubString(0, 12),
71 - txCostUsd: txCostUsd.toString().safeSubString(0, 4),
72 - overshotWei: overshotWei.toString(),
73 - overshotEth: overshotEth.toString().safeSubString(0, 12),
74 - overshotUsd: overshotUsd.toString().safeSubString(0, 4),
75 - );
76 - } catch (e) {}
77 - }
78 -
79 - // Define Regular Expressions to extract the numerical values
80 - RegExp balanceRegExp = RegExp(r'balance (\d+)');
81 - RegExp txCostRegExp = RegExp(r'tx cost (\d+)');
82 - RegExp overshotRegExp = RegExp(r'overshot (\d+)');
83 -
84 - // Match the patterns in the error message
85 - Match? balanceMatch = balanceRegExp.firstMatch(errorMessage);
86 - Match? txCostMatch = txCostRegExp.firstMatch(errorMessage);
87 - Match? overshotMatch = overshotRegExp.firstMatch(errorMessage);
88 -
89 - // Check if all required values are found
90 - if (balanceMatch != null && txCostMatch != null && overshotMatch != null) {
91 - try {
92 - // Extract the numerical strings
93 - String balanceStr = balanceMatch.group(1)!;
94 - String txCostStr = txCostMatch.group(1)!;
95 - String overshotStr = overshotMatch.group(1)!;
96 -
97 - // Parse the numerical strings to BigInt
98 - BigInt balanceWei = BigInt.parse(balanceStr);
99 - BigInt txCostWei = BigInt.parse(txCostStr);
100 - BigInt overshotWei = BigInt.parse(overshotStr);
101 -
102 - // Convert wei to ETH (1 ETH = 1e18 wei)
103 - double balanceEth = balanceWei.toDouble() / 1e18;
104 - double txCostEth = txCostWei.toDouble() / 1e18;
105 - double overshotEth = overshotWei.toDouble() / 1e18;
106 -
107 - // Calculate the USD values
108 - double balanceUsd = balanceEth * assetPriceUsd;
109 - double txCostUsd = txCostEth * assetPriceUsd;
110 - double overshotUsd = overshotEth * assetPriceUsd;
111 -
112 - return EVMTransactionErrorFeesHandler(
113 - balanceWei: balanceWei.toString(),
114 - balanceEth: balanceEth.toString().safeSubString(0, 12),
115 - balanceUsd: balanceUsd.toString().safeSubString(0, 4),
116 - txCostWei: txCostWei.toString(),
117 - txCostEth: txCostEth.toString().safeSubString(0, 12),
118 - txCostUsd: txCostUsd.toString().safeSubString(0, 4),
119 - overshotWei: overshotWei.toString(),
120 - overshotEth: overshotEth.toString().safeSubString(0, 12),
121 - overshotUsd: overshotUsd.toString().safeSubString(0, 4),
122 - );
123 - } catch (e) {
124 - // If parsing fails, continue to error case
35 + // Allows us define multiple patterns to parse the error message
36 + // Order matters: more specific patterns first, generic patterns last
37 + final patterns = [
38 + // Pattern 1: "have X want Y" format
39 + ErrorPattern(
40 + name: 'have_want',
41 + regex: RegExp(r'have\s+(\d+)\s+want\s+(\d+)', caseSensitive: false),
42 + extractor: (match) => {
43 + 'balance': match.group(1)!,
44 + 'required': match.group(2)!,
45 + },
46 + calculator: (values) => {
47 + 'balanceWei': values['balance']!,
48 + 'txCostWei': values['required']!,
49 + 'overshotWei':
50 + (BigInt.parse(values['required']!) - BigInt.parse(values['balance']!)).toString(),
51 + },
52 + ),
53 +
54 + // Pattern 2: "balance X, queued cost Y, tx cost Z, overshot W" format (most specific)
55 + ErrorPattern(
56 + name: 'balance_queued_txcost_overshot',
57 + regex: RegExp(r'balance\s+(\d+),\s*queued\s+cost\s+(\d+),\s*tx\s+cost\s+(\d+),\s*overshot\s+(\d+)', caseSensitive: false),
58 + extractor: (match) => {
59 + 'balance': match.group(1)!,
60 + 'txCost': match.group(3)!, // Skip queued cost, use tx cost
61 + 'overshot': match.group(4)!,
62 + },
63 + calculator: (values) => {
64 + 'balanceWei': values['balance']!,
65 + 'txCostWei': values['txCost']!,
66 + 'overshotWei': values['overshot']!,
67 + },
68 + ),
69 +
70 + // Pattern 3: "balance X, tx cost Y, overshot Z" format
71 + ErrorPattern(
72 + name: 'balance_txcost_overshot',
73 + regex: RegExp(r'balance\s+(\d+),\s*tx\s+cost\s+(\d+),\s*overshot\s+(\d+)', caseSensitive: false),
74 + extractor: (match) => {
75 + 'balance': match.group(1)!,
76 + 'txCost': match.group(2)!,
77 + 'overshot': match.group(3)!,
78 + },
79 + calculator: (values) => {
80 + 'balanceWei': values['balance']!,
81 + 'txCostWei': values['txCost']!,
82 + 'overshotWei': values['overshot']!,
83 + },
84 + ),
85 +
86 + // Pattern 4: Individual field matching (legacy fallback)
87 + ErrorPattern(
88 + name: 'individual_fields',
89 + regex: RegExp(r'balance\s+(\d+).*tx\s+cost\s+(\d+).*overshot\s+(\d+)', caseSensitive: false),
90 + extractor: (match) => {
91 + 'balance': match.group(1)!,
92 + 'txCost': match.group(2)!,
93 + 'overshot': match.group(3)!,
94 + },
95 + calculator: (values) => {
96 + 'balanceWei': values['balance']!,
97 + 'txCostWei': values['txCost']!,
98 + 'overshotWei': values['overshot']!,
99 + },
100 + ),
101 +
102 + // Pattern 5: Generic "insufficient funds for gas * price + value" (least specific - must be last)
103 + ErrorPattern(
104 + name: 'generic_insufficient_funds',
105 + regex: RegExp(r'insufficient\s+funds\s+for\s+gas\s*\*\s*price\s*\+\s*value', caseSensitive: false),
106 + extractor: (match) => {
107 + 'balance': '0', // We don't have specific values, so use 0
108 + 'txCost': '0',
109 + 'overshot': '0',
110 + },
111 + calculator: (values) => {
112 + 'balanceWei': values['balance']!,
113 + 'txCostWei': values['txCost']!,
114 + 'overshotWei': values['overshot']!,
115 + },
116 + ),
117 + ];
118 +
119 + for (final pattern in patterns) {
120 + final match = pattern.regex.firstMatch(errorMessage);
121 + if (match != null) {
122 + try {
123 + final extractedValues = pattern.extractor(match);
124 + final calculatedValues = pattern.calculator(extractedValues);
125 +
126 + return _createHandlerFromValues(calculatedValues, assetPriceUsd);
127 + } catch (e) {
128 + continue;
129 + }
130 }
131 }
132
128 - // If both parsing attempts fail, return an error message
133 return EVMTransactionErrorFeesHandler(error: 'Could not parse the error message.');
134 }
135 +
136 + /// Creates a handler instance from parsed values
137 + static EVMTransactionErrorFeesHandler _createHandlerFromValues(
138 + Map<String, String> values,
139 + double assetPriceUsd,
140 + ) {
141 + final balanceWei = BigInt.parse(values['balanceWei']!);
142 + final txCostWei = BigInt.parse(values['txCostWei']!);
143 + final overshotWei = BigInt.parse(values['overshotWei']!);
144 +
145 + final isGenericError = balanceWei == BigInt.zero &&
146 + txCostWei == BigInt.zero &&
147 + overshotWei == BigInt.zero;
148 +
149 + if (isGenericError) {
150 + return genericInsufficientFunds();
151 + }
152 +
153 + // Convert wei to ETH (1 ETH = 1e18 wei)
154 + final balanceEth = balanceWei.toDouble() / 1e18;
155 + final txCostEth = txCostWei.toDouble() / 1e18;
156 + final overshotEth = overshotWei.toDouble() / 1e18;
157 +
158 + // Calculate USD values
159 + final balanceUsd = balanceEth * assetPriceUsd;
160 + final txCostUsd = txCostEth * assetPriceUsd;
161 + final overshotUsd = overshotEth * assetPriceUsd;
162 +
163 + return EVMTransactionErrorFeesHandler(
164 + balanceWei: balanceWei.toString(),
165 + balanceEth: balanceEth.toString().safeSubString(0, 12),
166 + balanceUsd: balanceUsd.toString().safeSubString(0, 4),
167 + txCostWei: txCostWei.toString(),
168 + txCostEth: txCostEth.toString().safeSubString(0, 12),
169 + txCostUsd: txCostUsd.toString().safeSubString(0, 4),
170 + overshotWei: overshotWei.toString(),
171 + overshotEth: overshotEth.toString().safeSubString(0, 12),
172 + overshotUsd: overshotUsd.toString().safeSubString(0, 4),
173 + );
174 + }
175 +
176 + static EVMTransactionErrorFeesHandler genericInsufficientFunds() {
177 + return EVMTransactionErrorFeesHandler(
178 + balanceWei: '0',
179 + balanceEth: '0',
180 + balanceUsd: '0',
181 + txCostWei: '0',
182 + txCostEth: '0',
183 + txCostUsd: '0',
184 + overshotWei: '0',
185 + overshotEth: '0',
186 + overshotUsd: '0',
187 + error: 'generic_insufficient_funds',
188 + );
189 + }
190 +}
191 +
192 +/// Represents an error parsing pattern
193 +class ErrorPattern {
194 + final String name;
195 + final RegExp regex;
196 + final Map<String, String> Function(Match) extractor;
197 + final Map<String, String> Function(Map<String, String>) calculator;
198 +
199 + ErrorPattern({
200 + required this.name,
201 + required this.regex,
202 + required this.extractor,
203 + required this.calculator,
204 + });
205 }
lib/view_model/send/send_view_model.dart
+7
@@ -1031,10 +1031,17 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
1031 _fiatConversationStore.prices[currency] ?? 0.0,
1032 );
1033
1034 + // Handle generic insufficient funds error (no specific values available)
1035 + if (parsedErrorMessageResult.error == 'generic_insufficient_funds') {
1036 + return S.current.insufficient_funds_for_tx;
1037 + }
1038 +
1039 + // Handle parsing errors (couldn't parse the error message)
1040 if (parsedErrorMessageResult.error != null) {
1041 return S.current.insufficient_funds_for_tx;
1042 }
1043
1044 + // Handle successfully parsed errors with specific values
1045 return '''${S.current.insufficient_funds_for_tx} \n\n'''
1046 '''${S.current.balance}: ${parsedErrorMessageResult.balanceEth} ${walletType == WalletType.polygon ? "POL" : "ETH"} (${parsedErrorMessageResult.balanceUsd} ${fiatFromSettings.name})\n\n'''
1047 '''${S.current.transaction_cost}: ${parsedErrorMessageResult.txCostEth} ${walletType == WalletType.polygon ? "POL" : "ETH"} (${parsedErrorMessageResult.txCostUsd} ${fiatFromSettings.name})\n\n'''