Files
hash-wallet/lib/view_model/exchange/exchange_view_model.dart
T
75f3fe735b Introduce Money class and refactor currency handling (#3157)
* feat: add `Money` class for precise currency handling and amount parsing

- Introduced `Money` class for handling money values with fixed-point precision.
- Added parsing and comparison methods to `CryptoCurrency` and `FiatCurrency` interfaces.
- Implemented `smartAmountSanitizer` for format normalization.

Includes extensive unit tests for validation.

* refactor: optimize Money class with new factories and string formatting improvements

* refactor: unify balance handling with `Money` refactor

* refactor: update `ElectrumBalance` and transaction logic to use `Money` for currency precision

* refactor: standardize balance handling with `Money` across `Monero`, `Wownero`, and `Zano` wallets

* refactor: migrate EVM balance and transaction handling to `Money` for consistent currency precision

* refactor: migrate Decred balance and transaction handling to `Money` for consistent currency precision

* refactor: migrate nano balance and transaction handling to `Money` for consistent currency precision

* refactor: migrate Solana balance and transaction logic to `Money` for precise currency handling

* refactor: migrate Tron balance and transaction handling to `Money` for consistent currency precision

* refactor: migrate Zcash balance and transaction handling to `Money` for consistent currency precision

* refactor: adjust lib to handle money

* refactor: remove legacy Decred amount formatting and migrate to `Money`

* refactor: migrate pending transaction amount and fee to `Money`

* refactor: migrate dEuro savings and transaction logic to `Money` for improved precision and consistency

* refactor: migrate Bitcoin and lightning transaction logic to `Money` for consistent currency precision

* refactor: migrate Wownero, EVM, Zano, and Zcash transaction logic to `Money` for consistent and precise currency handling

* refactor: standardize balance handling across wallets, migrate to `Money.zero` and refine `Balance` class for consistency

* refactor: migrate Solana transaction and wallet logic to `Money` for consistent and precise currency handling

* refactor: improve fiat balance and formatting for consistency on dashboard

* fix: fiat input mode switching and standardize crypto amount display logic

* fix: cw_evm generator

* refactor: migrate exchange amount handling to `Money` for improved precision and consistency

* test: remove redundant and commented-out tests, tidy test cases, and apply minor formatting adjustments

* fix using Money wrapper in BridgeViewModel and USDT0Service

* refactor: streamline `AmountConverter` and `LightningWallet`, add `cw_core` tests to GitHub workflow, and remove unused/deprecated logic

* refactor: enhance `changeReceiveAmount` with `isCanonical` support, improve null handling, and refine amount parsing logic

* minor fix

* try/catch the lightning log file error

* feat: add copy-to-clipboard functionality for transaction amounts, fix typo in trailing widget method name (#3219)

* Cw 1234 disallow sp and mweb addresses as refund for swaps (#3187)

* disallow SP and MWEB for swaps

* Simplify address selection in addressForExchange

Refactor addressForExchange method to simplify address selection logic.

* validate refund and receive address types

* localize address validation messages

* minor ui fix [skip ci]

* minor fix [skip ci]

* Revert "localize address validation messages"

* fix translations

---------

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

* refactor: replace manual amount sanitization with `sanitized()` extension across the codebase, add unit tests for `AmountSanitizer`

* fix: correct `sanitized()` test expectations for decimal handling in `amount_sanitizer_test.dart`

* refactor: fee display logic and append currency symbol in `send_confirm_sheet.dart`

* refactor: simplify `ZcashBalance` class by replacing custom fields with inherited properties, update references accordingly

* refactor: replace hardcoded USDT contract address with `DefaultTronTokens` lookup, improve maintainability

* refactor: replace `currency` with `inputAmount.currency` in Solana transaction signing methods to avoid inconsistency

* fix: handle null `fee` in `electrum_transaction_info.dart` to prevent potential runtime errors

* refactor: replace raw fee and amount handling with `Money` object for improved consistency and maintainability

* fix: limit fiat balance display to 2 decimal places in `balance_view_model.dart`

* refactor: replace raw `amount` and `fee` types with `Money` in Solana transaction methods for consistency and precision

* refactor: replace raw `amount` and `fee` handling with `Money` and improve balance calculation consistency across view models

* refactor: replace raw balance strings with `Money` and simplify `UnconfirmedBalanceModal` widget logic

* refactor: replace `print` with `printV` for better logging, add equality and hashCode overrides to `ERC20Currency` for object comparison

* feat: prepare deuro savings migration

* feat: add V1 savings migration support and improve formatting consistency in DEuro view model

* feat: add V1 savings withdrawal support and update associated UI components and logic

* refactor: replace `Image.asset` with `CakeImageWidget`, update balance string handling, and streamline address validation logic

* refactor: use `Money.tryParse` with fallback to `Money.zero` in `NanoBalance.fromRawString` for safer balance parsing

* refactor: replace `ERC20Currency` with `Erc20Token` for consistency and remove unused `evm_erc20_currency.dart` file

* refactor: add computed `hasDepositAmount` for deposit null-checks, optimize balance handling in `zano_wallet`, update Breez SDK to v0.14.0, and implement equality/hashCode for `Currency`

* refactor: rename `amountSats` to `amount` in `PrepareLnurlPayRequest` for consistency

* revert: downgrade Breez SDK to v0.11.0 in `pubspec.yaml` and update relevant references in `pubspec.lock`

* fix: use `cryptoAmount.copyWith` for currency consistency and replace `Image.asset` with `CakeImageWidget` in swap address modal

* fix: ensure currency consistency by using `cryptoAmount.copyWith` in Solana and EVM wallet transactions

* refactor: simplify CSV row generation logic by removing `_splitAmountCurrency` and streamlining amount/fee handling

* fix: LateInitializationError Field 'sdk' has not been initialized

* fix: use `Money.tryParse` for safer parsing of deposit and receive amounts

* chore: remove debug print statement

* refactor: update transaction processing to consistently use `Money` for amounts and fees

* refactor: improve currency and amount handling, simplify logic, and enhance code readability in Send and Swap pages

* refactor: integrate `DecimalInputFormatter` for consistent decimal input across Send and Swap pages, simplify input handling logic

* fix: "fiatMode" per output on sendPage

* refactor: make transaction_details_height applicable more readable

* fix: use correct base unit for deposit

* refactor: use Money in estimateFakeSendAllTxAmount method

* fix decred frozen balance
minor fixes

* fix decred frozen balance
minor fixes

* fix: automatically switch to lightning if lnurl or invoice detected

* fix: getBolt11Amount test cases

* fix: standardise crypto amount formatting and display

* fix: use base unit for Tron transaction fee parsing
* refactor: hardcode fractional digits to 8 in `AmountParsingProxy` display methods
* refactor: ensure consistent 8-decimal precision for fee formatting in `PendingTransaction`
* fix: use `AmountParsingProxy` for available balance display in exchange view model
* fix: improve amount formatting and layout in swap and send confirmation sheets
* chore: remove redundant decimal truncation in balance view model and send confirmation sheet

* fix: allow for editing text field if it is filled with "ALL"

* fix: allow for editing text field if it is filled with "ALL"

* fix: update currency detection logic for lightning mode

* avoid potential infinite loop [skip ci]

* fix: getDisplayCryptoAmount

* fix: update estimated fee calculation

* chore: simplify fee calculation logic [skip-ci]

* fix value display in hidden mode

* fix stale state in send confirm sheet

* fix decimals for mweb

* fix available balance display for sol/trx on swap

* restore proper hint text

* fix swap receive amount calculation for xno

---------

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>
Co-authored-by: Serhii <17529954+serhii-bor@users.noreply.github.com>
Co-authored-by: Robert Malikowski <malikowskirobert@gmail.com>
2026-06-30 02:45:44 +03:00

1960 lines
65 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'dart:async';
import 'dart:collection';
import 'dart:convert';
import 'dart:developer';
import 'dart:math' show min;
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:cake_wallet/.secrets.g.dart' as secrets;
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/core/address_validator.dart';
import 'package:cake_wallet/core/amount_parsing_proxy.dart';
import 'package:cake_wallet/core/create_trade_result.dart';
import 'package:cake_wallet/core/fiat_conversion_service.dart';
import 'package:cake_wallet/core/lightning_invoice_service.dart';
import 'package:cake_wallet/core/utilities.dart';
import 'package:cake_wallet/core/wallet_change_listener_view_model.dart';
import 'package:cake_wallet/entities/calculate_fiat_amount.dart';
import 'package:cake_wallet/entities/exchange_api_mode.dart';
import 'package:cake_wallet/entities/fiat_api_mode.dart';
import 'package:cake_wallet/entities/fiat_currency.dart';
import 'package:cake_wallet/entities/preferences_key.dart';
import 'package:cake_wallet/entities/wallet_contact.dart';
import 'package:cake_wallet/exchange/exchange_provider_description.dart';
import 'package:cake_wallet/exchange/exchange_template.dart';
import 'package:cake_wallet/exchange/exchange_trade_state.dart';
import 'package:cake_wallet/exchange/limits.dart';
import 'package:cake_wallet/exchange/limits_state.dart';
import 'package:cake_wallet/exchange/provider/chainflip_exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/jupiter_exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/letsexchange_exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/changenow_exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/exolix_exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/near_Intents_exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/stealth_ex_exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/swapsxyz_exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/swaptrade_exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/trocador_exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/xoswap_exchange_provider.dart';
import 'package:cake_wallet/exchange/trade.dart';
import 'package:cake_wallet/exchange/trade_request.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/new-ui/widgets/currency_picker/fiat_currency_picker_sheet.dart';
import 'package:cake_wallet/store/app_store.dart';
import 'package:cake_wallet/utils/exchange_provider_logger.dart';
import 'package:cw_core/amount/amount_sanitizer.dart';
import 'package:cw_core/amount/money.dart';
import "package:cw_core/wallet_info.dart";
import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart';
import 'package:cake_wallet/store/dashboard/trades_store.dart';
import 'package:cake_wallet/store/settings_store.dart';
import 'package:cake_wallet/store/templates/exchange_template_store.dart';
import 'package:cake_wallet/evm/evm.dart';
import 'package:cake_wallet/reactions/wallet_connect.dart';
import 'package:cake_wallet/utils/feature_flag.dart';
import 'package:cake_wallet/utils/token_utilities.dart';
import 'package:cake_wallet/view_model/contact_list/contact_list_view_model.dart';
import 'package:cake_wallet/view_model/send/fees_view_model.dart';
import 'package:cake_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/currencies_with_memo.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<ReactionDisposer> _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 = null,
_receiveAmount = null,
receiveAddress = '',
depositAddress = '',
isDepositAddressEnabled = false,
isReceiveAmountEditable = false,
_useTorOnly = false,
receiveCurrencies = ObservableList<CryptoCurrency>(),
depositCurrencies = ObservableList<CryptoCurrency>(),
limits = Limits(min: 0, max: 0),
tradeState = ExchangeTradeStateInitial(),
limitsState = LimitsInitialState(),
receiveCurrency = _appStore.wallet!.currency,
depositCurrency = _appStore.wallet!.currency,
providerList = [],
selectedProviders = ObservableList<ExchangeProvider>(),
super(appStore: _appStore) {
_useTorOnly = _settingsStore.exchangeStatus == ExchangeApiMode.torOnly;
_setProviders();
const excludeDepositCurrencies = [CryptoCurrency.btt];
const excludeReceiveCurrencies = [CryptoCurrency.btt];
_initialPairBasedOnWallet();
unspentCoinsListViewModel.initialSetup().then((_) {
unspentCoinsListViewModel.resetUnspentCoinsInfoSelections();
});
final Map<String, dynamic> exchangeProvidersSelection =
json.decode(sharedPreferences.getString(PreferencesKey.exchangeProvidersSelection) ?? "{}")
as Map<String, dynamic>;
/// 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 = null;
_receiveAmount = null;
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.firstWhere((provider) => provider is ChangeNowExchangeProvider,
orElse: () => 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((cryptoCurrency) => !excludeReceiveCurrencies.contains(cryptoCurrency))
.toList()
.asObservable();
depositCurrencies = CryptoCurrency.all
.where((cryptoCurrency) => !excludeDepositCurrencies.contains(cryptoCurrency))
.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<void> 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.asDisplayString(balance.available);
return false;
}
return true;
}).timeout(Duration(seconds: 20));
} 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.asDisplayStringWithSymbol(balanceForCurrency.available);
}
} else {
final currency = depositCurrency;
final sendingBalance = Money.fromInt(
await unspentCoinsListViewModel.getSendingBalance(UnspentCoinType.any), currency);
final amount = _appStore.amountParsingProxy.asDisplayStringWithSymbol(sendingBalance);
if (depositCurrency == currency) {
depositAvailableAmount = amount;
}
}
}
void showFiatCurrencyPicker(BuildContext context) {
FiatCurrencyPickerSheet.show(
context: context,
selected: fiat,
onSelected: (cur) => _settingsStore.fiatCurrency = cur,
);
}
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, WalletType.zcash].contains(wallet.type) ||
isElectrumWallet;
bool _useTorOnly;
final ExchangeTemplateStore _exchangeTemplateStore;
final TradesStore tradesStore;
final SharedPreferences sharedPreferences;
List<ExchangeProvider> get _allProviders => [
ChangeNowExchangeProvider(settingsStore: _settingsStore),
// SideShiftExchangeProvider(),
ChainflipExchangeProvider(),
if (FeatureFlag.isExolixEnabled) ExolixExchangeProvider(),
SwapTradeExchangeProvider(),
LetsExchangeExchangeProvider(),
StealthExExchangeProvider(),
XOSwapExchangeProvider(),
SwapsXyzExchangeProvider(),
JupiterExchangeProvider(),
NearIntentsExchangeProvider(),
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<double, ExchangeProvider> _sortedAvailableProviders =
SplayTreeMap<double, ExchangeProvider>((double a, double b) => b.compareTo(a));
final List<ExchangeProvider> _tradeAvailableProviders = [];
Map<ExchangeProvider, Limits?> _providerLimits = {};
@observable
ObservableList<ExchangeProvider> selectedProviders;
@observable
List<ExchangeProvider> providerList;
@observable
CryptoCurrency depositCurrency;
@observable
CryptoCurrency receiveCurrency;
@observable
LimitsState limitsState;
@observable
ExchangeTradeState tradeState;
@observable
Money? _depositAmount;
@computed
bool get hasDepositAmount => _depositAmount != null;
@computed
String get depositAmount =>
_depositAmount == null ? "" : amountParsingProxy.asDisplayString(_depositAmount!);
@computed
String get depositAmountCanonical => _depositAmount == null ? "0.0" : _depositAmount.toString();
@observable
Money? _receiveAmount;
@computed
String get receiveAmount =>
_receiveAmount == null ? "" : amountParsingProxy.asDisplayString(_receiveAmount!);
@action
// only set canonical formated amounts here;
void setCanonicalReceiveAmount(String value) =>
_receiveAmount = Money.tryParse(value, receiveCurrency);
@observable
String depositAddress;
@observable
String receiveAddress;
@observable
String receiveAddressExtraId = '';
@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<ExchangeTemplate> get templates => _exchangeTemplateStore.templates;
@computed
List<WalletContact> get walletContactsToShow => contactListViewModel.walletContacts
.where((element) => element.type == receiveCurrency)
.toList();
@computed
List<WalletContact> get refundWalletContactsToShow => contactListViewModel.walletContacts
.where((element) => element.type == depositCurrency)
.toList();
@computed
Future<List<WalletInfo>> get receiveWallets async {
try {
WalletType? type;
type = cryptoCurrencyOrTokenToWalletType(receiveCurrency);
if (type == null) {
type =
cryptoCurrencyOrTokenToWalletType(CryptoCurrency.fromString(receiveCurrency.tag ?? ""));
}
return await WalletInfo.selectList("type = ?", [type!.index]);
} catch (e) {
return [];
}
}
Future<List<WalletInfoAddressInfo>> addressesForAccountsWallet(WalletInfo wallet) async {
final List<WalletInfoAddressInfo> 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<List<WalletInfo>> 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) || wallet.type == WalletType.solana || wallet.type == WalletType.tron) {
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]?.available : null;
if (bal == null) return null;
return amountParsingProxy.asDisplayString(bal);
}
//* 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<CryptoCurrency> receiveCurrencies;
@observable
ObservableList<CryptoCurrency> 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 useDepositBaseUnit => _appStore.amountParsingProxy.useSatoshi(depositCurrency);
@computed
bool get useReceiveBaseUnit => _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<void> 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 != null) {
if (fiatConversionStore.prices[receiveCurrency] == null) return '';
amount = calculateFiatAmount(
price: fiatConversionStore.prices[receiveCurrency]!,
cryptoAmount: _receiveAmount.toString(),
);
}
} catch (_) {
log('Error calculating receive amount fiat formatted: $_');
}
return isFiatDisabled ? '' : '$amount';
}
@computed
String get depositAmountFiatFormatted {
var amount = '0.00';
try {
if (_depositAmount != null) {
if (fiatConversionStore.prices[depositCurrency] == null) return '';
amount = calculateFiatAmount(
price: fiatConversionStore.prices[depositCurrency]!,
cryptoAmount: _depositAmount.toString(),
);
}
} catch (_) {
log('Error calculating deposit amount fiat formatted: $_');
}
return isFiatDisabled ? '' : '$amount';
}
@computed
String get receiveAmountFiat {
var amount = '';
try {
if (_receiveAmount != null) {
if (fiatConversionStore.prices[receiveCurrency] == null) return '';
amount = calculateFiatAmount(
price: fiatConversionStore.prices[receiveCurrency]!,
cryptoAmount: _receiveAmount.toString(),
raw: true);
}
} catch (_) {
log('Error calculating receive amount fiat formatted: $_');
}
return amount;
}
@computed
String get depositAmountFiat {
var amount = '';
try {
if (_depositAmount != null) {
if (fiatConversionStore.prices[depositCurrency] == null) return '';
amount = calculateFiatAmount(
price: fiatConversionStore.prices[depositCurrency]!,
cryptoAmount: _depositAmount.toString(),
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 = "";
}
if (currency != receiveCurrency) {
receiveAddressExtraId = "";
}
receiveCurrency = currency;
isFixedRateMode = false;
isDepositAddressEnabled = !useSameWalletAddress(depositCurrency);
if (previousCurrency != currency) {
_onPairChange();
}
fetchFiatPrice(currency);
}
@action
Future<void> changeReceiveAmount({required String amount, bool isCanonical = false}) async {
if (amount.isEmpty) {
_depositAmount = null;
_receiveAmount = null;
return;
}
_receiveAmount = isCanonical
? Money.tryParse(amount.sanitized(), receiveCurrency)
: _appStore.amountParsingProxy.tryParseCryptoString(amount.sanitized(), receiveCurrency);
if (_receiveAmount == null) {
_depositAmount = null;
return;
}
final _enteredAmount = double.tryParse(_receiveAmount.toString()) ?? 0;
if (bestRate == 0) {
_depositAmount = null;
await calculateBestRate();
}
final amount_ = _enteredAmount / (forcedProvider == null ? bestRate : forcedProviderRate);
_depositAmount = Money.tryParse(amount_.toStringAsFixed(depositCurrency.decimals), depositCurrency);
}
@action
void setReceiveAmountFromFiat({required String fiatAmount}) {
final _enteredAmount = double.tryParse(fiatAmount.sanitized()) ?? 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.sanitized()) ?? 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<void> changeDepositAmount({required String amount, bool isCanonical = false}) async {
if (amount.isEmpty) {
_depositAmount = null;
_receiveAmount = null;
return;
}
_depositAmount = isCanonical
? Money.tryParse(amount.sanitized(), depositCurrency)
: _appStore.amountParsingProxy.tryParseCryptoString(amount.sanitized(), depositCurrency);
if (_depositAmount == null) {
_receiveAmount = null;
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.toString()) ?? 0;
/// in case the best rate was not calculated yet
if (bestRate == 0) {
_receiveAmount = null;
await calculateBestRate();
}
final amount_ = _enteredAmount * (forcedProvider == null ? bestRate : forcedProviderRate);
_receiveAmount =
Money.tryParse(amount_.toStringAsFixed(min(20, receiveCurrency.decimals)), receiveCurrency);
}
bool checkIfInputMeetsMinOrMaxCondition(String input) {
final _enteredAmount = double.tryParse(input.sanitized()) ?? 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<void> calculateForcedProviderRate() async {
if (forcedProvider == null || depositCurrency == receiveCurrency) {
forcedProviderRate = 0.0;
return;
}
final amount =
double.tryParse(isFixedRateMode ? _receiveAmount.toString() : _depositAmount.toString()) ??
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);
bool _excludeProviderForReceiveExtraId(ExchangeProvider provider) =>
memoLabelTypeFor(receiveCurrency) != null && !provider.supportsMemoOrDestinationTag;
Future<void> calculateBestRate() async {
if (depositCurrency == receiveCurrency) {
bestRate = 0.0;
bestRateProvider = null;
return;
}
final amount =
double.tryParse(isFixedRateMode ? _receiveAmount.toString() : _depositAmount.toString()) ??
initialAmountByAssets(isFixedRateMode ? receiveCurrency : depositCurrency);
final validProvidersForAmount = _tradeAvailableProviders.where((provider) {
if (_excludeProviderForSwapAll(provider)) return false;
if (_excludeProviderForReceiveExtraId(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<double>(
_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, ExchangeProvider>((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<void> 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))
.where((provider) => !_excludeProviderForReceiveExtraId(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<Limits>()
.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<void> createTrade() async {
final depositAmountValue = _depositAmount ?? Money.zero(depositCurrency);
final receiveAmountValue = _receiveAmount ?? Money.zero(receiveCurrency);
if (depositAmountValue.isZero ||
depositAmountValue.isNegative ||
receiveAmountValue.isZero ||
receiveAmountValue.isNegative) {
ExchangeProviderLogger.logError(
provider: forcedProvider?.description ?? bestRateProvider?.description,
function: 'createTrade',
error: 'Invalid swap amount: deposit=$_depositAmount receive=$_receiveAmount',
requestData: {
'from': depositCurrency.title,
'to': receiveCurrency.title,
'fromAmount': _depositAmount,
'toAmount': _receiveAmount,
'isFixedRateMode': isFixedRateMode,
'forcedProvider': forcedProvider?.title,
'bestRateProvider': bestRateProvider?.title,
},
);
final invalidAmountError = depositAmountValue.isZero || depositAmountValue.isNegative
? '$depositAmountValue is not a valid amount for depositAmount'
: '$receiveAmountValue is not a valid amount for receiveAmount';
tradeState = TradeIsCreatedFailure(
title: S.current.trade_not_created,
error: invalidAmountError
);
return;
}
if (isSendAllEnabled) {
await calculateDepositAllAmount();
final amount = double.tryParse(_depositAmount.toString());
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 &&
wallet.type == WalletType.bitcoin &&
depositAddress == wallet.walletAddresses.addressForExchange) {
final invoice = await bitcoin!.getLightningInvoice(wallet, BigInt.zero);
if (invoice != null) {
depositAddress = invoice;
}
}
if (receiveCurrency == CryptoCurrency.btcln &&
wallet.type == WalletType.bitcoin &&
receiveAddress == wallet.walletAddresses.addressForExchange) {
final invoice = await bitcoin!.getLightningInvoice(wallet, BigInt.zero);
if (invoice != null) {
receiveAddress = invoice;
}
}
if (forcedProvider != null && _excludeProviderForReceiveExtraId(forcedProvider!)) {
tradeState = TradeIsCreatedFailure(
title: S.current.trade_not_created,
error: S.current.none_of_selected_providers_can_exchange,
);
return;
}
Map<double, ExchangeProvider> 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;
}
if (_excludeProviderForReceiveExtraId(provider)) {
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.toString(), isCanonical: true);
final request = TradeRequest(
fromCurrency: depositCurrency,
toCurrency: receiveCurrency,
fromAmount: _depositAmount.toString(),
toAmount: _receiveAmount.toString(),
refundAddress: depositAddress,
toAddress: receiveAddress,
toAddressExtraId: receiveAddressExtraId.trim(),
isFixedRate: isFixedRateMode,
);
if (hideAddressAfterExchange) {
wallet.walletAddresses.hiddenAddresses.add(depositAddress);
await wallet.walletAddresses.saveAddressesInBox();
}
var amount = isFixedRateMode ? _receiveAmount.toString() : _depositAmount.toString();
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;
if(trade.from == null) {
trade.from = depositCurrency;
}
if(trade.to == null) {
trade.to = receiveCurrency;
}
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 = null;
_receiveAmount = null;
depositAddress =
depositCurrency == wallet.currency ? wallet.walletAddresses.addressForExchange : '';
receiveAddress =
receiveCurrency == wallet.currency ? wallet.walletAddresses.addressForExchange : '';
receiveAddressExtraId = '';
isDepositAddressEnabled = !(depositCurrency == wallet.currency);
isFixedRateMode = false;
_onPairChange();
}
@action
void enableSendAllAmount() {
isSendAllEnabled = true;
isFixedRateMode = false;
calculateDepositAllAmount();
}
@action
void enableFixedRateMode() {
isSendAllEnabled = false;
isFixedRateMode = true;
}
@action
Future<void> 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) -
Money.fromInt(10, depositCurrency))
: await bitcoin!.estimateFakeSendAllTxAmount(
wallet,
priority,
coinTypeToSpendFrom: wallet.type == WalletType.litecoin
? UnspentCoinType.nonMweb
: depositCurrency == CryptoCurrency.btcln
? UnspentCoinType.lightning
: UnspentCoinType.any,
);
changeDepositAmount(amount: amount.toString(), 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.asDisplayString(balanceForCurrency.available);
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.available.amount;
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 = null;
_receiveAmount = null;
if (!hasAllAmount) isSendAllEnabled = false;
} else {
if (isFixedRateMode) {
_depositAmount = null;
} else {
_receiveAmount = null;
}
}
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.haven:
depositCurrency = CryptoCurrency.xhv;
receiveCurrency = CryptoCurrency.btc;
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.solana:
depositCurrency = CryptoCurrency.sol;
receiveCurrency = CryptoCurrency.xmr;
break;
case WalletType.tron:
depositCurrency = CryptoCurrency.trx;
receiveCurrency = CryptoCurrency.xmr;
break;
case WalletType.wownero:
depositCurrency = CryptoCurrency.wow;
receiveCurrency = CryptoCurrency.xmr;
break;
case WalletType.zano:
depositCurrency = CryptoCurrency.zano;
receiveCurrency = CryptoCurrency.xmr;
break;
case WalletType.decred:
depositCurrency = CryptoCurrency.dcr;
receiveCurrency = CryptoCurrency.xmr;
break;
case WalletType.zcash:
depositCurrency = CryptoCurrency.zec;
receiveCurrency = CryptoCurrency.xmr;
break;
case WalletType.none:
break;
}
}
String? _addressTypeValidation(String refundAddress, String receiveAddress) {
final isRefundAddressSP = RegExp(AddressValidator.silentPaymentAddressPatternMainnet).hasMatch(refundAddress);
if (isRefundAddressSP) return 'Silent Payment ${S.current.address_not_allowed_as_refund}';
final isReceiveAddressSP = RegExp(AddressValidator.silentPaymentAddressPatternMainnet).hasMatch(receiveAddress);
if (isReceiveAddressSP) return 'Silent Payment ${S.current.address_not_allowed_as_receive}';
final isRefundAddressMWEB = RegExp(AddressValidator.mWebAddressPattern).hasMatch(refundAddress);
if (isRefundAddressMWEB) return 'MWEB ${S.current.address_not_allowed_as_refund}';
final isReceiveAddressMWEB = RegExp(AddressValidator.mWebAddressPattern).hasMatch(receiveAddress);
if (isReceiveAddressMWEB) return 'MWEB ${S.current.address_not_allowed_as_receive}';
return null;
}
void _defineIsReceiveAmountEditable() {
/*if ((provider is ChangeNowExchangeProvider)
&&(depositCurrency == CryptoCurrency.xmr)
&&(receiveCurrency == CryptoCurrency.btc)) {
isReceiveAmountEditable = true;
} else {
isReceiveAmountEditable = false;
}*/
//isReceiveAmountEditable = false;
// isReceiveAmountEditable = selectedProviders.any((provider) => provider is ChangeNowExchangeProvider);
// isReceiveAmountEditable = provider is ChangeNowExchangeProvider || provider is SimpleSwapExchangeProvider;
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 = null;
_receiveAmount = null;
isFixedRateMode = false;
_defineIsReceiveAmountEditable();
bestRateProvider = null;
bestRate = 0.0;
loadLimits();
final Map<String, dynamic> exchangeProvidersSelection =
json.decode(sharedPreferences.getString(PreferencesKey.exchangeProvidersSelection) ?? "{}")
as Map<String, dynamic>;
for (var provider in providerList) {
exchangeProvidersSelection[provider.title] = selectedProviders.contains(provider);
}
sharedPreferences.setString(
PreferencesKey.exchangeProvidersSelection,
json.encode(exchangeProvidersSelection),
);
}
@action
Future<void> 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<CreateTradeResult> 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<bool> _isExternallyOwnedAccountAddress(String receivingAddress) async {
final normalizedReceiveCurrency = _normalizeReceiveCurrency(receiveCurrency);
final isEOAAddress = !(await _isContractAddress(normalizedReceiveCurrency, receivingAddress));
return isEOAAddress;
}
Future<bool> _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<String, dynamic>;
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<void> _injectUserEthTokensIntoCurrencyLists() async {
final tokens = await TokenUtilities.loadEvmTokensForSwap();
final toAddReceive = <CryptoCurrency>[];
final toAddDeposit = <CryptoCurrency>[];
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<CryptoCurrency> 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<CryptoCurrency> 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<void> _injectUserSplTokensIntoCurrencyLists() async {
final tokens = await TokenUtilities.loadSolTokensForSwap();
final toAddReceive = <CryptoCurrency>[];
final toAddDeposit = <CryptoCurrency>[];
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<CryptoCurrency> 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<void> _injectUserTronTokensIntoCurrencyLists() async {
final tokens = await TokenUtilities.loadTronTokensForSwap();
final toAddReceive = <CryptoCurrency>[];
final toAddDeposit = <CryptoCurrency>[];
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();
}
}