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.
This commit is contained in:
Blazebrain 2025-06-27 09:53:18 +01:00
parent 65bb917bfb
commit 2aebb4d912
4 changed files with 194 additions and 78 deletions

View file

@ -6,12 +6,12 @@ import 'package:cw_core/crypto_currency.dart';
import 'package:cw_ethereum/deuro/deuro_savings_contract.dart'; import 'package:cw_ethereum/deuro/deuro_savings_contract.dart';
import 'package:cw_ethereum/ethereum_wallet.dart'; import 'package:cw_ethereum/ethereum_wallet.dart';
import 'package:cw_evm/contract/erc20.dart'; import 'package:cw_evm/contract/erc20.dart';
import 'package:cw_evm/evm_chain_exceptions.dart';
import 'package:cw_evm/evm_chain_transaction_priority.dart'; import 'package:cw_evm/evm_chain_transaction_priority.dart';
import 'package:cw_evm/pending_evm_chain_transaction.dart'; import 'package:cw_evm/pending_evm_chain_transaction.dart';
import 'package:web3dart/web3dart.dart'; import 'package:web3dart/web3dart.dart';
const String savingsGatewayAddress = const String savingsGatewayAddress = "0x073493d73258C4BEb6542e8dd3e1b2891C972303";
"0x073493d73258C4BEb6542e8dd3e1b2891C972303";
const String dEuroAddress = "0xbA3f535bbCcCcA2A154b573Ca6c5A49BAAE0a3ea"; const String dEuroAddress = "0xbA3f535bbCcCcA2A154b573Ca6c5A49BAAE0a3ea";
@ -35,81 +35,123 @@ class DEuro {
client: client, client: client,
); );
final frontendCode = final frontendCode = Uint8List.fromList(sha256.convert(utf8.encode("wallet")).bytes);
Uint8List.fromList(sha256.convert(utf8.encode("wallet")).bytes);
EthereumAddress get _address => EthereumAddress get _address => EthereumAddress.fromHex(_wallet.walletAddresses.primaryAddress);
EthereumAddress.fromHex(_wallet.walletAddresses.primaryAddress);
Future<BigInt> get savingsBalance async => Future<BigInt> get savingsBalance async =>
(await _savingsGateway.savings(accountOwner: _address)).saved; (await _savingsGateway.savings(accountOwner: _address)).saved;
Future<BigInt> get accruedInterest => Future<BigInt> get accruedInterest => _savingsGateway.accruedInterest(accountOwner: _address);
_savingsGateway.accruedInterest(accountOwner: _address);
Future<BigInt> get interestRate => _savingsGateway.currentRatePPM(); Future<BigInt> get interestRate => _savingsGateway.currentRatePPM();
Future<BigInt> get approvedBalance => Future<BigInt> get approvedBalance => _dEuro.allowance(_address, _savingsGateway.self.address);
_dEuro.allowance(_address, _savingsGateway.self.address);
Future<PendingEVMChainTransaction> depositSavings( Future<void> _checkEthBalanceForGasFees(EVMChainTransactionPriority priority) async {
BigInt amount, EVMChainTransactionPriority priority) async { final ethBalance = await _wallet.getWeb3Client()!.getBalance(_address);
final signedTransaction = await _savingsGateway.save( final currentBalance = ethBalance.getInWei;
(amount: amount, frontendCode: frontendCode),
credentials: _wallet.evmChainPrivateKey,
);
final fee = await _wallet.calculateActualEstimatedFeeForCreateTransaction( final gasFeesModel = await _wallet.calculateActualEstimatedFeeForCreateTransaction(
amount: amount, amount: BigInt.zero,
contractAddress: _savingsGateway.self.address.hexEip55, contractAddress: _savingsGateway.self.address.hexEip55,
receivingAddressHex: _savingsGateway.self.address.hexEip55, receivingAddressHex: _savingsGateway.self.address.hexEip55,
priority: priority, priority: priority,
data: _savingsGateway.self.abi.functions[17] data: _savingsGateway.self.abi.functions[17].encodeCall([BigInt.zero, frontendCode]),
.encodeCall([amount, frontendCode]),
); );
final sendTransaction = final estimatedGasFee = BigInt.from(gasFeesModel.estimatedGasFee);
() => _wallet.getWeb3Client()!.sendRawTransaction(signedTransaction);
return PendingEVMChainTransaction( final requiredBalance = estimatedGasFee;
sendTransaction: sendTransaction,
signedTransaction: signedTransaction, if (currentBalance < requiredBalance) {
fee: BigInt.from(fee.estimatedGasFee), throw DeuroGasFeeException(
amount: amount.toString(), requiredGasFee: requiredBalance,
exponent: 18); currentBalance: currentBalance,
);
}
}
Future<PendingEVMChainTransaction> depositSavings(
BigInt amount, EVMChainTransactionPriority priority) async {
try {
await _checkEthBalanceForGasFees(priority);
final signedTransaction = await _savingsGateway.save(
(amount: amount, frontendCode: frontendCode),
credentials: _wallet.evmChainPrivateKey,
);
final fee = await _wallet.calculateActualEstimatedFeeForCreateTransaction(
amount: amount,
contractAddress: _savingsGateway.self.address.hexEip55,
receivingAddressHex: _savingsGateway.self.address.hexEip55,
priority: priority,
data: _savingsGateway.self.abi.functions[17].encodeCall([amount, frontendCode]),
);
final sendTransaction = () => _wallet.getWeb3Client()!.sendRawTransaction(signedTransaction);
return PendingEVMChainTransaction(
sendTransaction: sendTransaction,
signedTransaction: signedTransaction,
fee: BigInt.from(fee.estimatedGasFee),
amount: amount.toString(),
exponent: 18);
} catch (e) {
if (e.toString().contains('insufficient funds for gas')) {
final ethBalance = await _wallet.getWeb3Client()!.getBalance(_address);
throw DeuroGasFeeException(
currentBalance: ethBalance.getInWei,
);
}
rethrow;
}
} }
Future<PendingEVMChainTransaction> withdrawSavings( Future<PendingEVMChainTransaction> withdrawSavings(
BigInt amount, EVMChainTransactionPriority priority) async { BigInt amount, EVMChainTransactionPriority priority) async {
final signedTransaction = await _savingsGateway.withdraw( try {
(target: _address, amount: amount, frontendCode: frontendCode), await _checkEthBalanceForGasFees(priority);
credentials: _wallet.evmChainPrivateKey,
);
final fee = await _wallet.calculateActualEstimatedFeeForCreateTransaction( final signedTransaction = await _savingsGateway.withdraw(
amount: amount, (target: _address, amount: amount, frontendCode: frontendCode),
contractAddress: _savingsGateway.self.address.hexEip55, credentials: _wallet.evmChainPrivateKey,
receivingAddressHex: _savingsGateway.self.address.hexEip55, );
priority: priority,
data: _savingsGateway.self.abi.functions[17]
.encodeCall([amount, frontendCode]),
);
final sendTransaction = final fee = await _wallet.calculateActualEstimatedFeeForCreateTransaction(
() => _wallet.getWeb3Client()!.sendRawTransaction(signedTransaction); amount: amount,
contractAddress: _savingsGateway.self.address.hexEip55,
receivingAddressHex: _savingsGateway.self.address.hexEip55,
priority: priority,
data: _savingsGateway.self.abi.functions[17].encodeCall([amount, frontendCode]),
);
return PendingEVMChainTransaction( final sendTransaction = () => _wallet.getWeb3Client()!.sendRawTransaction(signedTransaction);
sendTransaction: sendTransaction,
signedTransaction: signedTransaction, return PendingEVMChainTransaction(
fee: BigInt.from(fee.estimatedGasFee), sendTransaction: sendTransaction,
amount: amount.toString(), signedTransaction: signedTransaction,
exponent: 18); fee: BigInt.from(fee.estimatedGasFee),
amount: amount.toString(),
exponent: 18);
} catch (e) {
if (e.toString().contains('insufficient funds for gas')) {
final ethBalance = await _wallet.getWeb3Client()!.getBalance(_address);
throw DeuroGasFeeException(
currentBalance: ethBalance.getInWei,
);
}
rethrow;
}
} }
// Set an infinite approval to save gas in the future // Set an infinite approval to save gas in the future
Future<PendingEVMChainTransaction> enableSavings( Future<PendingEVMChainTransaction> enableSavings(EVMChainTransactionPriority priority) async {
EVMChainTransactionPriority priority) async => try {
(await _wallet.createApprovalTransaction( await _checkEthBalanceForGasFees(priority);
return (await _wallet.createApprovalTransaction(
BigInt.parse( BigInt.parse(
'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff',
radix: 16, radix: 16,
@ -118,4 +160,14 @@ class DEuro {
CryptoCurrency.deuro, CryptoCurrency.deuro,
priority, priority,
)) as PendingEVMChainTransaction; )) as PendingEVMChainTransaction;
} catch (e) {
if (e.toString().contains('insufficient funds for gas')) {
final ethBalance = await _wallet.getWeb3Client()!.getBalance(_address);
throw DeuroGasFeeException(
currentBalance: ethBalance.getInWei,
);
}
rethrow;
}
}
} }

View file

@ -22,3 +22,32 @@ class EVMChainTransactionFeesException implements Exception {
@override @override
String toString() => exceptionMessage; String toString() => exceptionMessage;
} }
class DeuroGasFeeException implements Exception {
final String exceptionMessage;
final BigInt? requiredGasFee;
final BigInt? currentBalance;
DeuroGasFeeException({
this.requiredGasFee,
this.currentBalance,
}) : exceptionMessage = _buildMessage(requiredGasFee, currentBalance);
static String _buildMessage(BigInt? requiredGasFee, BigInt? currentBalance) {
const baseMessage = 'Insufficient ETH for gas fees.';
const addEthMessage = ' Please add ETH to your wallet to cover transaction fees.';
if (requiredGasFee != null) {
final requiredEth = (requiredGasFee / BigInt.from(10).pow(18)).toStringAsFixed(8);
final balanceInfo = currentBalance != null
? ', Available: ${(currentBalance / BigInt.from(10).pow(18)).toStringAsFixed(8)} ETH'
: '';
return '$baseMessage Required: ~$requiredEth ETH$balanceInfo.$addEthMessage';
}
return '$baseMessage$addEthMessage';
}
@override
String toString() => exceptionMessage;
}

