import 'dart:async'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/core/address_validator.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/models.dart'; import 'package:cake_wallet/core/open_crypto_pay/open_cryptopay_service.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/parsed_address.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/ethereum/ethereum.dart'; import 'package:cake_wallet/exchange/provider/exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/thorchain_exchange.provider.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/polygon/polygon.dart'; import 'package:cake_wallet/reactions/wallet_connect.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/solana/solana.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/ledger_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_template_view_model.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:cw_core/crypto_currency.dart'; import 'package:cw_core/exceptions.dart'; import 'package:cw_core/pending_transaction.dart'; import 'package:cw_core/sync_status.dart'; import 'package:cw_core/transaction_info.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: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 = wallet.currency; hasMultipleTokens = isEVMCompatibleChain(wallet.type) || wallet.type == WalletType.solana || wallet.type == WalletType.tron || wallet.type == WalletType.zano; } UnspentCoinsListViewModel unspentCoinsListViewModel; SendViewModelBase( AppStore appStore, this.sendTemplateViewModel, this._fiatConversationStore, this.balanceViewModel, this.contactListViewModel, this.transactionDescriptionBox, this.ledgerViewModel, this.unspentCoinsListViewModel, this.feesViewModel, { this.coinTypeToSpendFrom = UnspentCoinType.nonMweb, }) : state = InitialExecutionState(), currencies = appStore.wallet!.balance.keys.toList(), selectedCryptoCurrency = appStore.wallet!.currency, hasMultipleTokens = isEVMCompatibleChain(appStore.wallet!.type) || appStore.wallet!.type == WalletType.solana || appStore.wallet!.type == WalletType.tron || appStore.wallet!.type == WalletType.zano, outputs = ObservableList(), _settingsStore = appStore.settingsStore, fiatFromSettings = appStore.settingsStore.fiatCurrency, super(appStore: appStore) { outputs .add(Output(wallet, _settingsStore, _fiatConversationStore, () => selectedCryptoCurrency)); unspentCoinsListViewModel.initialSetup().then((_) { unspentCoinsListViewModel.resetUnspentCoinsInfoSelections(); }); } @observable ExecutionState state; ObservableList outputs; @observable UnspentCoinType coinTypeToSpendFrom; bool get showAddressBookPopup => _settingsStore.showAddressBookPopupEnabled; @action void setShowAddressBookPopup(bool value) { _settingsStore.showAddressBookPopupEnabled = value; } @action void addOutput() { outputs .add(Output(wallet, _settingsStore, _fiatConversationStore, () => selectedCryptoCurrency)); } @action void removeOutput(Output output) { if (isBatchSending) { outputs.remove(output); } } @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 fiat = calculateFiatAmount( price: _fiatConversationStore.prices[selectedCryptoCurrency]!, cryptoAmount: pendingTransaction!.amountFormatted); 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!.feeFormatted); return fiat; } else { return '0.00'; } } catch (_) { return '0.00'; } } CryptoCurrency pendingTransactionFeeCurrency(WalletType type) { switch (type) { case WalletType.ethereum: case WalletType.polygon: case WalletType.tron: case WalletType.solana: return wallet.currency; default: return selectedCryptoCurrency; } } FiatCurrency get fiat => _settingsStore.fiatCurrency; CryptoCurrency get currency => wallet.currency; Validator get amountValidator => AmountValidator(currency: walletTypeToCryptoCurrency(wallet.type)); Validator get allAmountValidator => AllAmountValidator(); Validator get addressValidator => AddressValidator(type: selectedCryptoCurrency); Validator get textValidator => TextValidator(); final FiatCurrency fiatFromSettings; @observable PendingTransaction? pendingTransaction; @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; } return wallet.balance[selectedCryptoCurrency]!.formattedFullAvailableBalance; } @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: case WalletType.litecoin: case WalletType.bitcoinCash: case WalletType.monero: case WalletType.wownero: case WalletType.decred: return wallet.formatCryptoAmount( (await unspentCoinsListViewModel.getSendingBalance(coinTypeToSpendFrom)).toString()); 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); @computed List