import 'dart:math' show min; import 'package:hash_wallet/bitcoin/bitcoin.dart'; import 'package:hash_wallet/di.dart'; import 'package:hash_wallet/entities/calculate_fiat_amount.dart'; import 'package:hash_wallet/entities/calculate_fiat_amount_raw.dart'; import 'package:hash_wallet/entities/contact_base.dart'; import 'package:hash_wallet/entities/parse_address_from_domain.dart'; import 'package:hash_wallet/entities/parsed_address.dart'; import 'package:hash_wallet/evm/evm.dart'; import 'package:hash_wallet/generated/i18n.dart'; import 'package:hash_wallet/monero/monero.dart'; import 'package:hash_wallet/reactions/wallet_connect.dart'; import 'package:hash_wallet/src/screens/send/widgets/extract_address_from_parsed.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/wownero/wownero.dart'; import 'package:cw_core/balance.dart'; import 'package:cw_core/crypto_amount_format.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/currency_for_wallet_type.dart'; import 'package:cw_core/format_fixed.dart'; import 'package:cw_core/transaction_history.dart'; import 'package:cw_core/transaction_info.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; import 'package:mobx/mobx.dart'; part 'output.g.dart'; const String cryptoNumberPattern = '0.0'; class Output = OutputBase with _$Output; abstract class OutputBase with Store { OutputBase(this._wallet, this._appStore, this._fiatConversationStore, this.cryptoCurrencyHandler) : key = UniqueKey(), sendAll = false, cryptoAmount = '', cryptoFullBalance = '', fiatAmount = '', address = '', note = '', memo = "", extractedAddress = '', estimatedFee = '0.0', parsedAddress = ParsedAddress(addresses: []) { autorun((_) { final status = _wallet.syncStatus; printV("Sync status changed to $status. Recalculating fees"); calculateEstimatedFee(); }); } Key key; bool get useSatoshi => _appStore.amountParsingProxy.useSatoshi(cryptoCurrencyHandler()); @observable String fiatAmount; @observable String cryptoAmount; @observable String? displayName; @computed String get displayCryptoAmount => _appStore.amountParsingProxy.getDisplayCryptoAmount(cryptoAmount, cryptoCurrencyHandler()); @observable String cryptoFullBalance; @observable String address; @observable String note; @observable String memo; @observable bool sendAll; @observable ParsedAddress parsedAddress; @observable String extractedAddress; @computed bool get isParsedAddress => parsedAddress.parseFrom != ParseFrom.notParsed && parsedAddress.name.isNotEmpty; String roundedCryptoAmount(int digits) => displayCryptoAmount.withMaxDecimals(digits); String roundedFiatAmount(int digits) { if (fiatAmount.split(".").last.length <= digits) { return fiatAmount; } return double.tryParse(fiatAmount.replaceAll(",", ""))?.toStringAsPrecision(digits) ?? '0.0'; } @observable String? stealthAddress; @computed int get formattedCryptoAmount { int amount = 0; try { if (cryptoAmount.isNotEmpty) { final _cryptoAmount = cryptoAmount.replaceAll(',', '.'); int _amount = 0; switch (walletType) { case WalletType.monero: _amount = monero!.formatterMoneroParseAmount(amount: _cryptoAmount); break; case WalletType.bitcoin: case WalletType.litecoin: case WalletType.bitcoinCash: case WalletType.dogecoin: _amount = cryptoCurrencyHandler().parseAmount(_cryptoAmount).toInt(); break; case WalletType.ethereum: case WalletType.polygon: case WalletType.base: case WalletType.arbitrum: case WalletType.bsc: _amount = evm!.formatterEVMParseAmount(_cryptoAmount); break; case WalletType.wownero: _amount = wownero!.formatterWowneroParseAmount(amount: _cryptoAmount); break; case WalletType.none: case WalletType.nano: case WalletType.banano: break; } if (_amount > 0) { amount = _amount; } } } catch (e) { amount = 0; } return amount; } @observable String estimatedFee; @action Future calculateEstimatedFee() async { try { final priority = _settingsStore.getPriority(_wallet.type, chainId: _wallet.chainId); if (isEVMCompatibleChain(_wallet.type)) { await _wallet.updateEstimatedFeesParams(priority); } int fee = 0; if (_settingsStore.getPriority(_wallet.type, chainId: _wallet.chainId) != null) { fee = _wallet.calculateEstimatedFee( _settingsStore.getPriority(_wallet.type, chainId: _wallet.chainId)!, formattedCryptoAmount, ); } switch (_wallet.type) { case WalletType.monero: case WalletType.wownero: case WalletType.litecoin: case WalletType.bitcoinCash: case WalletType.dogecoin: estimatedFee = walletTypeToCryptoCurrency(_wallet.type).formatAmount(BigInt.from(fee)); break; case WalletType.bitcoin: if (cryptoCurrencyHandler() == CryptoCurrency.btcln) { estimatedFee = _appStore.amountParsingProxy.getDisplayCryptoString(10, cryptoCurrencyHandler()); break; } if (_settingsStore.getPriority(_wallet.type) == bitcoin!.getBitcoinTransactionPriorityCustom()) { fee = bitcoin!.getEstimatedFeeWithFeeRate( _wallet, _settingsStore.customBitcoinFeeRate, formattedCryptoAmount); } estimatedFee = _appStore.amountParsingProxy.getDisplayCryptoString(fee, cryptoCurrencyHandler()); break; /// EVMs case WalletType.ethereum: case WalletType.polygon: case WalletType.base: case WalletType.arbitrum: case WalletType.bsc: final isNative = cryptoCurrencyHandler() == CryptoCurrency.eth || cryptoCurrencyHandler() == CryptoCurrency.maticpoly || cryptoCurrencyHandler() == CryptoCurrency.baseEth || cryptoCurrencyHandler() == CryptoCurrency.arbEth || cryptoCurrencyHandler() == CryptoCurrency.bnb; String? fee = isNative ? evm!.getEVMNativeEstimatedFee(_wallet) : evm!.getEVMERC20EstimatedFee(_wallet); estimatedFee = formatFixed(BigInt.parse(fee ?? '0.0'), 18, fractionalDigits: 12); break; /// end EVMs case WalletType.nano: case WalletType.banano: case WalletType.none: // will not reach here as it doesn't have priority and this function is triggered only when priority changes break; } } catch (e) { printV(e.toString()); } } @computed String get estimatedFeeFiatAmount { // forces mobx to rebuild the computed value final _ = _wallet.syncStatus; try { final currency = isEVMCompatibleChain(_wallet.type) ? _wallet.currency : cryptoCurrencyHandler(); final cryptoAmount = double.parse(_appStore.amountParsingProxy.getCanonicalCryptoAmount(estimatedFee, currency)); return calculateFiatAmountRaw( price: _fiatConversationStore.prices[currency]!, cryptoAmount: cryptoAmount); } catch (_) { return '0.00'; } } @observable WalletBase, TransactionInfo> _wallet; WalletType get walletType => _wallet.type; final CryptoCurrency Function() cryptoCurrencyHandler; final FiatConversionStore _fiatConversationStore; final AppStore _appStore; SettingsStore get _settingsStore => _appStore.settingsStore; @action void setSendAll(String fullBalance) { cryptoFullBalance = _appStore.amountParsingProxy.getCanonicalCryptoAmount(fullBalance, cryptoCurrencyHandler()); sendAll = true; _updateFiatAmount(); } @action void updateWallet( WalletBase, TransactionInfo> newWallet) { _wallet = newWallet; } @action void reset() { sendAll = false; cryptoAmount = ''; fiatAmount = ''; address = ''; note = ''; memo = ""; resetParsedAddress(); } void resetParsedAddress() { displayName = null; extractedAddress = ''; parsedAddress = ParsedAddress(addresses: []); } @action /// [setCryptoAmount] always takes in the canonical representation eg. Bitcoin and not Sats void setCryptoAmount(String amount) { if (amount.toUpperCase() != S.current.all) sendAll = false; cryptoAmount = amount; _updateFiatAmount(); } @action void setFiatAmount(String amount) { fiatAmount = amount; _updateCryptoAmount(); } @action void _updateFiatAmount() { try { var cryptoAmount_ = sendAll ? cryptoFullBalance.replaceAll(",", ".") : cryptoAmount.replaceAll(',', '.'); var cryptoCurrency = cryptoCurrencyHandler() == CryptoCurrency.btcln ? CryptoCurrency.btc : cryptoCurrencyHandler(); final fiat = calculateFiatAmount( price: _fiatConversationStore.prices[cryptoCurrency]!, cryptoAmount: cryptoAmount_); if (fiatAmount != fiat) { fiatAmount = fiat; } } catch (_) { fiatAmount = ''; } } @action void _updateCryptoAmount() { try { var cryptoCurrency = cryptoCurrencyHandler() == CryptoCurrency.btcln ? CryptoCurrency.btc : cryptoCurrencyHandler(); final decimals = min(20, cryptoCurrencyHandler().decimals); final crypto = (double.parse(fiatAmount.replaceAll(',', '.')) / _fiatConversationStore.prices[cryptoCurrency]!) .toStringAsFixed(decimals); if (cryptoAmount != crypto) cryptoAmount = crypto; } catch (e) { printV(e); cryptoAmount = ''; } } Map get extra { final fields = {}; if (parsedAddress.parseFrom == ParseFrom.bip353) { fields['bip353_name'] = parsedAddress.name; fields['bip353_proof'] = parsedAddress.bip353DnsProof; } return fields; } Future fetchParsedAddress(BuildContext context) async { final domain = address; final currency = cryptoCurrencyHandler(); parsedAddress = await getIt.get().resolve(context, domain, currency); if(parsedAddress.name.isNotEmpty) { displayName = parsedAddress.name; } extractedAddress = await extractAddressFromParsed(context, parsedAddress); note = parsedAddress.description; } void loadContact(ContactBase contact) { address = contact.name; parsedAddress = ParsedAddress.fetchContactAddress(address: contact.address, name: contact.name); extractedAddress = parsedAddress.addresses.first; note = parsedAddress.description; } } extension OutputCopyWith on Output { Output OutputCopyWithParsedAddress({ ParsedAddress? parsedAddress, String? fiatAmount, }) { final clone = Output( _wallet, _appStore, _fiatConversationStore, cryptoCurrencyHandler, ); clone ..cryptoAmount = cryptoAmount ..cryptoFullBalance = cryptoFullBalance ..note = note ..sendAll = sendAll ..memo = memo ..stealthAddress = stealthAddress ..parsedAddress = parsedAddress ?? this.parsedAddress ..fiatAmount = fiatAmount ?? this.fiatAmount; return clone; } }