import 'dart:async'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/core/address_resolver/parsed_address.dart'; import 'package:cake_wallet/core/address_resolver/address_resolver_service.dart'; import 'package:cake_wallet/core/address_validator.dart'; import 'package:cake_wallet/core/amount_parsing_proxy.dart'; import 'package:cake_wallet/core/amount_validator.dart'; import 'package:cake_wallet/core/execution_state.dart'; import 'package:cake_wallet/core/open_crypto_pay/exceptions.dart'; import 'package:cake_wallet/core/open_crypto_pay/models.dart'; import 'package:cake_wallet/core/open_crypto_pay/open_cryptopay_service.dart'; import 'package:cake_wallet/core/utilities.dart'; import 'package:cake_wallet/core/validator.dart'; import 'package:cake_wallet/core/wallet_change_listener_view_model.dart'; import 'package:cake_wallet/decred/decred.dart'; import 'package:cake_wallet/entities/calculate_fiat_amount.dart'; import 'package:cake_wallet/entities/contact.dart'; import 'package:cake_wallet/entities/contact_record.dart'; import 'package:cake_wallet/entities/evm_transaction_error_fees_handler.dart'; import 'package:cake_wallet/entities/fiat_currency.dart'; import 'package:cake_wallet/entities/preferences_key.dart'; import 'package:cake_wallet/entities/template.dart'; import 'package:cake_wallet/entities/transaction_description.dart'; import 'package:cake_wallet/entities/wallet_contact.dart'; import 'package:cake_wallet/evm/evm.dart'; import 'package:cake_wallet/exchange/exchange_provider_description.dart'; import 'package:cake_wallet/exchange/provider/exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/jupiter_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/near_Intents_exchange_provider.dart'; import 'package:cake_wallet/solana/solana.dart'; import 'package:cake_wallet/exchange/provider/swapsxyz_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/thorchain_exchange.provider.dart'; import 'package:cake_wallet/exchange/trade.dart'; import 'package:cake_wallet/exchange/trade_state.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/nano/nano.dart'; import 'package:cake_wallet/reactions/wallet_connect.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/tron/tron.dart'; import 'package:cake_wallet/utils/payment_request.dart'; import 'package:cake_wallet/view_model/contact_list/contact_list_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart'; import 'package:cake_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart'; import 'package:cake_wallet/view_model/send/fees_view_model.dart'; import 'package:cake_wallet/view_model/send/output.dart'; import 'package:cake_wallet/view_model/send/send_view_model_state.dart'; import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_list_view_model.dart'; import 'package:cake_wallet/wownero/wownero.dart'; import 'package:cake_wallet/zano/zano.dart'; import 'package:cake_wallet/zcash/zcash.dart'; import 'package:cw_core/amount/amount_sanitizer.dart'; import 'package:cw_core/amount/money.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/currency_for_wallet_type.dart'; import 'package:cw_core/erc20_token.dart'; import 'package:cw_core/exceptions.dart'; import 'package:cw_core/lnurl.dart'; import 'package:cw_core/pending_transaction.dart'; import 'package:cw_core/sync_status.dart'; import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/transaction_info.dart'; import 'package:cw_core/transaction_priority.dart'; import 'package:cw_core/unspent_coin_type.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/utils/token_utilities.dart'; import 'package:shared_preferences/shared_preferences.dart'; part 'send_view_model.g.dart'; class SendViewModel = SendViewModelBase with _$SendViewModel; abstract class SendViewModelBase extends WalletChangeListenerViewModel with Store { @override void onWalletChange(wallet) { currencies = wallet.balance.keys.toList(); selectedCryptoCurrency = coinTypeToSpendFrom == UnspentCoinType.lightning ? CryptoCurrency.btcln : wallet.currency; hasMultipleTokens = isEVMWallet || [WalletType.solana, WalletType.tron, WalletType.zano].contains(wallet.type); for (final output in outputs) { output.updateWallet(wallet); } // Update unspent coins list view model with the new wallet reference unspentCoinsListViewModel.updateWallet(wallet); // Update sending balance to reflect the new wallet's balance updateSendingBalance(); } UnspentCoinsListViewModel unspentCoinsListViewModel; SendViewModelBase( this._appStore, this._fiatConversationStore, this._adrResService, this.balanceViewModel, this.contactListViewModel, this.transactionDescriptionBox, this.hardwareWalletViewModel, this.unspentCoinsListViewModel, this.feesViewModel, { this.coinTypeToSpendFrom = UnspentCoinType.nonMweb, }) : state = InitialExecutionState(), currencies = _appStore.wallet!.balance.keys.toList(), selectedCryptoCurrency = coinTypeToSpendFrom == UnspentCoinType.lightning ? CryptoCurrency.btcln : _appStore.wallet!.currency, hasMultipleTokens = isEVMCompatibleChain(_appStore.wallet!.type) || [WalletType.solana, WalletType.tron, WalletType.zano].contains(_appStore.wallet!.type), selectedChainId = _appStore.wallet!.chainId, outputs = ObservableList(), fiatFromSettings = _appStore.settingsStore.fiatCurrency, fiatCurrencies = FiatCurrency.all, super(appStore: _appStore) { outputs.add(Output(wallet, _appStore, _fiatConversationStore, _outputCryptoCurrencyHandler)); unspentCoinsListViewModel.initialSetup(); // .then((_) => unspentCoinsListViewModel.resetUnspentCoinsInfoSelections()); reaction((_) { if (isEVMCompatibleChain(wallet.type)) { // Access currency which depends on selectedChainId, so MobX tracks the change return wallet.currency; } return null; }, (_) async { // When chain changes, update currencies and selected currency final selectionAtChainChange = selectedCryptoCurrency; await Future.delayed(const Duration(milliseconds: 100)); currencies = wallet.balance.keys.toList(); if (selectedCryptoCurrency == selectionAtChainChange) { selectedCryptoCurrency = wallet.currency; } updateSendingBalance(); }); } // Store trade and provider references for post-commit updates (e.g., Jupiter trade ID update) Trade? _currentTrade; ExchangeProvider? _currentProvider; @observable ExecutionState state; ObservableList outputs; @observable UnspentCoinType coinTypeToSpendFrom; bool get showAddressBookPopup => _settingsStore.showAddressBookPopupEnabled; bool get isMwebEnabled => balanceViewModel.mwebEnabled; bool get isMwebAvailable => wallet.currency == CryptoCurrency.ltc && balanceViewModel.mwebEnabled; bool get isEVMWallet => isEVMCompatibleChain(walletType); CryptoCurrency _outputCryptoCurrencyHandler([CryptoCurrency? override]) { if (override != null && override != selectedCryptoCurrency) { runInAction(() => selectedCryptoCurrency = override); } return selectedCryptoCurrency; } @action void setShowAddressBookPopup(bool value) => _settingsStore.showAddressBookPopupEnabled = value; @action void addOutput() => outputs.add(Output(wallet, _appStore, _fiatConversationStore, _outputCryptoCurrencyHandler)); @action void removeOutput(Output output) { if (isBatchSending) outputs.remove(output); } Future resolveAddressForOutput(Output output) async { final query = output.address.trim(); if (query.isEmpty) return null; // Check if the address is valid for the current currency (except for Zano, which can use handles as addresses) if (selectedCryptoCurrency != CryptoCurrency.zano) { final isValidAddress = AddressValidator( type: selectedCryptoCurrency, network: wallet.walletInfo.network, ).isValid(query); if (isValidAddress) return null; } final results = await _adrResService.resolve( query: query, wallet: wallet, currency: selectedCryptoCurrency, ); return results.isEmpty ? null : results.first; } @action void clearOutputs() { outputs.clear(); addOutput(); } @action void setAllowMwebCoins(bool allow) { if (wallet.type == WalletType.litecoin) { coinTypeToSpendFrom = allow ? UnspentCoinType.any : UnspentCoinType.nonMweb; } } @computed bool get isBatchSending => outputs.length > 1; bool get shouldDisplaySendALL { if (walletType == WalletType.solana) return false; // if (walletType == WalletType.ethereum && selectedCryptoCurrency == CryptoCurrency.eth) // return false; // if (walletType == WalletType.polygon && selectedCryptoCurrency == CryptoCurrency.maticpoly) // return false; return true; } @computed String get pendingTransactionFiatAmount { if (pendingTransaction == null) return '0.00'; try { final selectedCurrency = selectedCryptoCurrency == CryptoCurrency.btcln ? CryptoCurrency.btc : selectedCryptoCurrency; var currency = _fiatConversationStore.prices.keys .firstWhere((k) => k.titleAndTagEqual(selectedCurrency)); final fiat = calculateFiatAmount( price: _fiatConversationStore.prices[currency], cryptoAmount: pendingTransaction!.amountFormatted); return fiat; } catch (_) { return '0.00'; } } String calculateTransactionFiatAmount(String amountValue) { try { final fiat = calculateFiatAmount( price: _fiatConversationStore.prices[_fiatConversationStore.prices.keys .firstWhere((k) => k.titleAndTagEqual(selectedCryptoCurrency))], cryptoAmount: amountValue); return fiat; } catch (_) { return '0.00'; } } @computed String get pendingTransactionFeeFiatAmount { try { if (pendingTransaction != null) { final currency = pendingTransactionFeeCurrency(walletType); final fiat = calculateFiatAmount( price: _fiatConversationStore.prices[currency]!, cryptoAmount: pendingTransaction!.feeFormattedValue, ); return fiat; } else { return '0.00'; } } catch (_) { return '0.00'; } } CryptoCurrency pendingTransactionFeeCurrency(WalletType type) { switch (type) { case WalletType.ethereum: case WalletType.polygon: case WalletType.base: case WalletType.arbitrum: case WalletType.bsc: case WalletType.robinhood: case WalletType.tron: case WalletType.solana: case WalletType.bitcoin: return wallet.currency; default: return selectedCryptoCurrency; } } FiatCurrency get fiat => _settingsStore.fiatCurrency; CryptoCurrency get currency => selectedCryptoCurrency == CryptoCurrency.btcln ? CryptoCurrency.btcln : wallet.currency; String get currencySymbol => _appStore.amountParsingProxy.getCryptoSymbol(currency); Validator amountValidator(Output output) => AmountValidator( currency: wallet.currency, amountParsingProxy: _appStore.amountParsingProxy, minValue: isSendToSilentPayments(output) ? // TODO: get from server // bitcoin!.silentPaymentsMinAmount '0.00001' : null, ); Validator get allAmountValidator => AllAmountValidator(); Validator get addressValidator => AddressValidator( type: selectedCryptoCurrency, isTestnet: wallet.isTestnet, network: wallet.walletInfo.network, ); Validator get textValidator => TextValidator(); final FiatCurrency fiatFromSettings; @observable PendingTransaction? pendingTransaction; String? get pendingTransactionAdditionalCostNotice { final additionalCost = pendingTransaction?.additionalCost; if (additionalCost == null) { return null; } return S.current.recipient_account_creation_fee( _appStore.amountParsingProxy.asDisplayStringWithSymbol(additionalCost), ); } @computed String get balance { if (walletType == WalletType.litecoin && coinTypeToSpendFrom == UnspentCoinType.mweb) { return balanceViewModel.balances.values.first.secondAvailableBalance; } else if (walletType == WalletType.litecoin && coinTypeToSpendFrom == UnspentCoinType.nonMweb) { return balanceViewModel.balances.values.first.availableBalance; } // Handle case where balance might not be available yet (e.g., during chain switch) final balanceForCurrency = wallet.balance[selectedCryptoCurrency]; if (balanceForCurrency == null) { return _appStore.amountParsingProxy.asDisplayString(Money.zero(selectedCryptoCurrency)); } return _appStore.amountParsingProxy .asDisplayString(wallet.balance[selectedCryptoCurrency]!.available); } @action Future updateSendingBalance() async { // force the sendingBalance to recompute since unspent coins aren't observable // or at least mobx can't detect the changes final currentType = coinTypeToSpendFrom; if (currentType == UnspentCoinType.any) { coinTypeToSpendFrom = UnspentCoinType.nonMweb; } else if (currentType == UnspentCoinType.nonMweb) { coinTypeToSpendFrom = UnspentCoinType.any; } else if (currentType == UnspentCoinType.mweb) { coinTypeToSpendFrom = UnspentCoinType.nonMweb; } // set it back to the original value: coinTypeToSpendFrom = currentType; } @computed Future get sendingBalance async { // only for electrum, monero, wownero, decred wallets atm: switch (wallet.type) { case WalletType.bitcoin: if (selectedCryptoCurrency == CryptoCurrency.btcln) return balance; return _appStore.amountParsingProxy.getDisplayCryptoString( await unspentCoinsListViewModel.getSendingBalance(coinTypeToSpendFrom), walletTypeToCryptoCurrency(walletType)); case WalletType.litecoin: case WalletType.bitcoinCash: case WalletType.dogecoin: case WalletType.monero: case WalletType.wownero: case WalletType.decred: final sendingBalance = await unspentCoinsListViewModel.getSendingBalance(coinTypeToSpendFrom); return walletTypeToCryptoCurrency(walletType).formatAmount(BigInt.from(sendingBalance)); default: return balance; } } @computed bool get isFiatDisabled => balanceViewModel.isFiatDisabled; @computed String get pendingTransactionFiatAmountFormatted => isFiatDisabled ? '' : '$pendingTransactionFiatAmount ${fiat.title}'; @computed String get pendingTransactionFeeFiatAmountFormatted => isFiatDisabled ? '' : '$pendingTransactionFeeFiatAmount ${fiat.title}'; @computed bool get isReadyForSend => wallet.syncStatus is SyncedSyncStatus || // If silent payments scanning, can still send payments (wallet.type == WalletType.bitcoin && wallet.syncStatus is SyncingSyncStatus); bool isSendToSilentPayments(Output output) { if (wallet.type != WalletType.bitcoin) return false; final sp = RegExp(AddressValidator.silentPaymentAddressPatternMainnet); final address = output.extractedAddress.isNotEmpty ? output.extractedAddress : output.address; return sp.hasMatch(address); } @computed bool get hasCoinControl => [ WalletType.bitcoin, WalletType.litecoin, WalletType.monero, WalletType.wownero, WalletType.decred, WalletType.bitcoinCash, WalletType.dogecoin ].contains(wallet.type) && coinTypeToSpendFrom != UnspentCoinType.lightning; @computed bool get hasFees => feesViewModel.hasFees && coinTypeToSpendFrom != UnspentCoinType.lightning; @computed bool get isElectrumWallet => [ WalletType.bitcoin, WalletType.litecoin, WalletType.bitcoinCash, WalletType.dogecoin ].contains(wallet.type); @observable CryptoCurrency selectedCryptoCurrency; @computed String get selectedCryptoCurrencySymbol => amountParsingProxy.getCryptoSymbol(selectedCryptoCurrency); @computed bool get useBaseUnits => amountParsingProxy.useSatoshi(selectedCryptoCurrency); List currencies; WalletType get walletType => wallet.type; String? get walletCurrencyName => wallet.currency.fullName?.toLowerCase() ?? wallet.currency.name; @computed FiatCurrency get fiatCurrency => _settingsStore.fiatCurrency; set fiatCurrency(FiatCurrency value) { _settingsStore.fiatCurrency = value; } List fiatCurrencies; final AppStore _appStore; SettingsStore get _settingsStore => _appStore.settingsStore; final BalanceViewModel balanceViewModel; final ContactListViewModel contactListViewModel; final HardwareWalletViewModel? hardwareWalletViewModel; final FeesViewModel feesViewModel; final FiatConversionStore _fiatConversationStore; final AddressResolverService _adrResService; final Box transactionDescriptionBox; @computed AmountParsingProxy get amountParsingProxy => _appStore.amountParsingProxy; bool get hasMultiRecipient => wallet.type != WalletType.haven && wallet.type != WalletType.solana && wallet.type != WalletType.tron && !isEVMCompatibleChain(wallet.type) && coinTypeToSpendFrom != UnspentCoinType.lightning; @computed String get languageCode => _appStore.settingsStore.languageCode; @observable bool hasMultipleTokens; @observable int? selectedChainId; @computed List get contactsToShow => contactListViewModel.contacts .where((element) => element.type == selectedCryptoCurrency) .toList(); @computed List get walletContactsToShow => contactListViewModel.walletContacts .where((element) => element.type == selectedCryptoCurrency) .toList(); @action bool checkIfAddressIsAContact(String address) => contactsToShow.where((element) => element.address == address).toList().isNotEmpty; @action bool checkIfWalletIsAnInternalWallet(String address) => walletContactsToShow.where((element) => element.address == address).toList().isNotEmpty; @computed bool get shouldDisplayTOTP2FAForContact => _settingsStore.shouldRequireTOTP2FAForSendsToContact; @computed bool get shouldDisplayTOTP2FAForNonContact => _settingsStore.shouldRequireTOTP2FAForSendsToNonContact; @computed bool get shouldDisplayTOTP2FAForSendsToInternalWallet => _settingsStore.shouldRequireTOTP2FAForSendsToInternalWallets; @computed TransactionInfo? get transactionInfo { if (isEVMCompatibleChain(walletType)) { return wallet.transactionHistory.transactions[pendingTransaction?.evmTxHashFromRawHex]; } if (walletType == WalletType.monero) { // monero tx history keys are in format txhash_amount_accountindex_addressindex return wallet.transactionHistory.transactions[wallet.transactionHistory.transactions.keys .firstWhereOrNull((item) => item.split("_").first == pendingTransaction?.id)]; } if (walletType == WalletType.zcash) { // zcash has a txHash getter, but the pendingTransaction id contains quotation marks for whatever reason? final txIdNoQuotes = pendingTransaction?.id.replaceAll("\"", ""); return wallet.transactionHistory.transactions.values .firstWhereOrNull((item) => item.txHash == txIdNoQuotes); } return wallet.transactionHistory.transactions[pendingTransaction?.id]; } //* Still open to further optimize these checks //* It works but can be made better @action bool checkThroughChecksToDisplayTOTP(String address) { final isContact = checkIfAddressIsAContact(address); final isInternalWallet = checkIfWalletIsAnInternalWallet(address); if (isContact) { return shouldDisplayTOTP2FAForContact; } else if (isInternalWallet) { return shouldDisplayTOTP2FAForSendsToInternalWallet; } else { return shouldDisplayTOTP2FAForNonContact; } } bool shouldDisplayTotp() { List conditionsList = []; for (var output in outputs) { final show = checkThroughChecksToDisplayTOTP(output.extractedAddress); conditionsList.add(show); } return conditionsList.contains(true); } final _ocpService = OpenCryptoPayService(); @observable OpenCryptoPayRequest? ocpRequest; @action Future dismissTransaction() async { state = InitialExecutionState(); if (ocpRequest != null) { clearOutputs(); _ocpService.cancelOpenCryptoPayRequest(ocpRequest!); ocpRequest = null; } } @action Future getOpenCryptoPayRequest(String uri) async { try { final originalOCPRequest = await _ocpService.getOpenCryptoPayInvoice(uri.toString()); final paymentUri = await _ocpService.getOpenCryptoPayAddress( originalOCPRequest, selectedCryptoCurrency, ); ocpRequest = originalOCPRequest; clearOutputs(); return PaymentRequest.fromUri(paymentUri); } on OpenCryptoPayNotSupportedException catch (e) { printV(e.message); if (walletType == WalletType.bitcoin) { state = InitialExecutionState(); } else { state = FailureState(translateErrorMessage(e, walletType, currency)); } } catch (e) { printV(e); state = FailureState(translateErrorMessage(e, walletType, currency)); } return null; } @action Future createOpenCryptoPayTransaction(String uri) async { state = IsExecutingState(); try { final originalOCPRequest = await _ocpService.getOpenCryptoPayInvoice(uri.toString()); final paymentUri = await _ocpService.getOpenCryptoPayAddress( originalOCPRequest, selectedCryptoCurrency, ); ocpRequest = originalOCPRequest; final paymentRequest = PaymentRequest.fromUri(paymentUri); clearOutputs(); outputs.first.address = paymentRequest.address; outputs.first.parsedAddress = ParsedAddress( parsedAddressByCurrencyMap: {currency: paymentRequest.address}, handle: ocpRequest!.receiverName); outputs.first.setCryptoAmount(paymentRequest.amount); outputs.first.note = ocpRequest!.receiverName; return createTransaction(); } on OpenCryptoPayNotSupportedException catch (e) { printV(e.message); if (walletType == WalletType.bitcoin) { state = InitialExecutionState(); } else { state = FailureState(translateErrorMessage(e, walletType, currency)); } } catch (e) { printV(e); state = FailureState(translateErrorMessage(e, walletType, currency)); } return null; } static bool isLightningInvoice(String txt) { return RegExp(AddressValidator.bolt11InvoiceMatcher, caseSensitive: false).hasMatch(txt); } static bool isNonZeroAmountLightningInvoice(String txt) { return RegExp(AddressValidator.bolt11InvoiceMatcher, caseSensitive: false).hasMatch(txt) && !isBolt11ZeroInvoice(txt); } static bool isLnurlInvoice(String txt) { return RegExp(AddressValidator.lnurlMatcher, caseSensitive: false).hasMatch(txt); } Timer? _ledgerTxStateTimer; @action Future createTransaction({ExchangeProvider? provider, Trade? trade}) async { _currentTrade = trade; _currentProvider = provider; pendingTransaction = null; try { if (!(state is IsExecutingState)) state = IsExecutingState(); if (wallet.isHardwareWallet) { if (walletType == WalletType.monero) { _ledgerTxStateTimer = Timer.periodic(Duration(seconds: 1), (timer) { if (monero!.getLastLedgerCommand() == "INS_CLSAG") { timer.cancel(); state = IsDeviceSigningResponseState(); } }); } else { state = IsAwaitingDeviceResponseState(); } } // Swaps.xyz (EVM) path if (isEVMWallet && trade != null && provider is SwapsXyzExchangeProvider) { final routerTo = trade.inputAddress; final routerData = trade.routerData; if (routerData != null && routerData != '0x') { final tokenContract = (trade.sourceTokenAddress ?? '').toLowerCase(); final priority = _settingsStore.getPriority(walletType, chainId: selectedChainId); final routerValueWei = BigInt.tryParse(trade.routerValue ?? '0') ?? BigInt.zero; if (routerTo == null || routerTo.isEmpty) { state = FailureState('Invalid router address'); return null; } try { final selector = _decodeMethodSelector(routerData); const transferSig = '0xa9059cbb'; const swapAndExecuteSig = '0x9be111d1'; // Direct Transfer (Simple routing, no approval needed) if (selector == transferSig) { pendingTransaction = await evm!.createRawCallDataTransaction( wallet, routerTo, routerData, Money.zero(wallet.currency), priority, useBlinkProtection: canSupportBlinkProtection(selectedChainId) ? _settingsStore.useBlinkProtection : false, ); state = ExecutedSuccessfullyState(); return pendingTransaction; } // Smart Swap (Requires Approval) if (selector == swapAndExecuteSig) { final requiredAmount = BigInt.tryParse((trade.sourceTokenAmountRaw ?? '0').replaceAll('n', '')) ?? BigInt.zero; final needsApproval = tokenContract.isNotEmpty && requiredAmount > BigInt.zero ? await evm!.isApprovalRequired(wallet, tokenContract, routerTo, requiredAmount) : false; printV( '[Swaps.xyz sending flow] Approval required: $needsApproval for token ${trade.from?.title} ${trade.from?.tag ?? ''} with amount $requiredAmount'); if (needsApproval) { // USDT Approval Flow (Special Case). We must reset allowance to 0 first. final isUSDTMainnet = selectedChainId == 1 && tokenContract.toLowerCase() == '0xdac17f958d2ee523a2206206994597c13d831ec7'; if (isUSDTMainnet) { final currentAllowance = await evm!.getAllowance(wallet, tokenContract, routerTo); if (currentAllowance != null && currentAllowance > BigInt.zero) { printV( '[Swaps.xyz sending flow] currentAllowance USDT: $currentAllowance. Resetting to 0 before setting new allowance.'); final resetTx = await buildApprovalNeeded( spender: routerTo, tokenContract: tokenContract, requiredAmount: BigInt.zero, // Approve 0 sourceTokenDecimals: trade.sourceTokenDecimals, priority: priority); if (resetTx != null) { await resetTx.commit(); final resetConfirmed = await _waitForApprovalUpdate( tokenContract: tokenContract, spender: routerTo, requiredAmount: BigInt.zero, // Wait until it equals 0 waitForExactMatch: true, ); if (!resetConfirmed) { state = FailureState('Failed to reset USDT allowance. Please try again.'); return null; } printV( '[Swaps.xyz sending flow] USDT allowance reset to 0 confirmed on-chain.'); } } } // Standard Approval Flow final approvalTx = await buildApprovalNeeded( spender: routerTo, tokenContract: tokenContract, requiredAmount: requiredAmount, sourceTokenDecimals: trade.sourceTokenDecimals, priority: priority); if (approvalTx == null) { state = FailureState('Failed to build approval transaction'); return null; } pendingTransaction = null; try { printV( '[Swaps.xyz sending flow] Submitting approval transaction for token ${trade.from?.title} ${trade.from?.tag ?? ''} '); await approvalTx.commit(); // Wait for the approval to be mined on-chain final isApproved = await _waitForApprovalUpdate( tokenContract: tokenContract, spender: routerTo, requiredAmount: requiredAmount, ); if (!isApproved) { state = FailureState( 'Approval transaction failed or timed out on-chain. Try again.'); return null; } printV( '[Swaps.xyz sending flow] Approval transaction confirmed on-chain. Proceeding with swap execution.'); } catch (e, s) { printV('[Swaps.xyz sending flow] Approval transaction error: $e\n$s'); state = FailureState(translateErrorMessage(e, wallet.type, wallet.currency)); return null; } } // Construct Final Swap Transaction printV('[Swaps.xyz sending flow] Building swap transaction'); pendingTransaction = await evm!.createRawCallDataTransaction( wallet, routerTo, routerData, Money(routerValueWei, wallet.currency), priority, sourceTokenAddress: tokenContract, sourceTokenAmount: requiredAmount, useBlinkProtection: canSupportBlinkProtection(selectedChainId) ? _settingsStore.useBlinkProtection : false, ); state = ExecutedSuccessfullyState(); return pendingTransaction; } state = FailureState('Unsupported Swaps.xyz transaction type'); return null; } catch (e, s) { printV('Swaps.xyz transaction error: $e\n$s'); state = FailureState( 'Failed to create Swaps.xyz transaction - ${translateErrorMessage(e, wallet.type, wallet.currency)}'); return null; } } } // END Swaps.xyz path // Jupiter (Solana) swap path if (walletType == WalletType.solana && trade != null && provider is JupiterExchangeProvider) { final swapTransactionBase64 = trade.routerData; final requestId = trade.routerValue; if (swapTransactionBase64?.isNotEmpty == true && requestId?.isNotEmpty == true && solana != null) { try { final actualFee = trade.fee ?? 0.0005; // Fallback to estimate if not available final fee = actualFee > 0 ? actualFee : 0.0005; final fromCurrency = trade.from ?? CryptoCurrency.sol; final amount = Money.tryParse( trade.amount.sanitized(), fromCurrency, strictParsing: false, ) ?? Money.zero(fromCurrency); pendingTransaction = await solana!.signAndPrepareJupiterSwapTransaction( wallet, swapTransactionBase64!, requestId!, trade.payoutAddress ?? '', amount, Money.tryParse(fee.toString(), CryptoCurrency.sol) ?? Money.zero(CryptoCurrency.sol), ); state = ExecutedSuccessfullyState(); return pendingTransaction; } catch (e, s) { printV('Jupiter swap error: $e\n$s'); throw Exception('Failed to process Jupiter swap: $e'); } } } // Regular flow final isSendAll = outputs.any((output) => output.sendAll); if (!isSendAll) { final estimateTxAmount = outputs.fold( BigInt.zero, (acc, output) => acc + output.cryptoAmountMoney.amount); if (estimateTxAmount <= BigInt.zero) { throw Exception('Amount must be greater than 0'); } } pendingTransaction = await wallet.createTransaction(_credentials(provider)); final txAmountDouble = double.tryParse(pendingTransaction?.amountFormatted ?? '0') ?? 0.0; final bool isTradeTx = trade != null && provider != null; if (isTradeTx) { final tradeAmountMoney = Money.tryParse( trade.amount.sanitized(), trade.from ?? selectedCryptoCurrency, strictParsing: false, ); if (tradeAmountMoney == null || tradeAmountMoney.sign <= 0) { throw Exception('Trade amount must be greater than 0'); } final tradeAmountDouble = double.tryParse(tradeAmountMoney.toString()) ?? 0.0; if (trade.isSendAll == true) { if (provider is NearIntentsExchangeProvider) { if (txAmountDouble != tradeAmountDouble) { throw Exception( 'Transaction amount $txAmountDouble does not match expected trade amount $tradeAmountDouble'); } } } if (provider is ThorChainExchangeProvider) { final outputCount = pendingTransaction?.outputCount ?? 0; if (outputCount > 10) { throw Exception("THORChain does not support more than 10 outputs"); } if (_hasTaprootInput(pendingTransaction)) { throw Exception("THORChain does not support Taproot addresses"); } } } if (wallet.type == WalletType.bitcoin) { final updatedOutputs = bitcoin!.updateOutputs(pendingTransaction!, outputs); if (outputs.length == updatedOutputs.length) { outputs.replaceRange(0, outputs.length, updatedOutputs); } } state = ExecutedSuccessfullyState(); return pendingTransaction; } catch (e) { _ledgerTxStateTimer?.cancel(); // if (e is LedgerException) { // final errorCode = e.errorCode.toRadixString(16); // final fallbackMsg = // e.message.isNotEmpty ? e.message : "Unexpected Ledger Error Code: $errorCode"; // final errorMsg = ledgerViewModel!.interpretErrorCode(errorCode) ?? fallbackMsg; // // state = FailureState(errorMsg); // } else { state = FailureState(translateErrorMessage(e, wallet.type, wallet.currency)); // } } return null; } @action Future replaceByFee(TransactionInfo tx, String newFee) async { state = IsExecutingState(); try { final isSufficient = await bitcoin!.isChangeSufficientForFee(wallet, tx.id, newFee); if (!isSufficient) { state = AwaitingConfirmationState( title: S.current.confirm_fee_deduction, message: S.current.confirm_fee_deduction_content, onConfirm: () async => await _executeReplaceByFee(tx, newFee), onCancel: () => state = FailureState('Insufficient change for fee')); } else { await _executeReplaceByFee(tx, newFee); } } catch (e) { state = FailureState(e.toString()); } } Future _executeReplaceByFee(TransactionInfo tx, String newFee) async { clearOutputs(); final output = outputs.first; output.address = tx.outputAddresses?.first ?? ''; try { pendingTransaction = await bitcoin!.replaceByFee(wallet, tx.id, newFee); state = ExecutedSuccessfullyState(); } catch (e) { state = FailureState(e.toString()); } } Future _handleOcpRequest() async { if (OpenCryptoPayService.requiresClientCommit(selectedCryptoCurrency)) { await pendingTransaction!.commit(); } await _ocpService.commitOpenCryptoPayRequest( pendingTransaction!.hex, txId: pendingTransaction!.id, request: ocpRequest!, asset: selectedCryptoCurrency, ); } Future _commitUR(BuildContext context) async { final urstr = await pendingTransaction!.commitUR(); final result = await Navigator.of(context).pushNamed(Routes.urqrAnimatedPage, arguments: urstr); if (result == null) { throw "Canceled by user"; } } @action Future commitTransaction(BuildContext context) async { if (pendingTransaction == null) { throw Exception("Pending transaction doesn't exist. It should not be happened."); } try { state = wallet.isHardwareWallet && walletType == WalletType.monero ? IsAwaitingDeviceResponseState() : TransactionCommitting(); if (ocpRequest != null) { await _handleOcpRequest(); } else if (pendingTransaction!.shouldCommitUR()) { await _commitUR(context); } else { await pendingTransaction!.commit(); } state = TransactionCommitted(); if (_currentTrade != null) { final provider = _currentTrade!.provider; if (provider == ExchangeProviderDescription.swapsXyz) { registerSwapsXyzTransaction(_currentTrade!); } } await _updateSolanaTrade(signature: pendingTransaction!.id, isSuccess: true); if (walletType == WalletType.solana) { Future.delayed(Duration(seconds: 1), () async { try { await solana!.pollForTransaction( wallet, pendingTransaction!.id, initialDelay: const Duration(seconds: 1), maxRetries: 5, ); } catch (e) { printV('Failed to poll for transaction: $e'); } }); // Update balances for currencies involved in swap if (_currentTrade != null) { Future.delayed(Duration(seconds: 2), () async { try { final tokenMints = []; // Extract from currency mint (skip native SOL) if (_currentTrade!.from != null && _currentTrade!.from != CryptoCurrency.sol) { try { final fromMint = solana!.getTokenAddress(_currentTrade!.from!); tokenMints.add(fromMint); } catch (e) { printV('Error getting from currency mint: $e'); } } // Extract to currency mint (skip native SOL) if (_currentTrade!.to != null && _currentTrade!.to != CryptoCurrency.sol) { try { final toMint = solana!.getTokenAddress(_currentTrade!.to!); tokenMints.add(toMint); } catch (e) { printV('Error getting to currency mint: $e'); } } if (tokenMints.isNotEmpty) { solana!.updateTokenBalances( wallet, tokenMints: tokenMints, ); // Retry after a bit more time to ensure balance is updated Future.delayed(Duration(seconds: 2), () async { try { await solana!.updateTokenBalances( wallet, tokenMints: tokenMints, ); } catch (e) { printV('Error retrying balance update: $e'); } }); } } catch (e) { printV('Failed to update balances after send: $e'); } finally { _currentTrade = null; _currentProvider = null; } }); } } // Immediate transaction update for EVM chains, Tron, and Nano if (isEVMWallet || [WalletType.bitcoin, WalletType.solana, WalletType.tron, WalletType.nano] .contains(walletType)) { Future.delayed(Duration(seconds: 4), () async { try { await Future.wait([ wallet.updateTransactionsHistory(), wallet.updateBalance() as Future, ]); } catch (e) { printV('Failed to update transactions after send: $e'); } }); } // FIXME(malik) ideally, this should be done wallet-side. // it is required because evm, solana and tron don't actually save the transaction info when you send something. // instead, they rely on the tx to eventually get fetched at sync time, which can take a while if (isEVMWallet) { final selectedToken = evm!.getERC20Currencies(wallet).firstWhereOrNull( (token) => token.title.toUpperCase() == selectedCryptoCurrency.title.toUpperCase(), ); wallet.transactionHistory.addOne(evm!.getTransactionInfo( id: pendingTransaction!.evmTxHashFromRawHex!, height: 0, amount: outputs.first.cryptoAmountMoney, fee: pendingTransaction!.fee, tokenSymbol: selectedCryptoCurrency.title, direction: TransactionDirection.outgoing, isPending: true, date: DateTime.now(), confirmations: 0, chainId: wallet.chainId ?? 0, contractAddress: selectedToken?.contractAddress, )); } if (walletType == WalletType.solana) { wallet.transactionHistory.addOne(solana!.getTransactionInfo( id: pendingTransaction!.id, blockTime: DateTime.now(), to: "", from: "", direction: TransactionDirection.outgoing, amount: pendingTransaction!.amount, isPending: true, fee: pendingTransaction!.fee, )); } if (walletType == WalletType.tron) { wallet.transactionHistory.addOne(tron!.getTransactionInfo( id: pendingTransaction!.id, blockTime: DateTime.now(), direction: TransactionDirection.outgoing, amount: outputs.first.cryptoAmountMoney, isPending: true, fee: outputs.first.estimatedFee, )); } if (pendingTransaction!.id.isNotEmpty) { _addTransactionDescription(); } final sharedPreferences = await SharedPreferences.getInstance(); await sharedPreferences.setString(PreferencesKey.backgroundSyncLastTrigger(wallet.name), DateTime.now().add(Duration(minutes: 1)).toIso8601String()); } catch (e) { state = FailureState(translateErrorMessage(e, wallet.type, wallet.currency)); final failedSignature = e is JupiterSwapFailedException ? e.signature : ""; await _updateSolanaTrade(signature: failedSignature, isSuccess: false); } } /// Update Jupiter trade with relevant details after transaction is committed Future _updateSolanaTrade({required String signature, required bool isSuccess}) async { if (_currentTrade == null || _currentProvider?.title != 'Jupiter' || walletType != WalletType.solana) return; _currentTrade!.txId = signature; _currentTrade!.stateRaw = isSuccess ? TradeState.completed.raw : TradeState.failed.raw; await _currentTrade!.save(); } @action Future updateWalletBalance() async => await wallet.updateBalance(); Future _addTransactionDescription() async { String address = outputs.fold('', (acc, value) { final canonical = value.extractedAddress.trim().isNotEmpty ? value.extractedAddress.trim() : value.address.trim(); if (canonical.isEmpty) { return acc; } return '$acc$canonical\n\n'; }); address = address.trim(); String note = outputs.fold('', (acc, value) => '$acc${value.note}\n'); note = note.trim(); TransactionInfo? tx; if (walletType == WalletType.monero) { await Future.delayed(Duration(milliseconds: 450)); await wallet.fetchTransactions(); final txhistory = monero!.getTransactionHistory(wallet); tx = txhistory.transactions.values.last; } final descriptionKey = '${pendingTransaction!.id}_${wallet.walletAddresses.primaryAddress}'; _settingsStore.shouldSaveRecipientAddress ? await transactionDescriptionBox.add(TransactionDescription( id: descriptionKey, recipientAddress: address, transactionNote: note, transactionKey: tx?.additionalInfo["key"] as String?, )) : await transactionDescriptionBox.add(TransactionDescription( id: descriptionKey, transactionNote: note, transactionKey: tx?.additionalInfo["key"] as String?, )); } Object _credentials([ExchangeProvider? provider]) { final priority = _settingsStore.getPriority(wallet.type, chainId: wallet.chainId); if (priority == null && ![ WalletType.nano, WalletType.banano, WalletType.solana, WalletType.tron, WalletType.arbitrum, WalletType.robinhood, WalletType.zcash, ].contains(wallet.type)) { throw Exception('Priority is null for wallet type: ${wallet.type}'); } switch (wallet.type) { case WalletType.bitcoin: case WalletType.bitcoinCash: case WalletType.dogecoin: return bitcoin!.createBitcoinTransactionCredentials( outputs, priority: priority!, feeRate: feesViewModel.customBitcoinFeeRate, coinTypeToSpendFrom: coinTypeToSpendFrom, payjoinUri: _settingsStore.usePayjoin ? payjoinUri : null, ); case WalletType.litecoin: return bitcoin!.createBitcoinTransactionCredentials( outputs, priority: priority!, feeRate: feesViewModel.customBitcoinFeeRate, // if it's an exchange flow then disable sending from mweb coins coinTypeToSpendFrom: provider != null ? UnspentCoinType.nonMweb : coinTypeToSpendFrom, ); case WalletType.monero: return monero! .createMoneroTransactionCreationCredentials(outputs: outputs, priority: priority!); case WalletType.wownero: return wownero! .createWowneroTransactionCreationCredentials(outputs: outputs, priority: priority!); case WalletType.ethereum: case WalletType.polygon: case WalletType.base: case WalletType.arbitrum: case WalletType.bsc: case WalletType.robinhood: return evm!.createEVMTransactionCredentials( outputs, priority: priority, currency: selectedCryptoCurrency, useBlinkProtection: canSupportBlinkProtection(selectedChainId) ? _settingsStore.useBlinkProtection : false, ); case WalletType.nano: return nano!.createNanoTransactionCredentials(outputs); case WalletType.solana: return solana! .createSolanaTransactionCredentials(outputs, currency: selectedCryptoCurrency); case WalletType.tron: return tron!.createTronTransactionCredentials(outputs, currency: selectedCryptoCurrency); case WalletType.zano: return zano!.createZanoTransactionCredentials( outputs: outputs, priority: priority!, currency: selectedCryptoCurrency); case WalletType.decred: this.coinTypeToSpendFrom = UnspentCoinType.any; return decred!.createDecredTransactionCredentials(outputs, priority!); case WalletType.zcash: return zcash!.createZcashTransactionCredentials( outputs, currency: selectedCryptoCurrency, // priority: priority, ); default: throw Exception('Unexpected wallet type: ${wallet.type} for send'); } } bool _isEqualCurrency(String currency) => wallet.balance.keys.any((e) => currency.toLowerCase() == e.title.toLowerCase()); void onClose() => _settingsStore.fiatCurrency = fiatFromSettings; @action FiatCurrency setFiatCurrency(FiatCurrency fiat) { _settingsStore.fiatCurrency = fiat; return fiat; } @action void setSelectedCryptoCurrency(String cryptoCurrency) { try { selectedCryptoCurrency = wallet.balance.keys .firstWhere((e) => cryptoCurrency.toLowerCase() == e.title.toLowerCase()); } catch (e) { selectedCryptoCurrency = wallet.currency; } } @computed bool get hasMemos => [WalletType.zcash].contains(wallet.type); final Map _maxMemoLengths = { WalletType.zcash: 512, }; @computed int get maxMemoLength => _maxMemoLengths[wallet.type] ?? 9999999; ContactRecord? newContactAddress() { final Set contactAddresses = Set.from(contactListViewModel.contacts.map((contact) => contact.address)) ..addAll(contactListViewModel.walletContacts.map((contact) => contact.address)); for (final output in outputs) { final address = output.isParsedAddress ? output.parsedAddress.parsedAddressByCurrencyMap[selectedCryptoCurrency] ?? '' : output.address; if (address.isNotEmpty && !contactAddresses.contains(address) && selectedCryptoCurrency.raw != -1) { return ContactRecord( contactListViewModel.contactSource, Contact( name: '', address: address, type: selectedCryptoCurrency, ), ); } } return null; } // Helper functions for EVM transaction monitoring and approval flow // Polls the token contract directly to see if allowance is updated. Future _waitForApprovalUpdate({ required String tokenContract, required String spender, required BigInt requiredAmount, bool waitForExactMatch = false, }) async { if (!isEVMWallet || evm == null) return false; int attempts = 0; const int maxAttempts = 30; // ~60 seconds printV( '[Swaps.xyz sending flow] Starting allowance check. Target: $requiredAmount (Exact match: $waitForExactMatch)'); while (attempts < maxAttempts) { try { final currentAllowance = await evm!.getAllowance(wallet, tokenContract, spender); if (currentAllowance != null) { printV( '[Swaps.xyz sending flow] Current Allowance: $currentAllowance / Target: $requiredAmount'); if (waitForExactMatch) { // For Reset (Target 0): We need it to be exactly 0 (or less, though it can't be negative) if (currentAllowance <= requiredAmount) { printV('[Swaps.xyz sending flow] Allowance reset verified!'); return true; } } else { // For Approval: We need it to be at least the required amount if (currentAllowance >= requiredAmount) { printV('[Swaps.xyz sending flow] Allowance verified!'); return true; } } } } catch (e) { printV('[Swaps.xyz sending flow] Allowance check error: $e'); } await Future.delayed(const Duration(seconds: 1)); attempts++; } printV('[Swaps.xyz sending flow] Allowance check timed out.'); return false; } // Builds a token approval transaction Future buildApprovalNeeded({ required String spender, required String tokenContract, required BigInt requiredAmount, required TransactionPriority? priority, int? sourceTokenDecimals, }) async { final erc20Token = wallet.balance.keys.whereType().firstWhere( (t) => t.contractAddress.toLowerCase() == tokenContract.toLowerCase(), orElse: () => Erc20Token( name: '', symbol: '', contractAddress: tokenContract, decimal: sourceTokenDecimals ?? 18, enabled: true, ), ); return await evm!.createTokenApproval( wallet, Money(requiredAmount, erc20Token), spender, priority, useBlinkProtection: canSupportBlinkProtection(selectedChainId) ? _settingsStore.useBlinkProtection : false, ); } // End EVM helper functions String translateErrorMessage( Object error, WalletType walletType, CryptoCurrency currency, ) { String errorMessage = error.toString(); if (walletType == WalletType.solana) { if (errorMessage.contains('insufficient lamports')) { double solValueNeeded = 0.0; // Regular expression to match the number after "need". This shows the exact lamports the user needs to perform the transaction. RegExp regExp = RegExp(r'need (\d+)'); // Find the match Match? match = regExp.firstMatch(errorMessage); if (match != null) { String neededAmount = match.group(1)!; final lamportsNeeded = int.tryParse(neededAmount); // 5000 lamport used here is the constant for sending a transaction on solana int lamportsPerSol = 1000000000; solValueNeeded = lamportsNeeded != null ? ((lamportsNeeded + 5000) / lamportsPerSol) : 0.0; return S.current.insufficient_lamports(solValueNeeded.toString()); } else { return S.current.insufficient_lamport_for_tx; } } if (error is SignNativeTokenTransactionRentException) { return S.current.solana_sign_native_transaction_rent_exception; } if (error is CreateAssociatedTokenAccountException) { return "${S.current.solana_create_associated_token_account_exception} ${S.current.added_message_for_ata_error}"; } if (error is SignSPLTokenTransactionRentException) { return S.current.solana_sign_spl_token_transaction_rent_exception; } if (error is NoAssociatedTokenAccountException) { return S.current.solana_no_associated_token_account_exception; } if (error is AmbiguousTokenSymbolException) { return S.current.ambiguous_token_symbol_exception(error.symbol); } if (errorMessage.contains('found no record of a prior credit')) { return S.current.insufficient_funds_for_tx; } if (errorMessage.contains('insufficient funds for rent') && errorMessage.contains('Transaction simulation failed') && errorMessage.contains('account_index')) { final accountIndexMatch = RegExp(r'account_index: (\d+)').firstMatch(errorMessage); if (accountIndexMatch != null) { return int.parse(accountIndexMatch.group(1)!) == 0 ? S.current.insufficientFundsForRentError : S.current.insufficientFundsForRentErrorReceiver; } } if (errorMessage.contains('invalid account data')) { return S.current.solana_invalid_data_message; } if (errorMessage.contains('Blockhash not found') || errorMessage.contains('BlockhashNotFound') || errorMessage.contains('BlockhashMNotFound')) { return 'Transaction failed because its recent blockhash expired. ' 'Please retry your send; if this keeps happening, try again in ' 'a few seconds or switch to a different Solana node.'; } return errorMessage; } if (isEVMWallet || walletType == WalletType.haven) { if (errorMessage.contains('gas required exceeds allowance')) { return S.current.gas_exceeds_allowance; } if (errorMessage.contains('insufficient funds')) { final feeCurrency = switch (walletType) { WalletType.bsc => "BNB", WalletType.polygon => "POL", _ => "ETH", }; final parsedErrorMessageResult = EVMTransactionErrorFeesHandler.parseEthereumFeesErrorMessage( errorMessage, _fiatConversationStore.prices[currency] ?? 0.0, ); // Handle generic insufficient funds error (no specific values available) if (parsedErrorMessageResult.error == 'generic_insufficient_funds') { return S.current.insufficient_funds_for_tx; } // Handle parsing errors (couldn't parse the error message) if (parsedErrorMessageResult.error != null) { return S.current.insufficient_funds_for_tx; } // Handle successfully parsed errors with specific values return '''${S.current.insufficient_funds_for_tx} \n\n''' '''${S.current.balance}: ${parsedErrorMessageResult.balanceEth} ${feeCurrency} (${parsedErrorMessageResult.balanceUsd} ${fiatFromSettings.name})\n\n''' '''${S.current.transaction_cost}: ${parsedErrorMessageResult.txCostEth} ${feeCurrency} (${parsedErrorMessageResult.txCostUsd} ${fiatFromSettings.name})\n\n''' '''${S.current.overshot}: ${parsedErrorMessageResult.overshotEth} ${feeCurrency} (${parsedErrorMessageResult.overshotUsd} ${fiatFromSettings.name})'''; } if (errorMessage.contains('max fee per gas less than block base fee')) { return S.current.tx_retry_message; } return errorMessage; } if (walletType == WalletType.tron) { if (errorMessage.contains('balance is not sufficient')) { return S.current.do_not_have_enough_gas_asset(currency.toString()); } if (errorMessage.contains('Transaction expired')) { return S.current.tx_retry_message; } } if (error is TransactionWrongBalanceException) { if (error.amount != null) return S.current .tx_wrong_balance_with_amount_exception(currency.toString(), error.amount.toString()); return S.current.tx_wrong_balance_exception(currency.toString()); } if (error is TransactionNoInputsException) { return S.current.tx_not_enough_inputs_exception; } if (error is TransactionNoFeeException) { return S.current.tx_zero_fee_exception; } if (error is TransactionNoDustException) { return S.current.tx_no_dust_exception; } if (error is TransactionCommitFailedBIP68Final) { return S.current.trying_to_spend_locked_funds; } if (error is TransactionCommitFailed) { if (error.errorMessage != null && error.errorMessage!.contains("no peers replied")) { return S.current.tx_commit_failed_no_peers; } return "${S.current.tx_commit_failed}\nsupport@cakewallet.com${error.errorMessage != null ? "\n\n${error.errorMessage}" : ""}"; } if (error is TransactionCommitFailedDustChange) { return S.current.tx_rejected_dust_change; } if (error is TransactionCommitFailedDustOutput) { return S.current.tx_rejected_dust_output; } if (error is TransactionCommitFailedDustOutputSendAll) { return S.current.tx_rejected_dust_output_send_all; } if (error is TransactionCommitFailedVoutNegative) { return S.current.tx_rejected_vout_negative; } if (error is TransactionCommitFailedBIP68Final) { return S.current.tx_rejected_bip68_final; } if (error is TransactionCommitFailedLessThanMin) { return S.current.fee_less_than_min; } if (error is TransactionNoDustOnChangeException) { return S.current.tx_commit_exception_no_dust_on_change(error.min, error.max); } if (error is TransactionInputNotSupported) { return S.current.tx_invalid_input; } if (wallet.type == WalletType.bitcoin) { final lnError = getLightningErrorMessage(error); if (lnError != null) return lnError; } return errorMessage; } String? getLightningErrorMessage(Object error) { // TODO add more patterns Map errorPatterns = {"insufficient funds": S.current.insufficient_funds_for_tx}; for (final pattern in errorPatterns.keys) { if (error.toString().contains(pattern)) return errorPatterns[pattern]!; } return null; } bool _hasTaprootInput(PendingTransaction? pendingTransaction) { if (walletType == WalletType.bitcoin && pendingTransaction != null) { return bitcoin!.hasTaprootInput(pendingTransaction); } return false; } Future registerSwapsXyzTransaction(Trade trade) async { try { // register only for vmId is alt-vm or bridgeId is alt-vm (trade.needToRegisterInSwapXyz) final needToRegister = trade.needToRegisterInSwapXyz ?? false; if (!needToRegister) return; final vmId = (trade.providerId ?? '').toLowerCase(); if (vmId.isEmpty) { printV('SwapsXyz: transaction register: skipped (vmId empty)'); return; } final txHash = pendingTransaction?.evmTxHashFromRawHex ?? pendingTransaction?.id ?? ''; if (txHash.isEmpty) { printV('SwapsXyz: transaction register: skipped (txHash empty)'); return; } final chainId = int.tryParse(trade.router ?? '') ?? 0; if (chainId <= 0) { printV('SwapsXyz: transaction register: skipped (invalid chainId)'); return; } printV( 'SwapsXyz: attempting to register transaction: tradeId = ${trade.id}, txHash = $txHash, chainId = $chainId, vmId = $vmId'); final registered = await SwapsXyzExchangeProvider.registerAltVmTx( txId: trade.id, txHash: txHash, chainId: chainId, vmId: vmId, ); if (!registered) { printV('SwapsXyz: transaction register: failed'); } else { printV('SwapsXyz: transaction register: success'); } } catch (e) { printV('registerSwapsXyzTransaction error: $e'); } } @computed bool get usePayjoin => _settingsStore.usePayjoin; @observable String? payjoinUri; @action Future fetchTokenForContractAddress(String contractAddress, {WalletType? walletType}) async { final token = await TokenUtilities.findTokenByAddress( walletType: walletType ?? wallet.type, address: contractAddress, ); if (token != null) { selectedCryptoCurrency = token; } } @action void applyAnyPayCurrency(CryptoCurrency currency) { selectedCryptoCurrency = currency; } String _decodeMethodSelector(String s) => (s.startsWith('0x') && s.length >= 10) ? s.substring(0, 10) : ''; }