diff --git a/cw_ethereum/lib/deuro/deuro_savings.dart b/cw_ethereum/lib/deuro/deuro_savings.dart index 4c37f944c..32f817d8d 100644 --- a/cw_ethereum/lib/deuro/deuro_savings.dart +++ b/cw_ethereum/lib/deuro/deuro_savings.dart @@ -6,12 +6,12 @@ import 'package:cw_core/crypto_currency.dart'; import 'package:cw_ethereum/deuro/deuro_savings_contract.dart'; import 'package:cw_ethereum/ethereum_wallet.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/pending_evm_chain_transaction.dart'; import 'package:web3dart/web3dart.dart'; -const String savingsGatewayAddress = - "0x073493d73258C4BEb6542e8dd3e1b2891C972303"; +const String savingsGatewayAddress = "0x073493d73258C4BEb6542e8dd3e1b2891C972303"; const String dEuroAddress = "0xbA3f535bbCcCcA2A154b573Ca6c5A49BAAE0a3ea"; @@ -35,81 +35,123 @@ class DEuro { client: client, ); - final frontendCode = - Uint8List.fromList(sha256.convert(utf8.encode("wallet")).bytes); + final frontendCode = Uint8List.fromList(sha256.convert(utf8.encode("wallet")).bytes); - EthereumAddress get _address => - EthereumAddress.fromHex(_wallet.walletAddresses.primaryAddress); + EthereumAddress get _address => EthereumAddress.fromHex(_wallet.walletAddresses.primaryAddress); Future get savingsBalance async => (await _savingsGateway.savings(accountOwner: _address)).saved; - Future get accruedInterest => - _savingsGateway.accruedInterest(accountOwner: _address); + Future get accruedInterest => _savingsGateway.accruedInterest(accountOwner: _address); Future get interestRate => _savingsGateway.currentRatePPM(); - Future get approvedBalance => - _dEuro.allowance(_address, _savingsGateway.self.address); + Future get approvedBalance => _dEuro.allowance(_address, _savingsGateway.self.address); - Future depositSavings( - BigInt amount, EVMChainTransactionPriority priority) async { - final signedTransaction = await _savingsGateway.save( - (amount: amount, frontendCode: frontendCode), - credentials: _wallet.evmChainPrivateKey, - ); + Future _checkEthBalanceForGasFees(EVMChainTransactionPriority priority) async { + final ethBalance = await _wallet.getWeb3Client()!.getBalance(_address); + final currentBalance = ethBalance.getInWei; - final fee = await _wallet.calculateActualEstimatedFeeForCreateTransaction( - amount: amount, + final gasFeesModel = await _wallet.calculateActualEstimatedFeeForCreateTransaction( + amount: BigInt.zero, contractAddress: _savingsGateway.self.address.hexEip55, receivingAddressHex: _savingsGateway.self.address.hexEip55, priority: priority, - data: _savingsGateway.self.abi.functions[17] - .encodeCall([amount, frontendCode]), + data: _savingsGateway.self.abi.functions[17].encodeCall([BigInt.zero, frontendCode]), ); - final sendTransaction = - () => _wallet.getWeb3Client()!.sendRawTransaction(signedTransaction); + final estimatedGasFee = BigInt.from(gasFeesModel.estimatedGasFee); - return PendingEVMChainTransaction( - sendTransaction: sendTransaction, - signedTransaction: signedTransaction, - fee: BigInt.from(fee.estimatedGasFee), - amount: amount.toString(), - exponent: 18); + final requiredBalance = estimatedGasFee; + + if (currentBalance < requiredBalance) { + throw DeuroGasFeeException( + requiredGasFee: requiredBalance, + currentBalance: currentBalance, + ); + } + } + + Future 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 withdrawSavings( BigInt amount, EVMChainTransactionPriority priority) async { - final signedTransaction = await _savingsGateway.withdraw( - (target: _address, amount: amount, frontendCode: frontendCode), - credentials: _wallet.evmChainPrivateKey, - ); + try { + await _checkEthBalanceForGasFees(priority); - 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 signedTransaction = await _savingsGateway.withdraw( + (target: _address, amount: amount, frontendCode: frontendCode), + credentials: _wallet.evmChainPrivateKey, + ); - final sendTransaction = - () => _wallet.getWeb3Client()!.sendRawTransaction(signedTransaction); + 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]), + ); - return PendingEVMChainTransaction( - sendTransaction: sendTransaction, - signedTransaction: signedTransaction, - fee: BigInt.from(fee.estimatedGasFee), - amount: amount.toString(), - exponent: 18); + 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; + } } // Set an infinite approval to save gas in the future - Future enableSavings( - EVMChainTransactionPriority priority) async => - (await _wallet.createApprovalTransaction( + Future enableSavings(EVMChainTransactionPriority priority) async { + try { + await _checkEthBalanceForGasFees(priority); + + return (await _wallet.createApprovalTransaction( BigInt.parse( 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', radix: 16, @@ -118,4 +160,14 @@ class DEuro { CryptoCurrency.deuro, priority, )) 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; + } + } } diff --git a/cw_evm/lib/evm_chain_exceptions.dart b/cw_evm/lib/evm_chain_exceptions.dart index c7509a17f..19bb047ef 100644 --- a/cw_evm/lib/evm_chain_exceptions.dart +++ b/cw_evm/lib/evm_chain_exceptions.dart @@ -22,3 +22,32 @@ class EVMChainTransactionFeesException implements Exception { @override 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; +} diff --git a/lib/src/screens/integrations/deuro/savings_page.dart b/lib/src/screens/integrations/deuro/savings_page.dart index a58578893..0b0f4941a 100644 --- a/lib/src/screens/integrations/deuro/savings_page.dart +++ b/lib/src/screens/integrations/deuro/savings_page.dart @@ -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/savings_card_widget.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/info_bottom_sheet_widget.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/send/send_view_model_state.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( + 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; diff --git a/lib/view_model/integrations/deuro_view_model.dart b/lib/view_model/integrations/deuro_view_model.dart index 09f9a363c..eca3c2376 100644 --- a/lib/view_model/integrations/deuro_view_model.dart +++ b/lib/view_model/integrations/deuro_view_model.dart @@ -46,10 +46,8 @@ abstract class DEuroViewModelBase with Store { @action Future reloadSavingsUserData() async { - final savingsBalanceRaw = - ethereum!.getDEuroSavingsBalance(_appStore.wallet!); - final accruedInterestRaw = - ethereum!.getDEuroAccruedInterest(_appStore.wallet!); + final savingsBalanceRaw = ethereum!.getDEuroSavingsBalance(_appStore.wallet!); + final accruedInterestRaw = ethereum!.getDEuroAccruedInterest(_appStore.wallet!); approvedTokens = await ethereum!.getDEuroSavingsApproved(_appStore.wallet!); @@ -63,56 +61,73 @@ abstract class DEuroViewModelBase with Store { @action Future reloadInterestRate() async { - final interestRateRaw = - await ethereum!.getDEuroInterestRate(_appStore.wallet!); + final interestRateRaw = await ethereum!.getDEuroInterestRate(_appStore.wallet!); interestRate = (interestRateRaw / BigInt.from(10000)).toString(); } @action Future prepareApproval() async { - final priority = _appStore.settingsStore.priority[WalletType.ethereum]!; - approvalTransaction = - await ethereum!.enableDEuroSaving(_appStore.wallet!, priority); + try { + state = TransactionCommitting(); + final priority = _appStore.settingsStore.priority[WalletType.ethereum]!; + approvalTransaction = await ethereum!.enableDEuroSaving(_appStore.wallet!, priority); + state = InitialExecutionState(); + } catch (e) { + state = FailureState(e.toString()); + } } @action Future prepareSavingsEdit(String amountRaw, bool isAdding) async { - final amount = BigInt.from(num.parse(amountRaw) * pow(10, 18)); - final priority = _appStore.settingsStore.priority[WalletType.ethereum]!; - transaction = await (isAdding - ? ethereum!.addDEuroSaving(_appStore.wallet!, amount, priority) - : ethereum!.removeDEuroSaving(_appStore.wallet!, amount, priority)); + try { + state = TransactionCommitting(); + final amount = BigInt.from(num.parse(amountRaw) * pow(10, 18)); + final priority = _appStore.settingsStore.priority[WalletType.ethereum]!; + transaction = await (isAdding + ? ethereum!.addDEuroSaving(_appStore.wallet!, amount, priority) + : ethereum!.removeDEuroSaving(_appStore.wallet!, amount, priority)); + state = InitialExecutionState(); + } catch (e) { + state = FailureState(e.toString()); + } } - Future prepareCollectInterest() => - prepareSavingsEdit(accruedInterest, false); + Future prepareCollectInterest() => prepareSavingsEdit(accruedInterest, false); @action Future commitTransaction() async { if (transaction != null) { - state = TransactionCommitting(); - await transaction!.commit(); - transaction = null; - reloadSavingsUserData(); - state = TransactionCommitted(); + try { + state = TransactionCommitting(); + await transaction!.commit(); + transaction = null; + reloadSavingsUserData(); + state = TransactionCommitted(); + } catch (e) { + state = FailureState(e.toString()); + } } } @action Future commitApprovalTransaction() async { if (approvalTransaction != null) { - state = TransactionCommitting(); - await approvalTransaction!.commit(); - approvalTransaction = null; - reloadSavingsUserData(); - state = TransactionCommitted(); + try { + state = TransactionCommitting(); + await approvalTransaction!.commit(); + approvalTransaction = null; + reloadSavingsUserData(); + state = TransactionCommitted(); + } catch (e) { + state = FailureState(e.toString()); + } } } @action void dismissTransaction() { - transaction == null; + transaction = null; approvalTransaction = null; state = InitialExecutionState(); }