cake_wallet/lib/view_model/buy/buy_sell_view_model.dart
Serhii 02f53055b1
Cw 657 new buy sell flow (#1553)
* init commit

* onramper

* moonPay

* dfx provider

* meld

* dfx payment methods

* fiat buy credentials

* moonpay payment method

* payment loading state

* dfx sell quote

* onramper launch trade

* meld launch trade

* country picker

* update option tile

* buy/sell action

* meld refactor

* update pr_test_build.yml

* ui fixes

* revert country picker commit

* update the ui

* recommended providers

* payment method [skip ci]

* provider option tile

* remove buy action

* minor fixes

* update the best rate when the amount is changed

* fixes for currency title

* fix icons

* code refactoring

* null issue

* code review fixes

* Update pr_test_build_linux.yml

* Update meld_buy_provider.dart

* Update meld_buy_provider.dart

* add show wallets action

* remove default sell / buy provider setting

* localisation

* providerTypes

* icons

* remove duplicate file [skip ci]

* minor changes [skip ci]

* fixes from review

* disable dfx for non eur/chf currencies
fix providers to be fetched with the selected currency

* fix breaking from loop if one element failed

* fix minor naming issue from merging conflicts

* add fiat check for moonpay

* fix address validation

* merge conflict

* fix destination and source currency

* minor fix

* minor fix

* update the flow

* fix bch address format

* fix wallet addresses

* fix initial fetching amount.

* Update address_validator.dart

* review comments

* revert switch case to return null

* minor fix

---------

Co-authored-by: OmarHatem <omarh.ismail1@gmail.com>
2024-11-09 21:00:56 +02:00

446 lines
13 KiB
Dart

import 'dart:async';
import 'package:cake_wallet/buy/buy_provider.dart';
import 'package:cake_wallet/buy/buy_quote.dart';
import 'package:cake_wallet/buy/onramper/onramper_buy_provider.dart';
import 'package:cake_wallet/buy/payment_method.dart';
import 'package:cake_wallet/buy/sell_buy_states.dart';
import 'package:cake_wallet/core/selectable_option.dart';
import 'package:cake_wallet/core/wallet_change_listener_view_model.dart';
import 'package:cake_wallet/entities/fiat_currency.dart';
import 'package:cake_wallet/entities/provider_types.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/store/app_store.dart';
import 'package:cake_wallet/store/settings_store.dart';
import 'package:cake_wallet/themes/theme_base.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/currency_for_wallet_type.dart';
import 'package:flutter/cupertino.dart';
import 'package:intl/intl.dart';
import 'package:mobx/mobx.dart';
part 'buy_sell_view_model.g.dart';
class BuySellViewModel = BuySellViewModelBase with _$BuySellViewModel;
abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with Store {
BuySellViewModelBase(
AppStore appStore,
) : _cryptoNumberFormat = NumberFormat(),
cryptoAmount = '',
fiatAmount = '',
cryptoCurrencyAddress = '',
isCryptoCurrencyAddressEnabled = false,
cryptoCurrencies = <CryptoCurrency>[],
fiatCurrencies = <FiatCurrency>[],
paymentMethodState = InitialPaymentMethod(),
buySellQuotState = InitialBuySellQuotState(),
cryptoCurrency = appStore.wallet!.currency,
fiatCurrency = appStore.settingsStore.fiatCurrency,
providerList = [],
sortedRecommendedQuotes = ObservableList<Quote>(),
sortedQuotes = ObservableList<Quote>(),
paymentMethods = ObservableList<PaymentMethod>(),
settingsStore = appStore.settingsStore,
super(appStore: appStore) {
const excludeFiatCurrencies = [];
const excludeCryptoCurrencies = [];
fiatCurrencies =
FiatCurrency.all.where((currency) => !excludeFiatCurrencies.contains(currency)).toList();
cryptoCurrencies = CryptoCurrency.all
.where((currency) => !excludeCryptoCurrencies.contains(currency))
.toList();
_initialize();
isCryptoCurrencyAddressEnabled = !(cryptoCurrency == wallet.currency);
}
final NumberFormat _cryptoNumberFormat;
late Timer bestRateSync;
List<BuyProvider> get availableBuyProviders {
final providerTypes = ProvidersHelper.getAvailableBuyProviderTypes(
walletTypeForCurrency(cryptoCurrency) ?? wallet.type);
return providerTypes
.map((type) => ProvidersHelper.getProviderByType(type))
.where((provider) => provider != null)
.cast<BuyProvider>()
.toList();
}
List<BuyProvider> get availableSellProviders {
final providerTypes = ProvidersHelper.getAvailableSellProviderTypes(
walletTypeForCurrency(cryptoCurrency) ?? wallet.type);
return providerTypes
.map((type) => ProvidersHelper.getProviderByType(type))
.where((provider) => provider != null)
.cast<BuyProvider>()
.toList();
}
@override
void onWalletChange(wallet) {
cryptoCurrency = wallet.currency;
}
bool get isDarkTheme => settingsStore.currentTheme.type == ThemeType.dark;
double get amount {
final formattedFiatAmount = double.tryParse(fiatAmount) ?? 200.0;
final formattedCryptoAmount =
double.tryParse(cryptoAmount) ?? (cryptoCurrency == CryptoCurrency.btc ? 0.001 : 1);
return isBuyAction ? formattedFiatAmount : formattedCryptoAmount;
}
SettingsStore settingsStore;
Quote? bestRateQuote;
Quote? selectedQuote;
@observable
List<CryptoCurrency> cryptoCurrencies;
@observable
List<FiatCurrency> fiatCurrencies;
@observable
bool isBuyAction = true;
@observable
List<BuyProvider> providerList;
@observable
ObservableList<Quote> sortedRecommendedQuotes;
@observable
ObservableList<Quote> sortedQuotes;
@observable
ObservableList<PaymentMethod> paymentMethods;
@observable
FiatCurrency fiatCurrency;
@observable
CryptoCurrency cryptoCurrency;
@observable
String cryptoAmount;
@observable
String fiatAmount;
@observable
String cryptoCurrencyAddress;
@observable
bool isCryptoCurrencyAddressEnabled;
@observable
PaymentMethod? selectedPaymentMethod;
@observable
PaymentMethodLoadingState paymentMethodState;
@observable
BuySellQuotLoadingState buySellQuotState;
@computed
bool get isReadyToTrade {
final hasSelectedQuote = selectedQuote != null;
final hasSelectedPaymentMethod = selectedPaymentMethod != null;
final isPaymentMethodLoaded = paymentMethodState is PaymentMethodLoaded;
final isBuySellQuotLoaded = buySellQuotState is BuySellQuotLoaded;
return hasSelectedQuote &&
hasSelectedPaymentMethod &&
isPaymentMethodLoaded &&
isBuySellQuotLoaded;
}
@action
void reset() {
cryptoCurrency = wallet.currency;
fiatCurrency = settingsStore.fiatCurrency;
isCryptoCurrencyAddressEnabled = !(cryptoCurrency == wallet.currency);
_initialize();
}
@action
void changeBuySellAction() {
isBuyAction = !isBuyAction;
_initialize();
}
@action
void changeFiatCurrency({required FiatCurrency currency}) {
fiatCurrency = currency;
_onPairChange();
}
@action
void changeCryptoCurrency({required CryptoCurrency currency}) {
cryptoCurrency = currency;
_onPairChange();
isCryptoCurrencyAddressEnabled = !(cryptoCurrency == wallet.currency);
}
@action
void changeCryptoCurrencyAddress(String address) => cryptoCurrencyAddress = address;
@action
Future<void> changeFiatAmount({required String amount}) async {
fiatAmount = amount;
if (amount.isEmpty) {
fiatAmount = '';
cryptoAmount = '';
return;
}
final enteredAmount = double.tryParse(amount.replaceAll(',', '.')) ?? 0;
if (!isReadyToTrade) {
cryptoAmount = S.current.fetching;
return;
}
if (bestRateQuote != null) {
_cryptoNumberFormat.maximumFractionDigits = cryptoCurrency.decimals;
cryptoAmount = _cryptoNumberFormat
.format(enteredAmount / bestRateQuote!.rate)
.toString()
.replaceAll(RegExp('\\,'), '');
} else {
await calculateBestRate();
}
}
@action
Future<void> changeCryptoAmount({required String amount}) async {
cryptoAmount = amount;
if (amount.isEmpty) {
fiatAmount = '';
cryptoAmount = '';
return;
}
final enteredAmount = double.tryParse(amount.replaceAll(',', '.')) ?? 0;
if (!isReadyToTrade) {
fiatAmount = S.current.fetching;
}
if (bestRateQuote != null) {
fiatAmount = _cryptoNumberFormat
.format(enteredAmount * bestRateQuote!.rate)
.toString()
.replaceAll(RegExp('\\,'), '');
} else {
await calculateBestRate();
}
}
@action
void changeOption(SelectableOption option) {
if (option is Quote) {
sortedRecommendedQuotes.forEach((element) => element.setIsSelected = false);
sortedQuotes.forEach((element) => element.setIsSelected = false);
option.setIsSelected = true;
selectedQuote = option;
} else if (option is PaymentMethod) {
paymentMethods.forEach((element) => element.isSelected = false);
option.isSelected = true;
selectedPaymentMethod = option;
} else {
throw ArgumentError('Unknown option type');
}
}
void onTapChoseProvider(BuildContext context) async {
final initialQuotes = List<Quote>.from(sortedRecommendedQuotes + sortedQuotes);
await calculateBestRate();
final newQuotes = (sortedRecommendedQuotes + sortedQuotes);
for (var quote in newQuotes) quote.limits = null;
final newQuoteProviders = newQuotes
.map((quote) => quote.provider.isAggregator ? quote.rampName : quote.provider.title)
.toSet();
final outOfLimitQuotes = initialQuotes.where((initialQuote) {
return !newQuoteProviders.contains(
initialQuote.provider.isAggregator ? initialQuote.rampName : initialQuote.provider.title);
}).map((missingQuote) {
final quote = Quote(
rate: missingQuote.rate,
feeAmount: missingQuote.feeAmount,
networkFee: missingQuote.networkFee,
transactionFee: missingQuote.transactionFee,
payout: missingQuote.payout,
rampId: missingQuote.rampId,
rampName: missingQuote.rampName,
rampIconPath: missingQuote.rampIconPath,
paymentType: missingQuote.paymentType,
quoteId: missingQuote.quoteId,
recommendations: missingQuote.recommendations,
provider: missingQuote.provider,
isBuyAction: missingQuote.isBuyAction,
limits: missingQuote.limits,
);
quote.setFiatCurrency = missingQuote.fiatCurrency;
quote.setCryptoCurrency = missingQuote.cryptoCurrency;
return quote;
}).toList();
final updatedQuoteOptions = List<SelectableItem>.from([
OptionTitle(title: 'Recommended'),
...sortedRecommendedQuotes,
if (sortedQuotes.isNotEmpty) OptionTitle(title: 'All Providers'),
...sortedQuotes,
if (outOfLimitQuotes.isNotEmpty) OptionTitle(title: 'Out of Limits'),
...outOfLimitQuotes,
]);
await Navigator.of(context).pushNamed(
Routes.buyOptionsPage,
arguments: [
updatedQuoteOptions,
changeOption,
launchTrade,
],
).then((value) => calculateBestRate());
}
void _onPairChange() {
_initialize();
}
void _setProviders() =>
providerList = isBuyAction ? availableBuyProviders : availableSellProviders;
Future<void> _initialize() async {
_setProviders();
cryptoAmount = '';
fiatAmount = '';
cryptoCurrencyAddress = _getInitialCryptoCurrencyAddress();
paymentMethodState = InitialPaymentMethod();
buySellQuotState = InitialBuySellQuotState();
await _getAvailablePaymentTypes();
await calculateBestRate();
}
String _getInitialCryptoCurrencyAddress() {
return cryptoCurrency == wallet.currency ? wallet.walletAddresses.address : '';
}
@action
Future<void> _getAvailablePaymentTypes() async {
paymentMethodState = PaymentMethodLoading();
selectedPaymentMethod = null;
final result = await Future.wait(providerList.map((element) => element
.getAvailablePaymentTypes(fiatCurrency.title, cryptoCurrency.title, isBuyAction)
.timeout(
Duration(seconds: 10),
onTimeout: () => [],
)));
final Map<PaymentType, PaymentMethod> uniquePaymentMethods = {};
for (var methods in result) {
for (var method in methods) {
uniquePaymentMethods[method.paymentMethodType] = method;
}
}
paymentMethods = ObservableList<PaymentMethod>.of(uniquePaymentMethods.values);
if (paymentMethods.isNotEmpty) {
paymentMethods.insert(0, PaymentMethod.all());
selectedPaymentMethod = paymentMethods.first;
selectedPaymentMethod!.isSelected = true;
paymentMethodState = PaymentMethodLoaded();
} else {
paymentMethodState = PaymentMethodFailed();
}
}
@action
Future<void> calculateBestRate() async {
buySellQuotState = BuySellQuotLoading();
final result = await Future.wait<List<Quote>?>(providerList.map((element) => element
.fetchQuote(
cryptoCurrency: cryptoCurrency,
fiatCurrency: fiatCurrency,
amount: amount,
paymentType: selectedPaymentMethod?.paymentMethodType,
isBuyAction: isBuyAction,
walletAddress: wallet.walletAddresses.address,
)
.timeout(
Duration(seconds: 10),
onTimeout: () => null,
)));
sortedRecommendedQuotes.clear();
sortedQuotes.clear();
final validQuotes = result
.where((element) => element != null && element.isNotEmpty)
.expand((element) => element!)
.toList();
if (validQuotes.isEmpty) {
buySellQuotState = BuySellQuotFailed();
return;
}
validQuotes.sort((a, b) => a.rate.compareTo(b.rate));
final Set<String> addedProviders = {};
final List<Quote> uniqueProviderQuotes = validQuotes.where((element) {
if (addedProviders.contains(element.provider.title)) return false;
addedProviders.add(element.provider.title);
return true;
}).toList();
sortedRecommendedQuotes.addAll(uniqueProviderQuotes);
sortedQuotes = ObservableList.of(
validQuotes.where((element) => !uniqueProviderQuotes.contains(element)).toList());
if (sortedRecommendedQuotes.isNotEmpty) {
sortedRecommendedQuotes.first
..setIsBestRate = true
..recommendations.insert(0, ProviderRecommendation.bestRate);
bestRateQuote = sortedRecommendedQuotes.first;
sortedRecommendedQuotes.sort((a, b) {
if (a.provider is OnRamperBuyProvider) return -1;
if (b.provider is OnRamperBuyProvider) return 1;
return 0;
});
selectedQuote = sortedRecommendedQuotes.first;
sortedRecommendedQuotes.first.setIsSelected = true;
}
buySellQuotState = BuySellQuotLoaded();
}
@action
Future<void> launchTrade(BuildContext context) async {
final provider = selectedQuote!.provider;
await provider.launchProvider(
context: context,
quote: selectedQuote!,
amount: amount,
isBuyAction: isBuyAction,
cryptoCurrencyAddress: cryptoCurrencyAddress,
);
}
}