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