Deuro Savings Error Handling (#2340)
* feat(deuro): Enhance gas fee handling and error management for Deuro Savings Transactions. This change: - Introduces DeuroGasFeeException to handle insufficient ETH for gas fees. - Adds check for ETH balance before savings transactions to prevent failures due to insufficient funds. - Updates savings transaction methods to include error handling. - Adds UI feedback for transaction failures in DEuroSavingsPage. * Fix conflicts * Update cw_ethereum/lib/deuro/deuro_savings.dart Co-authored-by: Konstantin Ullrich <konstantinullrich12@gmail.com> --------- Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com> Co-authored-by: Konstantin Ullrich <konstantinullrich12@gmail.com>
David Adegoke committed
Jun 27, 2025 at 15:53 UTC
5aeb6b752291ee129084aad59ed99b3c54dad5b2
4 files changed
+186
-59
cw_ethereum/lib/deuro/deuro_savings.dart
+95
-32
@@ -2,6 +2,7 @@ import 'package:cw_core/crypto_currency.dart';
2
import 'package:cw_ethereum/deuro/deuro_savings_contract.dart';
3
import 'package:cw_ethereum/ethereum_wallet.dart';
4
import 'package:cw_evm/contract/erc20.dart';
5
+import 'package:cw_evm/evm_chain_exceptions.dart';
6
import 'package:cw_evm/evm_chain_transaction_priority.dart';
7
import 'package:cw_evm/pending_evm_chain_transaction.dart';
8
import 'package:web3dart/crypto.dart';
@@ -43,59 +44,111 @@ class DEuro {
44
45
Future<BigInt> get approvedBalance => _dEuro.allowance(_address, _savingsGateway.self.address);
46
46
- Future<PendingEVMChainTransaction> depositSavings(
47
- BigInt amount, EVMChainTransactionPriority priority) async {
48
- final signedTransaction = await _savingsGateway.save(
49
- (amount: amount, frontendCode: hexToBytes(frontendCode)),
50
- credentials: _wallet.evmChainPrivateKey,
51
- );
47
+ Future<void> _checkEthBalanceForGasFees(EVMChainTransactionPriority priority) async {
48
+ final ethBalance = await _wallet.getWeb3Client()!.getBalance(_address);
49
+ final currentBalance = ethBalance.getInWei;
50
53
- final fee = await _wallet.calculateActualEstimatedFeeForCreateTransaction(
54
- amount: amount,
51
+ final gasFeesModel = await _wallet.calculateActualEstimatedFeeForCreateTransaction(
52
+ amount: BigInt.zero,
53
contractAddress: _savingsGateway.self.address.hexEip55,
54
receivingAddressHex: _savingsGateway.self.address.hexEip55,
55
priority: priority,
58
- data: _savingsGateway.self.abi.functions[17].encodeCall([amount, hexToBytes(frontendCode)]),
56
+ data: _savingsGateway.self.abi.functions[17]
57
+ .encodeCall([BigInt.zero, hexToBytes(frontendCode)]),
58
);
59
61
- final sendTransaction = () => _wallet.getWeb3Client()!.sendRawTransaction(signedTransaction);
60
+ final estimatedGasFee = BigInt.from(gasFeesModel.estimatedGasFee);
61
+ final requiredBalance = estimatedGasFee;
62
+
63
+ if (currentBalance < requiredBalance) {
64
+ throw DeuroGasFeeException(
65
+ requiredGasFee: requiredBalance,
66
+ currentBalance: currentBalance,
67
+ );
68
+ }
69
+ }
70
+
71
+ Future<PendingEVMChainTransaction> depositSavings(
72
+ BigInt amount, EVMChainTransactionPriority priority) async {
73
+ try {
74
+ await _checkEthBalanceForGasFees(priority);
75
+
76
+ final signedTransaction = await _savingsGateway.save(
77
+ (amount: amount, frontendCode: hexToBytes(frontendCode)),
78
+ credentials: _wallet.evmChainPrivateKey,
79
+ );
80
+
81
+ final fee = await _wallet.calculateActualEstimatedFeeForCreateTransaction(
82
+ amount: amount,
83
+ contractAddress: _savingsGateway.self.address.hexEip55,
84
+ receivingAddressHex: _savingsGateway.self.address.hexEip55,
85
+ priority: priority,
86
+ data: _savingsGateway.self.abi.functions[17].encodeCall([amount, hexToBytes(frontendCode)]),
87
+ );
88
+
89
+ final sendTransaction = () => _wallet.getWeb3Client()!.sendRawTransaction(signedTransaction);
90
63
- return PendingEVMChainTransaction(
91
+ return PendingEVMChainTransaction(
92
sendTransaction: sendTransaction,
93
signedTransaction: signedTransaction,
94
fee: BigInt.from(fee.estimatedGasFee),
95
amount: amount.toString(),
68
- exponent: 18);
96
+ exponent: 18,
97
+ );
98
+ } catch (e) {
99
+ if (e.toString().contains('insufficient funds for gas')) {
100
+ final ethBalance = await _wallet.getWeb3Client()!.getBalance(_address);
101
+ throw DeuroGasFeeException(
102
+ currentBalance: ethBalance.getInWei,
103
+ );
104
+ }
105
+ rethrow;
106
+ }
107
}
108
109
Future<PendingEVMChainTransaction> withdrawSavings(
110
BigInt amount, EVMChainTransactionPriority priority) async {
73
- final signedTransaction = await _savingsGateway.withdraw(
74
- (target: _address, amount: amount, frontendCode: hexToBytes(frontendCode)),
75
- credentials: _wallet.evmChainPrivateKey,
76
- );
111
+ try {
112
+ await _checkEthBalanceForGasFees(priority);
113
78
- final fee = await _wallet.calculateActualEstimatedFeeForCreateTransaction(
79
- amount: amount,
80
- contractAddress: _savingsGateway.self.address.hexEip55,
81
- receivingAddressHex: _savingsGateway.self.address.hexEip55,
82
- priority: priority,
83
- data: _savingsGateway.self.abi.functions[17].encodeCall([amount, hexToBytes(frontendCode)]),
84
- );
114
+ final signedTransaction = await _savingsGateway.withdraw(
115
+ (target: _address, amount: amount, frontendCode: hexToBytes(frontendCode)),
116
+ credentials: _wallet.evmChainPrivateKey,
117
+ );
118
86
- final sendTransaction = () => _wallet.getWeb3Client()!.sendRawTransaction(signedTransaction);
119
+ final fee = await _wallet.calculateActualEstimatedFeeForCreateTransaction(
120
+ amount: amount,
121
+ contractAddress: _savingsGateway.self.address.hexEip55,
122
+ receivingAddressHex: _savingsGateway.self.address.hexEip55,
123
+ priority: priority,
124
+ data: _savingsGateway.self.abi.functions[17].encodeCall([amount, hexToBytes(frontendCode)]),
125
+ );
126
88
- return PendingEVMChainTransaction(
89
- sendTransaction: sendTransaction,
90
- signedTransaction: signedTransaction,
91
- fee: BigInt.from(fee.estimatedGasFee),
92
- amount: amount.toString(),
93
- exponent: 18);
127
+ final sendTransaction = () => _wallet.getWeb3Client()!.sendRawTransaction(signedTransaction);
128
+
129
+ return PendingEVMChainTransaction(
130
+ sendTransaction: sendTransaction,
131
+ signedTransaction: signedTransaction,
132
+ fee: BigInt.from(fee.estimatedGasFee),
133
+ amount: amount.toString(),
134
+ exponent: 18);
135
+ } catch (e) {
136
+ if (e.toString().contains('insufficient funds for gas')) {
137
+ final ethBalance = await _wallet.getWeb3Client()!.getBalance(_address);
138
+ throw DeuroGasFeeException(
139
+ currentBalance: ethBalance.getInWei,
140
+ );
141
+ }
142
+ rethrow;
143
+ }
144
}
145
146
// Set an infinite approval to save gas in the future
97
- Future<PendingEVMChainTransaction> enableSavings(EVMChainTransactionPriority priority) async =>
98
- (await _wallet.createApprovalTransaction(
147
+ Future<PendingEVMChainTransaction> enableSavings(EVMChainTransactionPriority priority) async {
148
+ try {
149
+ await _checkEthBalanceForGasFees(priority);
150
+
151
+ return (await _wallet.createApprovalTransaction(
152
BigInt.parse(
153
'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff',
154
radix: 16,
@@ -104,4 +157,14 @@ class DEuro {
157
CryptoCurrency.deuro,
158
priority,
159
)) as PendingEVMChainTransaction;
160
+ } catch (e) {
161
+ if (e.toString().contains('insufficient funds for gas')) {
162
+ final ethBalance = await _wallet.getWeb3Client()!.getBalance(_address);
163
+ throw DeuroGasFeeException(
164
+ currentBalance: ethBalance.getInWei,
165
+ );
166
+ }
167
+ rethrow;
168
+ }
169
+ }
170
}
cw_evm/lib/evm_chain_exceptions.dart
+29
@@ -22,3 +22,32 @@ class EVMChainTransactionFeesException implements Exception {
22
@override
23
String toString() => exceptionMessage;
24
}
25
+
26
+class DeuroGasFeeException implements Exception {
27
+ final String exceptionMessage;
28
+ final BigInt? requiredGasFee;
29
+ final BigInt? currentBalance;
30
+
31
+ DeuroGasFeeException({
32
+ this.requiredGasFee,
33
+ this.currentBalance,
34
+ }) : exceptionMessage = _buildMessage(requiredGasFee, currentBalance);
35
+
36
+ static String _buildMessage(BigInt? requiredGasFee, BigInt? currentBalance) {
37
+ const baseMessage = 'Insufficient ETH for gas fees.';
38
+ const addEthMessage = ' Please add ETH to your wallet to cover transaction fees.';
39
+
40
+ if (requiredGasFee != null) {
41
+ final requiredEth = (requiredGasFee / BigInt.from(10).pow(18)).toStringAsFixed(8);
42
+ final balanceInfo = currentBalance != null
43
+ ? ', Available: ${(currentBalance / BigInt.from(10).pow(18)).toStringAsFixed(8)} ETH'
44
+ : '';
45
+ return '$baseMessage Required: ~$requiredEth ETH$balanceInfo.$addEthMessage';
46
+ }
47
+
48
+ return '$baseMessage$addEthMessage';
49
+ }
50
+
51
+ @override
52
+ String toString() => exceptionMessage;
53
+}
lib/src/screens/integrations/deuro/savings_page.dart
+20
@@ -4,9 +4,11 @@ import 'package:cake_wallet/src/screens/base_page.dart';
4
import 'package:cake_wallet/src/screens/integrations/deuro/widgets/interest_card_widget.dart';
5
import 'package:cake_wallet/src/screens/integrations/deuro/widgets/savings_card_widget.dart';
6
import 'package:cake_wallet/src/screens/integrations/deuro/widgets/savings_edit_sheet.dart';
7
+import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
8
import 'package:cake_wallet/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart';
9
import 'package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart';
10
import 'package:cake_wallet/src/widgets/gradient_background.dart';
11
+import 'package:cake_wallet/utils/show_pop_up.dart';
12
import 'package:cake_wallet/view_model/integrations/deuro_view_model.dart';
13
import 'package:cake_wallet/view_model/send/send_view_model_state.dart';
14
import 'package:cw_core/crypto_currency.dart';
@@ -190,6 +192,24 @@ class DEuroSavingsPage extends BasePage {
192
);
193
});
194
}
195
+
196
+ if (state is FailureState) {
197
+ WidgetsBinding.instance.addPostFrameCallback((_) async {
198
+ if (!context.mounted) return;
199
+
200
+ await showPopUp<void>(
201
+ context: context,
202
+ builder: (BuildContext popupContext) {
203
+ return AlertWithOneAction(
204
+ alertTitle: S.of(popupContext).error,
205
+ alertContent: state.error,
206
+ buttonText: S.of(popupContext).ok,
207
+ buttonAction: () => Navigator.of(popupContext).pop(),
208
+ );
209
+ },
210
+ );
211
+ });
212
+ }
213
});
214
215
_isReactionsSet = true;
lib/view_model/integrations/deuro_view_model.dart
+42
-27
@@ -46,10 +46,8 @@ abstract class DEuroViewModelBase with Store {
46
47
@action
48
Future<void> reloadSavingsUserData() async {
49
- final savingsBalanceRaw =
50
- ethereum!.getDEuroSavingsBalance(_appStore.wallet!);
51
- final accruedInterestRaw =
52
- ethereum!.getDEuroAccruedInterest(_appStore.wallet!);
49
+ final savingsBalanceRaw = ethereum!.getDEuroSavingsBalance(_appStore.wallet!);
50
+ final accruedInterestRaw = ethereum!.getDEuroAccruedInterest(_appStore.wallet!);
51
52
approvedTokens = await ethereum!.getDEuroSavingsApproved(_appStore.wallet!);
53
@@ -63,56 +61,73 @@ abstract class DEuroViewModelBase with Store {
61
62
@action
63
Future<void> reloadInterestRate() async {
66
- final interestRateRaw =
67
- await ethereum!.getDEuroInterestRate(_appStore.wallet!);
64
+ final interestRateRaw = await ethereum!.getDEuroInterestRate(_appStore.wallet!);
65
66
interestRate = (interestRateRaw / BigInt.from(10000)).toString();
67
}
68
69
@action
70
Future<void> prepareApproval() async {
74
- final priority = _appStore.settingsStore.priority[WalletType.ethereum]!;
75
- approvalTransaction =
76
- await ethereum!.enableDEuroSaving(_appStore.wallet!, priority);
71
+ try {
72
+ state = TransactionCommitting();
73
+ final priority = _appStore.settingsStore.priority[WalletType.ethereum]!;
74
+ approvalTransaction = await ethereum!.enableDEuroSaving(_appStore.wallet!, priority);
75
+ state = InitialExecutionState();
76
+ } catch (e) {
77
+ state = FailureState(e.toString());
78
+ }
79
}
80
81
@action
82
Future<void> prepareSavingsEdit(String amountRaw, bool isAdding) async {
81
- final amount = BigInt.from(num.parse(amountRaw) * pow(10, 18));
82
- final priority = _appStore.settingsStore.priority[WalletType.ethereum]!;
83
- transaction = await (isAdding
84
- ? ethereum!.addDEuroSaving(_appStore.wallet!, amount, priority)
85
- : ethereum!.removeDEuroSaving(_appStore.wallet!, amount, priority));
83
+ try {
84
+ state = TransactionCommitting();
85
+ final amount = BigInt.from(num.parse(amountRaw) * pow(10, 18));
86
+ final priority = _appStore.settingsStore.priority[WalletType.ethereum]!;
87
+ transaction = await (isAdding
88
+ ? ethereum!.addDEuroSaving(_appStore.wallet!, amount, priority)
89
+ : ethereum!.removeDEuroSaving(_appStore.wallet!, amount, priority));
90
+ state = InitialExecutionState();
91
+ } catch (e) {
92
+ state = FailureState(e.toString());
93
+ }
94
}
95
88
- Future<void> prepareCollectInterest() =>
89
- prepareSavingsEdit(accruedInterest, false);
96
+ Future<void> prepareCollectInterest() => prepareSavingsEdit(accruedInterest, false);
97
98
@action
99
Future<void> commitTransaction() async {
100
if (transaction != null) {
94
- state = TransactionCommitting();
95
- await transaction!.commit();
96
- transaction = null;
97
- reloadSavingsUserData();
98
- state = TransactionCommitted();
101
+ try {
102
+ state = TransactionCommitting();
103
+ await transaction!.commit();
104
+ transaction = null;
105
+ reloadSavingsUserData();
106
+ state = TransactionCommitted();
107
+ } catch (e) {
108
+ state = FailureState(e.toString());
109
+ }
110
}
111
}
112
113
@action
114
Future<void> commitApprovalTransaction() async {
115
if (approvalTransaction != null) {
105
- state = TransactionCommitting();
106
- await approvalTransaction!.commit();
107
- approvalTransaction = null;
108
- reloadSavingsUserData();
109
- state = TransactionCommitted();
116
+ try {
117
+ state = TransactionCommitting();
118
+ await approvalTransaction!.commit();
119
+ approvalTransaction = null;
120
+ reloadSavingsUserData();
121
+ state = TransactionCommitted();
122
+ } catch (e) {
123
+ state = FailureState(e.toString());
124
+ }
125
}
126
}
127
128
@action
129
void dismissTransaction() {
115
- transaction == null;
130
+ transaction = null;
131
approvalTransaction = null;
132
state = InitialExecutionState();
133
}