View file

@ -4,9 +4,11 @@ import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/screens/integrations/deuro/widgets/interest_card_widget.dart'; import 'package:cake_wallet/src/screens/integrations/deuro/widgets/interest_card_widget.dart';
import 'package:cake_wallet/src/screens/integrations/deuro/widgets/savings_card_widget.dart'; import 'package:cake_wallet/src/screens/integrations/deuro/widgets/savings_card_widget.dart';
import 'package:cake_wallet/src/screens/integrations/deuro/widgets/savings_edit_sheet.dart'; import 'package:cake_wallet/src/screens/integrations/deuro/widgets/savings_edit_sheet.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart'; import 'package:cake_wallet/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart';
import 'package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart'; import 'package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart';
import 'package:cake_wallet/src/widgets/gradient_background.dart'; import 'package:cake_wallet/src/widgets/gradient_background.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/view_model/integrations/deuro_view_model.dart'; import 'package:cake_wallet/view_model/integrations/deuro_view_model.dart';
import 'package:cake_wallet/view_model/send/send_view_model_state.dart'; import 'package:cake_wallet/view_model/send/send_view_model_state.dart';
import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/crypto_currency.dart';
@ -190,6 +192,24 @@ class DEuroSavingsPage extends BasePage {
); );
}); });
} }
if (state is FailureState) {
WidgetsBinding.instance.addPostFrameCallback((_) async {
if (!context.mounted) return;
await showPopUp<void>(
context: context,
builder: (BuildContext popupContext) {
return AlertWithOneAction(
alertTitle: S.of(popupContext).error,
alertContent: state.error,
buttonText: S.of(popupContext).ok,
buttonAction: () => Navigator.of(popupContext).pop(),
);
},
);
});
}
}); });
_isReactionsSet = true; _isReactionsSet = true;

