cake_wallet/lib/view_model/exchange/exchange_view_model.dart
Omar Hatem 36eacd8698
Wownero (#1485)
* fix: scanning issues

* fix: sync, storing silent unspents

* chore: deps

* fix: label issues, clear spent utxo

* chore: deps

* fix: build

* fix: missing types

* feat: new electrs API & changes, fixes for last block scanning

* feat: Scan Silent Payments homepage toggle

* chore: build configure

* feat: generic fixes, testnet UI improvements, useSSL on bitcoin nodes

* fix: invalid Object in sendData

* feat: improve addresses page & address book displays

* feat: silent payments labeled addresses disclaimer

* fix: missing i18n

* chore: print

* feat: single block scan, rescan by date working for btc mainnet

* feat: new cake features page replace market page, move sp scan toggle, auto switch node pop up alert

* feat: delete silent addresses

* fix: red dot in non ssl nodes

* fix: inconsistent connection states, fix tx history

* fix: tx & balance displays, cpfp sending

* feat: new rust lib

* chore: node path

* fix: check node based on network

* fix: missing txcount from addresses

* style: padding in feature page cards

* fix: restore not getting all wallet addresses by type

* fix: auto switch node broken

* fix: silent payment txs not being restored

* feat: change scanning to subscription model, sync improvements

* fix: scan re-subscription

* fix: default nodes

* fix: improve scanning by date, fix single block scan

* refactor: common function for input tx selection

* various fixes for build issues

* initial monero.dart implementation

* ...

* multiple wallets
new lib
minor fixes

* other fixes from monero.dart and monero_c

* fix: nodes & build

* update build scripts
fix polyseed

* remove unnecessary code

* Add windows app, build scripts and build guide for it.

* Minor fix in generated monero configs

* fix: send all with multiple outs

* add missing monero_c command

* add android build script

* Merge and fix main

* undo android ndk removal

* Fix modified exception_handler.dart

* Temporarily remove haven

* fix build issues

* fix pr script

* Fixes for build monero.dart (monero_c) for windows.

* monero build script

* wip: ios build script

* refactor: unchanged file

* Added build guides for iOS and macOS. Replaced nproc call on macOS. Added macOS configuration for configure_cake_wallet.sh script.

* Update monero.dart and monero_c versions.

* Add missed windows build scripts

* Update the application configuration for windows build script.

* Update cw_monero pubspec lock file for monero.dart

* Update pr_test_build.yml

* chore: upgrade

* chore: merge changes

* refactor: unchanged files [skip ci]

* Fix conflicts with main

* fix for multiple wallets

* Add tron to windows application configuration.

* Add macOS option for description message in configure_cake_wallet.sh

* Include missed monero dll for windows.

* fix conflicts with main

* Disable haven configuration for iOS as default. Add ability to configure cakewallet for iOS with  for configuration script. Remove cw_shared configuration for cw_monero.

* fix: scan fixes, add date, allow sending while scanning

* add missing nano secrets file [skip ci]

* ios library

* don't pull prebuilds android

* Add auto generation of manifest file for android project even for iOS, macOS, Windows.

* feat: sync fixes, sp settings

* feat: fix resyncing

* store crash fix

* make init async so it won't lag
disable print starts

* fix monero_c build issues

* libstdc++

* Fix MacOS saving wallet file issue
Fix Secure Storage issue (somehow)

* update pubspec.lock

* fix build script

* Use dylib as iOS framework. Use custom path for loading of iOS framework for monero.dart. Add script for generate iOS framework for monero wallet.

* fix: date from height logic, status disconnected & chain tip get

* fix: params

* feat: electrum migration if using cake electrum

* fix nodes
update versions

* re-enable tron

* update sp_scanner to work on iOS [skip ci]

* bump monero_c hash

* bump monero_c commit

* bump moneroc version

* bump monero_c commit

* Add ability to build monero wallet lib as universal lib. Update macOS build guide. Change default arch for macOS project to .

* fix: wrong socket for old electrum nodes

* Fix unchecked wallet type call

* get App Dir correctly in default_settings_migration.dart

* handle previous issue with fetching linux documents directory [skip ci]

* backup fix

* fix NTFS issues

* Close the wallet when the wallet gets changed

* fix: double balance

* feat: node domain

* fix: menu name

* bump monero_c commit

* fix: update tip on set scanning

* fix: connection switching back and forth

* feat: check if node is electrs, and supports sp

* chore: fix build

* minor enhancements

* fixes and enhancements

* solve conflicts with main

* Only stop wallet on rename and delete

* fix: status toggle

* minor enhancement

* Monero.com fixes

* bump monero_c commit

* update sp_scanner to include windows and linux

* Update macOS build guide. Change brew dependencies for build unbound locally.

* fix conflicts and update macos build guide

* remove build cache when on gh actions

* update secure storage

* free up even more storage

* free up more storage

* Add initial wownero

* fix conflicts

* fix workflow issue

* build wownero

* ios and windows changes

* macos

* complete wownero flow (app side)

* add keychain group entitlement and update script for RunnerBase on macos

* update secure_storage version to 8.1.0 in configure.dart

* add wownero framework

* update ios builds

* proper path for wownero and monero

* finalizing wownero

* finalizing wownero

* free up even more storage

* revert commenting of build gradle configs

* revert commenting of secrets [skip ci]

* free more storage

* minor fixes

* link android wownero libraries

* bump monero_c commit

* wownero fixes

* rename target

* build_single.sh using clean env

* bump monero_c commit

* minor fix

* Add wownero polyseed

* fix conflicts with main

* fix: wallet seed display
fix: wownero not refreshing

* fix: wallet seed display
fix: wownero not refreshing

* bump monero_c commit

* minor fixes

* fix: incorrectly displaying XMR instead of WOW

* fix: incorrect restore height in wownero

* bump monero_c commit

* Add Inno Setup Script for windows exe installer

* drop libc++_shared.so

* fixes from comments

* Fix CMake for windows

* Merge latest monero dart changes [skip ci]

* bump monero_c commit

* add wownero to build scripts for macos [skip ci]

* add 14 word seed support to wownero

* UI fixes for wownero seed restore

* minor fixes

* reformat code to pass lints

* wownero: fixes
haven: removal popup

* minor iOS fix [skip ci]

* fix: wownero confirmation count (it is spendable after 3 confirms)
fix: transaction history not displaying in WOW and XMR
when tx has 0 confirms,
This is more of a workaround, because I have no idea
why would the cpp code not return pending transaction.

* Update preferences_key.dart [skip ci]

* minor fixes

---------

Co-authored-by: Rafael Saes <git@rafael.saes.dev>
Co-authored-by: Czarek Nakamoto <cyjan@mrcyjanek.net>
Co-authored-by: M <m@cakewallet.com>
Co-authored-by: Konstantin Ullrich <konstantinullrich12@gmail.com>
Co-authored-by: Matthew Fosse <matt@fosse.co>
2024-07-04 22:43:17 +03:00

897 lines
29 KiB
Dart

import 'dart:async';
import 'dart:collection';
import 'dart:convert';
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:cake_wallet/core/create_trade_result.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/sync_status.dart';
import 'package:cw_core/transaction_priority.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:hive/hive.dart';
import 'package:http/http.dart' as http;
import 'package:intl/intl.dart';
import 'package:mobx/mobx.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:cake_wallet/.secrets.g.dart' as secrets;
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart';
import 'package:cake_wallet/core/wallet_change_listener_view_model.dart';
import 'package:cake_wallet/entities/exchange_api_mode.dart';
import 'package:cake_wallet/entities/preferences_key.dart';
import 'package:cake_wallet/entities/wallet_contact.dart';
import 'package:cake_wallet/ethereum/ethereum.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/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/quantex_exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/sideshift_exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/simpleswap_exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/thorchain_exchange.provider.dart';
import 'package:cake_wallet/exchange/provider/trocador_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/monero/monero.dart';
import 'package:cake_wallet/polygon/polygon.dart';
import 'package:cake_wallet/store/app_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/utils/feature_flag.dart';
import 'package:cake_wallet/view_model/contact_list/contact_list_view_model.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,
) : _cryptoNumberFormat = NumberFormat(),
isSendAllEnabled = false,
isFixedRateMode = false,
isReceiveAmountEntered = false,
depositAmount = '',
receiveAmount = '',
receiveAddress = '',
depositAddress = '',
isDepositAddressEnabled = false,
isReceiveAmountEditable = false,
_useTorOnly = false,
receiveCurrencies = <CryptoCurrency>[],
depositCurrencies = <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();
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();
_calculateBestRate();
bestRateSync = Timer.periodic(Duration(seconds: 10), (timer) => _calculateBestRate());
isDepositAddressEnabled = !(depositCurrency == wallet.currency);
depositAmount = '';
receiveAmount = '';
receiveAddress = '';
depositAddress = depositCurrency == wallet.currency ? wallet.walletAddresses.address : '';
provider = providersForCurrentPair().first;
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();
depositCurrencies = CryptoCurrency.all
.where((cryptoCurrency) => !excludeDepositCurrencies.contains(cryptoCurrency))
.toList();
_defineIsReceiveAmountEditable();
loadLimits();
reaction((_) => isFixedRateMode, (Object _) {
loadLimits();
_bestRate = 0;
_calculateBestRate();
});
}
bool _useTorOnly;
final Box<Trade> trades;
final ExchangeTemplateStore _exchangeTemplateStore;
final TradesStore tradesStore;
final SharedPreferences sharedPreferences;
List<ExchangeProvider> get _allProviders => [
ChangeNowExchangeProvider(settingsStore: _settingsStore),
SideShiftExchangeProvider(),
SimpleSwapExchangeProvider(),
ThorChainExchangeProvider(tradesStore: trades),
if (FeatureFlag.isExolixEnabled) ExolixExchangeProvider(),
QuantexExchangeProvider(),
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 = [];
@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.priority[wallet.type];
if (priority == null) {
throw Exception('Unexpected type ${wallet.type.toString()}');
}
return priority;
}
bool get hasAllAmount =>
(wallet.type == WalletType.bitcoin ||
wallet.type == WalletType.litecoin ||
wallet.type == WalletType.bitcoinCash) &&
depositCurrency == wallet.currency;
bool get isMoneroWallet => wallet.type == WalletType.monero;
bool get isLowFee {
switch (wallet.type) {
case WalletType.monero:
case WalletType.wownero:
case WalletType.haven:
return transactionPriority == monero!.getMoneroTransactionPrioritySlow();
case WalletType.bitcoin:
return transactionPriority == bitcoin!.getBitcoinTransactionPrioritySlow();
case WalletType.litecoin:
return transactionPriority == bitcoin!.getLitecoinTransactionPrioritySlow();
case WalletType.ethereum:
return transactionPriority == ethereum!.getEthereumTransactionPrioritySlow();
case WalletType.bitcoinCash:
return transactionPriority == bitcoinCash!.getBitcoinCashTransactionPrioritySlow();
case WalletType.polygon:
return transactionPriority == polygon!.getPolygonTransactionPrioritySlow();
default:
return false;
}
}
List<CryptoCurrency> receiveCurrencies;
List<CryptoCurrency> depositCurrencies;
final NumberFormat _cryptoNumberFormat;
final SettingsStore _settingsStore;
final ContactListViewModel contactListViewModel;
double _bestRate = 0.0;
late Timer bestRateSync;
@action
void changeDepositCurrency({required CryptoCurrency currency}) {
depositCurrency = currency;
isFixedRateMode = false;
_onPairChange();
isDepositAddressEnabled = !(depositCurrency == wallet.currency);
}
@action
void changeReceiveCurrency({required CryptoCurrency currency}) {
receiveCurrency = currency;
isFixedRateMode = false;
_onPairChange();
isDepositAddressEnabled = !(depositCurrency == wallet.currency);
}
@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
Future<void> changeDepositAmount({required String amount}) async {
depositAmount = amount;
if (amount.isEmpty) {
depositAmount = '';
receiveAmount = '';
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 {
final amount = double.tryParse(isFixedRateMode ? receiveAmount : depositAmount) ?? 1;
final _providers = _tradeAvailableProviders
.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)));
_sortedAvailableProviders.clear();
for (int i = 0; i < result.length; i++) {
if (result[i] != 0) {
/// add this provider as its valid for this trade
try {
_sortedAvailableProviders[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
}
}
}
if (_sortedAvailableProviders.isNotEmpty) _bestRate = _sortedAvailableProviders.keys.first;
}
@action
Future<void> loadLimits() async {
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 {
for (var provider in selectedProviders) {
/// if this provider is not valid for the current pair, skip it
if (!providersForCurrentPair().contains(provider)) continue;
try {
final tempLimits =
await provider.fetchLimits(from: from, to: to, isFixedRateMode: isFixedRateMode);
if (lowestMin != null && (tempLimits.min ?? -1) < lowestMin) lowestMin = tempLimits.min;
if (highestMax != null && (tempLimits.max ?? double.maxFinite) > highestMax)
highestMax = tempLimits.max;
} catch (e) {
continue;
}
}
} 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');
}
}
@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;
}
}
try {
for (var provider in _sortedAvailableProviders.values) {
if (!(await provider.checkIsAvailable())) continue;
final request = TradeRequest(
fromCurrency: depositCurrency,
toCurrency: receiveCurrency,
fromAmount: depositAmount.replaceAll(',', '.'),
toAmount: receiveAmount.replaceAll(',', '.'),
refundAddress: depositAddress,
toAddress: receiveAddress,
isFixedRate: isFixedRateMode);
var amount = isFixedRateMode ? receiveAmount : depositAmount;
amount = amount.replaceAll(',', '.');
if (limitsState is LimitsLoadedSuccessfully) {
if (double.tryParse(amount) == null) continue;
if (limits.max != 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.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.address : '';
receiveAddress = receiveCurrency == wallet.currency ? wallet.walletAddresses.address : '';
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 (wallet.type == WalletType.litecoin ||
wallet.type == WalletType.bitcoin ||
wallet.type == WalletType.bitcoinCash) {
final priority = _settingsStore.priority[wallet.type]!;
final amount = await bitcoin!.estimateFakeSendAllTxAmount(wallet, priority);
changeDepositAmount(amount: bitcoin!.formatterBitcoinAmountToString(amount: amount));
}
}
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);
List<ExchangeProvider> providersForCurrentPair() =>
_providersForPair(from: depositCurrency, to: receiveCurrency);
List<ExchangeProvider> _providersForPair(
{required CryptoCurrency from, required CryptoCurrency to}) =>
providerList
.where((provider) =>
provider.pairList.where((pair) => pair.from == from && pair.to == to).isNotEmpty)
.toList();
void _onPairChange() {
depositAmount = '';
receiveAmount = '';
loadLimits();
_setAvailableProviders();
_bestRate = 0;
_calculateBestRate();
}
void _initialPairBasedOnWallet() {
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.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.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.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 (providersForCurrentPair().contains(provider)) _tradeAvailableProviders.add(provider);
}
@action
void removeExchangeProvider(ExchangeProvider provider) {
selectedProviders.remove(provider);
_tradeAvailableProviders.remove(provider);
}
@action
void saveSelectedProviders() {
depositAmount = '';
receiveAmount = '';
isFixedRateMode = false;
_defineIsReceiveAmountEditable();
loadLimits();
_bestRate = 0;
_calculateBestRate();
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),
);
}
bool get isAvailableInSelected {
final providersForPair = providersForCurrentPair();
return selectedProviders
.any((element) => element.isAvailable && providersForPair.contains(element));
}
void _setAvailableProviders() {
_tradeAvailableProviders.clear();
_tradeAvailableProviders.addAll(
selectedProviders.where((provider) => providersForCurrentPair().contains(provider)));
}
@action
void setDefaultTransactionPriority() {
switch (wallet.type) {
case WalletType.monero:
case WalletType.haven:
case WalletType.wownero:
_settingsStore.priority[wallet.type] = monero!.getMoneroTransactionPriorityAutomatic();
break;
case WalletType.bitcoin:
_settingsStore.priority[wallet.type] = bitcoin!.getBitcoinTransactionPriorityMedium();
break;
case WalletType.litecoin:
_settingsStore.priority[wallet.type] = bitcoin!.getLitecoinTransactionPriorityMedium();
break;
case WalletType.ethereum:
_settingsStore.priority[wallet.type] = ethereum!.getDefaultTransactionPriority();
break;
case WalletType.bitcoinCash:
_settingsStore.priority[wallet.type] = bitcoinCash!.getDefaultTransactionPriority();
break;
case WalletType.polygon:
_settingsStore.priority[wallet.type] = polygon!.getDefaultTransactionPriority();
break;
default:
break;
}
}
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,
);
}
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 httpClient = http.Client();
final uri = Uri.https(
'deep-index.moralis.io',
'/api/v2.2/erc20/metadata',
{
"chain": chainName,
"addresses": contractAddress,
},
);
try {
final response = await httpClient.get(
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) {
print(e);
return false;
}
}
}