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 }