import 'dart:async'; import 'dart:collection'; import 'dart:convert'; import 'dart:developer'; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:hash_wallet/.secrets.g.dart' as secrets; import 'package:hash_wallet/bitcoin/bitcoin.dart'; import 'package:hash_wallet/core/amount_parsing_proxy.dart'; import 'package:hash_wallet/core/create_trade_result.dart'; import 'package:hash_wallet/core/fiat_conversion_service.dart'; import 'package:hash_wallet/core/lightning_invoice_service.dart'; import 'package:hash_wallet/core/utilities.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/exchange_api_mode.dart'; import 'package:hash_wallet/entities/fiat_api_mode.dart'; import 'package:hash_wallet/entities/fiat_currency.dart'; import 'package:hash_wallet/entities/preferences_key.dart'; import 'package:hash_wallet/entities/wallet_contact.dart'; import 'package:hash_wallet/exchange/exchange_provider_description.dart'; import 'package:hash_wallet/exchange/exchange_template.dart'; import 'package:hash_wallet/exchange/exchange_trade_state.dart'; import 'package:hash_wallet/exchange/limits.dart'; import 'package:hash_wallet/exchange/limits_state.dart'; import 'package:hash_wallet/exchange/provider/exchange_provider.dart'; import 'package:hash_wallet/exchange/provider/trocador_exchange_provider.dart'; import 'package:hash_wallet/exchange/trade.dart'; import 'package:hash_wallet/exchange/trade_request.dart'; import 'package:hash_wallet/generated/i18n.dart'; import 'package:hash_wallet/src/screens/exchange/widgets/currency_picker.dart'; import 'package:hash_wallet/store/app_store.dart'; import 'package:hash_wallet/utils/exchange_provider_logger.dart'; import 'package:hash_wallet/utils/show_pop_up.dart'; import 'package:cw_core/currency.dart'; import "package:cw_core/wallet_info.dart"; import 'package:hash_wallet/store/dashboard/fiat_conversion_store.dart'; import 'package:hash_wallet/store/dashboard/trades_store.dart'; import 'package:hash_wallet/store/settings_store.dart'; import 'package:hash_wallet/store/templates/exchange_template_store.dart'; import 'package:hash_wallet/evm/evm.dart'; import 'package:hash_wallet/reactions/wallet_connect.dart'; import 'package:hash_wallet/utils/token_utilities.dart'; import 'package:hash_wallet/view_model/contact_list/contact_list_view_model.dart'; import 'package:hash_wallet/view_model/send/fees_view_model.dart'; import 'package:hash_wallet/view_model/unspent_coins/unspent_coins_list_view_model.dart'; import 'package:cw_core/crypto_amount_format.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/erc20_token.dart'; import 'package:cw_core/spl_token.dart'; import 'package:cw_core/sync_status.dart'; import 'package:cw_core/transaction_priority.dart'; import 'package:cw_core/tron_token.dart'; import 'package:cw_core/unspent_coin_type.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; import 'package:mobx/mobx.dart'; import 'package:shared_preferences/shared_preferences.dart'; part 'exchange_view_model.g.dart'; class ExchangeViewModel = ExchangeViewModelBase with _$ExchangeViewModel; abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with Store { @override void onWalletChange(wallet) { // FIXME this caused a bug when switching wallet, i can't figure out why it's here? // receiveCurrency = wallet.currency; // depositCurrency = wallet.currency; } final List _disposers = []; void dispose() { bestRateSync.cancel(); for (final disposer in _disposers) { disposer(); } _disposers.clear(); } ExchangeViewModelBase( this._appStore, this._exchangeTemplateStore, this.tradesStore, this.sharedPreferences, this.contactListViewModel, this.unspentCoinsListViewModel, this.feesViewModel, this.fiatConversionStore, ) : isSendAllEnabled = false, isFixedRateMode = false, isReceiveAmountEntered = false, _depositAmount = '', _receiveAmount = '', receiveAddress = '', depositAddress = '', isDepositAddressEnabled = false, isReceiveAmountEditable = false, _useTorOnly = false, receiveCurrencies = ObservableList(), depositCurrencies = ObservableList(), limits = Limits(min: 0, max: 0), tradeState = ExchangeTradeStateInitial(), limitsState = LimitsInitialState(), receiveCurrency = _appStore.wallet!.currency, depositCurrency = _appStore.wallet!.currency, providerList = [], selectedProviders = ObservableList(), super(appStore: _appStore) { _useTorOnly = _settingsStore.exchangeStatus == ExchangeApiMode.torOnly; _setProviders(); // Hash Bags: the swap picker is a curated allowlist — natives of chains // the wallet actually ships plus major stables. Everything else stays // defined in CryptoCurrency (raw ids back Hive-stored trade history) but // is hidden from the swap UI. LTC is receive-only; BNB stays deposit-only // (inherited upstream restriction). const swapDepositAllowlist = [ CryptoCurrency.wow, CryptoCurrency.xmr, CryptoCurrency.btc, CryptoCurrency.bch, CryptoCurrency.doge, CryptoCurrency.nano, CryptoCurrency.eth, CryptoCurrency.baseEth, CryptoCurrency.arbEth, CryptoCurrency.bnb, CryptoCurrency.maticpoly, CryptoCurrency.usdterc20, CryptoCurrency.usdtbsc, CryptoCurrency.usdc, CryptoCurrency.usdcpoly, CryptoCurrency.dai, ]; const swapReceiveAllowlist = [ CryptoCurrency.wow, CryptoCurrency.xmr, CryptoCurrency.btc, CryptoCurrency.bch, CryptoCurrency.doge, CryptoCurrency.nano, CryptoCurrency.eth, CryptoCurrency.baseEth, CryptoCurrency.arbEth, CryptoCurrency.maticpoly, CryptoCurrency.usdterc20, CryptoCurrency.usdtbsc, CryptoCurrency.usdc, CryptoCurrency.usdcpoly, CryptoCurrency.dai, CryptoCurrency.ltc, ]; _initialPairBasedOnWallet(); unspentCoinsListViewModel.initialSetup().then((_) { unspentCoinsListViewModel.resetUnspentCoinsInfoSelections(); }); final Map exchangeProvidersSelection = json.decode(sharedPreferences.getString(PreferencesKey.exchangeProvidersSelection) ?? "{}") as Map; /// if the provider is not in the user settings (user's first time or newly added provider) /// then use its default value decided by us selectedProviders = ObservableList.of(providerList .where((element) => (!forceDecentralizedExchanges || !element.description.isCentralized) && (exchangeProvidersSelection[element.title] == null ? element.isEnabled : (exchangeProvidersSelection[element.title] as bool))) .toList()); _setAvailableProviders(); autorun((_) { if (selectedProviders.any((provider) => provider is TrocadorExchangeProvider)) { final trocadorProvider = selectedProviders.firstWhere((provider) => provider is TrocadorExchangeProvider) as TrocadorExchangeProvider; updateAllTrocadorProviderStates(trocadorProvider); } }); bestRateSync = Timer.periodic(Duration(seconds: 10), (timer) { if (tradeState is! TradeIsCreating) { calculateBestRate(); } }); isDepositAddressEnabled = !useSameWalletAddress(depositCurrency); _depositAmount = ''; _receiveAmount = ''; receiveAddress = ''; depositAddress = useSameWalletAddress(depositCurrency) ? wallet.walletAddresses.addressForExchange : ''; _disposers.add(reaction((_) => receiveAddress, (_) { if(!(tradeState is TradeIsCreatedSuccessfully)) { receiveAddressDisplayName = null; } })); provider = providerList.firstOrNull; final initialProvider = provider; provider!.checkIsAvailable().then((bool isAvailable) { if (!isAvailable && provider == initialProvider) { provider = providerList.last; _onPairChange(); } }); // providerDisplay is read by ui to display auto-selected provider. // it's on a delay so it doesn't flicker. _disposers.add(reaction((_) => bestRateProvider, (val) { providerDisplay = val; }, delay: 300)); receiveCurrencies = CryptoCurrency.all .where(swapReceiveAllowlist.contains) .toList() .asObservable(); depositCurrencies = CryptoCurrency.all .where(swapDepositAllowlist.contains) .toList() .asObservable(); _injectUserEthTokensIntoCurrencyLists(); _injectUserSplTokensIntoCurrencyLists(); _injectUserTronTokensIntoCurrencyLists(); _defineIsReceiveAmountEditable(); loadLimits(); _disposers.add(reaction((_) => isFixedRateMode, (Object _) { bestRateProvider = null; bestRate = 0.0; loadLimits(); })); _disposers.add(reaction((_) => forceDecentralizedExchanges, (val) { if (val && (bestRateProvider?.description.isCentralized ?? false)) { bestRateProvider = null; bestRate = 0.0; loadLimits(); } })); if (isElectrumWallet) { bitcoin!.updateFeeRates(wallet); } fetchFiatPrice(receiveCurrency); updateDepositAvailableAmount(); } Future updateDepositAvailableAmount() async { if (depositCurrency == CryptoCurrency.btcln) { final currency = depositCurrency; Future.doWhile(() async { await Future.delayed(const Duration(milliseconds: 400)); if (depositCurrency != currency) { return false; } final balance = wallet.balance[depositCurrency]; if (balance != null) { depositAvailableAmount = _appStore.amountParsingProxy .getDisplayCryptoStringFromBigInt(balance.fullAvailableBalance, depositCurrency); return false; } return true; }); } else if (isEVMCompatibleChain(wallet.type)) { final currency = depositCurrency; final balanceCurrency = wallet.balance.keys.firstWhereOrNull( (c) => c.title == depositCurrency.title && (c.tag == depositCurrency.tag || c.tag == depositCurrency.title), ); final balanceForCurrency = balanceCurrency != null ? wallet.balance[balanceCurrency] : null; if (depositCurrency == currency && balanceForCurrency != null) { depositAvailableAmount = _appStore.amountParsingProxy .getDisplayCryptoStringFromBigInt( balanceForCurrency.fullAvailableBalance, balanceCurrency!); } } else { final currency = depositCurrency; final amount = _appStore.amountParsingProxy.getDisplayCryptoString( (await unspentCoinsListViewModel.getSendingBalance(UnspentCoinType.any)), depositCurrency); if(depositCurrency == currency) { depositAvailableAmount = amount; } } } void showFiatCurrencyPicker(BuildContext context) { showPopUp( context: context, builder: (_) => CurrencyPicker( key: ValueKey('send_page_currency_picker_dialog_button_key'), selectedAtIndex: FiatCurrency.all.indexOf(fiat), items: FiatCurrency.all, hintText: S.of(context).search_currency, onItemSelected: (Currency cur) async { _settingsStore.fiatCurrency = (cur as FiatCurrency); }, ), ); } bool useSameWalletAddress(CryptoCurrency currency) => currency == wallet.currency || (currency == CryptoCurrency.btcln && wallet.currency == CryptoCurrency.btc && wallet.isSoftwareWallet) || (currency.tag != null && currency.tag == wallet.currency.tag) || currency.tag == wallet.currency.title; bool get isElectrumWallet => [ WalletType.bitcoin, WalletType.litecoin, WalletType.bitcoinCash, WalletType.dogecoin ].contains(wallet.type); bool get hideAddressAfterExchange => [WalletType.monero, WalletType.wownero].contains(wallet.type); bool _useTorOnly; final ExchangeTemplateStore _exchangeTemplateStore; final TradesStore tradesStore; final SharedPreferences sharedPreferences; // Hash Bags: trimmed to Trocador only. Trocador's aggregator already fronts // ChangeNow, LetsExchange, Exolix, StealthEx, FixedFloat, Quantex, etc., // letting us collect markup as the affiliate. The individual provider // implementations were deleted; restore them from Cake Wallet upstream and // register Such Software affiliate keys per provider before re-adding any. List get _allProviders => [ TrocadorExchangeProvider( useTorOnly: _useTorOnly, providerStates: _settingsStore.trocadorProviderStates), ]; @observable ExchangeProvider? provider; @observable ExchangeProvider? providerDisplay; /// Maps in dart are not sorted by default /// SplayTreeMap is a map sorted by keys /// will use it to sort available providers /// based on the rate they yield for the current trade /// /// /// initialize with descending comparator /// since we want largest rate first final SplayTreeMap _sortedAvailableProviders = SplayTreeMap((double a, double b) => b.compareTo(a)); final List _tradeAvailableProviders = []; Map _providerLimits = {}; @observable ObservableList selectedProviders; @observable List providerList; @observable CryptoCurrency depositCurrency; @observable CryptoCurrency receiveCurrency; @observable LimitsState limitsState; @observable ExchangeTradeState tradeState; @observable String _depositAmount; @computed String get depositAmount => _depositAmount == "..."? _depositAmount:amountParsingProxy.getDisplayCryptoAmount(_depositAmount, depositCurrency); @computed String get depositAmountCanonical => _depositAmount; @observable String _receiveAmount; @computed String get receiveAmount => _receiveAmount == "..." ? _receiveAmount:amountParsingProxy.getDisplayCryptoAmount(_receiveAmount, receiveCurrency); @action // only set canonical formated amounts here; void setCanonicalReceiveAmount(String value) => _receiveAmount = value; @observable String depositAddress; @observable String receiveAddress; @observable String? receiveAddressDisplayName; @observable bool isDepositAddressEnabled; @observable bool isReceiveAmountEntered; @observable bool isReceiveAmountEditable; @observable bool isFixedRateMode; @observable bool isSendAllEnabled; @observable Limits limits; @observable bool tradeStarted = false; @observable bool isSendFromExternal = false; @computed SyncStatus get status => wallet.syncStatus; @computed ObservableList get templates => _exchangeTemplateStore.templates; @computed List get walletContactsToShow => contactListViewModel.walletContacts .where((element) => element.type == receiveCurrency) .toList(); @computed List get refundWalletContactsToShow => contactListViewModel.walletContacts .where((element) => element.type == depositCurrency) .toList(); @computed Future> get receiveWallets async { WalletType? type; type = cryptoCurrencyOrTokenToWalletType(receiveCurrency); if (type == null) { type = cryptoCurrencyOrTokenToWalletType(CryptoCurrency.fromString(receiveCurrency.tag ?? "")); } return await WalletInfo.selectList("type = ?", [type!.index]); } Future> addressesForAccountsWallet(WalletInfo wallet) async { final List ret = []; final addresses = await wallet.getAddressInfos(); for (var list in addresses.values) { // we only want the "primary" account addresses - those that contain account names. ret.addAll(list.where((item) => item.label.split(" ").length > 1)); } return ret; } @computed Future> get depositWallets async { WalletType? type; type = cryptoCurrencyOrTokenToWalletType(depositCurrency); if (type == null) { try { type = cryptoCurrencyOrTokenToWalletType(CryptoCurrency.fromString(depositCurrency.tag ?? "")); } catch (_) {} } if (type != null) { return await WalletInfo.selectList("type = ?", [type.index]); } return await WalletInfo.getAll(); } @action bool checkIfWalletIsAnInternalWallet(String address) { final walletContactList = walletContactsToShow.where((element) => element.address == address).toList(); return walletContactList.isNotEmpty; } @computed bool get shouldDisplayTOTP2FAForExchangesToInternalWallet => _settingsStore.shouldRequireTOTP2FAForExchangesToInternalWallets; @computed bool get shouldDisplayTOTP2FAForExchangesToExternalWallet => _settingsStore.shouldRequireTOTP2FAForExchangesToExternalWallets; @computed String? get balanceDisplay { CryptoCurrency? balanceCurrency; if (isEVMCompatibleChain(wallet.type)) { balanceCurrency = wallet.balance.keys.firstWhereOrNull( (c) => c.title == depositCurrency.title && (c.tag == depositCurrency.tag || c.tag == depositCurrency.title), ); } else { balanceCurrency = depositCurrency; } final bal = balanceCurrency != null ? wallet.balance[balanceCurrency]?.fullAvailableBalance : null; if (bal == null) return null; return amountParsingProxy.getDisplayCryptoStringFromBigInt( bal, balanceCurrency ?? depositCurrency); } //* Still open to further optimize these checks //* It works but can be made better @action bool shouldDisplayTOTP() { final isInternalWallet = checkIfWalletIsAnInternalWallet(receiveAddress); if (isInternalWallet) { return shouldDisplayTOTP2FAForExchangesToInternalWallet; } else { return shouldDisplayTOTP2FAForExchangesToExternalWallet; } } @computed bool get decentralizedExchangesPromptDismissed => _settingsStore.decentralizedExchangesPromptDismissed; @action void dismissDecentralizedExchangesPrompt() { _settingsStore.decentralizedExchangesPromptDismissed = true; } @computed TransactionPriority get transactionPriority { final priority = _settingsStore.getPriority(wallet.type, chainId: wallet.chainId); if (priority == null) { throw Exception('Unexpected type ${wallet.type.toString()}'); } return priority; } bool get hasAllAmount { if ([ WalletType.monero, WalletType.bitcoin, WalletType.litecoin, WalletType.bitcoinCash, WalletType.dogecoin, ].contains(wallet.type)) return (depositCurrency == wallet.currency); if (!isEVMCompatibleChain(wallet.type)) return false; if (depositCurrency == wallet.currency) return true; return wallet.balance.keys.any((c) => c.title == depositCurrency.title && (c.tag == depositCurrency.tag || c.tag == depositCurrency.title)); } bool get isMoneroWallet => wallet.type == WalletType.monero; @observable ObservableList receiveCurrencies; @observable ObservableList depositCurrencies; final AppStore _appStore; SettingsStore get _settingsStore => _appStore.settingsStore; final ContactListViewModel contactListViewModel; final UnspentCoinsListViewModel unspentCoinsListViewModel; final FeesViewModel feesViewModel; @observable double bestRate = 0.0; @computed bool get useSatoshiDeposit => _appStore.amountParsingProxy.useSatoshi(depositCurrency); @computed bool get useSatoshisReceive => _appStore.amountParsingProxy.useSatoshi(receiveCurrency); @computed AmountParsingProxy get amountParsingProxy => _appStore.amountParsingProxy; @observable ExchangeProvider? bestRateProvider; @observable ExchangeProvider? forcedProvider; @action void setForcedProvider(ExchangeProvider? provider) { forcedProvider = provider; forcedProviderRate = 0.0; calculateForcedProviderRate(); } WalletInfo? selectedAddressBookWallet; @observable double forcedProviderRate = 0.0; late Timer bestRateSync; final FiatConversionStore fiatConversionStore; FiatCurrency get fiat => _settingsStore.fiatCurrency; @computed bool get isFiatDisabled => feesViewModel.isFiatDisabled; @action Future fetchFiatPrice(CryptoCurrency currency) async { if (fiatConversionStore.prices[currency] != null) { return; } fiatConversionStore.prices[currency] = await FiatConversionService.fetchPrice( crypto: currency, fiat: fiat, torOnly: _settingsStore.fiatApiMode == FiatApiMode.torOnly, ); } @computed String get receiveAmountFiatFormatted { var amount = '0.00'; try { if (_receiveAmount.isNotEmpty) { if (fiatConversionStore.prices[receiveCurrency] == null) return ''; amount = calculateFiatAmount( price: fiatConversionStore.prices[receiveCurrency]!, cryptoAmount: _receiveAmount, ); } } catch (_) { log('Error calculating receive amount fiat formatted: $_'); } return isFiatDisabled ? '' : '$amount'; } @computed String get depositAmountFiatFormatted { var amount = '0.00'; try { if (_depositAmount.isNotEmpty) { if (fiatConversionStore.prices[depositCurrency] == null) return ''; amount = calculateFiatAmount( price: fiatConversionStore.prices[depositCurrency]!, cryptoAmount: _depositAmount, ); } } catch (_) { log('Error calculating deposit amount fiat formatted: $_'); } return isFiatDisabled ? '' : '$amount'; } @computed String get receiveAmountFiat { var amount = ''; try { if (_receiveAmount.isNotEmpty) { if (fiatConversionStore.prices[receiveCurrency] == null) return ''; amount = calculateFiatAmount( price: fiatConversionStore.prices[receiveCurrency]!, cryptoAmount: _receiveAmount, raw: true); } } catch (_) { log('Error calculating receive amount fiat formatted: $_'); } return amount; } @computed String get depositAmountFiat { var amount = ''; try { if (_depositAmount.isNotEmpty) { if (fiatConversionStore.prices[depositCurrency] == null) return ''; amount = calculateFiatAmount( price: fiatConversionStore.prices[depositCurrency]!, cryptoAmount: _depositAmount, raw: true); } } catch (_) { log('Error calculating deposit amount fiat formatted: $_'); } return amount; } String roundedDepositAmount(int digits) { if (depositAmount.split(".").last.length <= digits) { return depositAmount; } try { return double.parse(depositAmount).toStringAsPrecision(digits); } catch(e) { return "0"; } } String roundedReceiveAmount(int digits) { if (receiveAmount.split(".").last.length <= digits) { return receiveAmount; } try { return double.parse(receiveAmount).toStringAsPrecision(digits); } catch(e) { return "0"; } } String roundedReceiveAmountFiat(int digits) { if (receiveAmountFiat.split(".").last.length <= digits) { return receiveAmountFiat; } return double.tryParse(receiveAmountFiat)?.toStringAsPrecision(digits) ?? '0.00'; } String roundedDepositAmountFiat(int digits) { if (depositAmountFiat.split(".").last.length <= digits) { return depositAmountFiat; } return double.tryParse(depositAmountFiat)?.toStringAsPrecision(digits) ?? '0.00'; } @action void changeDepositCurrency({required CryptoCurrency currency}) { final previousCurrency = depositCurrency; final wasSendAllEnabled = isSendAllEnabled; depositCurrency = currency; isFixedRateMode = false; isDepositAddressEnabled = !useSameWalletAddress(depositCurrency); if (previousCurrency != currency) { _onPairChange(clearBoth: wasSendAllEnabled); } fetchFiatPrice(currency); updateDepositAvailableAmount(); } @action void changeReceiveCurrency({required CryptoCurrency currency}) { final previousCurrency = receiveCurrency; if (!(currency.tag == receiveCurrency.tag || currency.tag == receiveCurrency.title || currency.title == receiveCurrency.tag || currency == receiveCurrency)) { receiveAddress = ""; } receiveCurrency = currency; isFixedRateMode = false; isDepositAddressEnabled = !useSameWalletAddress(depositCurrency); if (previousCurrency != currency) { _onPairChange(); } fetchFiatPrice(currency); } @action Future changeReceiveAmount({required String amount}) async { amount = amountParsingProxy.getCanonicalCryptoAmount(amount, receiveCurrency); _receiveAmount = amount; if (amount.isEmpty) { _depositAmount = ''; _receiveAmount = ''; return; } final _enteredAmount = double.tryParse(_receiveAmount.replaceAll(',', '.')) ?? 0; if (bestRate == 0) { _depositAmount = "..."; await calculateBestRate(); } final amount_ = _enteredAmount / (forcedProvider == null ? bestRate : forcedProviderRate); _depositAmount = amount_.toString().withMaxDecimals(depositCurrency.decimals); } @action void setReceiveAmountFromFiat({required String fiatAmount}) { final _enteredAmount = double.tryParse(fiatAmount.replaceAll(',', '.')) ?? 0.0; final price = fiatConversionStore.prices[receiveCurrency]; if (price == null || price == 0.0) return; final crypto = _enteredAmount / price; final _receiveAmountTmp = crypto.toString().withMaxDecimals(receiveCurrency.decimals); if (_receiveAmount != _receiveAmountTmp) { changeReceiveAmount(amount: _receiveAmountTmp); } } @action void setDepositAmountFromFiat({required String fiatAmount}) { final _enteredAmount = double.tryParse(fiatAmount.replaceAll(',', '.')) ?? 0.0; final price = fiatConversionStore.prices[depositCurrency]; if (price == null || price == 0.0) return; final crypto = _enteredAmount / price; final depositAmountTmp = crypto.toString().withMaxDecimals(depositCurrency.decimals); if (_depositAmount != depositAmountTmp) { changeDepositAmount(amount: depositAmountTmp, isCanonical: true); } } @action Future changeDepositAmount({required String amount, bool isCanonical = false}) async { _depositAmount = isCanonical ? amount.replaceAll(',', '.') : _appStore.amountParsingProxy .getCanonicalCryptoAmount(amount.replaceAll(',', '.'), depositCurrency); if (amount.isEmpty) { _depositAmount = ''; _receiveAmount = ''; return; } /// For fixed-rate transactions, we don't want to recalculate receive amount /// as it should remain exactly what the user set if (isFixedRateMode) return; final _enteredAmount = double.tryParse(_depositAmount) ?? 0; /// in case the best rate was not calculated yet if (bestRate == 0) { _receiveAmount = "..."; await calculateBestRate(); } final amount_ = _enteredAmount * (forcedProvider == null ? bestRate : forcedProviderRate); _receiveAmount = amount_.toString().withMaxDecimals(receiveCurrency.decimals); } bool checkIfInputMeetsMinOrMaxCondition(String input) { final _enteredAmount = double.tryParse(input.replaceAll(',', '.')) ?? 0; double minLimit = limits.min ?? 0; double? maxLimit = limits.max; if (_enteredAmount < minLimit) return false; if (maxLimit != null && _enteredAmount > maxLimit) return false; return true; } Future calculateForcedProviderRate() async { if (forcedProvider == null || depositCurrency == receiveCurrency) { forcedProviderRate = 0.0; return; } final amount = double.tryParse(isFixedRateMode ? _receiveAmount : _depositAmount) ?? initialAmountByAssets(isFixedRateMode ? receiveCurrency : depositCurrency); forcedProviderRate = await forcedProvider!.fetchRate( from: depositCurrency, to: receiveCurrency, amount: amount, isFixedRateMode: isFixedRateMode, isReceiveAmount: isFixedRateMode); } bool _excludeProviderForSwapAll(ExchangeProvider provider) => isSendAllEnabled && (provider.description == ExchangeProviderDescription.swapsXyz || provider.description == ExchangeProviderDescription.nearIntents); Future calculateBestRate() async { if (depositCurrency == receiveCurrency) { bestRate = 0.0; bestRateProvider = null; return; } final amount = double.tryParse(isFixedRateMode ? _receiveAmount : _depositAmount) ?? initialAmountByAssets(isFixedRateMode ? receiveCurrency : depositCurrency); final validProvidersForAmount = _tradeAvailableProviders.where((provider) { if (_excludeProviderForSwapAll(provider)) return false; final limits = _providerLimits[provider]; if (limits == null) return false; if (limits.min != null && amount < limits.min!) return false; if (limits.max != null && amount > limits.max!) return false; return true; }).toList(); final _providers = validProvidersForAmount .where((element) => !isFixedRateMode || element.supportsFixedRate) .toList(); final result = await Future.wait( _providers.map( (element) => element .fetchRate( from: depositCurrency, to: receiveCurrency, amount: amount, isFixedRateMode: isFixedRateMode, isReceiveAmount: isFixedRateMode) .timeout( Duration(seconds: 7), onTimeout: () => 0.0, ), ), ); // We'll use a new SplayTreeMap to avoid concurrent modification issues final newSortedProviders = SplayTreeMap((double a, double b) => b.compareTo(a)); for (int i = 0; i < result.length; i++) { if (result[i] != 0) { /// add this provider as its valid for this trade try { newSortedProviders[result[i]] = _providers[i]; } catch (e) { // will throw "Concurrent modification during iteration" error if modified at the same // time [createTrade] is called, as this is not a normal map, but a sorted map } } else { printV('calculateBestRate: ${_providers[i].title} returned rate=0'); } } // Replace the old map with the new one _sortedAvailableProviders.clear(); _sortedAvailableProviders.addAll(newSortedProviders); if (_sortedAvailableProviders.isNotEmpty) { bestRate = _sortedAvailableProviders.keys.first; bestRateProvider = _sortedAvailableProviders.values.first; } } @action Future loadLimits() async { if (depositCurrency == receiveCurrency) { limitsState = LimitsLoadedSuccessfully(limits: Limits(min: 0, max: 0)); return; } if (selectedProviders.isEmpty) return; limitsState = LimitsIsLoading(); final from = isFixedRateMode ? receiveCurrency : depositCurrency; final to = isFixedRateMode ? depositCurrency : receiveCurrency; double? lowestMin = double.maxFinite; double? highestMax = 0.0; try { final futures = selectedProviders .where((provider) => providerList.contains(provider)) .map((provider) async { final limits = await provider .fetchLimits( from: from, to: to, isFixedRateMode: isFixedRateMode, ) .onError((error, stackTrace) => null) .timeout( Duration(seconds: 7), onTimeout: () => null, ); return MapEntry(provider, limits); }).toList(); final entries = await Future.wait(futures); _providerLimits = Map.fromEntries(entries); _providerLimits.values .whereType() .forEach((tempLimits) { if (lowestMin != null && (tempLimits.min ?? -1) < lowestMin!) { lowestMin = tempLimits.min; } if (highestMax != null && (tempLimits.max ?? double.maxFinite) > highestMax!) { highestMax = tempLimits.max; } }); } on ConcurrentModificationError { /// if user changed the selected providers while fetching limits /// then delay the fetching limits a bit and try again /// /// this is because the limitation of collections that /// you can't modify it while iterating through it Future.delayed(Duration(milliseconds: 200), loadLimits); } if (lowestMin != double.maxFinite) { limits = Limits(min: lowestMin, max: highestMax); limitsState = LimitsLoadedSuccessfully(limits: limits); } else { limitsState = LimitsLoadedFailure(error: 'Limits loading failed'); } calculateBestRate(); } @action Future createTrade() async { if (isSendAllEnabled) { await calculateDepositAllAmount(); final amount = double.tryParse(_depositAmount); if (limits.min != null && amount != null && amount < limits.min!) { tradeState = TradeIsCreatedFailure( title: S.current.trade_not_created, error: S.current.amount_is_below_minimum_limit(limits.min!.toString())); return; } } if (depositCurrency == CryptoCurrency.btcln && depositAddress == wallet.walletAddresses.addressForExchange) { final invoice = await bitcoin!.getLightningInvoice(wallet, BigInt.zero); if (invoice != null) { depositAddress = invoice; } } if (receiveCurrency == CryptoCurrency.btcln && receiveAddress == wallet.walletAddresses.addressForExchange) { final invoice = await bitcoin!.getLightningInvoice(wallet, BigInt.zero); if (invoice != null) { receiveAddress = invoice; } } Map providers; if (forcedProvider != null) { providers = {forcedProviderRate: forcedProvider!}; } else { providers = Map.fromEntries( _sortedAvailableProviders.entries.where( (e) => selectedProviders.contains(e.value), ), ); } // Ensure we have providers available before attempting to create trade if (providers.isEmpty) { await calculateBestRate(); if (forcedProvider != null) { providers = {forcedProviderRate: forcedProvider!}; } else { providers = Map.fromEntries( _sortedAvailableProviders.entries.where( (e) => selectedProviders.contains(e.value), ), ); } if (providers.isEmpty) { ExchangeProviderLogger.logError( provider: null, function: 'createTrade', error: 'No providers available for $depositCurrency->$receiveCurrency', requestData: { 'from': depositCurrency.title, 'to': receiveCurrency.title, 'fromAmount': _depositAmount, 'toAmount': _receiveAmount, }, ); tradeState = TradeIsCreatedFailure( title: S.current.trade_not_created, error: S.current.none_of_selected_providers_can_exchange); return; } } try { if (selectedAddressBookWallet?.type == WalletType.bitcoin) { final _walletAddresses = await selectedAddressBookWallet!.getAddresses(); // if receive currency is lightning pick the lightning address // if normal bitcoin, then pick the segwit address if (receiveCurrency == CryptoCurrency.btcln) { final lightningAddressOfWallet = _walletAddresses.entries.firstWhereOrNull((e) => e.value.contains("LN"))?.key; if (lightningAddressOfWallet != null) { receiveAddress = lightningAddressOfWallet; } } if (receiveCurrency == CryptoCurrency.btc) { final segwitAddressOfWallet = _walletAddresses.entries.firstWhereOrNull((e) => e.value.contains("P2WPKH"))?.key; if (segwitAddressOfWallet != null) { receiveAddress = segwitAddressOfWallet; } } } // parse Lightning address to bolt11 invoice if (receiveAddress.contains("@")) { receiveAddress = await getBolt11FromLightingAddress(receiveAddress) ?? receiveAddress; } // snapshot of providers to avoid concurrent modification issues final providersSnapshot = providers.values.toList(); final ratesSnapshot = providers.keys.toList(); for (var i = 0; i < providersSnapshot.length; i++) { final provider = providersSnapshot[i]; final providerRate = ratesSnapshot[i]; printV('createTrade: trying provider=${provider.title}'); if (_excludeProviderForSwapAll(provider)) { printV('Skipping Swaps.xyz or Near Intents for swap all'); continue; } // should not happen but just as an extra check if (isFixedRateMode && provider.supportsFixedRate == false) { continue; } // Skip Swaps.xyz when sending from external if (isSendFromExternal && provider.description == ExchangeProviderDescription.swapsXyz) { printV('Skipping Swaps.xyz for external send'); continue; } if (!(await provider.checkIsAvailable())) continue; bestRate = providerRate; bestRateProvider = provider; await changeDepositAmount(amount: _depositAmount, isCanonical: true); final request = TradeRequest( fromCurrency: depositCurrency, toCurrency: receiveCurrency, fromAmount: _depositAmount.replaceAll(',', '.'), toAmount: _receiveAmount.replaceAll(',', '.'), refundAddress: depositAddress, toAddress: receiveAddress, isFixedRate: isFixedRateMode, ); if (hideAddressAfterExchange) { wallet.walletAddresses.hiddenAddresses.add(depositAddress); await wallet.walletAddresses.saveAddressesInBox(); } var amount = isFixedRateMode ? _receiveAmount : _depositAmount; amount = amount.replaceAll(',', '.'); if (limitsState is LimitsLoadedSuccessfully) { if (double.tryParse(amount) == null) { printV('createTrade: ${provider.title} amount parse failed: "$amount"'); continue; } if (limits.min != null && double.parse(amount) < limits.min!) { continue; } else if (limits.max != null && double.parse(amount) > limits.max!) { continue; } else { try { tradeState = TradeIsCreating(); final trade = await provider.createTrade( request: request, isFixedRateMode: isFixedRateMode, isSendAll: isSendAllEnabled, ); trade.walletId = wallet.id; trade.chainId = wallet.chainId; trade.fromWalletAddress = wallet.walletAddresses.address; final canCreateTrade = await isCanCreateTrade(trade); if (!canCreateTrade.result) { ExchangeProviderLogger.logError( provider: provider.description, function: 'createTrade', error: canCreateTrade.errorMessage ?? 'isCanCreateTrade returned false', requestData: { 'from': depositCurrency.title, 'to': receiveCurrency.title, 'fromAmount': _depositAmount, 'toAmount': _receiveAmount, }, ); continue; } tradesStore.setTrade(trade); if (trade.provider != ExchangeProviderDescription.thorChain) await trade.save(); tradeState = TradeIsCreatedSuccessfully(trade: trade); /// return after the first successful trade return; } catch (e, s) { ExchangeProviderLogger.logError( provider: provider.description, function: 'createTrade', error: e, stackTrace: s, requestData: { 'from': depositCurrency.title, 'to': receiveCurrency.title, 'fromAmount': _depositAmount, 'toAmount': _receiveAmount, 'toAddress': receiveAddress, 'refundAddress': depositAddress, }, ); continue; } } } } /// if the code reached here then none of the providers succeeded tradeState = TradeIsCreatedFailure( title: S.current.trade_not_created, error: S.current.none_of_selected_providers_can_exchange); } on ConcurrentModificationError { /// if create trade happened at the exact same time of the scheduled rate update /// then delay the create trade a bit and try again /// /// this is because the limitation of the SplayTreeMap that /// you can't modify it while iterating through it Future.delayed(Duration(milliseconds: 200), createTrade); } } @action void reset() { _initialPairBasedOnWallet(); isReceiveAmountEntered = false; _depositAmount = ''; _receiveAmount = ''; depositAddress = depositCurrency == wallet.currency ? wallet.walletAddresses.addressForExchange : ''; receiveAddress = receiveCurrency == wallet.currency ? wallet.walletAddresses.addressForExchange : ''; isDepositAddressEnabled = !(depositCurrency == wallet.currency); isFixedRateMode = false; _onPairChange(); } @action void enableSendAllAmount() { isSendAllEnabled = true; isFixedRateMode = false; calculateDepositAllAmount(); } @action void enableFixedRateMode() { isSendAllEnabled = false; isFixedRateMode = true; } @action Future calculateDepositAllAmount() async { if ([ WalletType.litecoin, WalletType.bitcoin, WalletType.bitcoinCash, WalletType.dogecoin, ].contains(wallet.type)) { final priority = _settingsStore.getPriority(wallet.type)!; final amount = depositCurrency == CryptoCurrency.btcln // FIXME amount estimation is broken/impossible for ln, konsti suggested this ? (amountParsingProxy .parseCryptoString(depositAvailableAmount, depositCurrency).toInt() - 10) : await bitcoin!.estimateFakeSendAllTxAmount( wallet, priority, coinTypeToSpendFrom: wallet.type == WalletType.litecoin ? UnspentCoinType.nonMweb : depositCurrency == CryptoCurrency.btcln ? UnspentCoinType.lightning : UnspentCoinType.any, ); changeDepositAmount(amount: wallet.currency.formatAmount(BigInt.from(amount)), isCanonical: true); } else if (wallet.type == WalletType.monero) { final amount = await unspentCoinsListViewModel.getSendingBalance(UnspentCoinType.any); changeDepositAmount(amount: wallet.currency.formatAmount(BigInt.from(amount)), isCanonical: true); } else if (isEVMCompatibleChain(wallet.type)) { final balanceCurrency = wallet.balance.keys.firstWhereOrNull( (currency) => currency.title == depositCurrency.title && (currency.tag == depositCurrency.tag || currency.tag == depositCurrency.title), ); final balanceForCurrency = balanceCurrency != null ? wallet.balance[balanceCurrency] : null; if (balanceForCurrency == null) { changeDepositAmount(amount: wallet.currency.formatAmount(BigInt.zero), isCanonical: true); return; } final balanceAmount = _appStore.amountParsingProxy.getDisplayCryptoStringFromBigInt(balanceForCurrency.fullAvailableBalance, balanceCurrency!); final balanceDouble = double.tryParse(balanceAmount.replaceAll(',', '.')) ?? 0.0; if (balanceDouble <= 0) { changeDepositAmount(amount: wallet.currency.formatAmount(BigInt.zero), isCanonical: true); return; } final isNative = depositCurrency == wallet.currency; if (!isNative) { changeDepositAmount(amount: balanceAmount, isCanonical: true); return; } try { final priority = _settingsStore.getPriority(wallet.type, chainId: wallet.chainId); await wallet.updateEstimatedFeesParams(priority); String? feeString; if (isEVMCompatibleChain(wallet.type)) { feeString = evm!.getEVMNativeEstimatedFee(wallet); } if (feeString == null || feeString.isEmpty) { changeDepositAmount(amount: balanceAmount, isCanonical: true); return; } final balanceWei = balanceForCurrency.fullAvailableBalance; final feeWei = BigInt.parse(feeString); final amountAfterFeeWei = balanceWei > feeWei ? balanceWei - feeWei : BigInt.zero; changeDepositAmount( amount: wallet.currency.formatAmount(amountAfterFeeWei), isCanonical: true); } catch (e) { printV('Error calculating send all for EVM: $e'); changeDepositAmount(amount: balanceAmount, isCanonical: true); } } } @observable String depositAvailableAmount = ""; @action void reverseSwapDirection() { final tmpAmount = _receiveAmount; final tmpCurrency = depositCurrency; changeDepositCurrency(currency: receiveCurrency); changeReceiveCurrency(currency: tmpCurrency); _depositAmount = tmpAmount; } void updateTemplate() => _exchangeTemplateStore.update(); void addTemplate( {required String amount, required String depositCurrency, required String receiveCurrency, required String provider, required String depositAddress, required String receiveAddress, required String depositCurrencyTitle, required String receiveCurrencyTitle}) => _exchangeTemplateStore.addTemplate( amount: amount, depositCurrency: depositCurrency, receiveCurrency: receiveCurrency, provider: provider, depositAddress: depositAddress, receiveAddress: receiveAddress, depositCurrencyTitle: depositCurrencyTitle, receiveCurrencyTitle: receiveCurrencyTitle); void removeTemplate({required ExchangeTemplate template}) => _exchangeTemplateStore.remove(template: template); void _onPairChange({bool clearBoth = false}) { if (clearBoth) { _depositAmount = ''; _receiveAmount = ''; if (!hasAllAmount) isSendAllEnabled = false; } else { if (isFixedRateMode) { _depositAmount = ''; } else { _receiveAmount = ''; } } bestRate = 0.0; bestRateProvider = null; _sortedAvailableProviders.clear(); loadLimits().then((_) { if (clearBoth && hasAllAmount) { calculateDepositAllAmount(); } }); _setAvailableProviders(); } void _initialPairBasedOnWallet() { if (isEVMCompatibleChain(wallet.type)) { depositCurrency = wallet.currency; receiveCurrency = CryptoCurrency.xmr; return; } switch (wallet.type) { case WalletType.monero: depositCurrency = CryptoCurrency.xmr; receiveCurrency = CryptoCurrency.btc; break; case WalletType.bitcoin: depositCurrency = CryptoCurrency.btc; receiveCurrency = CryptoCurrency.xmr; break; case WalletType.litecoin: depositCurrency = CryptoCurrency.ltc; receiveCurrency = CryptoCurrency.xmr; break; case WalletType.bitcoinCash: depositCurrency = CryptoCurrency.bch; receiveCurrency = CryptoCurrency.xmr; break; case WalletType.dogecoin: depositCurrency = CryptoCurrency.doge; receiveCurrency = CryptoCurrency.xmr; break; case WalletType.ethereum: depositCurrency = CryptoCurrency.eth; receiveCurrency = CryptoCurrency.xmr; break; case WalletType.nano: depositCurrency = CryptoCurrency.nano; receiveCurrency = CryptoCurrency.xmr; break; case WalletType.banano: depositCurrency = CryptoCurrency.banano; receiveCurrency = CryptoCurrency.xmr; break; case WalletType.polygon: depositCurrency = CryptoCurrency.maticpoly; receiveCurrency = CryptoCurrency.xmr; break; case WalletType.base: depositCurrency = CryptoCurrency.baseEth; receiveCurrency = CryptoCurrency.xmr; break; case WalletType.arbitrum: depositCurrency = CryptoCurrency.arbEth; receiveCurrency = CryptoCurrency.xmr; break; case WalletType.bsc: depositCurrency = CryptoCurrency.bnb; receiveCurrency = CryptoCurrency.xmr; break; case WalletType.wownero: depositCurrency = CryptoCurrency.wow; receiveCurrency = CryptoCurrency.xmr; break; case WalletType.none: break; } } void _defineIsReceiveAmountEditable() { isReceiveAmountEditable = true; } @action void addExchangeProvider(ExchangeProvider provider) { if (provider.description.isCentralized && forceDecentralizedExchanges) return; if (!selectedProviders.contains(provider)) selectedProviders.add(provider); if (providerList.contains(provider)) _tradeAvailableProviders.add(provider); } @action void removeExchangeProvider(ExchangeProvider provider) { selectedProviders.remove(provider); _tradeAvailableProviders.remove(provider); if (forcedProvider == provider) { setForcedProvider(null); } } @action void saveSelectedProviders() { _depositAmount = ''; _receiveAmount = ''; isFixedRateMode = false; _defineIsReceiveAmountEditable(); bestRateProvider = null; bestRate = 0.0; loadLimits(); final Map exchangeProvidersSelection = json.decode(sharedPreferences.getString(PreferencesKey.exchangeProvidersSelection) ?? "{}") as Map; for (var provider in providerList) { exchangeProvidersSelection[provider.title] = selectedProviders.contains(provider); } sharedPreferences.setString( PreferencesKey.exchangeProvidersSelection, json.encode(exchangeProvidersSelection), ); } @action Future updateAllTrocadorProviderStates(TrocadorExchangeProvider trocadorProvider) async { try { var providers = await trocadorProvider.fetchProviders(); var providerNames = providers.map((e) => e.name).toList(); await _settingsStore.updateAllTrocadorProviderStates(providerNames); } catch (e) { printV('Error updating trocador provider states: $e'); } } bool get isAvailableInSelected { return selectedProviders .any((element) => element.isAvailable && providerList.contains(element)); } @computed bool get forceDecentralizedExchanges => _settingsStore.forceDecentralizedExchanges; @action void toggleForceDecentralizedExchanges() { _settingsStore.forceDecentralizedExchanges = !_settingsStore.forceDecentralizedExchanges; if(forceDecentralizedExchanges) { final providers = selectedProviders.toList(); for (final provider in providers) { if (forceDecentralizedExchanges && provider.description.isCentralized) { removeExchangeProvider(provider); } } } else { for(final provider in providerList) { if(!selectedProviders.contains(provider) && provider.description.isCentralized) { addExchangeProvider(provider); } } } } void _setAvailableProviders() { _tradeAvailableProviders.clear(); _tradeAvailableProviders .addAll(selectedProviders.where((provider) => providerList.contains(provider))); } void _setProviders() { if (_settingsStore.exchangeStatus == ExchangeApiMode.torOnly) providerList = _allProviders.where((provider) => provider.supportsOnionAddress).toList(); else providerList = _allProviders; } int get depositMaxDigits => depositCurrency.decimals; int get receiveMaxDigits => receiveCurrency.decimals; Future isCanCreateTrade(Trade trade) async { if (trade.provider == ExchangeProviderDescription.swapsXyz) { final tradeFrom = trade.from; if (tradeFrom == null) { return CreateTradeResult( result: false, errorMessage: 'From currency is null', ); } final isNativeSupportedToken = walletTypes.contains(cryptoCurrencyOrTokenToWalletType(tradeFrom)); if (!isNativeSupportedToken) { bool _isEthToken() => wallet.currency == CryptoCurrency.eth && tradeFrom.tag == CryptoCurrency.eth.title; bool _isPolygonToken() => wallet.currency == CryptoCurrency.maticpoly && tradeFrom.tag == CryptoCurrency.maticpoly.tag; bool _isBaseToken() => wallet.currency == CryptoCurrency.baseEth && tradeFrom.tag == CryptoCurrency.baseEth.tag; bool _isTronToken() => wallet.currency == CryptoCurrency.trx && tradeFrom.tag == CryptoCurrency.trx.title; bool _isSplToken() => wallet.currency == CryptoCurrency.sol && tradeFrom.tag == CryptoCurrency.sol.title; bool isArbitrumToken() => wallet.currency == CryptoCurrency.arbEth && tradeFrom.tag == CryptoCurrency.arbEth.tag; bool isBscToken() => wallet.currency == CryptoCurrency.bnb && tradeFrom.tag == CryptoCurrency.bnb.tag; if(!(_isEthToken() || _isPolygonToken() || _isBaseToken() || _isTronToken() || _isSplToken() || isArbitrumToken() || isBscToken())) { return CreateTradeResult( result: false, errorMessage: 'This token isn’t supported on the current wallet/network for Swaps.xyz. Switch to a supported wallet or asset', ); } } } if (trade.provider == ExchangeProviderDescription.thorChain) { final payoutAddress = trade.payoutAddress ?? ''; final fromWalletAddress = trade.fromWalletAddress ?? ''; final tapRootPattern = RegExp(P2trAddress.regex.pattern); if (tapRootPattern.hasMatch(payoutAddress) || tapRootPattern.hasMatch(fromWalletAddress)) { return CreateTradeResult( result: false, errorMessage: S.current.thorchain_taproot_address_not_supported, ); } if ((trade.memo == null || trade.memo!.isEmpty)) { return CreateTradeResult( result: false, errorMessage: 'Memo is required for Thorchain trade', ); } final currenciesToCheckPattern = RegExp('0x[0-9a-zA-Z]'); // Perform checks for payOutAddress final isPayOutAddressAccordingToPattern = currenciesToCheckPattern.hasMatch(payoutAddress); if (isPayOutAddressAccordingToPattern) { final isPayOutAddressEOA = await _isExternallyOwnedAccountAddress(payoutAddress); return CreateTradeResult( result: isPayOutAddressEOA, errorMessage: !isPayOutAddressEOA ? S.current.thorchain_contract_address_not_supported : null, ); } // Perform checks for fromWalletAddress final isFromWalletAddressAddressAccordingToPattern = currenciesToCheckPattern.hasMatch(fromWalletAddress); if (isFromWalletAddressAddressAccordingToPattern) { final isFromWalletAddressEOA = await _isExternallyOwnedAccountAddress(fromWalletAddress); return CreateTradeResult( result: isFromWalletAddressEOA, errorMessage: !isFromWalletAddressEOA ? S.current.thorchain_contract_address_not_supported : null, ); } } return CreateTradeResult(result: true); } String _normalizeReceiveCurrency(CryptoCurrency receiveCurrency) { switch (receiveCurrency) { case CryptoCurrency.eth: return 'eth'; case CryptoCurrency.maticpoly: return 'polygon'; default: return receiveCurrency.tag ?? ''; } } Future _isExternallyOwnedAccountAddress(String receivingAddress) async { final normalizedReceiveCurrency = _normalizeReceiveCurrency(receiveCurrency); final isEOAAddress = !(await _isContractAddress(normalizedReceiveCurrency, receivingAddress)); return isEOAAddress; } Future _isContractAddress(String chainName, String contractAddress) async { final uri = Uri.https( 'deep-index.moralis.io', '/api/v2.2/erc20/metadata', { "chain": chainName, "addresses": contractAddress, }, ); try { final response = await ProxyWrapper().get( clearnetUri: uri, headers: { "Accept": "application/json", "X-API-Key": secrets.moralisApiKey, }, ); final decodedResponse = jsonDecode(response.body)[0] as Map; final name = decodedResponse['name'] as String?; bool isContractAddress = name!.isNotEmpty; return isContractAddress; } catch (e) { printV(e); return false; } } // Adding user's Erc20 tokens to the list of currencies @action Future _injectUserEthTokensIntoCurrencyLists() async { final tokens = await TokenUtilities.loadEvmTokensForSwap(); final toAddReceive = []; final toAddDeposit = []; for (final token in tokens) { if (!_listContainsToken(receiveCurrencies, token)) toAddReceive.add(token); if (!_listContainsToken(depositCurrencies, token)) toAddDeposit.add(token); } if (toAddReceive.isNotEmpty) receiveCurrencies.addAll(toAddReceive); if (toAddDeposit.isNotEmpty) depositCurrencies.addAll(toAddDeposit); } bool _listContainsToken(List list, Erc20Token token) { return list.any((item) { if (item is Erc20Token) { return item.contractAddress.toLowerCase() == token.contractAddress.toLowerCase(); } return item.title.toUpperCase() == token.symbol.toUpperCase() && (item.tag?.toUpperCase() == token.tag?.toUpperCase() || item.tag == null); }); } // Adding user's Solana tokens to the list of currencies bool _listContainsSplToken(List list, SPLToken token) { return list.any((item) { if (item is SPLToken) { return item.mintAddress.toLowerCase() == token.mintAddress.toLowerCase(); } return item.title.toUpperCase() == token.symbol.toUpperCase() && (item.tag?.toUpperCase() == token.tag?.toUpperCase() || item.tag == null); }); } @action Future _injectUserSplTokensIntoCurrencyLists() async { final tokens = await TokenUtilities.loadSolTokensForSwap(); final toAddReceive = []; final toAddDeposit = []; for (final token in tokens) { if (!_listContainsSplToken(receiveCurrencies, token)) toAddReceive.add(token); if (!_listContainsSplToken(depositCurrencies, token)) toAddDeposit.add(token); } if (toAddReceive.isNotEmpty) receiveCurrencies.addAll(toAddReceive); if (toAddDeposit.isNotEmpty) depositCurrencies.addAll(toAddDeposit); } // Adding user's Tron tokens to the list of currencies bool _listContainsTronToken(List list, TronToken token) { return list.any((item) { if (item is TronToken) { return item.contractAddress.toLowerCase() == token.contractAddress.toLowerCase(); } return item.title.toUpperCase() == token.symbol.toUpperCase() && (item.tag?.toUpperCase() == token.tag?.toUpperCase() || item.tag == null); }); } @action Future _injectUserTronTokensIntoCurrencyLists() async { final tokens = await TokenUtilities.loadTronTokensForSwap(); final toAddReceive = []; final toAddDeposit = []; for (final token in tokens) { if (!_listContainsTronToken(receiveCurrencies, token)) toAddReceive.add(token); if (!_listContainsTronToken(depositCurrencies, token)) toAddDeposit.add(token); } if (toAddReceive.isNotEmpty) receiveCurrencies.addAll(toAddReceive); if (toAddDeposit.isNotEmpty) depositCurrencies.addAll(toAddDeposit); } double initialAmountByAssets(CryptoCurrency ticker) { final amount = switch (ticker) { CryptoCurrency.trx => 1000, CryptoCurrency.nano => 10, CryptoCurrency.zano => 10, CryptoCurrency.wow => 1000, CryptoCurrency.ada => 1000, CryptoCurrency.dash => 10, CryptoCurrency.rune => 10, _ => 1 }; return amount.toDouble(); } }