View file

@ -46,10 +46,8 @@ abstract class DEuroViewModelBase with Store {
@action @action
Future<void> reloadSavingsUserData() async { Future<void> reloadSavingsUserData() async {
final savingsBalanceRaw = final savingsBalanceRaw = ethereum!.getDEuroSavingsBalance(_appStore.wallet!);
ethereum!.getDEuroSavingsBalance(_appStore.wallet!); final accruedInterestRaw = ethereum!.getDEuroAccruedInterest(_appStore.wallet!);
final accruedInterestRaw =
ethereum!.getDEuroAccruedInterest(_appStore.wallet!);
approvedTokens = await ethereum!.getDEuroSavingsApproved(_appStore.wallet!); approvedTokens = await ethereum!.getDEuroSavingsApproved(_appStore.wallet!);
@ -63,56 +61,73 @@ abstract class DEuroViewModelBase with Store {
@action @action
Future<void> reloadInterestRate() async { Future<void> reloadInterestRate() async {
final interestRateRaw = final interestRateRaw = await ethereum!.getDEuroInterestRate(_appStore.wallet!);
await ethereum!.getDEuroInterestRate(_appStore.wallet!);
interestRate = (interestRateRaw / BigInt.from(10000)).toString(); interestRate = (interestRateRaw / BigInt.from(10000)).toString();
} }
@action @action
Future<void> prepareApproval() async { Future<void> prepareApproval() async {
final priority = _appStore.settingsStore.priority[WalletType.ethereum]!; try {
approvalTransaction = state = TransactionCommitting();
await ethereum!.enableDEuroSaving(_appStore.wallet!, priority); final priority = _appStore.settingsStore.priority[WalletType.ethereum]!;
approvalTransaction = await ethereum!.enableDEuroSaving(_appStore.wallet!, priority);
state = InitialExecutionState();
} catch (e) {
state = FailureState(e.toString());
}
} }
@action @action
Future<void> prepareSavingsEdit(String amountRaw, bool isAdding) async { Future<void> prepareSavingsEdit(String amountRaw, bool isAdding) async {
final amount = BigInt.from(num.parse(amountRaw) * pow(10, 18)); try {
final priority = _appStore.settingsStore.priority[WalletType.ethereum]!; state = TransactionCommitting();
transaction = await (isAdding final amount = BigInt.from(num.parse(amountRaw) * pow(10, 18));
? ethereum!.addDEuroSaving(_appStore.wallet!, amount, priority) final priority = _appStore.settingsStore.priority[WalletType.ethereum]!;
: ethereum!.removeDEuroSaving(_appStore.wallet!, amount, priority)); transaction = await (isAdding
? ethereum!.addDEuroSaving(_appStore.wallet!, amount, priority)
: ethereum!.removeDEuroSaving(_appStore.wallet!, amount, priority));
state = InitialExecutionState();
} catch (e) {
state = FailureState(e.toString());
}
} }
Future<void> prepareCollectInterest() => Future<void> prepareCollectInterest() => prepareSavingsEdit(accruedInterest, false);
prepareSavingsEdit(accruedInterest, false);
@action @action
Future<void> commitTransaction() async { Future<void> commitTransaction() async {
if (transaction != null) { if (transaction != null) {
state = TransactionCommitting(); try {
await transaction!.commit(); state = TransactionCommitting();
transaction = null; await transaction!.commit();
reloadSavingsUserData(); transaction = null;
state = TransactionCommitted(); reloadSavingsUserData();
state = TransactionCommitted();
} catch (e) {
state = FailureState(e.toString());
}
} }
} }
@action @action
Future<void> commitApprovalTransaction() async { Future<void> commitApprovalTransaction() async {
if (approvalTransaction != null) { if (approvalTransaction != null) {
state = TransactionCommitting(); try {
await approvalTransaction!.commit(); state = TransactionCommitting();
approvalTransaction = null; await approvalTransaction!.commit();
reloadSavingsUserData(); approvalTransaction = null;
state = TransactionCommitted(); reloadSavingsUserData();
state = TransactionCommitted();
} catch (e) {
state = FailureState(e.toString());
}
} }
} }
@action @action
void dismissTransaction() { void dismissTransaction() {
transaction == null; transaction = null;
approvalTransaction = null; approvalTransaction = null;
state = InitialExecutionState(); state = InitialExecutionState();
} }