dev
dart 1,967 lines 65.1 KB
Raw
1 import 'dart:async';
2 import 'dart:collection';
3 import 'dart:convert';
4 import 'dart:developer';
5
6 import 'package:bitcoin_base/bitcoin_base.dart';
7 import 'package:cake_wallet/.secrets.g.dart' as secrets;
8 import 'package:cake_wallet/bitcoin/bitcoin.dart';
9 import 'package:cake_wallet/core/amount_parsing_proxy.dart';
10 import 'package:cake_wallet/core/create_trade_result.dart';
11 import 'package:cake_wallet/core/fiat_conversion_service.dart';
12 import 'package:cake_wallet/core/lightning_invoice_service.dart';
13 import 'package:cake_wallet/core/utilities.dart';
14 import 'package:cake_wallet/core/wallet_change_listener_view_model.dart';
15 import 'package:cake_wallet/entities/calculate_fiat_amount.dart';
16 import 'package:cake_wallet/entities/exchange_api_mode.dart';
17 import 'package:cake_wallet/entities/fiat_api_mode.dart';
18 import 'package:cake_wallet/entities/fiat_currency.dart';
19 import 'package:cake_wallet/entities/preferences_key.dart';
20 import 'package:cake_wallet/entities/wallet_contact.dart';
21 import 'package:cake_wallet/exchange/exchange_provider_description.dart';
22 import 'package:cake_wallet/exchange/exchange_template.dart';
23 import 'package:cake_wallet/exchange/exchange_trade_state.dart';
24 import 'package:cake_wallet/exchange/limits.dart';
25 import 'package:cake_wallet/exchange/limits_state.dart';
26 import 'package:cake_wallet/exchange/provider/chainflip_exchange_provider.dart';
27 import 'package:cake_wallet/exchange/provider/jupiter_exchange_provider.dart';
28 import 'package:cake_wallet/exchange/provider/letsexchange_exchange_provider.dart';
29 import 'package:cake_wallet/exchange/provider/changenow_exchange_provider.dart';
30 import 'package:cake_wallet/exchange/provider/exchange_provider.dart';
31 import 'package:cake_wallet/exchange/provider/exolix_exchange_provider.dart';
32 import 'package:cake_wallet/exchange/provider/near_Intents_exchange_provider.dart';
33 import 'package:cake_wallet/exchange/provider/stealth_ex_exchange_provider.dart';
34 import 'package:cake_wallet/exchange/provider/swapsxyz_exchange_provider.dart';
35 import 'package:cake_wallet/exchange/provider/swaptrade_exchange_provider.dart';
36 import 'package:cake_wallet/exchange/provider/trocador_exchange_provider.dart';
37 import 'package:cake_wallet/exchange/provider/xoswap_exchange_provider.dart';
38 import 'package:cake_wallet/exchange/trade.dart';
39 import 'package:cake_wallet/exchange/trade_request.dart';
40 import 'package:cake_wallet/generated/i18n.dart';
41 import 'package:cake_wallet/new-ui/widgets/currency_picker/fiat_currency_picker_sheet.dart';
42 import 'package:cake_wallet/store/app_store.dart';
43 import 'package:cake_wallet/utils/exchange_provider_logger.dart';
44 import 'package:cw_core/amount/amount_sanitizer.dart';
45 import 'package:cw_core/amount/money.dart';
46 import 'package:cw_core/amount/money_double.dart';
47 import "package:cw_core/wallet_info.dart";
48 import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart';
49 import 'package:cake_wallet/store/dashboard/trades_store.dart';
50 import 'package:cake_wallet/store/settings_store.dart';
51 import 'package:cake_wallet/store/templates/exchange_template_store.dart';
52 import 'package:cake_wallet/evm/evm.dart';
53 import 'package:cake_wallet/reactions/wallet_connect.dart';
54 import 'package:cake_wallet/utils/feature_flag.dart';
55 import 'package:cake_wallet/utils/token_utilities.dart';
56 import 'package:cake_wallet/view_model/contact_list/contact_list_view_model.dart';
57 import 'package:cake_wallet/view_model/send/fees_view_model.dart';
58 import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_list_view_model.dart';
59 import 'package:cw_core/crypto_amount_format.dart';
60 import 'package:cw_core/crypto_currency.dart';
61 import 'package:cw_core/currencies_with_memo.dart';
62 import 'package:cw_core/erc20_token.dart';
63 import 'package:cw_core/spl_token.dart';
64 import 'package:cw_core/sync_status.dart';
65 import 'package:cw_core/transaction_priority.dart';
66 import 'package:cw_core/tron_token.dart';
67 import 'package:cw_core/unspent_coin_type.dart';
68 import 'package:cw_core/utils/print_verbose.dart';
69 import 'package:cw_core/utils/proxy_wrapper.dart';
70 import 'package:cw_core/wallet_type.dart';
71 import 'package:flutter/material.dart';
72 import 'package:mobx/mobx.dart';
73 import 'package:shared_preferences/shared_preferences.dart';
74
75 part 'exchange_view_model.g.dart';
76
77 class ExchangeViewModel = ExchangeViewModelBase with _$ExchangeViewModel;
78
79 abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with Store {
80 @override
81 void onWalletChange(wallet) {
82 // FIXME this caused a bug when switching wallet, i can't figure out why it's here?
83 // receiveCurrency = wallet.currency;
84 // depositCurrency = wallet.currency;
85 }
86
87 final List<ReactionDisposer> _disposers = [];
88
89 void dispose() {
90 bestRateSync.cancel();
91 for (final disposer in _disposers) {
92 disposer();
93 }
94 _disposers.clear();
95 }
96
97 ExchangeViewModelBase(
98 this._appStore,
99 this._exchangeTemplateStore,
100 this.tradesStore,
101 this.sharedPreferences,
102 this.contactListViewModel,
103 this.unspentCoinsListViewModel,
104 this.feesViewModel,
105 this.fiatConversionStore,
106 ) : isSendAllEnabled = false,
107 isFixedRateMode = false,
108 isReceiveAmountEntered = false,
109 _depositAmount = null,
110 _receiveAmount = null,
111 receiveAddress = '',
112 depositAddress = '',
113 isDepositAddressEnabled = false,
114 isReceiveAmountEditable = false,
115 _useTorOnly = false,
116 receiveCurrencies = ObservableList<CryptoCurrency>(),
117 depositCurrencies = ObservableList<CryptoCurrency>(),
118 limits = Limits(min: 0, max: 0),
119 tradeState = ExchangeTradeStateInitial(),
120 limitsState = LimitsInitialState(),
121 receiveCurrency = _appStore.wallet!.currency,
122 depositCurrency = _appStore.wallet!.currency,
123 providerList = [],
124 selectedProviders = ObservableList<ExchangeProvider>(),
125 super(appStore: _appStore) {
126 _useTorOnly = _settingsStore.exchangeStatus == ExchangeApiMode.torOnly;
127 _setProviders();
128 const excludeDepositCurrencies = [CryptoCurrency.btt];
129 const excludeReceiveCurrencies = [CryptoCurrency.btt];
130 _initialPairBasedOnWallet();
131
132 unspentCoinsListViewModel.initialSetup().then((_) {
133 unspentCoinsListViewModel.resetUnspentCoinsInfoSelections();
134 });
135
136 final Map<String, dynamic> exchangeProvidersSelection =
137 json.decode(sharedPreferences.getString(PreferencesKey.exchangeProvidersSelection) ?? "{}")
138 as Map<String, dynamic>;
139
140 /// if the provider is not in the user settings (user's first time or newly added provider)
141 /// then use its default value decided by us
142 selectedProviders = ObservableList.of(providerList
143 .where((element) =>
144 (!forceDecentralizedExchanges || !element.description.isCentralized) &&
145 (exchangeProvidersSelection[element.title] == null
146 ? element.isEnabled
147 : (exchangeProvidersSelection[element.title] as bool)))
148 .toList());
149
150 _setAvailableProviders();
151
152 autorun((_) {
153 if (selectedProviders.any((provider) => provider is TrocadorExchangeProvider)) {
154 final trocadorProvider =
155 selectedProviders.firstWhere((provider) => provider is TrocadorExchangeProvider)
156 as TrocadorExchangeProvider;
157
158 updateAllTrocadorProviderStates(trocadorProvider);
159 }
160 });
161
162 bestRateSync = Timer.periodic(Duration(seconds: 10), (timer) {
163 if (tradeState is! TradeIsCreating) {
164 calculateBestRate();
165 }
166 });
167
168 isDepositAddressEnabled = !useSameWalletAddress(depositCurrency);
169 _depositAmount = null;
170 _receiveAmount = null;
171 receiveAddress = '';
172 depositAddress =
173 useSameWalletAddress(depositCurrency) ? wallet.walletAddresses.addressForExchange : '';
174
175 _disposers.add(reaction((_) => receiveAddress, (_) {
176 if (!(tradeState is TradeIsCreatedSuccessfully)) {
177 receiveAddressDisplayName = null;
178 }
179 }));
180
181 provider = providerList.firstOrNull;
182 final initialProvider = provider;
183 provider!.checkIsAvailable().then((bool isAvailable) {
184 if (!isAvailable && provider == initialProvider) {
185 provider = providerList.firstWhere((provider) => provider is ChangeNowExchangeProvider,
186 orElse: () => providerList.last);
187 _onPairChange();
188 }
189 });
190
191 // providerDisplay is read by ui to display auto-selected provider.
192 // it's on a delay so it doesn't flicker.
193 _disposers.add(reaction((_) => bestRateProvider, (val) {
194 providerDisplay = val;
195 }, delay: 300));
196
197 receiveCurrencies = CryptoCurrency.all
198 .where((cryptoCurrency) => !excludeReceiveCurrencies.contains(cryptoCurrency))
199 .toList()
200 .asObservable();
201 depositCurrencies = CryptoCurrency.all
202 .where((cryptoCurrency) => !excludeDepositCurrencies.contains(cryptoCurrency))
203 .toList()
204 .asObservable();
205
206 _injectUserEthTokensIntoCurrencyLists();
207 _injectUserSplTokensIntoCurrencyLists();
208 _injectUserTronTokensIntoCurrencyLists();
209 _defineIsReceiveAmountEditable();
210 loadLimits();
211 _disposers.add(reaction((_) => isFixedRateMode, (Object _) {
212 bestRateProvider = null;
213 bestRate = 0.0;
214 loadLimits();
215 }));
216
217 _disposers.add(reaction((_) => forceDecentralizedExchanges, (val) {
218 if (val && (bestRateProvider?.description.isCentralized ?? false)) {
219 bestRateProvider = null;
220 bestRate = 0.0;
221 loadLimits();
222 }
223 }));
224
225 if (isElectrumWallet) {
226 bitcoin!.updateFeeRates(wallet);
227 }
228 fetchFiatPrice(receiveCurrency);
229
230 updateDepositAvailableAmount();
231 }
232
233 Future<void> updateDepositAvailableAmount() async {
234 if (depositCurrency == CryptoCurrency.btcln) {
235 final currency = depositCurrency;
236 Future.doWhile(() async {
237 await Future.delayed(const Duration(milliseconds: 400));
238 if (depositCurrency != currency) {
239 return false;
240 }
241 final balance = wallet.balance[depositCurrency];
242 if (balance != null) {
243 depositAvailableAmount = _appStore.amountParsingProxy.asDisplayString(balance.available);
244 return false;
245 }
246 return true;
247 }).timeout(Duration(seconds: 20));
248 } else if (isEVMCompatibleChain(wallet.type)) {
249 final currency = depositCurrency;
250 final balanceCurrency = wallet.balance.keys.firstWhereOrNull(
251 (c) =>
252 c.title == depositCurrency.title &&
253 (c.tag == depositCurrency.tag || c.tag == depositCurrency.title),
254 );
255 final balanceForCurrency = balanceCurrency != null ? wallet.balance[balanceCurrency] : null;
256 if (depositCurrency == currency && balanceForCurrency != null) {
257 depositAvailableAmount =
258 _appStore.amountParsingProxy.asDisplayStringWithSymbol(balanceForCurrency.available);
259 }
260 } else {
261 final currency = depositCurrency;
262 final sendingBalance = Money.fromInt(
263 await unspentCoinsListViewModel.getSendingBalance(UnspentCoinType.any), currency);
264 final amount = _appStore.amountParsingProxy.asDisplayStringWithSymbol(sendingBalance);
265 if (depositCurrency == currency) {
266 depositAvailableAmount = amount;
267 }
268 }
269 }
270
271 void showFiatCurrencyPicker(BuildContext context) {
272 FiatCurrencyPickerSheet.show(
273 context: context,
274 selected: fiat,
275 onSelected: (cur) => _settingsStore.fiatCurrency = cur,
276 );
277 }
278
279 bool useSameWalletAddress(CryptoCurrency currency) =>
280 currency == wallet.currency ||
281 (currency == CryptoCurrency.btcln &&
282 wallet.currency == CryptoCurrency.btc &&
283 wallet.isSoftwareWallet) ||
284 (currency.tag != null && currency.tag == wallet.currency.tag) ||
285 currency.tag == wallet.currency.title;
286
287 bool get isElectrumWallet => [
288 WalletType.bitcoin,
289 WalletType.litecoin,
290 WalletType.bitcoinCash,
291 WalletType.dogecoin
292 ].contains(wallet.type);
293
294 bool get hideAddressAfterExchange =>
295 [WalletType.monero, WalletType.wownero, WalletType.zcash].contains(wallet.type) ||
296 isElectrumWallet;
297
298 bool _useTorOnly;
299 final ExchangeTemplateStore _exchangeTemplateStore;
300 final TradesStore tradesStore;
301 final SharedPreferences sharedPreferences;
302
303 List<ExchangeProvider> get _allProviders => [
304 ChangeNowExchangeProvider(settingsStore: _settingsStore),
305 // SideShiftExchangeProvider(),
306 ChainflipExchangeProvider(),
307 if (FeatureFlag.isExolixEnabled) ExolixExchangeProvider(),
308 SwapTradeExchangeProvider(),
309 LetsExchangeExchangeProvider(),
310 StealthExExchangeProvider(),
311 XOSwapExchangeProvider(),
312 SwapsXyzExchangeProvider(),
313 JupiterExchangeProvider(),
314 NearIntentsExchangeProvider(),
315 TrocadorExchangeProvider(
316 useTorOnly: _useTorOnly, providerStates: _settingsStore.trocadorProviderStates),
317 ];
318
319 @observable
320 ExchangeProvider? provider;
321
322 @observable
323 ExchangeProvider? providerDisplay;
324
325 /// Maps in dart are not sorted by default
326 /// SplayTreeMap is a map sorted by keys
327 /// will use it to sort available providers
328 /// based on the rate they yield for the current trade
329 ///
330 ///
331 /// initialize with descending comparator
332 /// since we want largest rate first
333 final SplayTreeMap<double, ExchangeProvider> _sortedAvailableProviders =
334 SplayTreeMap<double, ExchangeProvider>((double a, double b) => b.compareTo(a));
335
336 final List<ExchangeProvider> _tradeAvailableProviders = [];
337
338 Map<ExchangeProvider, Limits?> _providerLimits = {};
339
340 @observable
341 ObservableList<ExchangeProvider> selectedProviders;
342
343 @observable
344 List<ExchangeProvider> providerList;
345
346 @observable
347 CryptoCurrency depositCurrency;
348
349 @observable
350 CryptoCurrency receiveCurrency;
351
352 @observable
353 LimitsState limitsState;
354
355 @observable
356 ExchangeTradeState tradeState;
357
358 @observable
359 Money? _depositAmount;
360
361 @computed
362 bool get hasDepositAmount => _depositAmount != null;
363
364 @computed
365 String get depositAmount =>
366 _depositAmount == null ? "" : amountParsingProxy.asDisplayString(_depositAmount!);
367
368 @computed
369 String get depositAmountCanonical => _depositAmount == null ? "0.0" : _depositAmount.toString();
370
371 @observable
372 Money? _receiveAmount;
373
374 @computed
375 String get receiveAmount =>
376 _receiveAmount == null ? "" : amountParsingProxy.asDisplayString(_receiveAmount!);
377
378 @action
379 // only set canonical formated amounts here;
380 void setCanonicalReceiveAmount(String value) =>
381 _receiveAmount = Money.tryParse(value, receiveCurrency);
382
383 @observable
384 String depositAddress;
385
386 @observable
387 String receiveAddress;
388
389 @observable
390 String receiveAddressExtraId = '';
391
392 @observable
393 String? receiveAddressDisplayName;
394
395 @observable
396 bool isDepositAddressEnabled;
397
398 @observable
399 bool isReceiveAmountEntered;
400
401 @observable
402 bool isReceiveAmountEditable;
403
404 @observable
405 bool isFixedRateMode;
406
407 @observable
408 bool isSendAllEnabled;
409
410 @observable
411 Limits limits;
412
413 @observable
414 bool tradeStarted = false;
415
416 @observable
417 bool noProviderForPair = false;
418
419 @observable
420 bool isSendFromExternal = false;
421
422 @computed
423 SyncStatus get status => wallet.syncStatus;
424
425 @computed
426 ObservableList<ExchangeTemplate> get templates => _exchangeTemplateStore.templates;
427
428 @computed
429 List<WalletContact> get walletContactsToShow => contactListViewModel.walletContacts
430 .where((element) => element.type == receiveCurrency)
431 .toList();
432
433 @computed
434 List<WalletContact> get refundWalletContactsToShow => contactListViewModel.walletContacts
435 .where((element) => element.type == depositCurrency)
436 .toList();
437
438 @computed
439 Future<List<WalletInfo>> get receiveWallets async {
440 try {
441 WalletType? type;
442 type = cryptoCurrencyOrTokenToWalletType(receiveCurrency);
443 if (type == null) {
444 type =
445 cryptoCurrencyOrTokenToWalletType(CryptoCurrency.fromString(receiveCurrency.tag ?? ""));
446 }
447
448 return await WalletInfo.selectList("type = ?", [type!.index]);
449 } catch (e) {
450 return [];
451 }
452 }
453
454 Future<List<WalletInfoAddressInfo>> addressesForAccountsWallet(WalletInfo wallet) async {
455 final List<WalletInfoAddressInfo> ret = [];
456 final addresses = await wallet.getAddressInfos();
457 for (var list in addresses.values) {
458 // we only want the "primary" account addresses - those that contain account names.
459 ret.addAll(list.where((item) => item.label.split(" ").length > 1));
460 }
461 return ret;
462 }
463
464 @computed
465 Future<List<WalletInfo>> get depositWallets async {
466 WalletType? type;
467 type = cryptoCurrencyOrTokenToWalletType(depositCurrency);
468 if (type == null) {
469 try {
470 type =
471 cryptoCurrencyOrTokenToWalletType(CryptoCurrency.fromString(depositCurrency.tag ?? ""));
472 } catch (_) {}
473 }
474
475 if (type != null) {
476 return await WalletInfo.selectList("type = ?", [type.index]);
477 }
478
479 return await WalletInfo.getAll();
480 }
481
482 @action
483 bool checkIfWalletIsAnInternalWallet(String address) {
484 final walletContactList =
485 walletContactsToShow.where((element) => element.address == address).toList();
486
487 return walletContactList.isNotEmpty;
488 }
489
490 @computed
491 bool get shouldDisplayTOTP2FAForExchangesToInternalWallet =>
492 _settingsStore.shouldRequireTOTP2FAForExchangesToInternalWallets;
493
494 @computed
495 bool get shouldDisplayTOTP2FAForExchangesToExternalWallet =>
496 _settingsStore.shouldRequireTOTP2FAForExchangesToExternalWallets;
497
498 @computed
499 String? get balanceDisplay {
500 CryptoCurrency? balanceCurrency;
501 if (isEVMCompatibleChain(wallet.type) ||
502 wallet.type == WalletType.solana ||
503 wallet.type == WalletType.tron) {
504 balanceCurrency = wallet.balance.keys.firstWhereOrNull(
505 (c) =>
506 c.title == depositCurrency.title &&
507 (c.tag == depositCurrency.tag || c.tag == depositCurrency.title),
508 );
509 } else {
510 balanceCurrency = depositCurrency;
511 }
512 final bal = balanceCurrency != null ? wallet.balance[balanceCurrency]?.available : null;
513 if (bal == null) return null;
514 return amountParsingProxy.asDisplayString(bal);
515 }
516
517 //* Still open to further optimize these checks
518 //* It works but can be made better
519 @action
520 bool shouldDisplayTOTP() {
521 final isInternalWallet = checkIfWalletIsAnInternalWallet(receiveAddress);
522
523 if (isInternalWallet) {
524 return shouldDisplayTOTP2FAForExchangesToInternalWallet;
525 } else {
526 return shouldDisplayTOTP2FAForExchangesToExternalWallet;
527 }
528 }
529
530 @computed
531 bool get decentralizedExchangesPromptDismissed =>
532 _settingsStore.decentralizedExchangesPromptDismissed;
533
534 @action
535 void dismissDecentralizedExchangesPrompt() {
536 _settingsStore.decentralizedExchangesPromptDismissed = true;
537 }
538
539 @computed
540 TransactionPriority get transactionPriority {
541 final priority = _settingsStore.getPriority(wallet.type, chainId: wallet.chainId);
542
543 if (priority == null) {
544 throw Exception('Unexpected type ${wallet.type.toString()}');
545 }
546
547 return priority;
548 }
549
550 bool get hasAllAmount {
551 if ([
552 WalletType.monero,
553 WalletType.bitcoin,
554 WalletType.litecoin,
555 WalletType.bitcoinCash,
556 WalletType.dogecoin,
557 ].contains(wallet.type)) return (depositCurrency == wallet.currency);
558
559 if (!isEVMCompatibleChain(wallet.type)) return false;
560
561 if (depositCurrency == wallet.currency) return true;
562
563 return wallet.balance.keys.any((c) =>
564 c.title == depositCurrency.title &&
565 (c.tag == depositCurrency.tag || c.tag == depositCurrency.title));
566 }
567
568 bool get isMoneroWallet => wallet.type == WalletType.monero;
569
570 @observable
571 ObservableList<CryptoCurrency> receiveCurrencies;
572
573 @observable
574 ObservableList<CryptoCurrency> depositCurrencies;
575
576 final AppStore _appStore;
577 SettingsStore get _settingsStore => _appStore.settingsStore;
578
579 final ContactListViewModel contactListViewModel;
580
581 final UnspentCoinsListViewModel unspentCoinsListViewModel;
582
583 final FeesViewModel feesViewModel;
584
585 @observable
586 double bestRate = 0.0;
587
588 @computed
589 bool get useDepositBaseUnit => _appStore.amountParsingProxy.useSatoshi(depositCurrency);
590
591 @computed
592 bool get useReceiveBaseUnit => _appStore.amountParsingProxy.useSatoshi(receiveCurrency);
593
594 @computed
595 AmountParsingProxy get amountParsingProxy => _appStore.amountParsingProxy;
596
597 @observable
598 ExchangeProvider? bestRateProvider;
599
600 @observable
601 ExchangeProvider? forcedProvider;
602
603 @action
604 void setForcedProvider(ExchangeProvider? provider) {
605 forcedProvider = provider;
606 forcedProviderRate = 0.0;
607 calculateForcedProviderRate();
608 }
609
610 WalletInfo? selectedAddressBookWallet;
611
612 @observable
613 double forcedProviderRate = 0.0;
614
615 late Timer bestRateSync;
616
617 final FiatConversionStore fiatConversionStore;
618
619 FiatCurrency get fiat => _settingsStore.fiatCurrency;
620
621 @computed
622 bool get isFiatDisabled => feesViewModel.isFiatDisabled;
623
624 @action
625 Future<void> fetchFiatPrice(CryptoCurrency currency) async {
626 if (fiatConversionStore.prices[currency] != null) {
627 return;
628 }
629
630 fiatConversionStore.prices[currency] = await FiatConversionService.fetchPrice(
631 crypto: currency,
632 fiat: fiat,
633 torOnly: _settingsStore.fiatApiMode == FiatApiMode.torOnly,
634 );
635 }
636
637 @computed
638 String get receiveAmountFiatFormatted {
639 var amount = '0.00';
640 try {
641 if (_receiveAmount != null) {
642 if (fiatConversionStore.prices[receiveCurrency] == null) return '';
643
644 amount = calculateFiatAmount(
645 price: fiatConversionStore.prices[receiveCurrency]!,
646 cryptoAmount: _receiveAmount.toString(),
647 );
648 }
649 } catch (_) {
650 log('Error calculating receive amount fiat formatted: $_');
651 }
652 return isFiatDisabled ? '' : '$amount';
653 }
654
655 @computed
656 String get depositAmountFiatFormatted {
657 var amount = '0.00';
658 try {
659 if (_depositAmount != null) {
660 if (fiatConversionStore.prices[depositCurrency] == null) return '';
661
662 amount = calculateFiatAmount(
663 price: fiatConversionStore.prices[depositCurrency]!,
664 cryptoAmount: _depositAmount.toString(),
665 );
666 }
667 } catch (_) {
668 log('Error calculating deposit amount fiat formatted: $_');
669 }
670 return isFiatDisabled ? '' : '$amount';
671 }
672
673 @computed
674 String get receiveAmountFiat {
675 var amount = '';
676 try {
677 if (_receiveAmount != null) {
678 if (fiatConversionStore.prices[receiveCurrency] == null) return '';
679
680 amount = calculateFiatAmount(
681 price: fiatConversionStore.prices[receiveCurrency]!,
682 cryptoAmount: _receiveAmount.toString(),
683 raw: true);
684 }
685 } catch (_) {
686 log('Error calculating receive amount fiat formatted: $_');
687 }
688 return amount;
689 }
690
691 @computed
692 String get depositAmountFiat {
693 var amount = '';
694 try {
695 if (_depositAmount != null) {
696 if (fiatConversionStore.prices[depositCurrency] == null) return '';
697
698 amount = calculateFiatAmount(
699 price: fiatConversionStore.prices[depositCurrency]!,
700 cryptoAmount: _depositAmount.toString(),
701 raw: true,
702 );
703 }
704 } catch (_) {
705 log('Error calculating deposit amount fiat formatted: $_');
706 }
707 return amount;
708 }
709
710 String roundedDepositAmount(int digits) {
711 if (depositAmount.split(".").last.length <= digits) {
712 return depositAmount;
713 }
714 try {
715 return double.parse(depositAmount).toStringAsPrecision(digits);
716 } catch (e) {
717 return "0";
718 }
719 }
720
721 String roundedReceiveAmount(int digits) {
722 if (receiveAmount.split(".").last.length <= digits) {
723 return receiveAmount;
724 }
725 try {
726 return double.parse(receiveAmount).toStringAsPrecision(digits);
727 } catch (e) {
728 return "0";
729 }
730 }
731
732 String roundedReceiveAmountFiat(int digits) {
733 if (receiveAmountFiat.split(".").last.length <= digits) {
734 return receiveAmountFiat;
735 }
736
737 return double.tryParse(receiveAmountFiat)?.toStringAsPrecision(digits) ?? '0.00';
738 }
739
740 String roundedDepositAmountFiat(int digits) {
741 if (depositAmountFiat.split(".").last.length <= digits) {
742 return depositAmountFiat;
743 }
744
745 return double.tryParse(depositAmountFiat)?.toStringAsPrecision(digits) ?? '0.00';
746 }
747
748 @action
749 void changeDepositCurrency({required CryptoCurrency currency}) {
750 final previousCurrency = depositCurrency;
751 final wasSendAllEnabled = isSendAllEnabled;
752
753 depositCurrency = currency;
754 isFixedRateMode = false;
755 isDepositAddressEnabled = !useSameWalletAddress(depositCurrency);
756
757 if (previousCurrency != currency) {
758 _onPairChange(clearBoth: wasSendAllEnabled);
759 }
760 fetchFiatPrice(currency);
761 updateDepositAvailableAmount();
762 }
763
764 @action
765 void changeReceiveCurrency({required CryptoCurrency currency}) {
766 final previousCurrency = receiveCurrency;
767
768 if (!(currency.tag == receiveCurrency.tag ||
769 currency.tag == receiveCurrency.title ||
770 currency.title == receiveCurrency.tag ||
771 currency == receiveCurrency)) {
772 receiveAddress = "";
773 }
774 if (currency != receiveCurrency) {
775 receiveAddressExtraId = "";
776 }
777
778 receiveCurrency = currency;
779 isFixedRateMode = false;
780 isDepositAddressEnabled = !useSameWalletAddress(depositCurrency);
781
782 if (previousCurrency != currency) {
783 _onPairChange();
784 }
785 fetchFiatPrice(currency);
786 }
787
788 @action
789 Future<void> changeReceiveAmount({required String amount, bool isCanonical = false}) async {
790 if (amount.isEmpty) {
791 _depositAmount = null;
792 _receiveAmount = null;
793 return;
794 }
795
796 _receiveAmount = isCanonical
797 ? Money.tryParse(amount.sanitized(), receiveCurrency)
798 : _appStore.amountParsingProxy.tryParseCryptoString(amount.sanitized(), receiveCurrency);
799
800 if (_receiveAmount == null) {
801 _depositAmount = null;
802 return;
803 }
804
805 final _enteredAmount = double.tryParse(_receiveAmount.toString()) ?? 0;
806
807 if (bestRate == 0) {
808 _depositAmount = null;
809
810 await calculateBestRate();
811 }
812
813 final amount_ = _enteredAmount / (forcedProvider == null ? bestRate : forcedProviderRate);
814 _depositAmount = amount_.tryToMoney(depositCurrency);
815 }
816
817 @action
818 void setReceiveAmountFromFiat({required String fiatAmount}) {
819 final _enteredAmount = double.tryParse(fiatAmount.sanitized()) ?? 0.0;
820 final price = fiatConversionStore.prices[receiveCurrency];
821 if (price == null || price == 0.0) return;
822
823 final crypto = _enteredAmount / price;
824 final _receiveAmountTmp = crypto.toString().withMaxDecimals(receiveCurrency.decimals);
825 if (_receiveAmount != _receiveAmountTmp) {
826 changeReceiveAmount(amount: _receiveAmountTmp);
827 }
828 }
829
830 @action
831 void setDepositAmountFromFiat({required String fiatAmount}) {
832 final _enteredAmount = double.tryParse(fiatAmount.sanitized()) ?? 0.0;
833 final price = fiatConversionStore.prices[depositCurrency];
834 if (price == null || price == 0.0) return;
835
836 final crypto = _enteredAmount / price;
837 final depositAmountTmp = crypto.toString().withMaxDecimals(depositCurrency.decimals);
838 if (_depositAmount != depositAmountTmp) {
839 changeDepositAmount(amount: depositAmountTmp, isCanonical: true);
840 }
841 }
842
843 @action
844 Future<void> changeDepositAmount({required String amount, bool isCanonical = false}) async {
845 if (amount.isEmpty) {
846 _depositAmount = null;
847 _receiveAmount = null;
848 return;
849 }
850
851 _depositAmount = isCanonical
852 ? Money.tryParse(amount.sanitized(), depositCurrency)
853 : _appStore.amountParsingProxy.tryParseCryptoString(amount.sanitized(), depositCurrency);
854
855 if (_depositAmount == null) {
856 _receiveAmount = null;
857 return;
858 }
859
860 /// For fixed-rate transactions, we don't want to recalculate receive amount
861 /// as it should remain exactly what the user set
862 if (isFixedRateMode) return;
863
864 final _enteredAmount = double.tryParse(_depositAmount.toString()) ?? 0;
865
866 /// in case the best rate was not calculated yet
867 if (bestRate == 0) {
868 _receiveAmount = null;
869
870 await calculateBestRate();
871 }
872
873 final amount_ = _enteredAmount * (forcedProvider == null ? bestRate : forcedProviderRate);
874 _receiveAmount = amount_.tryToMoney(receiveCurrency);
875 }
876
877 bool checkIfInputMeetsMinOrMaxCondition(String input) {
878 final _enteredAmount = double.tryParse(input.sanitized()) ?? 0;
879 double minLimit = limits.min ?? 0;
880 double? maxLimit = limits.max;
881
882 if (_enteredAmount < minLimit) return false;
883
884 if (maxLimit != null && _enteredAmount > maxLimit) return false;
885
886 return true;
887 }
888
889 Future<void> calculateForcedProviderRate() async {
890 if (forcedProvider == null || depositCurrency == receiveCurrency) {
891 forcedProviderRate = 0.0;
892 return;
893 }
894
895 final amount =
896 double.tryParse(isFixedRateMode ? _receiveAmount.toString() : _depositAmount.toString()) ??
897 initialAmountByAssets(isFixedRateMode ? receiveCurrency : depositCurrency);
898
899 forcedProviderRate = await forcedProvider!.fetchRate(
900 from: depositCurrency,
901 to: receiveCurrency,
902 amount: amount,
903 isFixedRateMode: isFixedRateMode,
904 isReceiveAmount: isFixedRateMode);
905 }
906
907 bool _excludeProviderForSwapAll(ExchangeProvider provider) =>
908 isSendAllEnabled &&
909 (provider.description == ExchangeProviderDescription.swapsXyz ||
910 provider.description == ExchangeProviderDescription.nearIntents);
911
912 bool _excludeProviderForReceiveExtraId(ExchangeProvider provider) =>
913 memoLabelTypeFor(receiveCurrency) != null && !provider.supportsMemoOrDestinationTag;
914
915 Future<void> calculateBestRate() async {
916 if (depositCurrency == receiveCurrency) {
917 bestRate = 0.0;
918 bestRateProvider = null;
919 return;
920 }
921 final amount =
922 double.tryParse(isFixedRateMode ? _receiveAmount.toString() : _depositAmount.toString()) ??
923 initialAmountByAssets(isFixedRateMode ? receiveCurrency : depositCurrency);
924
925 final validProvidersForAmount = _tradeAvailableProviders.where((provider) {
926 if (_excludeProviderForSwapAll(provider)) return false;
927 if (_excludeProviderForReceiveExtraId(provider)) return false;
928
929 final limits = _providerLimits[provider];
930
931 if (limits == null) return false;
932 if (limits.min != null && amount < limits.min!) return false;
933 if (limits.max != null && amount > limits.max!) return false;
934
935 return true;
936 }).toList();
937
938 final _providers = validProvidersForAmount
939 .where((element) => !isFixedRateMode || element.supportsFixedRate)
940 .toList();
941
942 final result = await Future.wait<double>(
943 _providers.map(
944 (element) => element
945 .fetchRate(
946 from: depositCurrency,
947 to: receiveCurrency,
948 amount: amount,
949 isFixedRateMode: isFixedRateMode,
950 isReceiveAmount: isFixedRateMode)
951 .timeout(
952 Duration(seconds: 7),
953 onTimeout: () => 0.0,
954 ),
955 ),
956 );
957
958 // We'll use a new SplayTreeMap to avoid concurrent modification issues
959 final newSortedProviders =
960 SplayTreeMap<double, ExchangeProvider>((double a, double b) => b.compareTo(a));
961
962 for (int i = 0; i < result.length; i++) {
963 if (result[i] != 0) {
964 /// add this provider as its valid for this trade
965 try {
966 newSortedProviders[result[i]] = _providers[i];
967 } catch (e) {
968 // will throw "Concurrent modification during iteration" error if modified at the same
969 // time [createTrade] is called, as this is not a normal map, but a sorted map
970 }
971 } else {
972 printV('calculateBestRate: ${_providers[i].title} returned rate=0');
973 }
974 }
975
976 // Replace the old map with the new one
977 _sortedAvailableProviders.clear();
978 _sortedAvailableProviders.addAll(newSortedProviders);
979
980 if (_sortedAvailableProviders.isNotEmpty) {
981 bestRate = _sortedAvailableProviders.keys.first;
982 bestRateProvider = _sortedAvailableProviders.values.first;
983 }
984 noProviderForPair = _sortedAvailableProviders.isEmpty;
985 }
986
987 @action
988 Future<void> loadLimits() async {
989 if (depositCurrency == receiveCurrency) {
990 limitsState = LimitsLoadedSuccessfully(limits: Limits(min: 0, max: 0));
991 return;
992 }
993 if (selectedProviders.isEmpty) return;
994
995 limitsState = LimitsIsLoading();
996
997 final from = isFixedRateMode ? receiveCurrency : depositCurrency;
998 final to = isFixedRateMode ? depositCurrency : receiveCurrency;
999
1000 double? lowestMin = double.maxFinite;
1001 double? highestMax = 0.0;
1002
1003 try {
1004 final futures = selectedProviders
1005 .where((provider) => providerList.contains(provider))
1006 .where((provider) => !_excludeProviderForReceiveExtraId(provider))
1007 .map((provider) async {
1008 final limits = await provider
1009 .fetchLimits(
1010 from: from,
1011 to: to,
1012 isFixedRateMode: isFixedRateMode,
1013 )
1014 .onError((error, stackTrace) => null)
1015 .timeout(
1016 Duration(seconds: 7),
1017 onTimeout: () => null,
1018 );
1019 return MapEntry(provider, limits);
1020 }).toList();
1021
1022 final entries = await Future.wait(futures);
1023 _providerLimits = Map.fromEntries(entries);
1024
1025 _providerLimits.values.whereType<Limits>().forEach((tempLimits) {
1026 if (lowestMin != null && (tempLimits.min ?? -1) < lowestMin!) {
1027 lowestMin = tempLimits.min;
1028 }
1029
1030 if (highestMax != null && (tempLimits.max ?? double.maxFinite) > highestMax!) {
1031 highestMax = tempLimits.max;
1032 }
1033 });
1034 } on ConcurrentModificationError {
1035 /// if user changed the selected providers while fetching limits
1036 /// then delay the fetching limits a bit and try again
1037 ///
1038 /// this is because the limitation of collections that
1039 /// you can't modify it while iterating through it
1040 Future.delayed(Duration(milliseconds: 200), loadLimits);
1041 }
1042
1043 if (lowestMin != double.maxFinite) {
1044 limits = Limits(min: lowestMin, max: highestMax);
1045 limitsState = LimitsLoadedSuccessfully(limits: limits);
1046 } else {
1047 limitsState = LimitsLoadedFailure(error: 'Limits loading failed');
1048 }
1049
1050 calculateBestRate();
1051 }
1052
1053 @action
1054 Future<void> createTrade() async {
1055 final depositAmountValue = _depositAmount ?? Money.zero(depositCurrency);
1056 final receiveAmountValue = _receiveAmount ?? Money.zero(receiveCurrency);
1057
1058 if (depositAmountValue.isZero ||
1059 depositAmountValue.isNegative ||
1060 receiveAmountValue.isZero ||
1061 receiveAmountValue.isNegative) {
1062 ExchangeProviderLogger.logError(
1063 provider: forcedProvider?.description ?? bestRateProvider?.description,
1064 function: 'createTrade',
1065 error: 'Invalid swap amount: deposit=$_depositAmount receive=$_receiveAmount',
1066 requestData: {
1067 'from': depositCurrency.title,
1068 'to': receiveCurrency.title,
1069 'fromAmount': _depositAmount,
1070 'toAmount': _receiveAmount,
1071 'isFixedRateMode': isFixedRateMode,
1072 'forcedProvider': forcedProvider?.title,
1073 'bestRateProvider': bestRateProvider?.title,
1074 },
1075 );
1076
1077 final invalidAmountError = depositAmountValue.isZero || depositAmountValue.isNegative
1078 ? '$depositAmountValue is not a valid amount for depositAmount'
1079 : '$receiveAmountValue is not a valid amount for receiveAmount';
1080
1081 tradeState =
1082 TradeIsCreatedFailure(title: S.current.trade_not_created, error: invalidAmountError);
1083 return;
1084 }
1085
1086 if (isSendAllEnabled) {
1087 await calculateDepositAllAmount();
1088 final amount = double.tryParse(_depositAmount.toString());
1089
1090 if (limits.min != null && amount != null && amount < limits.min!) {
1091 tradeState = TradeIsCreatedFailure(
1092 title: S.current.trade_not_created,
1093 error: S.current.amount_is_below_minimum_limit(limits.min!.toString()),
1094 );
1095 return;
1096 }
1097 }
1098
1099 if (depositCurrency == CryptoCurrency.btcln &&
1100 wallet.type == WalletType.bitcoin &&
1101 depositAddress == wallet.walletAddresses.addressForExchange) {
1102 final invoice = await bitcoin!.getLightningInvoice(wallet, BigInt.zero);
1103 if (invoice != null) {
1104 depositAddress = invoice;
1105 }
1106 }
1107
1108 if (receiveCurrency == CryptoCurrency.btcln &&
1109 wallet.type == WalletType.bitcoin &&
1110 receiveAddress == wallet.walletAddresses.addressForExchange) {
1111 final invoice = await bitcoin!.getLightningInvoice(wallet, BigInt.zero);
1112 if (invoice != null) {
1113 receiveAddress = invoice;
1114 }
1115 }
1116
1117 if (forcedProvider != null && _excludeProviderForReceiveExtraId(forcedProvider!)) {
1118 tradeState = TradeIsCreatedFailure(
1119 title: S.current.trade_not_created,
1120 error: S.current.none_of_selected_providers_can_exchange,
1121 );
1122 return;
1123 }
1124
1125 Map<double, ExchangeProvider> providers;
1126 if (forcedProvider != null) {
1127 providers = {forcedProviderRate: forcedProvider!};
1128 } else {
1129 providers = Map.fromEntries(
1130 _sortedAvailableProviders.entries.where(
1131 (e) => selectedProviders.contains(e.value),
1132 ),
1133 );
1134 }
1135
1136 // Ensure we have providers available before attempting to create trade
1137 if (providers.isEmpty) {
1138 await calculateBestRate();
1139 if (forcedProvider != null) {
1140 providers = {forcedProviderRate: forcedProvider!};
1141 } else {
1142 providers = Map.fromEntries(
1143 _sortedAvailableProviders.entries.where(
1144 (e) => selectedProviders.contains(e.value),
1145 ),
1146 );
1147 }
1148
1149 if (providers.isEmpty) {
1150 ExchangeProviderLogger.logError(
1151 provider: null,
1152 function: 'createTrade',
1153 error: 'No providers available for $depositCurrency->$receiveCurrency',
1154 requestData: {
1155 'from': depositCurrency.title,
1156 'to': receiveCurrency.title,
1157 'fromAmount': _depositAmount,
1158 'toAmount': _receiveAmount,
1159 },
1160 );
1161 tradeState = TradeIsCreatedFailure(
1162 title: S.current.trade_not_created,
1163 error: S.current.none_of_selected_providers_can_exchange);
1164 return;
1165 }
1166 }
1167
1168 try {
1169 if (selectedAddressBookWallet?.type == WalletType.bitcoin) {
1170 final _walletAddresses = await selectedAddressBookWallet!.getAddresses();
1171
1172 // if receive currency is lightning pick the lightning address
1173 // if normal bitcoin, then pick the segwit address
1174 if (receiveCurrency == CryptoCurrency.btcln) {
1175 final lightningAddressOfWallet =
1176 _walletAddresses.entries.firstWhereOrNull((e) => e.value.contains("LN"))?.key;
1177 if (lightningAddressOfWallet != null) {
1178 receiveAddress = lightningAddressOfWallet;
1179 }
1180 }
1181 if (receiveCurrency == CryptoCurrency.btc) {
1182 final segwitAddressOfWallet =
1183 _walletAddresses.entries.firstWhereOrNull((e) => e.value.contains("P2WPKH"))?.key;
1184 if (segwitAddressOfWallet != null) {
1185 receiveAddress = segwitAddressOfWallet;
1186 }
1187 }
1188 }
1189
1190 if (receiveCurrency == CryptoCurrency.zec &&
1191 selectedAddressBookWallet?.type == WalletType.zcash) {
1192 if (wallet.type == WalletType.zcash && wallet.name == selectedAddressBookWallet!.name) {
1193 receiveAddress = wallet.walletAddresses.addressForExchange;
1194 } else {
1195 receiveAddress = (await selectedAddressBookWallet!.getAddresses())
1196 .entries
1197 .firstWhereOrNull((e) => e.value == 'transparent')
1198 ?.key ??
1199 receiveAddress;
1200 }
1201 }
1202
1203 // parse Lightning address to bolt11 invoice
1204 if (receiveAddress.contains("@")) {
1205 receiveAddress = await getBolt11FromLightingAddress(receiveAddress) ?? receiveAddress;
1206 }
1207
1208 // snapshot of providers to avoid concurrent modification issues
1209 final providersSnapshot = providers.values.toList();
1210 final ratesSnapshot = providers.keys.toList();
1211
1212 for (var i = 0; i < providersSnapshot.length; i++) {
1213 final provider = providersSnapshot[i];
1214 final providerRate = ratesSnapshot[i];
1215
1216 printV('createTrade: trying provider=${provider.title}');
1217
1218 if (_excludeProviderForSwapAll(provider)) {
1219 printV('Skipping Swaps.xyz or Near Intents for swap all');
1220 continue;
1221 }
1222 // should not happen but just as an extra check
1223 if (isFixedRateMode && provider.supportsFixedRate == false) {
1224 continue;
1225 }
1226
1227 if (_excludeProviderForReceiveExtraId(provider)) {
1228 continue;
1229 }
1230
1231 // Skip Swaps.xyz when sending from external
1232 if (isSendFromExternal && provider.description == ExchangeProviderDescription.swapsXyz) {
1233 printV('Skipping Swaps.xyz for external send');
1234 continue;
1235 }
1236
1237 if (!(await provider.checkIsAvailable())) continue;
1238
1239 bestRate = providerRate;
1240 bestRateProvider = provider;
1241
1242 await changeDepositAmount(amount: _depositAmount.toString(), isCanonical: true);
1243
1244 final request = TradeRequest(
1245 fromCurrency: depositCurrency,
1246 toCurrency: receiveCurrency,
1247 fromAmount: _depositAmount.toString(),
1248 toAmount: _receiveAmount.toString(),
1249 refundAddress: depositAddress,
1250 toAddress: receiveAddress,
1251 toAddressExtraId: receiveAddressExtraId.trim(),
1252 isFixedRate: isFixedRateMode,
1253 );
1254
1255 if (hideAddressAfterExchange) {
1256 wallet.walletAddresses.hiddenAddresses.add(depositAddress);
1257 await wallet.walletAddresses.saveAddressesInBox();
1258 }
1259
1260 var amount = isFixedRateMode ? _receiveAmount.toString() : _depositAmount.toString();
1261
1262 if (limitsState is LimitsLoadedSuccessfully) {
1263 if (double.tryParse(amount) == null) {
1264 printV('createTrade: ${provider.title} amount parse failed: "$amount"');
1265 continue;
1266 }
1267
1268 if (limits.min != null && double.parse(amount) < limits.min!) {
1269 continue;
1270 } else if (limits.max != null && double.parse(amount) > limits.max!) {
1271 continue;
1272 } else {
1273 try {
1274 tradeState = TradeIsCreating();
1275 final trade = await provider.createTrade(
1276 request: request,
1277 isFixedRateMode: isFixedRateMode,
1278 isSendAll: isSendAllEnabled,
1279 );
1280 trade.walletId = wallet.id;
1281 trade.chainId = wallet.chainId;
1282 trade.fromWalletAddress = wallet.walletAddresses.address;
1283 if (trade.from == null) {
1284 trade.from = depositCurrency;
1285 }
1286 if (trade.to == null) {
1287 trade.to = receiveCurrency;
1288 }
1289
1290 final canCreateTrade = await isCanCreateTrade(trade);
1291 if (!canCreateTrade.result) {
1292 ExchangeProviderLogger.logError(
1293 provider: provider.description,
1294 function: 'createTrade',
1295 error: canCreateTrade.errorMessage ?? 'isCanCreateTrade returned false',
1296 requestData: {
1297 'from': depositCurrency.title,
1298 'to': receiveCurrency.title,
1299 'fromAmount': _depositAmount,
1300 'toAmount': _receiveAmount,
1301 },
1302 );
1303 continue;
1304 }
1305
1306 tradesStore.setTrade(trade);
1307 if (trade.provider != ExchangeProviderDescription.thorChain) await trade.save();
1308 tradeState = TradeIsCreatedSuccessfully(trade: trade);
1309
1310 /// return after the first successful trade
1311 return;
1312 } catch (e, s) {
1313 ExchangeProviderLogger.logError(
1314 provider: provider.description,
1315 function: 'createTrade',
1316 error: e,
1317 stackTrace: s,
1318 requestData: {
1319 'from': depositCurrency.title,
1320 'to': receiveCurrency.title,
1321 'fromAmount': _depositAmount,
1322 'toAmount': _receiveAmount,
1323 'toAddress': receiveAddress,
1324 'refundAddress': depositAddress,
1325 },
1326 );
1327 continue;
1328 }
1329 }
1330 }
1331 }
1332
1333 /// if the code reached here then none of the providers succeeded
1334 tradeState = TradeIsCreatedFailure(
1335 title: S.current.trade_not_created,
1336 error: S.current.none_of_selected_providers_can_exchange);
1337 } on ConcurrentModificationError {
1338 /// if create trade happened at the exact same time of the scheduled rate update
1339 /// then delay the create trade a bit and try again
1340 ///
1341 /// this is because the limitation of the SplayTreeMap that
1342 /// you can't modify it while iterating through it
1343 Future.delayed(Duration(milliseconds: 200), createTrade);
1344 }
1345 }
1346
1347 @action
1348 void reset() {
1349 _initialPairBasedOnWallet();
1350 isReceiveAmountEntered = false;
1351 _depositAmount = null;
1352 _receiveAmount = null;
1353 depositAddress =
1354 depositCurrency == wallet.currency ? wallet.walletAddresses.addressForExchange : '';
1355 receiveAddress =
1356 receiveCurrency == wallet.currency ? wallet.walletAddresses.addressForExchange : '';
1357 receiveAddressExtraId = '';
1358 isDepositAddressEnabled = !(depositCurrency == wallet.currency);
1359 isFixedRateMode = false;
1360 _onPairChange();
1361 }
1362
1363 @action
1364 void enableSendAllAmount() {
1365 isSendAllEnabled = true;
1366 isFixedRateMode = false;
1367 calculateDepositAllAmount();
1368 }
1369
1370 @action
1371 void enableFixedRateMode() {
1372 isSendAllEnabled = false;
1373 isFixedRateMode = true;
1374 }
1375
1376 @action
1377 Future<void> calculateDepositAllAmount() async {
1378 if ([
1379 WalletType.litecoin,
1380 WalletType.bitcoin,
1381 WalletType.bitcoinCash,
1382 WalletType.dogecoin,
1383 ].contains(wallet.type)) {
1384 final priority = _settingsStore.getPriority(wallet.type)!;
1385
1386 final amount = depositCurrency == CryptoCurrency.btcln
1387 // FIXME amount estimation is broken/impossible for ln, konsti suggested this
1388 ? (amountParsingProxy.parseCryptoString(depositAvailableAmount, depositCurrency) -
1389 Money.fromInt(10, depositCurrency))
1390 : await bitcoin!.estimateFakeSendAllTxAmount(
1391 wallet,
1392 priority,
1393 coinTypeToSpendFrom: wallet.type == WalletType.litecoin
1394 ? UnspentCoinType.nonMweb
1395 : depositCurrency == CryptoCurrency.btcln
1396 ? UnspentCoinType.lightning
1397 : UnspentCoinType.any,
1398 );
1399
1400 changeDepositAmount(amount: amount.toString(), isCanonical: true);
1401 } else if (wallet.type == WalletType.monero) {
1402 final amount = await unspentCoinsListViewModel.getSendingBalance(UnspentCoinType.any);
1403
1404 changeDepositAmount(
1405 amount: wallet.currency.formatAmount(BigInt.from(amount)), isCanonical: true);
1406 } else if (isEVMCompatibleChain(wallet.type)) {
1407 final balanceCurrency = wallet.balance.keys.firstWhereOrNull(
1408 (currency) =>
1409 currency.title == depositCurrency.title &&
1410 (currency.tag == depositCurrency.tag || currency.tag == depositCurrency.title),
1411 );
1412
1413 final balanceForCurrency = balanceCurrency != null ? wallet.balance[balanceCurrency] : null;
1414 if (balanceForCurrency == null) {
1415 changeDepositAmount(amount: wallet.currency.formatAmount(BigInt.zero), isCanonical: true);
1416 return;
1417 }
1418
1419 final balanceAmount =
1420 _appStore.amountParsingProxy.asDisplayString(balanceForCurrency.available);
1421 final balanceDouble = double.tryParse(balanceAmount.replaceAll(',', '.')) ?? 0.0;
1422 if (balanceDouble <= 0) {
1423 changeDepositAmount(amount: wallet.currency.formatAmount(BigInt.zero), isCanonical: true);
1424 return;
1425 }
1426
1427 final isNative = depositCurrency == wallet.currency;
1428
1429 if (!isNative) {
1430 changeDepositAmount(amount: balanceAmount, isCanonical: true);
1431 return;
1432 }
1433
1434 try {
1435 final priority = _settingsStore.getPriority(wallet.type, chainId: wallet.chainId);
1436 await wallet.updateEstimatedFeesParams(priority);
1437
1438 String? feeString;
1439 if (isEVMCompatibleChain(wallet.type)) {
1440 feeString = evm!.getEVMNativeEstimatedFee(wallet);
1441 }
1442
1443 if (feeString == null || feeString.isEmpty) {
1444 changeDepositAmount(amount: balanceAmount, isCanonical: true);
1445 return;
1446 }
1447
1448 final balanceWei = balanceForCurrency.available.amount;
1449 final feeWei = BigInt.parse(feeString);
1450 final amountAfterFeeWei = balanceWei > feeWei ? balanceWei - feeWei : BigInt.zero;
1451 changeDepositAmount(
1452 amount: wallet.currency.formatAmount(amountAfterFeeWei), isCanonical: true);
1453 } catch (e) {
1454 printV('Error calculating send all for EVM: $e');
1455 changeDepositAmount(amount: balanceAmount, isCanonical: true);
1456 }
1457 }
1458 }
1459
1460 @observable
1461 String depositAvailableAmount = "";
1462
1463 @action
1464 void reverseSwapDirection() {
1465 final tmpAmount = _receiveAmount;
1466 final tmpCurrency = depositCurrency;
1467 changeDepositCurrency(currency: receiveCurrency);
1468 changeReceiveCurrency(currency: tmpCurrency);
1469 _depositAmount = tmpAmount;
1470 }
1471
1472 void updateTemplate() => _exchangeTemplateStore.update();
1473
1474 void addTemplate(
1475 {required String amount,
1476 required String depositCurrency,
1477 required String receiveCurrency,
1478 required String provider,
1479 required String depositAddress,
1480 required String receiveAddress,
1481 required String depositCurrencyTitle,
1482 required String receiveCurrencyTitle}) =>
1483 _exchangeTemplateStore.addTemplate(
1484 amount: amount,
1485 depositCurrency: depositCurrency,
1486 receiveCurrency: receiveCurrency,
1487 provider: provider,
1488 depositAddress: depositAddress,
1489 receiveAddress: receiveAddress,
1490 depositCurrencyTitle: depositCurrencyTitle,
1491 receiveCurrencyTitle: receiveCurrencyTitle);
1492
1493 void removeTemplate({required ExchangeTemplate template}) =>
1494 _exchangeTemplateStore.remove(template: template);
1495
1496 void _onPairChange({bool clearBoth = false}) {
1497 if (clearBoth) {
1498 _depositAmount = null;
1499 _receiveAmount = null;
1500 if (!hasAllAmount) isSendAllEnabled = false;
1501 } else {
1502 if (isFixedRateMode) {
1503 _depositAmount = null;
1504 } else {
1505 _receiveAmount = null;
1506 }
1507 }
1508 bestRate = 0.0;
1509 bestRateProvider = null;
1510 _sortedAvailableProviders.clear();
1511 loadLimits().then((_) {
1512 if (clearBoth && hasAllAmount) {
1513 calculateDepositAllAmount();
1514 }
1515 });
1516 _setAvailableProviders();
1517 }
1518
1519 void _initialPairBasedOnWallet() {
1520 if (isEVMCompatibleChain(wallet.type)) {
1521 depositCurrency = wallet.currency;
1522 receiveCurrency = CryptoCurrency.xmr;
1523 return;
1524 }
1525
1526 switch (wallet.type) {
1527 case WalletType.monero:
1528 depositCurrency = CryptoCurrency.xmr;
1529 receiveCurrency = CryptoCurrency.btc;
1530 break;
1531 case WalletType.bitcoin:
1532 depositCurrency = CryptoCurrency.btc;
1533 receiveCurrency = CryptoCurrency.xmr;
1534 break;
1535 case WalletType.litecoin:
1536 depositCurrency = CryptoCurrency.ltc;
1537 receiveCurrency = CryptoCurrency.xmr;
1538 break;
1539 case WalletType.bitcoinCash:
1540 depositCurrency = CryptoCurrency.bch;
1541 receiveCurrency = CryptoCurrency.xmr;
1542 break;
1543 case WalletType.dogecoin:
1544 depositCurrency = CryptoCurrency.doge;
1545 receiveCurrency = CryptoCurrency.xmr;
1546 break;
1547 case WalletType.haven:
1548 depositCurrency = CryptoCurrency.xhv;
1549 receiveCurrency = CryptoCurrency.btc;
1550 break;
1551 case WalletType.ethereum:
1552 depositCurrency = CryptoCurrency.eth;
1553 receiveCurrency = CryptoCurrency.xmr;
1554 break;
1555 case WalletType.nano:
1556 depositCurrency = CryptoCurrency.nano;
1557 receiveCurrency = CryptoCurrency.xmr;
1558 break;
1559 case WalletType.banano:
1560 depositCurrency = CryptoCurrency.banano;
1561 receiveCurrency = CryptoCurrency.xmr;
1562 break;
1563 case WalletType.polygon:
1564 depositCurrency = CryptoCurrency.maticpoly;
1565 receiveCurrency = CryptoCurrency.xmr;
1566 break;
1567 case WalletType.base:
1568 depositCurrency = CryptoCurrency.baseEth;
1569 receiveCurrency = CryptoCurrency.xmr;
1570 break;
1571 case WalletType.arbitrum:
1572 depositCurrency = CryptoCurrency.arbEth;
1573 receiveCurrency = CryptoCurrency.xmr;
1574 break;
1575 case WalletType.bsc:
1576 depositCurrency = CryptoCurrency.bnb;
1577 receiveCurrency = CryptoCurrency.xmr;
1578 break;
1579 case WalletType.solana:
1580 depositCurrency = CryptoCurrency.sol;
1581 receiveCurrency = CryptoCurrency.xmr;
1582 break;
1583 case WalletType.tron:
1584 depositCurrency = CryptoCurrency.trx;
1585 receiveCurrency = CryptoCurrency.xmr;
1586 break;
1587 case WalletType.wownero:
1588 depositCurrency = CryptoCurrency.wow;
1589 receiveCurrency = CryptoCurrency.xmr;
1590 break;
1591 case WalletType.zano:
1592 depositCurrency = CryptoCurrency.zano;
1593 receiveCurrency = CryptoCurrency.xmr;
1594 break;
1595 case WalletType.decred:
1596 depositCurrency = CryptoCurrency.dcr;
1597 receiveCurrency = CryptoCurrency.xmr;
1598 break;
1599 case WalletType.zcash:
1600 depositCurrency = CryptoCurrency.zec;
1601 receiveCurrency = CryptoCurrency.xmr;
1602 break;
1603 case WalletType.none:
1604 break;
1605 }
1606 }
1607
1608 void _defineIsReceiveAmountEditable() {
1609 /*if ((provider is ChangeNowExchangeProvider)
1610 &&(depositCurrency == CryptoCurrency.xmr)
1611 &&(receiveCurrency == CryptoCurrency.btc)) {
1612 isReceiveAmountEditable = true;
1613 } else {
1614 isReceiveAmountEditable = false;
1615 }*/
1616 //isReceiveAmountEditable = false;
1617 // isReceiveAmountEditable = selectedProviders.any((provider) => provider is ChangeNowExchangeProvider);
1618 // isReceiveAmountEditable = provider is ChangeNowExchangeProvider || provider is SimpleSwapExchangeProvider;
1619 isReceiveAmountEditable = true;
1620 }
1621
1622 @action
1623 void addExchangeProvider(ExchangeProvider provider) {
1624 if (provider.description.isCentralized && forceDecentralizedExchanges) return;
1625 if (!selectedProviders.contains(provider)) selectedProviders.add(provider);
1626 if (providerList.contains(provider)) _tradeAvailableProviders.add(provider);
1627 }
1628
1629 @action
1630 void removeExchangeProvider(ExchangeProvider provider) {
1631 selectedProviders.remove(provider);
1632 _tradeAvailableProviders.remove(provider);
1633 if (forcedProvider == provider) {
1634 setForcedProvider(null);
1635 }
1636 }
1637
1638 @action
1639 void saveSelectedProviders() {
1640 _depositAmount = null;
1641 _receiveAmount = null;
1642 isFixedRateMode = false;
1643 _defineIsReceiveAmountEditable();
1644 bestRateProvider = null;
1645 bestRate = 0.0;
1646 loadLimits();
1647
1648 final Map<String, dynamic> exchangeProvidersSelection =
1649 json.decode(sharedPreferences.getString(PreferencesKey.exchangeProvidersSelection) ?? "{}")
1650 as Map<String, dynamic>;
1651
1652 for (var provider in providerList) {
1653 exchangeProvidersSelection[provider.title] = selectedProviders.contains(provider);
1654 }
1655
1656 sharedPreferences.setString(
1657 PreferencesKey.exchangeProvidersSelection,
1658 json.encode(exchangeProvidersSelection),
1659 );
1660 }
1661
1662 @action
1663 Future<void> updateAllTrocadorProviderStates(TrocadorExchangeProvider trocadorProvider) async {
1664 try {
1665 var providers = await trocadorProvider.fetchProviders();
1666 var providerNames = providers.map((e) => e.name).toList();
1667 await _settingsStore.updateAllTrocadorProviderStates(providerNames);
1668 } catch (e) {
1669 printV('Error updating trocador provider states: $e');
1670 }
1671 }
1672
1673 bool get isAvailableInSelected {
1674 return selectedProviders
1675 .any((element) => element.isAvailable && providerList.contains(element));
1676 }
1677
1678 @computed
1679 bool get forceDecentralizedExchanges => _settingsStore.forceDecentralizedExchanges;
1680
1681 @action
1682 void toggleForceDecentralizedExchanges() {
1683 _settingsStore.forceDecentralizedExchanges = !_settingsStore.forceDecentralizedExchanges;
1684 if (forceDecentralizedExchanges) {
1685 final providers = selectedProviders.toList();
1686 for (final provider in providers) {
1687 if (forceDecentralizedExchanges && provider.description.isCentralized) {
1688 removeExchangeProvider(provider);
1689 }
1690 }
1691 } else {
1692 for (final provider in providerList) {
1693 if (!selectedProviders.contains(provider) && provider.description.isCentralized) {
1694 addExchangeProvider(provider);
1695 }
1696 }
1697 }
1698 }
1699
1700 void _setAvailableProviders() {
1701 _tradeAvailableProviders.clear();
1702
1703 _tradeAvailableProviders
1704 .addAll(selectedProviders.where((provider) => providerList.contains(provider)));
1705 }
1706
1707 void _setProviders() {
1708 if (_settingsStore.exchangeStatus == ExchangeApiMode.torOnly)
1709 providerList = _allProviders.where((provider) => provider.supportsOnionAddress).toList();
1710 else
1711 providerList = _allProviders;
1712 }
1713
1714 int get depositMaxDigits => depositCurrency.decimals;
1715
1716 int get receiveMaxDigits => receiveCurrency.decimals;
1717
1718 Future<CreateTradeResult> isCanCreateTrade(Trade trade) async {
1719 if (trade.provider == ExchangeProviderDescription.swapsXyz) {
1720 final tradeFrom = trade.from;
1721
1722 if (tradeFrom == null) {
1723 return CreateTradeResult(
1724 result: false,
1725 errorMessage: 'From currency is null',
1726 );
1727 }
1728
1729 final isNativeSupportedToken =
1730 walletTypes.contains(cryptoCurrencyOrTokenToWalletType(tradeFrom));
1731
1732 if (!isNativeSupportedToken) {
1733 bool _isEthToken() =>
1734 wallet.currency == CryptoCurrency.eth && tradeFrom.tag == CryptoCurrency.eth.title;
1735
1736 bool _isPolygonToken() =>
1737 wallet.currency == CryptoCurrency.maticpoly &&
1738 tradeFrom.tag == CryptoCurrency.maticpoly.tag;
1739
1740 bool _isBaseToken() =>
1741 wallet.currency == CryptoCurrency.baseEth &&
1742 tradeFrom.tag == CryptoCurrency.baseEth.tag;
1743
1744 bool _isTronToken() =>
1745 wallet.currency == CryptoCurrency.trx && tradeFrom.tag == CryptoCurrency.trx.title;
1746
1747 bool _isSplToken() =>
1748 wallet.currency == CryptoCurrency.sol && tradeFrom.tag == CryptoCurrency.sol.title;
1749
1750 bool isArbitrumToken() =>
1751 wallet.currency == CryptoCurrency.arbEth && tradeFrom.tag == CryptoCurrency.arbEth.tag;
1752
1753 bool isBscToken() =>
1754 wallet.currency == CryptoCurrency.bnb && tradeFrom.tag == CryptoCurrency.bnb.tag;
1755
1756 if (!(_isEthToken() ||
1757 _isPolygonToken() ||
1758 _isBaseToken() ||
1759 _isTronToken() ||
1760 _isSplToken() ||
1761 isArbitrumToken() ||
1762 isBscToken())) {
1763 return CreateTradeResult(
1764 result: false,
1765 errorMessage:
1766 'This token isn’t supported on the current wallet/network for Swaps.xyz. Switch to a supported wallet or asset',
1767 );
1768 }
1769 }
1770 }
1771
1772 if (trade.provider == ExchangeProviderDescription.thorChain) {
1773 final payoutAddress = trade.payoutAddress ?? '';
1774 final fromWalletAddress = trade.fromWalletAddress ?? '';
1775 final tapRootPattern = RegExp(P2trAddress.regex.pattern);
1776
1777 if (tapRootPattern.hasMatch(payoutAddress) || tapRootPattern.hasMatch(fromWalletAddress)) {
1778 return CreateTradeResult(
1779 result: false,
1780 errorMessage: S.current.thorchain_taproot_address_not_supported,
1781 );
1782 }
1783
1784 if ((trade.memo == null || trade.memo!.isEmpty)) {
1785 return CreateTradeResult(
1786 result: false,
1787 errorMessage: 'Memo is required for Thorchain trade',
1788 );
1789 }
1790
1791 final currenciesToCheckPattern = RegExp('0x[0-9a-zA-Z]');
1792
1793 // Perform checks for payOutAddress
1794 final isPayOutAddressAccordingToPattern = currenciesToCheckPattern.hasMatch(payoutAddress);
1795
1796 if (isPayOutAddressAccordingToPattern) {
1797 final isPayOutAddressEOA = await _isExternallyOwnedAccountAddress(payoutAddress);
1798
1799 return CreateTradeResult(
1800 result: isPayOutAddressEOA,
1801 errorMessage:
1802 !isPayOutAddressEOA ? S.current.thorchain_contract_address_not_supported : null,
1803 );
1804 }
1805
1806 // Perform checks for fromWalletAddress
1807 final isFromWalletAddressAddressAccordingToPattern =
1808 currenciesToCheckPattern.hasMatch(fromWalletAddress);
1809
1810 if (isFromWalletAddressAddressAccordingToPattern) {
1811 final isFromWalletAddressEOA = await _isExternallyOwnedAccountAddress(fromWalletAddress);
1812
1813 return CreateTradeResult(
1814 result: isFromWalletAddressEOA,
1815 errorMessage:
1816 !isFromWalletAddressEOA ? S.current.thorchain_contract_address_not_supported : null,
1817 );
1818 }
1819 }
1820 return CreateTradeResult(result: true);
1821 }
1822
1823 String _normalizeReceiveCurrency(CryptoCurrency receiveCurrency) {
1824 switch (receiveCurrency) {
1825 case CryptoCurrency.eth:
1826 return 'eth';
1827 case CryptoCurrency.maticpoly:
1828 return 'polygon';
1829 default:
1830 return receiveCurrency.tag ?? '';
1831 }
1832 }
1833
1834 Future<bool> _isExternallyOwnedAccountAddress(String receivingAddress) async {
1835 final normalizedReceiveCurrency = _normalizeReceiveCurrency(receiveCurrency);
1836
1837 final isEOAAddress = !(await _isContractAddress(normalizedReceiveCurrency, receivingAddress));
1838 return isEOAAddress;
1839 }
1840
1841 Future<bool> _isContractAddress(String chainName, String contractAddress) async {
1842 final uri = Uri.https(
1843 'deep-index.moralis.io',
1844 '/api/v2.2/erc20/metadata',
1845 {
1846 "chain": chainName,
1847 "addresses": contractAddress,
1848 },
1849 );
1850
1851 try {
1852 final response = await ProxyWrapper().get(
1853 clearnetUri: uri,
1854 headers: {
1855 "Accept": "application/json",
1856 "X-API-Key": secrets.moralisApiKey,
1857 },
1858 );
1859
1860 final decodedResponse = jsonDecode(response.body)[0] as Map<String, dynamic>;
1861
1862 final name = decodedResponse['name'] as String?;
1863
1864 bool isContractAddress = name!.isNotEmpty;
1865
1866 return isContractAddress;
1867 } catch (e) {
1868 printV(e);
1869 return false;
1870 }
1871 }
1872
1873 // Adding user's Erc20 tokens to the list of currencies
1874
1875 @action
1876 Future<void> _injectUserEthTokensIntoCurrencyLists() async {
1877 final tokens = await TokenUtilities.loadEvmTokensForSwap();
1878 final toAddReceive = <CryptoCurrency>[];
1879 final toAddDeposit = <CryptoCurrency>[];
1880
1881 for (final token in tokens) {
1882 if (!_listContainsToken(receiveCurrencies, token)) toAddReceive.add(token);
1883 if (!_listContainsToken(depositCurrencies, token)) toAddDeposit.add(token);
1884 }
1885
1886 if (toAddReceive.isNotEmpty) receiveCurrencies.addAll(toAddReceive);
1887 if (toAddDeposit.isNotEmpty) depositCurrencies.addAll(toAddDeposit);
1888 }
1889
1890 bool _listContainsToken(List<CryptoCurrency> list, Erc20Token token) {
1891 return list.any((item) {
1892 if (item is Erc20Token) {
1893 return item.contractAddress.toLowerCase() == token.contractAddress.toLowerCase();
1894 }
1895 return item.title.toUpperCase() == token.symbol.toUpperCase() &&
1896 (item.tag?.toUpperCase() == token.tag?.toUpperCase() || item.tag == null);
1897 });
1898 }
1899
1900 // Adding user's Solana tokens to the list of currencies
1901
1902 bool _listContainsSplToken(List<CryptoCurrency> list, SPLToken token) {
1903 return list.any((item) {
1904 if (item is SPLToken) {
1905 return item.mintAddress.toLowerCase() == token.mintAddress.toLowerCase();
1906 }
1907 return item.title.toUpperCase() == token.symbol.toUpperCase() &&
1908 (item.tag?.toUpperCase() == token.tag?.toUpperCase() || item.tag == null);
1909 });
1910 }
1911
1912 @action
1913 Future<void> _injectUserSplTokensIntoCurrencyLists() async {
1914 final tokens = await TokenUtilities.loadSolTokensForSwap();
1915 final toAddReceive = <CryptoCurrency>[];
1916 final toAddDeposit = <CryptoCurrency>[];
1917
1918 for (final token in tokens) {
1919 if (!_listContainsSplToken(receiveCurrencies, token)) toAddReceive.add(token);
1920 if (!_listContainsSplToken(depositCurrencies, token)) toAddDeposit.add(token);
1921 }
1922
1923 if (toAddReceive.isNotEmpty) receiveCurrencies.addAll(toAddReceive);
1924 if (toAddDeposit.isNotEmpty) depositCurrencies.addAll(toAddDeposit);
1925 }
1926
1927 // Adding user's Tron tokens to the list of currencies
1928
1929 bool _listContainsTronToken(List<CryptoCurrency> list, TronToken token) {
1930 return list.any((item) {
1931 if (item is TronToken) {
1932 return item.contractAddress.toLowerCase() == token.contractAddress.toLowerCase();
1933 }
1934 return item.title.toUpperCase() == token.symbol.toUpperCase() &&
1935 (item.tag?.toUpperCase() == token.tag?.toUpperCase() || item.tag == null);
1936 });
1937 }
1938
1939 @action
1940 Future<void> _injectUserTronTokensIntoCurrencyLists() async {
1941 final tokens = await TokenUtilities.loadTronTokensForSwap();
1942 final toAddReceive = <CryptoCurrency>[];
1943 final toAddDeposit = <CryptoCurrency>[];
1944
1945 for (final token in tokens) {
1946 if (!_listContainsTronToken(receiveCurrencies, token)) toAddReceive.add(token);
1947 if (!_listContainsTronToken(depositCurrencies, token)) toAddDeposit.add(token);
1948 }
1949
1950 if (toAddReceive.isNotEmpty) receiveCurrencies.addAll(toAddReceive);
1951 if (toAddDeposit.isNotEmpty) depositCurrencies.addAll(toAddDeposit);
1952 }
1953
1954 double initialAmountByAssets(CryptoCurrency ticker) {
1955 final amount = switch (ticker) {
1956 CryptoCurrency.trx => 1000,
1957 CryptoCurrency.nano => 10,
1958 CryptoCurrency.zano => 10,
1959 CryptoCurrency.wow => 1000,
1960 CryptoCurrency.ada => 1000,
1961 CryptoCurrency.dash => 10,
1962 CryptoCurrency.rune => 10,
1963 _ => 1
1964 };
1965 return amount.toDouble();
1966 }
1967 }