import 'dart:async'; import 'package:hash_wallet/bitcoin/bitcoin.dart'; import 'package:hash_wallet/core/address_validator.dart'; import 'package:hash_wallet/core/amount_parsing_proxy.dart'; import 'package:hash_wallet/core/amount_validator.dart'; import 'package:hash_wallet/core/execution_state.dart'; import 'package:hash_wallet/core/open_crypto_pay/exceptions.dart'; import 'package:hash_wallet/core/open_crypto_pay/models.dart'; import 'package:hash_wallet/core/open_crypto_pay/open_cryptopay_service.dart'; import 'package:hash_wallet/core/validator.dart'; import 'package:hash_wallet/core/wallet_change_listener_view_model.dart'; import 'package:hash_wallet/entities/calculate_fiat_amount.dart'; import 'package:hash_wallet/entities/contact.dart'; import 'package:hash_wallet/entities/contact_record.dart'; import 'package:hash_wallet/entities/evm_transaction_error_fees_handler.dart'; import 'package:hash_wallet/entities/fiat_currency.dart'; import 'package:hash_wallet/entities/parsed_address.dart'; import 'package:hash_wallet/entities/preferences_key.dart'; import 'package:hash_wallet/entities/template.dart'; import 'package:hash_wallet/entities/transaction_description.dart'; import 'package:hash_wallet/entities/wallet_contact.dart'; import 'package:hash_wallet/evm/evm.dart'; import 'package:hash_wallet/exchange/provider/exchange_provider.dart'; import 'package:hash_wallet/exchange/provider/near_Intents_exchange_provider.dart'; import 'package:hash_wallet/exchange/trade.dart'; import 'package:hash_wallet/generated/i18n.dart'; import 'package:hash_wallet/monero/monero.dart'; import 'package:hash_wallet/nano/nano.dart'; import 'package:hash_wallet/reactions/wallet_connect.dart'; import 'package:hash_wallet/routes.dart'; import 'package:hash_wallet/store/app_store.dart'; import 'package:hash_wallet/store/dashboard/fiat_conversion_store.dart'; import 'package:hash_wallet/store/settings_store.dart'; import 'package:hash_wallet/utils/payment_request.dart'; import 'package:hash_wallet/view_model/contact_list/contact_list_view_model.dart'; import 'package:hash_wallet/view_model/dashboard/balance_view_model.dart'; import 'package:hash_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart'; import 'package:hash_wallet/view_model/send/fees_view_model.dart'; import 'package:hash_wallet/view_model/send/output.dart'; import 'package:hash_wallet/view_model/send/send_template_view_model.dart'; import 'package:hash_wallet/view_model/send/send_view_model_state.dart'; import 'package:hash_wallet/view_model/unspent_coins/unspent_coins_list_view_model.dart'; import 'package:hash_wallet/wownero/wownero.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/currency_for_wallet_type.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_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:hash_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; 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.sendTemplateViewModel, this._fiatConversationStore, 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), selectedChainId = _appStore.wallet!.chainId, outputs = ObservableList(), fiatFromSettings = _appStore.settingsStore.fiatCurrency, fiatCurrencies = FiatCurrency.all, super(appStore: _appStore) { outputs.add(Output(wallet, _appStore, _fiatConversationStore, () => selectedCryptoCurrency)); 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 await Future.delayed(const Duration(milliseconds: 100)); currencies = wallet.balance.keys.toList(); selectedCryptoCurrency = wallet.currency; updateSendingBalance(); }); } @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); @action void setShowAddressBookPopup(bool value) => _settingsStore.showAddressBookPopupEnabled = value; @action void addOutput() => outputs.add(Output(wallet, _appStore, _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.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.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); 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; } // 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.getDisplayCryptoString(0, selectedCryptoCurrency); } return _appStore.amountParsingProxy.getDisplayCryptoStringFromBigInt( wallet.balance[selectedCryptoCurrency]!.fullAvailableBalance, selectedCryptoCurrency); } @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 wallets atm: switch (wallet.type) { case WalletType.bitcoin: if (coinTypeToSpendFrom == UnspentCoinType.lightning) 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: 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) => wallet.type == WalletType.bitcoin && (RegExp(AddressValidator.silentPaymentAddressPatternMainnet).hasMatch(output.address) || RegExp(AddressValidator.silentPaymentAddressPatternMainnet) .hasMatch(output.extractedAddress) || (output.parsedAddress.addresses.isNotEmpty && RegExp(AddressValidator.silentPaymentAddressPatternMainnet) .hasMatch(output.parsedAddress.addresses[0]))); @computed List