mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-17 05:41:29 +02:00
* feat(zcash): initial commit - initial work on cw_zcash - use single .db for everything zcash to speed up sync and scanning - implement silent autoshielding of non-orchard funds - use FFI for everything to fetch the data - add support for passphrase by adding extra word to seed phrase - enforce lints on CI for cw_zcash directory - use correct zcash name, and call transparent zcash tZEC - minor cleanups outsite of cw_zcash - gitmodules, gitignore - db debug - fix broken addressPageType in WalletInfo - enable debug options in kDebugMode and kProfileMode * feat: T address rotation (and cache) wip: minor fixes * wip: shield tx list * fix fiat amounts fix derrivation of rotation account addresses screen for zcash block height fix T address shields improvements Amount in disposable T shields UI improvements show pending outgoing tx in tx history * fix null check on passphrase page fix tx showing as pending rename auto shield txs fixed send all to include fee use rotating address in addressForExchange wrap all calls of WarpApi.getLatestHeight in try catch to prevent leaking errors other minor fixes from notion * wallet group debug cache sent tx address wallet T address rotation fixes automatically pick the fee for auto-shield * fix T address cache deserialization fix autoshield icon fix autoshield text getCachedDestinationAddress fix send address cache * address comments from review * wip: iOS fix * fix swap * Allow cakepay payment from zcash wallet * fix: ios (I'm stupid) fix: reorder addresses to make orchard default fix: send all balance * 5.6.6 bump fix: ios apple review add transport error to ignored exceptions * fix: leaving dust behind when sweeping all * Change wallets order * fix: reflect balance of pending out txs fix: restore height support * linux support (disabled) * fix: date picker for zcash on restore screen fix: build script for linux match WarpApi.warpSync args with upstream * fix: linter script * ensure all exchange providers support ZEC (#2802) * ensure all exchange providers support ZEC * remove zaddr and tzec currencies and refactor providers * Update lib/exchange/provider/near_Intents_exchange_provider.dart * add back old zcash currencies for backward compatibility, thanks Czarek * minor fix [skip ci] --------- Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com> * Update android/app/src/main/AndroidManifestBase.xml [skip ci] * Update ios/Runner/InfoBase.plist [skip ci] * fix: remove unused code fix: macos fix: disable on linux and macos * update build numbers [skip ci] * Add zcash to birdpay --------- Co-authored-by: Czarek Nakamoto <cyjan@mrcyjanek.net> Co-authored-by: Serhii <borodenko.sv@gmail.com>
1217 lines
40 KiB
Dart
1217 lines
40 KiB
Dart
import 'dart:async';
|
|
import 'dart:collection';
|
|
import 'dart:convert';
|
|
import 'dart:developer';
|
|
|
|
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/create_trade_result.dart';
|
|
import 'package:cake_wallet/core/fiat_conversion_service.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/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/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/store/app_store.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/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/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:hive/hive.dart';
|
|
import 'package:intl/intl.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) {
|
|
receiveCurrency = wallet.currency;
|
|
depositCurrency = wallet.currency;
|
|
}
|
|
|
|
ExchangeViewModelBase(
|
|
AppStore appStore,
|
|
this.trades,
|
|
this._exchangeTemplateStore,
|
|
this.tradesStore,
|
|
this._settingsStore,
|
|
this.sharedPreferences,
|
|
this.contactListViewModel,
|
|
this.unspentCoinsListViewModel,
|
|
this.feesViewModel,
|
|
this.fiatConversionStore,
|
|
) : _cryptoNumberFormat = NumberFormat(),
|
|
isSendAllEnabled = false,
|
|
isFixedRateMode = false,
|
|
isReceiveAmountEntered = false,
|
|
depositAmount = '',
|
|
receiveAmount = '',
|
|
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.xlm,
|
|
CryptoCurrency.xrp,
|
|
CryptoCurrency.bnb,
|
|
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) => exchangeProvidersSelection[element.title] == null
|
|
? element.isEnabled
|
|
: (exchangeProvidersSelection[element.title] as bool))
|
|
.toList());
|
|
|
|
_setAvailableProviders();
|
|
|
|
autorun((_) {
|
|
if (selectedProviders.any((provider) => provider is TrocadorExchangeProvider)) {
|
|
final trocadorProvider =
|
|
selectedProviders.firstWhere((provider) => provider is TrocadorExchangeProvider)
|
|
as TrocadorExchangeProvider;
|
|
|
|
updateAllTrocadorProviderStates(trocadorProvider);
|
|
}
|
|
});
|
|
|
|
bestRateSync = Timer.periodic(Duration(seconds: 10), (timer) {
|
|
if (tradeState is! TradeIsCreating) {
|
|
calculateBestRate();
|
|
}
|
|
});
|
|
|
|
isDepositAddressEnabled = !useSameWalletAddress(depositCurrency);
|
|
depositAmount = '';
|
|
receiveAmount = '';
|
|
receiveAddress = '';
|
|
depositAddress =
|
|
useSameWalletAddress(depositCurrency) ? wallet.walletAddresses.addressForExchange : '';
|
|
|
|
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();
|
|
}
|
|
});
|
|
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();
|
|
reaction((_) => isFixedRateMode, (Object _) {
|
|
bestRate = 0.0;
|
|
loadLimits();
|
|
});
|
|
|
|
if (isElectrumWallet) {
|
|
bitcoin!.updateFeeRates(wallet);
|
|
}
|
|
reaction((_) => wallet.currency, (_) async {
|
|
// When currency changes (e.g., EVM chain switch), update currencies
|
|
await Future.delayed(const Duration(milliseconds: 100));
|
|
receiveCurrency = wallet.currency;
|
|
depositCurrency = wallet.currency;
|
|
|
|
// Only refresh ETH tokens for EVM wallets
|
|
if (isEVMCompatibleChain(wallet.type)) {
|
|
_injectUserEthTokensIntoCurrencyLists();
|
|
}
|
|
});
|
|
}
|
|
|
|
bool useSameWalletAddress(CryptoCurrency currency) =>
|
|
currency == wallet.currency ||
|
|
(currency.tag != null && currency.tag == wallet.currency.tag) ||
|
|
currency.tag == wallet.currency.title;
|
|
|
|
bool get isElectrumWallet => [
|
|
WalletType.bitcoin,
|
|
WalletType.litecoin,
|
|
WalletType.bitcoinCash,
|
|
WalletType.dogecoin
|
|
].contains(wallet.type);
|
|
|
|
bool get hideAddressAfterExchange =>
|
|
[WalletType.monero, WalletType.wownero].contains(wallet.type);
|
|
|
|
bool _useTorOnly;
|
|
final Box<Trade> trades;
|
|
final ExchangeTemplateStore _exchangeTemplateStore;
|
|
final TradesStore tradesStore;
|
|
final SharedPreferences sharedPreferences;
|
|
|
|
List<ExchangeProvider> get _allProviders => [
|
|
ChangeNowExchangeProvider(settingsStore: _settingsStore),
|
|
// SideShiftExchangeProvider(),
|
|
ChainflipExchangeProvider(tradesStore: trades),
|
|
if (FeatureFlag.isExolixEnabled) ExolixExchangeProvider(),
|
|
SwapTradeExchangeProvider(),
|
|
LetsExchangeExchangeProvider(),
|
|
StealthExExchangeProvider(),
|
|
XOSwapExchangeProvider(),
|
|
// SwapsXyzExchangeProvider(),
|
|
NearIntentsExchangeProvider(),
|
|
TrocadorExchangeProvider(
|
|
useTorOnly: _useTorOnly, providerStates: _settingsStore.trocadorProviderStates),
|
|
];
|
|
|
|
@observable
|
|
ExchangeProvider? provider;
|
|
|
|
/// 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
|
|
String depositAmount;
|
|
|
|
@observable
|
|
String receiveAmount;
|
|
|
|
@observable
|
|
String depositAddress;
|
|
|
|
@observable
|
|
String receiveAddress;
|
|
|
|
@observable
|
|
bool isDepositAddressEnabled;
|
|
|
|
@observable
|
|
bool isReceiveAmountEntered;
|
|
|
|
@observable
|
|
bool isReceiveAmountEditable;
|
|
|
|
@observable
|
|
bool isFixedRateMode;
|
|
|
|
@observable
|
|
bool isSendAllEnabled;
|
|
|
|
@observable
|
|
Limits limits;
|
|
|
|
@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();
|
|
|
|
@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;
|
|
|
|
//* 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
|
|
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 =>
|
|
[
|
|
WalletType.monero,
|
|
WalletType.bitcoin,
|
|
WalletType.litecoin,
|
|
WalletType.bitcoinCash,
|
|
WalletType.dogecoin,
|
|
].contains(wallet.type) &&
|
|
depositCurrency == wallet.currency;
|
|
|
|
bool get isMoneroWallet => wallet.type == WalletType.monero;
|
|
|
|
@observable
|
|
ObservableList<CryptoCurrency> receiveCurrencies;
|
|
|
|
@observable
|
|
ObservableList<CryptoCurrency> depositCurrencies;
|
|
|
|
final NumberFormat _cryptoNumberFormat;
|
|
|
|
final SettingsStore _settingsStore;
|
|
|
|
final ContactListViewModel contactListViewModel;
|
|
|
|
final UnspentCoinsListViewModel unspentCoinsListViewModel;
|
|
|
|
final FeesViewModel feesViewModel;
|
|
|
|
@observable
|
|
double bestRate = 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.isNotEmpty) {
|
|
if (fiatConversionStore.prices[receiveCurrency] == null) return '';
|
|
|
|
amount = calculateFiatAmount(
|
|
price: fiatConversionStore.prices[receiveCurrency]!,
|
|
cryptoAmount: receiveAmount,
|
|
);
|
|
}
|
|
} catch (_) {
|
|
log('Error calculating receive amount fiat formatted: $_');
|
|
}
|
|
return isFiatDisabled ? '' : '$amount';
|
|
}
|
|
|
|
@action
|
|
void changeDepositCurrency({required CryptoCurrency currency}) {
|
|
depositCurrency = currency;
|
|
isFixedRateMode = false;
|
|
_onPairChange();
|
|
isDepositAddressEnabled = !useSameWalletAddress(depositCurrency);
|
|
}
|
|
|
|
@action
|
|
void changeReceiveCurrency({required CryptoCurrency currency}) {
|
|
receiveCurrency = currency;
|
|
isFixedRateMode = false;
|
|
_onPairChange();
|
|
isDepositAddressEnabled = !useSameWalletAddress(depositCurrency);
|
|
}
|
|
|
|
@action
|
|
Future<void> changeReceiveAmount({required String amount}) async {
|
|
receiveAmount = amount;
|
|
|
|
if (amount.isEmpty) {
|
|
depositAmount = '';
|
|
receiveAmount = '';
|
|
return;
|
|
}
|
|
|
|
final _enteredAmount = double.tryParse(amount.replaceAll(',', '.')) ?? 0;
|
|
|
|
if (bestRate == 0) {
|
|
depositAmount = S.current.fetching;
|
|
|
|
await calculateBestRate();
|
|
}
|
|
|
|
_cryptoNumberFormat.maximumFractionDigits = depositMaxDigits;
|
|
|
|
depositAmount = _cryptoNumberFormat
|
|
.format(_enteredAmount / bestRate)
|
|
.toString()
|
|
.replaceAll(RegExp('\\,'), '');
|
|
}
|
|
|
|
@action
|
|
void setReceiveAmountFromFiat({required String fiatAmount}) {
|
|
final _enteredAmount = double.tryParse(fiatAmount.replaceAll(',', '.')) ?? 0.0;
|
|
final price = fiatConversionStore.prices[receiveCurrency];
|
|
if (price == null || price == 0.0) return;
|
|
|
|
final crypto = _enteredAmount / price;
|
|
final receiveAmountTmp = _cryptoNumberFormat.format(crypto);
|
|
if (receiveAmount != receiveAmountTmp) {
|
|
changeReceiveAmount(amount: receiveAmountTmp.withMaxDecimals(receiveCurrency.decimals));
|
|
}
|
|
}
|
|
|
|
@action
|
|
Future<void> changeDepositAmount({required String amount}) async {
|
|
depositAmount = amount;
|
|
|
|
if (amount.isEmpty) {
|
|
depositAmount = '';
|
|
receiveAmount = '';
|
|
return;
|
|
}
|
|
|
|
/// For fixed-rate transactions, we don't want to recalculate receive amount
|
|
/// as it should remain exactly what the user set
|
|
if (isFixedRateMode) return;
|
|
|
|
final _enteredAmount = double.tryParse(amount.replaceAll(',', '.')) ?? 0;
|
|
|
|
/// in case the best rate was not calculated yet
|
|
if (bestRate == 0) {
|
|
receiveAmount = S.current.fetching;
|
|
|
|
await calculateBestRate();
|
|
}
|
|
|
|
_cryptoNumberFormat.maximumFractionDigits = receiveMaxDigits;
|
|
|
|
receiveAmount = _cryptoNumberFormat
|
|
.format(bestRate * _enteredAmount)
|
|
.toString()
|
|
.replaceAll(RegExp('\\,'), '');
|
|
}
|
|
|
|
bool checkIfInputMeetsMinOrMaxCondition(String input) {
|
|
final _enteredAmount = double.tryParse(input.replaceAll(',', '.')) ?? 0;
|
|
double minLimit = limits.min ?? 0;
|
|
double? maxLimit = limits.max;
|
|
|
|
if (_enteredAmount < minLimit) return false;
|
|
|
|
if (maxLimit != null && _enteredAmount > maxLimit) return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
Future<void> calculateBestRate() async {
|
|
if (depositCurrency == receiveCurrency) {
|
|
bestRate = 0.0;
|
|
return;
|
|
}
|
|
final amount = double.tryParse(isFixedRateMode ? receiveAmount : depositAmount) ??
|
|
initialAmountByAssets(isFixedRateMode ? receiveCurrency : depositCurrency);
|
|
|
|
final validProvidersForAmount = _tradeAvailableProviders.where((provider) {
|
|
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
|
|
}
|
|
}
|
|
}
|
|
|
|
// Replace the old map with the new one
|
|
_sortedAvailableProviders.clear();
|
|
_sortedAvailableProviders.addAll(newSortedProviders);
|
|
|
|
if (_sortedAvailableProviders.isNotEmpty) bestRate = _sortedAvailableProviders.keys.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))
|
|
.map((provider) async {
|
|
final limits = await provider
|
|
.fetchLimits(
|
|
from: from,
|
|
to: to,
|
|
isFixedRateMode: isFixedRateMode,
|
|
)
|
|
.onError((error, stackTrace) => Limits(max: 0.0, min: double.maxFinite))
|
|
.timeout(
|
|
Duration(seconds: 7),
|
|
onTimeout: () => Limits(max: 0.0, min: double.maxFinite),
|
|
);
|
|
return MapEntry(provider, limits);
|
|
}).toList();
|
|
|
|
final entries = await Future.wait(futures);
|
|
_providerLimits = Map.fromEntries(entries);
|
|
|
|
_providerLimits.values.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 {
|
|
if (isSendAllEnabled) {
|
|
await calculateDepositAllAmount();
|
|
final amount = double.tryParse(depositAmount);
|
|
|
|
if (limits.min != null && amount != null && amount < limits.min!) {
|
|
tradeState = TradeIsCreatedFailure(
|
|
title: S.current.trade_not_created,
|
|
error: S.current.amount_is_below_minimum_limit(limits.min!.toString()));
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Ensure we have providers available before attempting to create trade
|
|
if (_sortedAvailableProviders.isEmpty) {
|
|
await calculateBestRate();
|
|
|
|
if (_sortedAvailableProviders.isEmpty) {
|
|
tradeState = TradeIsCreatedFailure(
|
|
title: S.current.trade_not_created,
|
|
error: S.current.none_of_selected_providers_can_exchange);
|
|
return;
|
|
}
|
|
}
|
|
|
|
try {
|
|
// snapshot of providers to avoid concurrent modification issues
|
|
final providersSnapshot = _sortedAvailableProviders.values.toList();
|
|
final ratesSnapshot = _sortedAvailableProviders.keys.toList();
|
|
|
|
for (var i = 0; i < providersSnapshot.length; i++) {
|
|
final provider = providersSnapshot[i];
|
|
final providerRate = ratesSnapshot[i];
|
|
|
|
if (!(await provider.checkIsAvailable())) continue;
|
|
|
|
bestRate = providerRate;
|
|
await changeDepositAmount(amount: depositAmount);
|
|
|
|
final request = TradeRequest(
|
|
fromCurrency: depositCurrency,
|
|
toCurrency: receiveCurrency,
|
|
fromAmount: depositAmount.replaceAll(',', '.'),
|
|
toAmount: receiveAmount.replaceAll(',', '.'),
|
|
refundAddress: depositAddress,
|
|
toAddress: receiveAddress,
|
|
isFixedRate: isFixedRateMode,
|
|
);
|
|
|
|
if (hideAddressAfterExchange) {
|
|
wallet.walletAddresses.hiddenAddresses.add(depositAddress);
|
|
await wallet.walletAddresses.saveAddressesInBox();
|
|
}
|
|
|
|
var amount = isFixedRateMode ? receiveAmount : depositAmount;
|
|
amount = amount.replaceAll(',', '.');
|
|
|
|
if (limitsState is LimitsLoadedSuccessfully) {
|
|
if (double.tryParse(amount) == null) continue;
|
|
|
|
if (limits.min != null && double.parse(amount) < limits.min!) {
|
|
continue;
|
|
} else if (limits.max != null && double.parse(amount) > limits.max!) {
|
|
continue;
|
|
} else {
|
|
try {
|
|
tradeState = TradeIsCreating();
|
|
final trade = await provider.createTrade(
|
|
request: request,
|
|
isFixedRateMode: isFixedRateMode,
|
|
isSendAll: isSendAllEnabled,
|
|
);
|
|
trade.walletId = wallet.id;
|
|
trade.chainId = wallet.chainId;
|
|
trade.fromWalletAddress = wallet.walletAddresses.address;
|
|
|
|
final canCreateTrade = await isCanCreateTrade(trade);
|
|
if (!canCreateTrade.result) {
|
|
tradeState = TradeIsCreatedFailure(
|
|
title: S.current.trade_not_created,
|
|
error: canCreateTrade.errorMessage ?? '',
|
|
);
|
|
return;
|
|
}
|
|
|
|
tradesStore.setTrade(trade);
|
|
if (trade.provider != ExchangeProviderDescription.thorChain) await trades.add(trade);
|
|
tradeState = TradeIsCreatedSuccessfully(trade: trade);
|
|
|
|
/// return after the first successful trade
|
|
return;
|
|
} catch (e) {
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// if the code reached here then none of the providers succeeded
|
|
tradeState = TradeIsCreatedFailure(
|
|
title: S.current.trade_not_created,
|
|
error: S.current.none_of_selected_providers_can_exchange);
|
|
} on ConcurrentModificationError {
|
|
/// if create trade happened at the exact same time of the scheduled rate update
|
|
/// then delay the create trade a bit and try again
|
|
///
|
|
/// this is because the limitation of the SplayTreeMap that
|
|
/// you can't modify it while iterating through it
|
|
Future.delayed(Duration(milliseconds: 200), createTrade);
|
|
}
|
|
}
|
|
|
|
@action
|
|
void reset() {
|
|
_initialPairBasedOnWallet();
|
|
isReceiveAmountEntered = false;
|
|
depositAmount = '';
|
|
receiveAmount = '';
|
|
depositAddress =
|
|
depositCurrency == wallet.currency ? wallet.walletAddresses.addressForExchange : '';
|
|
receiveAddress =
|
|
receiveCurrency == wallet.currency ? wallet.walletAddresses.addressForExchange : '';
|
|
isDepositAddressEnabled = !(depositCurrency == wallet.currency);
|
|
isFixedRateMode = false;
|
|
_onPairChange();
|
|
}
|
|
|
|
@action
|
|
void enableSendAllAmount() {
|
|
isSendAllEnabled = true;
|
|
isFixedRateMode = false;
|
|
calculateDepositAllAmount();
|
|
}
|
|
|
|
@action
|
|
void enableFixedRateMode() {
|
|
isSendAllEnabled = false;
|
|
isFixedRateMode = true;
|
|
}
|
|
|
|
@action
|
|
Future<void> calculateDepositAllAmount() async {
|
|
if ([
|
|
WalletType.litecoin,
|
|
WalletType.bitcoin,
|
|
WalletType.bitcoinCash,
|
|
WalletType.dogecoin,
|
|
].contains(wallet.type)) {
|
|
final priority = _settingsStore.getPriority(wallet.type)!;
|
|
|
|
final amount = await bitcoin!.estimateFakeSendAllTxAmount(
|
|
wallet,
|
|
priority,
|
|
coinTypeToSpendFrom:
|
|
wallet.type == WalletType.litecoin ? UnspentCoinType.nonMweb : UnspentCoinType.any,
|
|
);
|
|
|
|
changeDepositAmount(amount: wallet.formatCryptoAmount(amount.toString()));
|
|
} else if (wallet.type == WalletType.monero) {
|
|
final amount = await unspentCoinsListViewModel.getSendingBalance(UnspentCoinType.any);
|
|
|
|
changeDepositAmount(amount: wallet.formatCryptoAmount(amount.toString()));
|
|
}
|
|
}
|
|
|
|
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() {
|
|
depositAmount = '';
|
|
receiveAmount = '';
|
|
bestRate = 0.0;
|
|
loadLimits();
|
|
_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.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;
|
|
}
|
|
}
|
|
|
|
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) {
|
|
selectedProviders.add(provider);
|
|
if (providerList.contains(provider)) _tradeAvailableProviders.add(provider);
|
|
}
|
|
|
|
@action
|
|
void removeExchangeProvider(ExchangeProvider provider) {
|
|
selectedProviders.remove(provider);
|
|
_tradeAvailableProviders.remove(provider);
|
|
}
|
|
|
|
@action
|
|
void saveSelectedProviders() {
|
|
depositAmount = '';
|
|
receiveAmount = '';
|
|
isFixedRateMode = false;
|
|
_defineIsReceiveAmountEditable();
|
|
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));
|
|
}
|
|
|
|
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.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 userTokens = await TokenUtilities.loadAllUniqueEvmTokens();
|
|
|
|
final toAddReceive = <CryptoCurrency>[];
|
|
final toAddDeposit = <CryptoCurrency>[];
|
|
|
|
for (final token in userTokens) {
|
|
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 userTokens = await TokenUtilities.loadAllUniqueSolTokens();
|
|
|
|
final toAddReceive = <CryptoCurrency>[];
|
|
final toAddDeposit = <CryptoCurrency>[];
|
|
|
|
for (final token in userTokens) {
|
|
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 userTokens = await TokenUtilities.loadAllUniqueTronTokens();
|
|
|
|
final toAddReceive = <CryptoCurrency>[];
|
|
final toAddDeposit = <CryptoCurrency>[];
|
|
|
|
for (final token in userTokens) {
|
|
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();
|
|
}
|
|
}
|