diff --git a/assets/svg/exchange_icons/trocador.svg b/assets/svg/exchange_icons/trocador.svg new file mode 100644 index 000000000..b3d9171ff --- /dev/null +++ b/assets/svg/exchange_icons/trocador.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/svg/trocador_rating_a.svg b/assets/svg/trocador_rating_a.svg new file mode 100644 index 000000000..1e75af73b --- /dev/null +++ b/assets/svg/trocador_rating_a.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/svg/trocador_rating_b.svg b/assets/svg/trocador_rating_b.svg new file mode 100644 index 000000000..5d678305a --- /dev/null +++ b/assets/svg/trocador_rating_b.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/svg/trocador_rating_c.svg b/assets/svg/trocador_rating_c.svg new file mode 100644 index 000000000..87ecf6b24 --- /dev/null +++ b/assets/svg/trocador_rating_c.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/svg/trocador_rating_d.svg b/assets/svg/trocador_rating_d.svg new file mode 100644 index 000000000..8973c7e65 --- /dev/null +++ b/assets/svg/trocador_rating_d.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/lib/main.dart b/lib/main.dart index e08324572..10d1c4a12 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -299,7 +299,8 @@ class _MaterialAppWithThemeState extends ConsumerState await ref.read(prefsChangeNotifierProvider).isExternalCallsSet()) { if (Constants.enableExchange) { await ExchangeDataLoadingService.instance.setCurrenciesIfEmpty( - ref.read(exchangeFormStateProvider), + ref.read(efCurrencyPairProvider), + ref.read(efRateTypeProvider), ); unawaited(ExchangeDataLoadingService.instance.loadAll()); } diff --git a/lib/models/exchange/active_pair.dart b/lib/models/exchange/active_pair.dart new file mode 100644 index 000000000..4a2e80eba --- /dev/null +++ b/lib/models/exchange/active_pair.dart @@ -0,0 +1,35 @@ +import 'package:flutter/foundation.dart'; +import 'package:stackwallet/models/exchange/aggregate_currency.dart'; + +class ActivePair extends ChangeNotifier { + AggregateCurrency? _send; + AggregateCurrency? _receive; + + AggregateCurrency? get send => _send; + AggregateCurrency? get receive => _receive; + + void setSend( + AggregateCurrency? newSend, { + bool notifyListeners = false, + }) { + _send = newSend; + if (notifyListeners) { + this.notifyListeners(); + } + } + + void setReceive( + AggregateCurrency? newReceive, { + bool notifyListeners = false, + }) { + _receive = newReceive; + if (notifyListeners) { + this.notifyListeners(); + } + } + + @override + String toString() { + return "ActivePair{ send: $send, receive: $receive }"; + } +} diff --git a/lib/models/exchange/aggregate_currency.dart b/lib/models/exchange/aggregate_currency.dart index 6cd1ef6cf..1bbc767a5 100644 --- a/lib/models/exchange/aggregate_currency.dart +++ b/lib/models/exchange/aggregate_currency.dart @@ -5,8 +5,9 @@ import 'package:tuple/tuple.dart'; class AggregateCurrency { final Map _map = {}; - AggregateCurrency( - {required List> exchangeCurrencyPairs}) { + AggregateCurrency({ + required List> exchangeCurrencyPairs, + }) { assert(exchangeCurrencyPairs.isNotEmpty); for (final item in exchangeCurrencyPairs) { diff --git a/lib/models/exchange/exchange_form_state.dart b/lib/models/exchange/exchange_form_state.dart deleted file mode 100644 index 32578ebd9..000000000 --- a/lib/models/exchange/exchange_form_state.dart +++ /dev/null @@ -1,519 +0,0 @@ -import 'package:decimal/decimal.dart'; -import 'package:flutter/foundation.dart'; -import 'package:stackwallet/models/exchange/aggregate_currency.dart'; -import 'package:stackwallet/models/exchange/response_objects/estimate.dart'; -import 'package:stackwallet/services/exchange/change_now/change_now_exchange.dart'; -import 'package:stackwallet/services/exchange/exchange.dart'; -import 'package:stackwallet/services/exchange/majestic_bank/majestic_bank_exchange.dart'; -import 'package:stackwallet/utilities/enums/exchange_rate_type_enum.dart'; -import 'package:stackwallet/utilities/logger.dart'; - -class ExchangeFormState extends ChangeNotifier { - Exchange? _exchange; - Exchange get exchange => _exchange ??= Exchange.defaultExchange; - - ExchangeRateType _exchangeRateType = ExchangeRateType.estimated; - ExchangeRateType get exchangeRateType => _exchangeRateType; - set exchangeRateType(ExchangeRateType exchangeRateType) { - _exchangeRateType = exchangeRateType; - // - } - - Estimate? _estimate; - Estimate? get estimate => _estimate; - - bool _reversed = false; - bool get reversed => _reversed; - set reversed(bool reversed) { - _reversed = reversed; - // - } - - Decimal? _rate; - Decimal? get rate => _rate; - // set rate(Decimal? rate) { - // _rate = rate; - // // - // } - - Decimal? _sendAmount; - Decimal? get sendAmount => _sendAmount; - // set sendAmount(Decimal? sendAmount) { - // _sendAmount = sendAmount; - // // - // } - - Decimal? _receiveAmount; - Decimal? get receiveAmount => _receiveAmount; - set receiveAmount(Decimal? receiveAmount) { - _receiveAmount = receiveAmount; - // - } - - AggregateCurrency? _sendCurrency; - AggregateCurrency? get sendCurrency => _sendCurrency; - // set sendCurrency(Currency? sendCurrency) { - // _sendCurrency = sendCurrency; - // // - // } - - AggregateCurrency? _receiveCurrency; - AggregateCurrency? get receiveCurrency => _receiveCurrency; - // set receiveCurrency(Currency? receiveCurrency) { - // _receiveCurrency = receiveCurrency; - // // - // } - - Decimal? _minSendAmount; - Decimal? get minSendAmount => _minSendAmount; - // set minSendAmount(Decimal? minSendAmount) { - // _minSendAmount = minSendAmount; - // // - // } - - Decimal? _minReceiveAmount; - Decimal? get minReceiveAmount => _minReceiveAmount; - // set minReceiveAmount(Decimal? minReceiveAmount) { - // _minReceiveAmount = minReceiveAmount; - // // - // } - - Decimal? _maxSendAmount; - Decimal? get maxSendAmount => _maxSendAmount; - // set maxSendAmount(Decimal? maxSendAmount) { - // _maxSendAmount = maxSendAmount; - // // - // } - - Decimal? _maxReceiveAmount; - Decimal? get maxReceiveAmount => _maxReceiveAmount; - // set maxReceiveAmount(Decimal? maxReceiveAmount) { - // _maxReceiveAmount = maxReceiveAmount; - // // - // } - - //============================================================================ - // computed properties - //============================================================================ - - String? get fromTicker => _sendCurrency?.ticker; - String? get toTicker => _receiveCurrency?.ticker; - - String get fromAmountString => _sendAmount?.toStringAsFixed(8) ?? ""; - String get toAmountString => _receiveAmount?.toStringAsFixed(8) ?? ""; - - bool get canExchange { - return sendCurrency != null && - receiveCurrency != null && - sendAmount != null && - sendAmount! >= Decimal.zero && - receiveAmount != null && - rate != null && - rate! >= Decimal.zero && - sendCurrency!.forExchange(exchange.name) != null && - receiveCurrency!.forExchange(exchange.name) != null && - warning.isEmpty; - } - - String get warning { - if (reversed) { - if (_receiveCurrency != null && _receiveAmount != null) { - if (_minReceiveAmount != null && - _receiveAmount! < _minReceiveAmount! && - _receiveAmount! > Decimal.zero) { - return "Min receive amount ${_minReceiveAmount!.toString()} ${_receiveCurrency!.ticker.toUpperCase()}"; - } else if (_maxReceiveAmount != null && - _receiveAmount! > _maxReceiveAmount!) { - return "Max receive amount ${_maxReceiveAmount!.toString()} ${_receiveCurrency!.ticker.toUpperCase()}"; - } - } - } else { - if (_sendCurrency != null && _sendAmount != null) { - if (_minSendAmount != null && - _sendAmount! < _minSendAmount! && - _sendAmount! > Decimal.zero) { - return "Min send amount ${_minSendAmount!.toString()} ${_sendCurrency!.ticker.toUpperCase()}"; - } else if (_maxSendAmount != null && _sendAmount! > _maxSendAmount!) { - return "Max send amount ${_maxSendAmount!.toString()} ${_sendCurrency!.ticker.toUpperCase()}"; - } - } - } - - return ""; - } - - //============================================================================ - // public state updaters - //============================================================================ - - Future updateExchange({ - required Exchange exchange, - required bool shouldUpdateData, - required bool shouldNotifyListeners, - }) async { - _exchange = exchange; - if (shouldUpdateData) { - await _updateRangesAndEstimate( - shouldNotifyListeners: false, - ); - } - - if (shouldNotifyListeners) { - _notify(); - } - } - - void setCurrencies(AggregateCurrency? from, AggregateCurrency? to) { - _sendCurrency = from; - _receiveCurrency = to; - } - - void reset({ - required bool shouldNotifyListeners, - }) { - _exchange = null; - _reversed = false; - _rate = null; - _sendAmount = null; - _receiveAmount = null; - _sendCurrency = null; - _receiveCurrency = null; - _minSendAmount = null; - _minReceiveAmount = null; - _maxSendAmount = null; - _maxReceiveAmount = null; - - if (shouldNotifyListeners) { - _notify(); - } - } - - Future setSendAmountAndCalculateReceiveAmount( - Decimal? newSendAmount, - bool shouldNotifyListeners, - ) async { - if (newSendAmount == null) { - // todo: check if this breaks things and stuff - _receiveAmount = null; - _sendAmount = null; - } else { - if (newSendAmount <= Decimal.zero) { - _receiveAmount = Decimal.zero; - } - - _sendAmount = newSendAmount; - _reversed = false; - - await _updateRangesAndEstimate( - shouldNotifyListeners: false, - ); - } - - if (shouldNotifyListeners) { - _notify(); - } - } - - Future setReceivingAmountAndCalculateSendAmount( - Decimal? newReceiveAmount, - bool shouldNotifyListeners, - ) async { - if (newReceiveAmount == null) { - // todo: check if this breaks things and stuff - _receiveAmount = null; - _sendAmount = null; - } else { - if (newReceiveAmount <= Decimal.zero) { - _sendAmount = Decimal.zero; - } - - _receiveAmount = newReceiveAmount; - _reversed = true; - - await _updateRangesAndEstimate( - shouldNotifyListeners: false, - ); - } - - if (shouldNotifyListeners) { - _notify(); - } - } - - Future updateSendCurrency( - AggregateCurrency sendCurrency, - bool shouldNotifyListeners, - ) async { - try { - _sendCurrency = sendCurrency; - _minSendAmount = null; - _maxSendAmount = null; - - if (_receiveCurrency == null) { - _rate = null; - } else { - await _updateRangesAndEstimate( - shouldNotifyListeners: false, - ); - } - if (shouldNotifyListeners) { - _notify(); - } - } catch (e, s) { - Logging.instance.log("$e\n$s", level: LogLevel.Error); - } - } - - Future updateReceivingCurrency( - AggregateCurrency receiveCurrency, - bool shouldNotifyListeners, - ) async { - try { - _receiveCurrency = receiveCurrency; - _minReceiveAmount = null; - _maxReceiveAmount = null; - - if (_sendCurrency == null) { - _rate = null; - } else { - await _updateRangesAndEstimate( - shouldNotifyListeners: false, - ); - } - if (shouldNotifyListeners) { - _notify(); - } - } catch (e, s) { - Logging.instance.log("$e\n$s", level: LogLevel.Error); - } - } - - Future swap({ - required bool shouldNotifyListeners, - }) async { - final Decimal? temp = sendAmount; - _sendAmount = receiveAmount; - _receiveAmount = temp; - - _minSendAmount = null; - _maxSendAmount = null; - _minReceiveAmount = null; - _maxReceiveAmount = null; - - final AggregateCurrency? tmp = sendCurrency; - _sendCurrency = receiveCurrency; - _receiveCurrency = tmp; - - await _updateRangesAndEstimate( - shouldNotifyListeners: false, - ); - - if (shouldNotifyListeners) { - _notify(); - } - } - - Future refresh() => _updateRangesAndEstimate( - shouldNotifyListeners: true, - ); - - //============================================================================ - // private state updaters - //============================================================================ - - Future _updateRangesAndEstimate({ - required bool shouldNotifyListeners, - }) async { - try { - switch (exchange.name) { - case ChangeNowExchange.exchangeName: - if (!_exchangeSupported( - exchangeName: exchange.name, - sendCurrency: sendCurrency, - receiveCurrency: receiveCurrency, - exchangeRateType: exchangeRateType, - )) { - _exchange = MajesticBankExchange.instance; - } - break; - case MajesticBankExchange.exchangeName: - if (!_exchangeSupported( - exchangeName: exchange.name, - sendCurrency: sendCurrency, - receiveCurrency: receiveCurrency, - exchangeRateType: exchangeRateType, - )) { - _exchange = ChangeNowExchange.instance; - } - break; - } - - await _updateRanges(shouldNotifyListeners: false); - await _updateEstimate(shouldNotifyListeners: false); - if (shouldNotifyListeners) { - _notify(); - } - } catch (_) { - // - } - } - - Future _updateRanges({ - required bool shouldNotifyListeners, - }) async { - // if (exchange?.name == SimpleSwapExchange.exchangeName) { - // reversed = false; - // } - final _send = sendCurrency; - final _receive = receiveCurrency; - if (_send == null || _receive == null) { - Logging.instance.log( - "Tried to $runtimeType.updateRanges where ( $_send || $_receive) for: $exchange", - level: LogLevel.Info, - ); - return; - } - final response = await exchange.getRange( - _send.ticker, - _receive.ticker, - exchangeRateType == ExchangeRateType.fixed, - ); - - if (response.value == null) { - Logging.instance.log( - "Tried to $runtimeType.updateRanges for: $exchange where response: $response", - level: LogLevel.Info, - ); - return; - } - final responseReversed = await exchange.getRange( - _receive.ticker, - _send.ticker, - exchangeRateType == ExchangeRateType.fixed, - ); - - if (responseReversed.value == null) { - Logging.instance.log( - "Tried to $runtimeType.updateRanges for: $exchange where response: $responseReversed", - level: LogLevel.Info, - ); - return; - } - - final range = response.value!; - final rangeReversed = responseReversed.value!; - - _minSendAmount = range.min; - _maxSendAmount = range.max; - _minReceiveAmount = rangeReversed.min; - _maxReceiveAmount = rangeReversed.max; - - //todo: check if print needed - // debugPrint( - // "updated range for: $exchange for $_fromTicker-$_toTicker: $range"); - - if (shouldNotifyListeners) { - _notify(); - } - } - - Future _updateEstimate({ - required bool shouldNotifyListeners, - }) async { - // if (exchange?.name == SimpleSwapExchange.exchangeName) { - // reversed = false; - // } - final amount = reversed ? receiveAmount : sendAmount; - if (sendCurrency == null || - receiveCurrency == null || - amount == null || - amount <= Decimal.zero) { - Logging.instance.log( - "Tried to $runtimeType.updateEstimate for: $exchange where (from: $sendCurrency || to: $receiveCurrency || amount: $amount)", - level: LogLevel.Info, - ); - return; - } - final response = await exchange.getEstimate( - sendCurrency!.ticker, - receiveCurrency!.ticker, - amount, - exchangeRateType == ExchangeRateType.fixed, - reversed, - ); - - if (response.value == null) { - Logging.instance.log( - "Tried to $runtimeType.updateEstimate for: $exchange where response: $response", - level: LogLevel.Info, - ); - return; - } - - _estimate = response.value!; - - if (reversed) { - _sendAmount = _estimate!.estimatedAmount; - } else { - _receiveAmount = _estimate!.estimatedAmount; - } - - _rate = - (receiveAmount! / sendAmount!).toDecimal(scaleOnInfinitePrecision: 12); - - //todo: check if print needed - // debugPrint( - // "updated estimate for: $exchange for $fromTicker-$toTicker: $estimate"); - - if (shouldNotifyListeners) { - _notify(); - } - } - - //============================================================================ - - void _notify() { - debugPrint("ExFState NOTIFY: ${toString()}"); - notifyListeners(); - } - - bool _exchangeSupported({ - required String exchangeName, - required AggregateCurrency? sendCurrency, - required AggregateCurrency? receiveCurrency, - required ExchangeRateType exchangeRateType, - }) { - final send = sendCurrency?.forExchange(exchangeName); - if (send == null) return false; - - final rcv = receiveCurrency?.forExchange(exchangeName); - if (rcv == null) return false; - - if (exchangeRateType == ExchangeRateType.fixed) { - return send.supportsFixedRate && rcv.supportsFixedRate; - } else { - return send.supportsEstimatedRate && rcv.supportsEstimatedRate; - } - } - - @override - String toString() { - return "{" - "\n\t exchange: $exchange," - "\n\t exchangeRateType: $exchangeRateType," - "\n\t sendCurrency: $sendCurrency," - "\n\t receiveCurrency: $receiveCurrency," - "\n\t rate: $rate," - "\n\t reversed: $reversed," - "\n\t sendAmount: $sendAmount," - "\n\t receiveAmount: $receiveAmount," - "\n\t estimate: $estimate," - "\n\t minSendAmount: $minSendAmount," - "\n\t maxSendAmount: $maxSendAmount," - "\n\t minReceiveAmount: $minReceiveAmount," - "\n\t maxReceiveAmount: $maxReceiveAmount," - "\n\t canExchange: $canExchange," - "\n\t warning: $warning," - "\n}"; - } -} diff --git a/lib/models/exchange/incomplete_exchange.dart b/lib/models/exchange/incomplete_exchange.dart index 864a25490..2680b24e0 100644 --- a/lib/models/exchange/incomplete_exchange.dart +++ b/lib/models/exchange/incomplete_exchange.dart @@ -1,5 +1,6 @@ import 'package:decimal/decimal.dart'; import 'package:flutter/foundation.dart'; +import 'package:stackwallet/models/exchange/response_objects/estimate.dart'; import 'package:stackwallet/models/exchange/response_objects/trade.dart'; import 'package:stackwallet/utilities/enums/exchange_rate_type_enum.dart'; @@ -39,13 +40,13 @@ class IncompleteExchangeModel extends ChangeNotifier { } } - String? _rateId; + Estimate? _estimate; - String? get rateId => _rateId; + Estimate? get estimate => _estimate; - set rateId(String? rateId) { - if (_rateId != rateId) { - _rateId = rateId; + set estimate(Estimate? estimate) { + if (_estimate != estimate) { + _estimate = estimate; notifyListeners(); } } @@ -70,6 +71,6 @@ class IncompleteExchangeModel extends ChangeNotifier { required this.rateType, required this.reversed, required this.walletInitiated, - String? rateId, - }) : _rateId = rateId; + Estimate? estimate, + }) : _estimate = estimate; } diff --git a/lib/models/exchange/response_objects/estimate.dart b/lib/models/exchange/response_objects/estimate.dart index 7df490079..9284c8340 100644 --- a/lib/models/exchange/response_objects/estimate.dart +++ b/lib/models/exchange/response_objects/estimate.dart @@ -7,6 +7,8 @@ class Estimate { final bool reversed; final String? warningMessage; final String? rateId; + final String exchangeProvider; + final String? kycRating; Estimate({ required this.estimatedAmount, @@ -14,9 +16,15 @@ class Estimate { required this.reversed, this.warningMessage, this.rateId, + required this.exchangeProvider, + this.kycRating, }); - factory Estimate.fromMap(Map map) { + factory Estimate.fromMap( + Map map, { + required String exchangeProvider, + String? kycRating, + }) { try { return Estimate( estimatedAmount: Decimal.parse(map["estimatedAmount"] as String), @@ -24,6 +32,8 @@ class Estimate { reversed: map["reversed"] as bool, warningMessage: map["warningMessage"] as String?, rateId: map["rateId"] as String?, + exchangeProvider: exchangeProvider, + kycRating: kycRating, ); } catch (e, s) { Logging.instance.log("Estimate.fromMap(): $e\n$s", level: LogLevel.Error); @@ -38,6 +48,8 @@ class Estimate { "reversed": reversed, "warningMessage": warningMessage, "rateId": rateId, + "exchangeProvider": exchangeProvider, + "kycRating": kycRating, }; } diff --git a/lib/pages/exchange_view/exchange_form.dart b/lib/pages/exchange_view/exchange_form.dart index 389bedd05..c4d11d162 100644 --- a/lib/pages/exchange_view/exchange_form.dart +++ b/lib/pages/exchange_view/exchange_form.dart @@ -9,6 +9,8 @@ import 'package:intl/intl.dart'; import 'package:isar/isar.dart'; import 'package:stackwallet/models/exchange/aggregate_currency.dart'; import 'package:stackwallet/models/exchange/incomplete_exchange.dart'; +import 'package:stackwallet/models/exchange/response_objects/estimate.dart'; +import 'package:stackwallet/models/exchange/response_objects/range.dart'; import 'package:stackwallet/models/isar/exchange_cache/currency.dart'; import 'package:stackwallet/models/isar/exchange_cache/pair.dart'; import 'package:stackwallet/models/isar/models/ethereum/eth_contract.dart'; @@ -21,6 +23,8 @@ import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_ste import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/services/exchange/change_now/change_now_exchange.dart'; import 'package:stackwallet/services/exchange/exchange_data_loading_service.dart'; +import 'package:stackwallet/services/exchange/majestic_bank/majestic_bank_exchange.dart'; +import 'package:stackwallet/services/exchange/trocador/trocador_exchange.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; @@ -39,6 +43,9 @@ import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; import 'package:stackwallet/widgets/textfields/exchange_textfield.dart'; import 'package:tuple/tuple.dart'; +import 'package:uuid/uuid.dart'; + +import '../../services/exchange/exchange_response.dart'; class ExchangeForm extends ConsumerStatefulWidget { const ExchangeForm({ @@ -61,6 +68,12 @@ class _ExchangeFormState extends ConsumerState { late final Coin? coin; late final bool walletInitiated; + final exchanges = [ + MajesticBankExchange.instance, + ChangeNowExchange.instance, + TrocadorExchange.instance, + ]; + late final TextEditingController _sendController; late final TextEditingController _receiveController; final isDesktop = Util.isDesktop; @@ -70,7 +83,7 @@ class _ExchangeFormState extends ConsumerState { bool _swapLock = false; // todo: check and adjust this value? - static const _valueCheckInterval = Duration(milliseconds: 300); + static const _valueCheckInterval = Duration(milliseconds: 1500); Future showUpdatingExchangeRate({ required Future whileFuture, @@ -105,16 +118,17 @@ class _ExchangeFormState extends ConsumerState { } Timer? _sendFieldOnChangedTimer; - void sendFieldOnChanged(String value) async { + void sendFieldOnChanged(String value) { if (_sendFocusNode.hasFocus) { _sendFieldOnChangedTimer?.cancel(); _sendFieldOnChangedTimer = Timer(_valueCheckInterval, () async { final newFromAmount = _localizedStringToNum(value); - await ref - .read(exchangeFormStateProvider) - .setSendAmountAndCalculateReceiveAmount(newFromAmount, true); + ref.read(efSendAmountProvider.notifier).state = newFromAmount; + if (!_swapLock && !ref.read(efReversedProvider)) { + unawaited(update()); + } }); } } @@ -126,9 +140,10 @@ class _ExchangeFormState extends ConsumerState { _receiveFieldOnChangedTimer = Timer(_valueCheckInterval, () async { final newToAmount = _localizedStringToNum(value); - await ref - .read(exchangeFormStateProvider) - .setReceivingAmountAndCalculateSendAmount(newToAmount, true); + ref.read(efReceiveAmountProvider.notifier).state = newToAmount; + if (!_swapLock && ref.read(efReversedProvider)) { + unawaited(update()); + } }); } @@ -147,7 +162,7 @@ class _ExchangeFormState extends ConsumerState { } Future _getAggregateCurrency(Currency currency) async { - final rateType = ref.read(exchangeFormStateProvider).exchangeRateType; + final rateType = ref.read(efRateTypeProvider); final currencies = await ExchangeDataLoadingService.instance.isar.currencies .filter() .group((q) => rateType == ExchangeRateType.fixed @@ -178,8 +193,8 @@ class _ExchangeFormState extends ConsumerState { } void selectSendCurrency() async { - final type = (ref.read(exchangeFormStateProvider).exchangeRateType); - final fromTicker = ref.read(exchangeFormStateProvider).fromTicker ?? ""; + final type = ref.read(efRateTypeProvider); + final fromTicker = ref.read(efCurrencyPairProvider).send?.ticker ?? ""; if (walletInitiated) { if (widget.contract != null && @@ -194,24 +209,26 @@ class _ExchangeFormState extends ConsumerState { } final selectedCurrency = await _showCurrencySelectionSheet( - willChange: ref.read(exchangeFormStateProvider).sendCurrency?.ticker, + willChange: ref.read(efCurrencyPairProvider).send?.ticker, willChangeIsSend: true, - paired: ref.read(exchangeFormStateProvider).receiveCurrency?.ticker, + paired: ref.read(efCurrencyPairProvider).receive?.ticker, isFixedRate: type == ExchangeRateType.fixed, ); if (selectedCurrency != null) { await showUpdatingExchangeRate( whileFuture: _getAggregateCurrency(selectedCurrency).then( - (aggregateSelected) => ref - .read(exchangeFormStateProvider) - .updateSendCurrency(aggregateSelected, true)), + (aggregateSelected) => ref.read(efCurrencyPairProvider).setSend( + aggregateSelected, + notifyListeners: true, + ), + ), ); } } void selectReceiveCurrency() async { - final toTicker = ref.read(exchangeFormStateProvider).toTicker ?? ""; + final toTicker = ref.read(efCurrencyPairProvider).receive?.ticker ?? ""; if (walletInitiated && toTicker.toLowerCase() == coin!.ticker.toLowerCase()) { // do not allow changing away from wallet coin @@ -219,19 +236,20 @@ class _ExchangeFormState extends ConsumerState { } final selectedCurrency = await _showCurrencySelectionSheet( - willChange: ref.read(exchangeFormStateProvider).receiveCurrency?.ticker, + willChange: ref.read(efCurrencyPairProvider).receive?.ticker, willChangeIsSend: false, - paired: ref.read(exchangeFormStateProvider).sendCurrency?.ticker, - isFixedRate: ref.read(exchangeFormStateProvider).exchangeRateType == - ExchangeRateType.fixed, + paired: ref.read(efCurrencyPairProvider).send?.ticker, + isFixedRate: ref.read(efRateTypeProvider) == ExchangeRateType.fixed, ); if (selectedCurrency != null) { await showUpdatingExchangeRate( whileFuture: _getAggregateCurrency(selectedCurrency).then( - (aggregateSelected) => ref - .read(exchangeFormStateProvider) - .updateReceivingCurrency(aggregateSelected, true)), + (aggregateSelected) => ref.read(efCurrencyPairProvider).setReceive( + aggregateSelected, + notifyListeners: true, + ), + ), ); } } @@ -241,10 +259,25 @@ class _ExchangeFormState extends ConsumerState { _sendFocusNode.unfocus(); _receiveFocusNode.unfocus(); - await showUpdatingExchangeRate( - whileFuture: - ref.read(exchangeFormStateProvider).swap(shouldNotifyListeners: true), - ); + final temp = ref.read(efCurrencyPairProvider).send; + ref.read(efCurrencyPairProvider).setSend( + ref.read(efCurrencyPairProvider).receive, + notifyListeners: true, + ); + ref.read(efCurrencyPairProvider).setReceive( + temp, + notifyListeners: true, + ); + + // final reversed = ref.read(efReversedProvider); + + final amount = ref.read(efSendAmountProvider); + ref.read(efSendAmountProvider.notifier).state = + ref.read(efReceiveAmountProvider); + + ref.read(efReceiveAmountProvider.notifier).state = amount; + + unawaited(update()); _swapLock = false; } @@ -331,85 +364,20 @@ class _ExchangeFormState extends ConsumerState { } } - void onRateTypeChanged(ExchangeRateType newType) async { + void onRateTypeChanged(ExchangeRateType newType) { _receiveFocusNode.unfocus(); _sendFocusNode.unfocus(); - await showUpdatingExchangeRate( - whileFuture: _onRateTypeChangedFuture(newType), - ); - } - - Future _onRateTypeChangedFuture(ExchangeRateType newType) async { - ref.read(exchangeFormStateProvider).exchangeRateType = newType; - - final fromTicker = ref.read(exchangeFormStateProvider).fromTicker ?? "-"; - final toTicker = ref.read(exchangeFormStateProvider).toTicker ?? "-"; - - ref.read(exchangeFormStateProvider).reversed = false; - - if (!(toTicker == "-" || fromTicker == "-")) { - // final available = await ExchangeDataLoadingService.instance.isar.pairs - // .where() - // .exchangeNameEqualTo( - // ref.read(currentExchangeNameStateProvider.state).state) - // .filter() - // .fromEqualTo(fromTicker) - // .and() - // .toEqualTo(toTicker) - // .findAll(); - await ref.read(exchangeFormStateProvider).refresh(); - - // if (available.isNotEmpty) { - // final availableCurrencies = await ExchangeDataLoadingService - // .instance.isar.currencies - // .where() - // .exchangeNameEqualTo( - // ref.read(currentExchangeNameStateProvider.state).state) - // .filter() - // .tickerEqualTo(fromTicker) - // .or() - // .tickerEqualTo(toTicker) - // .findAll(); - // - // if (availableCurrencies.length > 1) { - // final from = - // availableCurrencies.firstWhere((e) => e.ticker == fromTicker); - // final to = - // availableCurrencies.firstWhere((e) => e.ticker == toTicker); - // - // final newFromAmount = Decimal.tryParse(_sendController.text); - // ref.read(exchangeFormStateProvider).receiveAmount = newFromAmount; - // if (newFromAmount == null) { - // _receiveController.text = ""; - // } - // - // await ref - // .read(exchangeFormStateProvider) - // .updateReceivingCurrency(to, false); - // await ref - // .read(exchangeFormStateProvider) - // .updateSendCurrency(from, true); - // - // _receiveController.text = - // ref.read(exchangeFormStateProvider).toAmountString.isEmpty - // ? "-" - // : ref.read(exchangeFormStateProvider).toAmountString; - // if (mounted) { - // Navigator.of(context, rootNavigator: isDesktop).pop(); - // } - // return; - // } - // } - } + ref.read(efRateTypeProvider.notifier).state = newType; + update(); } void onExchangePressed() async { - final rateType = ref.read(exchangeFormStateProvider).exchangeRateType; - final fromTicker = ref.read(exchangeFormStateProvider).fromTicker ?? ""; - final toTicker = ref.read(exchangeFormStateProvider).toTicker ?? ""; - final sendAmount = ref.read(exchangeFormStateProvider).sendAmount!; - final estimate = ref.read(exchangeFormStateProvider).estimate!; + final rateType = ref.read(efRateTypeProvider); + final fromTicker = ref.read(efCurrencyPairProvider).send?.ticker ?? ""; + final toTicker = ref.read(efCurrencyPairProvider).receive?.ticker ?? ""; + final estimate = ref.read(efEstimateProvider)!; + final sendAmount = ref.read(efSendAmountProvider)!; if (rateType == ExchangeRateType.fixed && toTicker.toUpperCase() == "WOW") { await showDialog( @@ -426,10 +394,16 @@ class _ExchangeFormState extends ConsumerState { String rate; + final amountToSend = + estimate.reversed ? estimate.estimatedAmount : sendAmount; + final amountToReceive = estimate.reversed + ? ref.read(efReceiveAmountProvider)! + : estimate.estimatedAmount; + switch (rateType) { case ExchangeRateType.estimated: rate = - "1 ${fromTicker.toUpperCase()} ~${(estimate.estimatedAmount / sendAmount).toDecimal(scaleOnInfinitePrecision: 8).toStringAsFixed(8)} ${toTicker.toUpperCase()}"; + "1 ${fromTicker.toUpperCase()} ~${(amountToReceive / sendAmount).toDecimal(scaleOnInfinitePrecision: 8).toStringAsFixed(8)} ${toTicker.toUpperCase()}"; break; case ExchangeRateType.fixed: bool? shouldCancel; @@ -541,7 +515,9 @@ class _ExchangeFormState extends ConsumerState { return; } rate = - "1 ${fromTicker.toUpperCase()} ~${ref.read(exchangeFormStateProvider).rate!.toStringAsFixed(8)} ${toTicker.toUpperCase()}"; + "1 ${fromTicker.toUpperCase()} ~${(amountToReceive / amountToSend).toDecimal( + scaleOnInfinitePrecision: 12, + ).toStringAsFixed(8)} ${toTicker.toUpperCase()}"; break; } @@ -549,12 +525,10 @@ class _ExchangeFormState extends ConsumerState { sendTicker: fromTicker.toUpperCase(), receiveTicker: toTicker.toUpperCase(), rateInfo: rate, - sendAmount: estimate.reversed ? estimate.estimatedAmount : sendAmount, - receiveAmount: estimate.reversed - ? ref.read(exchangeFormStateProvider).receiveAmount! - : estimate.estimatedAmount, + sendAmount: amountToSend, + receiveAmount: amountToReceive, rateType: rateType, - rateId: estimate.rateId, + estimate: estimate, reversed: estimate.reversed, walletInitiated: walletInitiated, ); @@ -622,8 +596,8 @@ class _ExchangeFormState extends ConsumerState { } String? ticker = isSend - ? ref.read(exchangeFormStateProvider).fromTicker - : ref.read(exchangeFormStateProvider).toTicker; + ? ref.read(efCurrencyPairProvider).send?.ticker + : ref.read(efCurrencyPairProvider).receive?.ticker; if (ticker == null) { return false; @@ -632,6 +606,97 @@ class _ExchangeFormState extends ConsumerState { return coin.ticker.toUpperCase() == ticker.toUpperCase(); } + Future update() async { + final uuid = const Uuid().v1(); + _latestUuid = uuid; + _addUpdate(uuid); + for (final exchange in exchanges) { + ref.read(efEstimatesListProvider(exchange.name).notifier).state = null; + } + + final reversed = ref.read(efReversedProvider); + final amount = reversed + ? ref.read(efReceiveAmountProvider) + : ref.read(efSendAmountProvider); + + final pair = ref.read(efCurrencyPairProvider); + if (amount == null || + amount <= Decimal.zero || + pair.send == null || + pair.receive == null) { + _removeUpdate(uuid); + return; + } + final rateType = ref.read(efRateTypeProvider); + final Map>, Range?>> + results = {}; + + for (final exchange in exchanges) { + final sendCurrency = pair.send?.forExchange(exchange.name); + final receiveCurrency = pair.receive?.forExchange(exchange.name); + + if (sendCurrency != null && receiveCurrency != null) { + final rangeResponse = await exchange.getRange( + reversed ? receiveCurrency.ticker : sendCurrency.ticker, + reversed ? sendCurrency.ticker : receiveCurrency.ticker, + rateType == ExchangeRateType.fixed, + ); + + final estimateResponse = await exchange.getEstimates( + sendCurrency.ticker, + receiveCurrency.ticker, + amount, + rateType == ExchangeRateType.fixed, + reversed, + ); + + results.addAll( + { + exchange.name: Tuple2( + estimateResponse, + rangeResponse.value, + ), + }, + ); + } + } + + for (final exchange in exchanges) { + if (uuid == _latestUuid) { + ref.read(efEstimatesListProvider(exchange.name).notifier).state = + results[exchange.name]; + } + } + + _removeUpdate(uuid); + } + + String? _latestUuid; + final Set _uuids = {}; + + void _addUpdate(String uuid) { + _uuids.add(uuid); + ref.read(efRefreshingProvider.notifier).state = true; + } + + void _removeUpdate(String uuid) { + _uuids.remove(uuid); + if (_uuids.isEmpty) { + WidgetsBinding.instance.addPostFrameCallback((_) { + ref.read(efRefreshingProvider.notifier).state = false; + }); + } + } + + void updateSend(Estimate? estimate) { + ref.read(efSendAmountProvider.notifier).state = estimate?.estimatedAmount; + } + + void updateReceive(Estimate? estimate) { + ref.read(efReceiveAmountProvider.notifier).state = + estimate?.estimatedAmount; + } + @override void initState() { _sendController = TextEditingController(); @@ -641,9 +706,40 @@ class _ExchangeFormState extends ConsumerState { coin = widget.coin; walletInitiated = walletId != null && coin != null; + _sendFocusNode.addListener(() { + if (_sendFocusNode.hasFocus) { + final reversed = ref.read(efReversedProvider); + WidgetsBinding.instance.addPostFrameCallback((_) { + ref.read(efReversedProvider.notifier).state = false; + if (reversed == true) { + update(); + } + }); + } + }); + _receiveFocusNode.addListener(() { + if (_receiveFocusNode.hasFocus && + ref.read(efExchangeProvider).name != ChangeNowExchange.exchangeName) { + final reversed = ref.read(efReversedProvider); + WidgetsBinding.instance.addPostFrameCallback((_) { + ref.read(efReversedProvider.notifier).state = true; + if (reversed != true) { + update(); + } + }); + } + }); + if (walletInitiated) { WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - ref.read(exchangeFormStateProvider).reset(shouldNotifyListeners: true); + ref.read(efSendAmountProvider.notifier).state = null; + ref.read(efReceiveAmountProvider.notifier).state = null; + ref.read(efReversedProvider.notifier).state = false; + ref.read(efRefreshingProvider.notifier).state = false; + ref.read(efCurrencyPairProvider).setSend(null, notifyListeners: true); + ref + .read(efCurrencyPairProvider) + .setReceive(null, notifyListeners: true); ExchangeDataLoadingService.instance .getAggregateCurrency( widget.contract == null ? coin!.ticker : widget.contract!.symbol, @@ -652,17 +748,17 @@ class _ExchangeFormState extends ConsumerState { ) .then((value) { if (value != null) { - ref.read(exchangeFormStateProvider).updateSendCurrency(value, true); + ref.read(efCurrencyPairProvider).setSend( + value, + notifyListeners: true, + ); } }); }); } else { - _sendController.text = - ref.read(exchangeFormStateProvider).fromAmountString; - _receiveController.text = - ref.read(exchangeFormStateProvider).toAmountString; WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - ref.read(exchangeFormStateProvider).refresh(); + _sendController.text = ref.read(efSendAmountStringProvider); + _receiveController.text = ref.read(efReceiveAmountStringProvider); }); } @@ -673,6 +769,8 @@ class _ExchangeFormState extends ConsumerState { void dispose() { _receiveController.dispose(); _sendController.dispose(); + _receiveFocusNode.dispose(); + _sendFocusNode.dispose(); super.dispose(); } @@ -680,34 +778,42 @@ class _ExchangeFormState extends ConsumerState { Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); - final rateType = ref.watch( - exchangeFormStateProvider.select((value) => value.exchangeRateType)); + final rateType = ref.watch(efRateTypeProvider); final isEstimated = rateType == ExchangeRateType.estimated; - ref.listen( - exchangeFormStateProvider.select((value) => value.toAmountString), - (previous, String next) { + ref.listen(efReceiveAmountStringProvider, (previous, String next) { if (!_receiveFocusNode.hasFocus) { _receiveController.text = isEstimated && next.isEmpty ? "-" : next; - if (_swapLock) { - _sendController.text = - ref.read(exchangeFormStateProvider).fromAmountString; - } + // if (_swapLock) { + _sendController.text = ref.read(efSendAmountStringProvider); + // } } }); - ref.listen( - exchangeFormStateProvider.select((value) => value.fromAmountString), - (previous, String next) { + ref.listen(efSendAmountStringProvider, (previous, String next) { if (!_sendFocusNode.hasFocus) { _sendController.text = next; - if (_swapLock) { - _receiveController.text = isEstimated - ? ref.read(exchangeFormStateProvider).toAmountString.isEmpty - ? "-" - : ref.read(exchangeFormStateProvider).toAmountString - : ref.read(exchangeFormStateProvider).toAmountString; - } + // if (_swapLock) { + _receiveController.text = + isEstimated && ref.read(efReceiveAmountStringProvider).isEmpty + ? "-" + : ref.read(efReceiveAmountStringProvider); + // } + } + }); + + ref.listen(efEstimateProvider.notifier, (previous, next) { + final estimate = (next as StateController).state; + if (ref.read(efReversedProvider)) { + updateSend(estimate); + } else { + updateReceive(estimate); + } + }); + + ref.listen(efCurrencyPairProvider, (previous, next) { + if (!_swapLock) { + update(); } }); @@ -725,8 +831,9 @@ class _ExchangeFormState extends ConsumerState { height: isDesktop ? 10 : 4, ), ExchangeTextField( - key: Key( - "exchangeTextFieldKeyFor_${Theme.of(context).extension()!.themeType.name}"), + key: Key("exchangeTextFieldKeyFor_" + "${Theme.of(context).extension()!.themeType.name}" + "${ref.watch(efCurrencyPairProvider.select((value) => value.send?.ticker))}"), controller: _sendController, focusNode: _sendFocusNode, textStyle: STextStyles.smallMed14(context).copyWith( @@ -745,8 +852,8 @@ class _ExchangeFormState extends ConsumerState { onChanged: sendFieldOnChanged, onButtonTap: selectSendCurrency, isWalletCoin: isWalletCoin(coin, true), - currency: ref.watch( - exchangeFormStateProvider.select((value) => value.sendCurrency)), + currency: + ref.watch(efCurrencyPairProvider.select((value) => value.send)), ), SizedBox( height: isDesktop ? 10 : 4, @@ -754,17 +861,6 @@ class _ExchangeFormState extends ConsumerState { SizedBox( height: isDesktop ? 10 : 4, ), - if (ref - .watch( - exchangeFormStateProvider.select((value) => value.warning)) - .isNotEmpty && - !ref.watch( - exchangeFormStateProvider.select((value) => value.reversed))) - Text( - ref.watch( - exchangeFormStateProvider.select((value) => value.warning)), - style: STextStyles.errorSmall(context), - ), Row( crossAxisAlignment: CrossAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -809,7 +905,7 @@ class _ExchangeFormState extends ConsumerState { ), ), ), - ) + ), ), ], ), @@ -829,34 +925,24 @@ class _ExchangeFormState extends ConsumerState { borderRadius: Constants.size.circularBorderRadius, background: Theme.of(context).extension()!.textFieldDefaultBG, - onTap: () { - if (!(ref.read(exchangeFormStateProvider).exchangeRateType == - ExchangeRateType.estimated) && - _receiveController.text == "-") { - _receiveController.text = ""; - } - }, + onTap: rateType == ExchangeRateType.estimated && + ref.watch(efExchangeProvider).name == + ChangeNowExchange.exchangeName + ? null + : () { + if (_sendController.text == "-") { + _sendController.text = ""; + } + }, onChanged: receiveFieldOnChanged, onButtonTap: selectReceiveCurrency, isWalletCoin: isWalletCoin(coin, true), - currency: ref.watch(exchangeFormStateProvider - .select((value) => value.receiveCurrency)), - readOnly: (rateType) == ExchangeRateType.estimated && - ref.watch(exchangeFormStateProvider - .select((value) => value.exchange.name)) == + currency: ref + .watch(efCurrencyPairProvider.select((value) => value.receive)), + readOnly: rateType == ExchangeRateType.estimated && + ref.watch(efExchangeProvider).name == ChangeNowExchange.exchangeName, ), - if (ref - .watch( - exchangeFormStateProvider.select((value) => value.warning)) - .isNotEmpty && - ref.watch( - exchangeFormStateProvider.select((value) => value.reversed))) - Text( - ref.watch( - exchangeFormStateProvider.select((value) => value.warning)), - style: STextStyles.errorSmall(context), - ), SizedBox( height: isDesktop ? 20 : 12, ), @@ -867,27 +953,19 @@ class _ExchangeFormState extends ConsumerState { onChanged: onRateTypeChanged, ), ), - // these reads should be watch - if (ref.watch(exchangeFormStateProvider).sendAmount != null && - ref.watch(exchangeFormStateProvider).sendAmount != Decimal.zero) - SizedBox( - height: isDesktop ? 20 : 12, - ), - // these reads should be watch - if (ref.watch(exchangeFormStateProvider).sendAmount != null && - ref.watch(exchangeFormStateProvider).sendAmount != Decimal.zero) - ExchangeProviderOptions( + Padding( + padding: EdgeInsets.only(top: isDesktop ? 20 : 12), + child: ExchangeProviderOptions( fixedRate: rateType == ExchangeRateType.fixed, - reversed: ref.watch( - exchangeFormStateProvider.select((value) => value.reversed)), + reversed: ref.watch(efReversedProvider), ), + ), SizedBox( height: isDesktop ? 20 : 12, ), PrimaryButton( buttonHeight: isDesktop ? ButtonHeight.l : null, - enabled: ref.watch( - exchangeFormStateProvider.select((value) => value.canExchange)), + enabled: ref.watch(efCanExchangeProvider), onPressed: onExchangePressed, label: "Swap", ) diff --git a/lib/pages/exchange_view/exchange_step_views/step_2_view.dart b/lib/pages/exchange_view/exchange_step_views/step_2_view.dart index a32df6e74..315228e13 100644 --- a/lib/pages/exchange_view/exchange_step_views/step_2_view.dart +++ b/lib/pages/exchange_view/exchange_step_views/step_2_view.dart @@ -124,9 +124,8 @@ class _Step2ViewState extends ConsumerState { @override Widget build(BuildContext context) { - final supportsRefund = ref.watch( - exchangeFormStateProvider.select((value) => value.exchange.name)) != - MajesticBankExchange.exchangeName; + final supportsRefund = + ref.watch(efExchangeProvider).name != MajesticBankExchange.exchangeName; return Background( child: Scaffold( diff --git a/lib/pages/exchange_view/exchange_step_views/step_3_view.dart b/lib/pages/exchange_view/exchange_step_views/step_3_view.dart index 22c356b5d..1079621e6 100644 --- a/lib/pages/exchange_view/exchange_step_views/step_3_view.dart +++ b/lib/pages/exchange_view/exchange_step_views/step_3_view.dart @@ -52,9 +52,8 @@ class _Step3ViewState extends ConsumerState { @override Widget build(BuildContext context) { - final supportsRefund = ref.watch( - exchangeFormStateProvider.select((value) => value.exchange.name)) != - MajesticBankExchange.exchangeName; + final supportsRefund = + ref.watch(efExchangeProvider).name != MajesticBankExchange.exchangeName; return Background( child: Scaffold( @@ -254,8 +253,7 @@ class _Step3ViewState extends ConsumerState { final ExchangeResponse response = await ref - .read(exchangeFormStateProvider) - .exchange + .read(efExchangeProvider) .createTrade( from: model.sendTicker, to: model.receiveTicker, @@ -271,24 +269,26 @@ class _Step3ViewState extends ConsumerState { ? model.refundAddress! : "", refundExtraId: "", - rateId: model.rateId, + estimate: model.estimate, reversed: model.reversed, ); if (response.value == null) { if (mounted) { Navigator.of(context).pop(); - } - unawaited(showDialog( - context: context, - barrierDismissible: true, - builder: (_) => StackDialog( - title: "Failed to create trade", - message: - response.exception?.toString(), - ), - )); + unawaited( + showDialog( + context: context, + barrierDismissible: true, + builder: (_) => StackDialog( + title: "Failed to create trade", + message: response.exception + ?.toString(), + ), + ), + ); + } return; } diff --git a/lib/pages/exchange_view/exchange_step_views/step_4_view.dart b/lib/pages/exchange_view/exchange_step_views/step_4_view.dart index b7bdf9e46..094e343d8 100644 --- a/lib/pages/exchange_view/exchange_step_views/step_4_view.dart +++ b/lib/pages/exchange_view/exchange_step_views/step_4_view.dart @@ -70,10 +70,8 @@ class _Step4ViewState extends ConsumerState { } Future _updateStatus() async { - final statusResponse = await ref - .read(exchangeFormStateProvider) - .exchange - .updateTrade(model.trade!); + final statusResponse = + await ref.read(efExchangeProvider).updateTrade(model.trade!); String status = "Waiting"; if (statusResponse.value != null) { status = statusResponse.value!.status; diff --git a/lib/pages/exchange_view/exchange_view.dart b/lib/pages/exchange_view/exchange_view.dart index 61cf2fe94..a17764b91 100644 --- a/lib/pages/exchange_view/exchange_view.dart +++ b/lib/pages/exchange_view/exchange_view.dart @@ -37,7 +37,8 @@ class _ExchangeViewState extends ConsumerState { ExchangeDataLoadingService.instance.onLoadingComplete = () { WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { await ExchangeDataLoadingService.instance.setCurrenciesIfEmpty( - ref.read(exchangeFormStateProvider), + ref.read(efCurrencyPairProvider), + ref.read(efRateTypeProvider), ); setState(() { _initialCachePopulationUnderway = false; @@ -53,7 +54,8 @@ class _ExchangeViewState extends ConsumerState { ExchangeDataLoadingService.instance.onLoadingComplete = () { WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { await ExchangeDataLoadingService.instance.setCurrenciesIfEmpty( - ref.read(exchangeFormStateProvider), + ref.read(efCurrencyPairProvider), + ref.read(efRateTypeProvider), ); setState(() { _initialCachePopulationUnderway = false; diff --git a/lib/pages/exchange_view/sub_widgets/exchange_provider_option.dart b/lib/pages/exchange_view/sub_widgets/exchange_provider_option.dart new file mode 100644 index 000000000..1212caf49 --- /dev/null +++ b/lib/pages/exchange_view/sub_widgets/exchange_provider_option.dart @@ -0,0 +1,349 @@ +import 'package:decimal/decimal.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/models/exchange/response_objects/estimate.dart'; +import 'package:stackwallet/providers/exchange/exchange_form_state_provider.dart'; +import 'package:stackwallet/providers/global/locale_provider.dart'; +import 'package:stackwallet/services/exchange/exchange.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/enums/exchange_rate_type_enum.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/animated_text.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; +import 'package:stackwallet/widgets/exchange/trocador/trocador_kyc_info_button.dart'; +import 'package:stackwallet/widgets/exchange/trocador/trocador_rating_type_enum.dart'; + +class ExchangeOption extends ConsumerStatefulWidget { + const ExchangeOption({ + Key? key, + required this.exchange, + required this.fixedRate, + required this.reversed, + }) : super(key: key); + + final Exchange exchange; + final bool fixedRate; + final bool reversed; + + @override + ConsumerState createState() => _ExchangeOptionState(); +} + +class _ExchangeOptionState extends ConsumerState { + final isDesktop = Util.isDesktop; + + @override + Widget build(BuildContext context) { + final sendCurrency = + ref.watch(efCurrencyPairProvider.select((value) => value.send)); + final receivingCurrency = + ref.watch(efCurrencyPairProvider.select((value) => value.receive)); + final reversed = ref.watch(efReversedProvider); + final amount = reversed + ? ref.watch(efReceiveAmountProvider) + : ref.watch(efSendAmountProvider); + + final data = ref.watch(efEstimatesListProvider(widget.exchange.name)); + final estimates = data?.item1.value; + + return AnimatedSize( + duration: const Duration(milliseconds: 500), + curve: Curves.easeInOutCubicEmphasized, + child: Builder( + builder: (_) { + if (ref.watch(efRefreshingProvider)) { + // show loading + return _ProviderOption( + exchange: widget.exchange, + estimate: null, + rateString: "", + loadingString: true, + ); + } else if (sendCurrency != null && + receivingCurrency != null && + amount != null && + amount > Decimal.zero) { + if (estimates != null && estimates.isNotEmpty) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + for (int i = 0; i < estimates.length; i++) + Builder( + builder: (context) { + final e = estimates[i]; + + int decimals; + try { + decimals = coinFromTickerCaseInsensitive( + receivingCurrency.ticker) + .decimals; + } catch (_) { + decimals = 8; // some reasonable alternative + } + Amount rate; + if (e.reversed) { + rate = (amount / e.estimatedAmount) + .toDecimal(scaleOnInfinitePrecision: 18) + .toAmount(fractionDigits: decimals); + } else { + rate = (e.estimatedAmount / amount) + .toDecimal(scaleOnInfinitePrecision: 18) + .toAmount(fractionDigits: decimals); + } + + final rateString = + "1 ${sendCurrency.ticker.toUpperCase()} ~ ${rate.localizedStringAsFixed( + locale: ref.watch( + localeServiceChangeNotifierProvider + .select((value) => value.locale), + ), + )} ${receivingCurrency.ticker.toUpperCase()}"; + + return ConditionalParent( + condition: i > 0, + builder: (child) => Column( + mainAxisSize: MainAxisSize.min, + children: [ + isDesktop + ? Container( + height: 1, + color: Theme.of(context) + .extension()! + .background, + ) + : const SizedBox( + height: 16, + ), + child, + ], + ), + child: _ProviderOption( + key: Key(widget.exchange.name + e.exchangeProvider), + exchange: widget.exchange, + estimate: e, + rateString: rateString, + kycRating: e.kycRating, + ), + ); + }, + ), + ], + ); + } else { + Logging.instance.log( + "$runtimeType rate unavailable for ${widget.exchange.name}: $data", + level: LogLevel.Warning, + ); + + return Consumer( + builder: (_, ref, __) { + String? message; + + final range = data?.item2; + if (range != null) { + if (range.min != null && amount < range.min!) { + message ??= "Amount too small"; + } else if (range.max != null && amount > range.max!) { + message ??= "Amount too large"; + } + } else if (data?.item1.value == null) { + final rateType = ref.watch(efRateTypeProvider) == + ExchangeRateType.estimated + ? "estimated" + : "fixed"; + message ??= "Pair unavailable on $rateType rate flow"; + } + + return _ProviderOption( + exchange: widget.exchange, + estimate: null, + rateString: message ?? "Failed to fetch rate", + rateColor: + Theme.of(context).extension()!.textError, + ); + }, + ); + } + } else { + // show n/a + return _ProviderOption( + exchange: widget.exchange, + estimate: null, + rateString: "n/a", + ); + } + }, + ), + ); + } +} + +class _ProviderOption extends ConsumerStatefulWidget { + const _ProviderOption({ + Key? key, + required this.exchange, + required this.estimate, + required this.rateString, + this.kycRating, + this.loadingString = false, + this.rateColor, + }) : super(key: key); + + final Exchange exchange; + final Estimate? estimate; + final String rateString; + final String? kycRating; + final bool loadingString; + final Color? rateColor; + + @override + ConsumerState<_ProviderOption> createState() => _ProviderOptionState(); +} + +class _ProviderOptionState extends ConsumerState<_ProviderOption> { + final isDesktop = Util.isDesktop; + + late final String _id; + + @override + void initState() { + _id = + "${widget.exchange.name} (${widget.estimate?.exchangeProvider ?? widget.exchange.name})"; + super.initState(); + } + + @override + Widget build(BuildContext context) { + String groupValue = ref.watch(currentCombinedExchangeIdProvider); + + if (ref.watch(efExchangeProvider).name == + (widget.estimate?.exchangeProvider ?? widget.exchange.name)) { + groupValue = _id; + } + + return ConditionalParent( + condition: isDesktop, + builder: (child) => MouseRegion( + cursor: SystemMouseCursors.click, + child: child, + ), + child: GestureDetector( + onTap: () { + ref.read(efExchangeProvider.notifier).state = widget.exchange; + ref.read(efExchangeProviderNameProvider.notifier).state = + widget.estimate?.exchangeProvider ?? widget.exchange.name; + }, + child: Container( + color: Colors.transparent, + child: Padding( + padding: + isDesktop ? const EdgeInsets.all(16) : const EdgeInsets.all(0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 20, + height: 20, + child: Padding( + padding: EdgeInsets.only(top: isDesktop ? 20.0 : 15.0), + child: Radio( + activeColor: Theme.of(context) + .extension()! + .radioButtonIconEnabled, + value: _id, + groupValue: groupValue, + onChanged: (_) { + ref.read(efExchangeProvider.notifier).state = + widget.exchange; + ref + .read(efExchangeProviderNameProvider.notifier) + .state = + widget.estimate?.exchangeProvider ?? + widget.exchange.name; + }, + ), + ), + ), + const SizedBox( + width: 14, + ), + Padding( + padding: const EdgeInsets.only(top: 5.0), + child: SizedBox( + width: isDesktop ? 32 : 24, + height: isDesktop ? 32 : 24, + child: SvgPicture.asset( + Assets.exchange.getIconFor( + exchangeName: widget.exchange.name, + ), + width: isDesktop ? 32 : 24, + height: isDesktop ? 32 : 24, + ), + ), + ), + const SizedBox( + width: 10, + ), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.estimate?.exchangeProvider ?? + widget.exchange.name, + style: STextStyles.titleBold12(context).copyWith( + color: Theme.of(context) + .extension()! + .textDark2, + ), + ), + widget.loadingString + ? AnimatedText( + stringsToLoopThrough: const [ + "Loading", + "Loading.", + "Loading..", + "Loading...", + ], + style: + STextStyles.itemSubtitle12(context).copyWith( + color: Theme.of(context) + .extension()! + .textSubtitle1, + ), + ) + : Text( + widget.rateString, + style: + STextStyles.itemSubtitle12(context).copyWith( + color: widget.rateColor ?? + Theme.of(context) + .extension()! + .textSubtitle1, + ), + ), + ], + ), + ), + if (widget.kycRating != null) + TrocadorKYCInfoButton( + kycType: TrocadorKYCType.fromString( + widget.kycRating!, + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/pages/exchange_view/sub_widgets/exchange_provider_options.dart b/lib/pages/exchange_view/sub_widgets/exchange_provider_options.dart index f770278f3..a145d36a4 100644 --- a/lib/pages/exchange_view/sub_widgets/exchange_provider_options.dart +++ b/lib/pages/exchange_view/sub_widgets/exchange_provider_options.dart @@ -1,24 +1,13 @@ -import 'package:decimal/decimal.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_svg/svg.dart'; -import 'package:stackwallet/exceptions/exchange/pair_unavailable_exception.dart'; import 'package:stackwallet/models/exchange/aggregate_currency.dart'; -import 'package:stackwallet/models/exchange/response_objects/estimate.dart'; +import 'package:stackwallet/pages/exchange_view/sub_widgets/exchange_provider_option.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/services/exchange/change_now/change_now_exchange.dart'; -import 'package:stackwallet/services/exchange/exchange_response.dart'; import 'package:stackwallet/services/exchange/majestic_bank/majestic_bank_exchange.dart'; -import 'package:stackwallet/utilities/amount/amount.dart'; -import 'package:stackwallet/utilities/assets.dart'; -import 'package:stackwallet/utilities/enums/coin_enum.dart'; -import 'package:stackwallet/utilities/logger.dart'; -import 'package:stackwallet/utilities/show_loading.dart'; -import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/services/exchange/trocador/trocador_exchange.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; -import 'package:stackwallet/widgets/animated_text.dart'; -import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; class ExchangeProviderOptions extends ConsumerStatefulWidget { @@ -60,11 +49,10 @@ class _ExchangeProviderOptionsState @override Widget build(BuildContext context) { - final sendCurrency = ref.watch(exchangeFormStateProvider).sendCurrency; + final sendCurrency = + ref.watch(efCurrencyPairProvider.select((value) => value.send)); final receivingCurrency = - ref.watch(exchangeFormStateProvider).receiveCurrency; - final fromAmount = ref.watch(exchangeFormStateProvider).sendAmount; - final toAmount = ref.watch(exchangeFormStateProvider).receiveAmount; + ref.watch(efCurrencyPairProvider.select((value) => value.receive)); final showChangeNow = exchangeSupported( exchangeName: ChangeNowExchange.exchangeName, @@ -76,6 +64,11 @@ class _ExchangeProviderOptionsState sendCurrency: sendCurrency, receiveCurrency: receivingCurrency, ); + final showTrocador = exchangeSupported( + exchangeName: TrocadorExchange.exchangeName, + sendCurrency: sendCurrency, + receiveCurrency: receivingCurrency, + ); return RoundedWhiteContainer( padding: isDesktop ? const EdgeInsets.all(0) : const EdgeInsets.all(12), @@ -85,239 +78,11 @@ class _ExchangeProviderOptionsState child: Column( children: [ if (showChangeNow) - ConditionalParent( - condition: isDesktop, - builder: (child) => MouseRegion( - cursor: SystemMouseCursors.click, - child: child, - ), - child: GestureDetector( - onTap: () { - if (ref.read(exchangeFormStateProvider).exchange.name != - ChangeNowExchange.exchangeName) { - showLoading( - whileFuture: - ref.read(exchangeFormStateProvider).updateExchange( - exchange: ChangeNowExchange.instance, - shouldUpdateData: true, - shouldNotifyListeners: true, - ), - context: context, - message: "Updating rates", - isDesktop: isDesktop, - ); - } - }, - child: Container( - color: Colors.transparent, - child: Padding( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(0), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: 20, - height: 20, - child: Padding( - padding: - EdgeInsets.only(top: isDesktop ? 20.0 : 15.0), - child: Radio( - activeColor: Theme.of(context) - .extension()! - .radioButtonIconEnabled, - value: ChangeNowExchange.exchangeName, - groupValue: ref.watch(exchangeFormStateProvider - .select((value) => value.exchange.name)), - onChanged: (_) { - if (ref - .read(exchangeFormStateProvider) - .exchange - .name != - ChangeNowExchange.exchangeName) { - ref - .read(exchangeFormStateProvider) - .updateExchange( - exchange: ChangeNowExchange.instance, - shouldUpdateData: true, - shouldNotifyListeners: true, - ); - } - }, - ), - ), - ), - const SizedBox( - width: 14, - ), - Padding( - padding: const EdgeInsets.only(top: 5.0), - child: SizedBox( - width: isDesktop ? 32 : 24, - height: isDesktop ? 32 : 24, - child: SvgPicture.asset( - Assets.exchange.changeNow, - width: isDesktop ? 32 : 24, - height: isDesktop ? 32 : 24, - ), - ), - ), - const SizedBox( - width: 10, - ), - Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - ChangeNowExchange.exchangeName, - style: - STextStyles.titleBold12(context).copyWith( - color: Theme.of(context) - .extension()! - .textDark2, - ), - ), - if (sendCurrency != null && - receivingCurrency != null && - toAmount != null && - toAmount > Decimal.zero && - fromAmount != null && - fromAmount > Decimal.zero) - FutureBuilder( - future: - ChangeNowExchange.instance.getEstimate( - sendCurrency.ticker, - receivingCurrency.ticker, - widget.reversed ? toAmount : fromAmount, - widget.fixedRate, - widget.reversed, - ), - builder: (context, - AsyncSnapshot> - snapshot) { - if (snapshot.connectionState == - ConnectionState.done && - snapshot.hasData) { - final estimate = snapshot.data?.value; - if (estimate != null) { - Coin coin; - try { - coin = coinFromTickerCaseInsensitive( - receivingCurrency.ticker); - } catch (_) { - coin = Coin.bitcoin; - } - Amount rate; - if (estimate.reversed) { - rate = (toAmount / - estimate.estimatedAmount) - .toDecimal( - scaleOnInfinitePrecision: 18) - .toAmount( - fractionDigits: - coin.decimals); - } else { - rate = (estimate.estimatedAmount / - fromAmount) - .toDecimal( - scaleOnInfinitePrecision: 18) - .toAmount( - fractionDigits: - coin.decimals); - } - - return Text( - "1 ${sendCurrency.ticker.toUpperCase()} ~ ${rate.localizedStringAsFixed( - locale: ref.watch( - localeServiceChangeNotifierProvider - .select( - (value) => value.locale), - ), - )} ${receivingCurrency.ticker.toUpperCase()}", - style: STextStyles.itemSubtitle12( - context) - .copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, - ), - ); - } else if (snapshot.data?.exception - is PairUnavailableException) { - return Text( - "Unsupported pair", - style: STextStyles.itemSubtitle12( - context) - .copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, - ), - ); - } else { - Logging.instance.log( - "$runtimeType failed to fetch rate for ChangeNOW: ${snapshot.data}", - level: LogLevel.Warning, - ); - return Text( - "Failed to fetch rate", - style: STextStyles.itemSubtitle12( - context) - .copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, - ), - ); - } - } else { - return AnimatedText( - stringsToLoopThrough: const [ - "Loading", - "Loading.", - "Loading..", - "Loading...", - ], - style: - STextStyles.itemSubtitle12(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, - ), - ); - } - }, - ), - if (!(sendCurrency != null && - receivingCurrency != null && - toAmount != null && - toAmount > Decimal.zero && - fromAmount != null && - fromAmount > Decimal.zero)) - Text( - "n/a", - style: STextStyles.itemSubtitle12(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, - ), - ), - ], - ), - ), - ], - ), - ), - ), - ), + ExchangeOption( + exchange: ChangeNowExchange.instance, + fixedRate: widget.fixedRate, + reversed: widget.reversed, ), - if (showChangeNow && showMajesticBank) isDesktop ? Container( @@ -328,446 +93,28 @@ class _ExchangeProviderOptionsState : const SizedBox( height: 16, ), - if (showMajesticBank) - ConditionalParent( - condition: isDesktop, - builder: (child) => MouseRegion( - cursor: SystemMouseCursors.click, - child: child, - ), - child: GestureDetector( - onTap: () { - if (ref.read(exchangeFormStateProvider).exchange.name != - MajesticBankExchange.exchangeName) { - showLoading( - whileFuture: - ref.read(exchangeFormStateProvider).updateExchange( - exchange: MajesticBankExchange.instance, - shouldUpdateData: true, - shouldNotifyListeners: true, - ), - context: context, - isDesktop: isDesktop, - message: "Updating rates", - ); - } - }, - child: Container( - color: Colors.transparent, - child: Padding( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(0), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: 20, - height: 20, - child: Padding( - padding: - EdgeInsets.only(top: isDesktop ? 20.0 : 15.0), - child: Radio( - activeColor: Theme.of(context) - .extension()! - .radioButtonIconEnabled, - value: MajesticBankExchange.exchangeName, - groupValue: ref.watch(exchangeFormStateProvider - .select((value) => value.exchange.name)), - onChanged: (_) { - if (ref - .read(exchangeFormStateProvider) - .exchange - .name != - MajesticBankExchange.exchangeName) { - ref - .read(exchangeFormStateProvider) - .updateExchange( - exchange: MajesticBankExchange.instance, - shouldUpdateData: true, - shouldNotifyListeners: true, - ); - } - }, - ), - ), - ), - const SizedBox( - width: 14, - ), - Padding( - padding: const EdgeInsets.only(top: 5.0), - child: SizedBox( - width: isDesktop ? 32 : 24, - height: isDesktop ? 32 : 24, - child: SvgPicture.asset( - Assets.exchange.majesticBankBlue, - width: isDesktop ? 32 : 24, - height: isDesktop ? 32 : 24, - ), - ), - ), - const SizedBox( - width: 10, - ), - Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - MajesticBankExchange.exchangeName, - style: - STextStyles.titleBold12(context).copyWith( - color: Theme.of(context) - .extension()! - .textDark2, - ), - ), - if (sendCurrency != null && - receivingCurrency != null && - toAmount != null && - toAmount > Decimal.zero && - fromAmount != null && - fromAmount > Decimal.zero) - FutureBuilder( - future: - MajesticBankExchange.instance.getEstimate( - sendCurrency.ticker, - receivingCurrency.ticker, - widget.reversed ? toAmount : fromAmount, - widget.fixedRate, - widget.reversed, - ), - builder: (context, - AsyncSnapshot> - snapshot) { - if (snapshot.connectionState == - ConnectionState.done && - snapshot.hasData) { - final estimate = snapshot.data?.value; - if (estimate != null) { - Coin coin; - try { - coin = coinFromTickerCaseInsensitive( - receivingCurrency.ticker); - } catch (_) { - coin = Coin.bitcoin; - } - Amount rate; - if (estimate.reversed) { - rate = (toAmount / - estimate.estimatedAmount) - .toDecimal( - scaleOnInfinitePrecision: 18) - .toAmount( - fractionDigits: coin.decimals, - ); - } else { - rate = (estimate.estimatedAmount / - fromAmount) - .toDecimal( - scaleOnInfinitePrecision: 18) - .toAmount( - fractionDigits: coin.decimals, - ); - } - - return Text( - "1 ${sendCurrency.ticker.toUpperCase()} ~ ${rate.localizedStringAsFixed( - locale: ref.watch( - localeServiceChangeNotifierProvider - .select( - (value) => value.locale), - ), - )} ${receivingCurrency.ticker.toUpperCase()}", - style: STextStyles.itemSubtitle12( - context) - .copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, - ), - ); - } else if (snapshot.data?.exception - is PairUnavailableException) { - return Text( - "Unsupported pair", - style: STextStyles.itemSubtitle12( - context) - .copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, - ), - ); - } else { - Logging.instance.log( - "$runtimeType failed to fetch rate for ChangeNOW: ${snapshot.data}", - level: LogLevel.Warning, - ); - return Text( - "Failed to fetch rate", - style: STextStyles.itemSubtitle12( - context) - .copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, - ), - ); - } - } else { - return AnimatedText( - stringsToLoopThrough: const [ - "Loading", - "Loading.", - "Loading..", - "Loading...", - ], - style: - STextStyles.itemSubtitle12(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, - ), - ); - } - }, - ), - if (!(sendCurrency != null && - receivingCurrency != null && - toAmount != null && - toAmount > Decimal.zero && - fromAmount != null && - fromAmount > Decimal.zero)) - Text( - "n/a", - style: STextStyles.itemSubtitle12(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, - ), - ), - ], - ), - ), - ], - ), - ), - ), - ), + ExchangeOption( + exchange: MajesticBankExchange.instance, + fixedRate: widget.fixedRate, + reversed: widget.reversed, + ), + if ((showChangeNow || showMajesticBank) && showTrocador) + isDesktop + ? Container( + height: 1, + color: + Theme.of(context).extension()!.background, + ) + : const SizedBox( + height: 16, + ), + if (showTrocador) + ExchangeOption( + fixedRate: widget.fixedRate, + reversed: widget.reversed, + exchange: TrocadorExchange.instance, ), - // if (isDesktop) - // Container( - // height: 1, - // color: Theme.of(context).extension()!.background, - // ), - // if (!isDesktop) - // const SizedBox( - // height: 16, - // ), - // ConditionalParent( - // condition: isDesktop, - // builder: (child) => MouseRegion( - // cursor: SystemMouseCursors.click, - // child: child, - // ), - // child: GestureDetector( - // onTap: () { - // if (ref.read(currentExchangeNameStateProvider.state).state != - // SimpleSwapExchange.exchangeName) { - // // ref.read(currentExchangeNameStateProvider.state).state = - // // SimpleSwapExchange.exchangeName; - // ref.read(exchangeFormStateProvider).exchange = - // Exchange.fromName(ref - // .read(currentExchangeNameStateProvider.state) - // .state); - // } - // }, - // child: Container( - // color: Colors.transparent, - // child: Padding( - // padding: isDesktop - // ? const EdgeInsets.all(16) - // : const EdgeInsets.all(0), - // child: Row( - // crossAxisAlignment: CrossAxisAlignment.start, - // children: [ - // SizedBox( - // width: 20, - // height: 20, - // child: Radio( - // activeColor: Theme.of(context) - // .extension()! - // .radioButtonIconEnabled, - // value: SimpleSwapExchange.exchangeName, - // groupValue: ref - // .watch(currentExchangeNameStateProvider.state) - // .state, - // onChanged: (value) { - // if (value is String) { - // ref - // .read(currentExchangeNameStateProvider.state) - // .state = value; - // ref.read(exchangeFormStateProvider).exchange = - // Exchange.fromName(ref - // .read(currentExchangeNameStateProvider - // .state) - // .state); - // } - // }, - // ), - // ), - // const SizedBox( - // width: 14, - // ), - // // SvgPicture.asset( - // // Assets.exchange.simpleSwap, - // // width: isDesktop ? 32 : 24, - // // height: isDesktop ? 32 : 24, - // // ), - // // const SizedBox( - // // width: 10, - // // ), - // // Expanded( - // // child: Column( - // // mainAxisAlignment: MainAxisAlignment.start, - // // mainAxisSize: MainAxisSize.min, - // // crossAxisAlignment: CrossAxisAlignment.start, - // // children: [ - // // Text( - // // SimpleSwapExchange.exchangeName, - // // style: STextStyles.titleBold12(context).copyWith( - // // color: Theme.of(context) - // // .extension()! - // // .textDark2, - // // ), - // // ), - // // if (from != null && - // // to != null && - // // toAmount != null && - // // toAmount! > Decimal.zero && - // // fromAmount != null && - // // fromAmount! > Decimal.zero) - // // FutureBuilder( - // // future: SimpleSwapExchange().getEstimate( - // // from!, - // // to!, - // // // reversed ? toAmount! : fromAmount!, - // // fromAmount!, - // // fixedRate, - // // // reversed, - // // false, - // // ), - // // builder: (context, - // // AsyncSnapshot> - // // snapshot) { - // // if (snapshot.connectionState == - // // ConnectionState.done && - // // snapshot.hasData) { - // // final estimate = snapshot.data?.value; - // // if (estimate != null) { - // // Decimal rate = (estimate.estimatedAmount / - // // fromAmount!) - // // .toDecimal( - // // scaleOnInfinitePrecision: 12); - // // - // // Coin coin; - // // try { - // // coin = - // // coinFromTickerCaseInsensitive(to!); - // // } catch (_) { - // // coin = Coin.bitcoin; - // // } - // // return Text( - // // "1 ${from!.toUpperCase()} ~ ${Format.localizedStringAsFixed( - // // value: rate, - // // locale: ref.watch( - // // localeServiceChangeNotifierProvider - // // .select( - // // (value) => value.locale), - // // ), - // // decimalPlaces: - // // Constants.decimalPlacesForCoin( - // // coin), - // // )} ${to!.toUpperCase()}", - // // style: - // // STextStyles.itemSubtitle12(context) - // // .copyWith( - // // color: Theme.of(context) - // // .extension()! - // // .textSubtitle1, - // // ), - // // ); - // // } else { - // // Logging.instance.log( - // // "$runtimeType failed to fetch rate for SimpleSwap: ${snapshot.data}", - // // level: LogLevel.Warning, - // // ); - // // return Text( - // // "Failed to fetch rate", - // // style: - // // STextStyles.itemSubtitle12(context) - // // .copyWith( - // // color: Theme.of(context) - // // .extension()! - // // .textSubtitle1, - // // ), - // // ); - // // } - // // } else { - // // return AnimatedText( - // // stringsToLoopThrough: const [ - // // "Loading", - // // "Loading.", - // // "Loading..", - // // "Loading...", - // // ], - // // style: STextStyles.itemSubtitle12(context) - // // .copyWith( - // // color: Theme.of(context) - // // .extension()! - // // .textSubtitle1, - // // ), - // // ); - // // } - // // }, - // // ), - // // // if (!(from != null && - // // // to != null && - // // // (reversed - // // // ? toAmount != null && toAmount! > Decimal.zero - // // // : fromAmount != null && - // // // fromAmount! > Decimal.zero))) - // // if (!(from != null && - // // to != null && - // // toAmount != null && - // // toAmount! > Decimal.zero && - // // fromAmount != null && - // // fromAmount! > Decimal.zero)) - // // Text( - // // "n/a", - // // style: STextStyles.itemSubtitle12(context) - // // .copyWith( - // // color: Theme.of(context) - // // .extension()! - // // .textSubtitle1, - // // ), - // // ), - // // ], - // // ), - // // ), - // ], - // ), - // ), - // ), - // ), - // ), ], ), ); diff --git a/lib/pages/exchange_view/sub_widgets/rate_type_toggle.dart b/lib/pages/exchange_view/sub_widgets/rate_type_toggle.dart index f85c0cfcd..b83883509 100644 --- a/lib/pages/exchange_view/sub_widgets/rate_type_toggle.dart +++ b/lib/pages/exchange_view/sub_widgets/rate_type_toggle.dart @@ -29,9 +29,7 @@ class RateTypeToggle extends ConsumerWidget { onChanged?.call(ExchangeRateType.estimated); } }, - isOn: ref.watch(exchangeFormStateProvider - .select((value) => value.exchangeRateType)) == - ExchangeRateType.fixed, + isOn: ref.watch(efRateTypeProvider) == ExchangeRateType.fixed, onColor: isDesktop ? Theme.of(context) .extension()! diff --git a/lib/pages/exchange_view/trade_details_view.dart b/lib/pages/exchange_view/trade_details_view.dart index f0cb7b7a8..5f8e9d471 100644 --- a/lib/pages/exchange_view/trade_details_view.dart +++ b/lib/pages/exchange_view/trade_details_view.dart @@ -20,6 +20,7 @@ import 'package:stackwallet/services/exchange/change_now/change_now_exchange.dar import 'package:stackwallet/services/exchange/exchange.dart'; import 'package:stackwallet/services/exchange/majestic_bank/majestic_bank_exchange.dart'; import 'package:stackwallet/services/exchange/simpleswap/simpleswap_exchange.dart'; +import 'package:stackwallet/services/exchange/trocador/trocador_exchange.dart'; import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/clipboard_interface.dart'; @@ -1205,6 +1206,13 @@ class _TradeDetailsViewState extends ConsumerState { url = "https://majesticbank.sc/track?trx=${trade.tradeId}"; break; + + default: + if (trade.exchangeName + .startsWith(TrocadorExchange.exchangeName)) { + url = + "https://trocador.app/en/checkout${trade.tradeId}"; + } } return ConditionalParent( condition: isDesktop, diff --git a/lib/pages/exchange_view/wallet_initiated_exchange_view.dart b/lib/pages/exchange_view/wallet_initiated_exchange_view.dart index 3c726b702..7085bd8aa 100644 --- a/lib/pages/exchange_view/wallet_initiated_exchange_view.dart +++ b/lib/pages/exchange_view/wallet_initiated_exchange_view.dart @@ -54,7 +54,8 @@ class _WalletInitiatedExchangeViewState ExchangeDataLoadingService.instance.onLoadingComplete = () { WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { await ExchangeDataLoadingService.instance.setCurrenciesIfEmpty( - ref.read(exchangeFormStateProvider), + ref.read(efCurrencyPairProvider), + ref.read(efRateTypeProvider), ); setState(() { _initialCachePopulationUnderway = false; @@ -70,7 +71,8 @@ class _WalletInitiatedExchangeViewState ExchangeDataLoadingService.instance.onLoadingComplete = () { WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { await ExchangeDataLoadingService.instance.setCurrenciesIfEmpty( - ref.read(exchangeFormStateProvider), + ref.read(efCurrencyPairProvider), + ref.read(efRateTypeProvider), ); setState(() { _initialCachePopulationUnderway = false; diff --git a/lib/pages_desktop_specific/desktop_exchange/desktop_all_trades_view.dart b/lib/pages_desktop_specific/desktop_exchange/desktop_all_trades_view.dart index 34d688b21..7d344f47d 100644 --- a/lib/pages_desktop_specific/desktop_exchange/desktop_all_trades_view.dart +++ b/lib/pages_desktop_specific/desktop_exchange/desktop_all_trades_view.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:isar/isar.dart'; +import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/models/exchange/change_now/exchange_transaction_status.dart'; import 'package:stackwallet/models/exchange/response_objects/trade.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart'; @@ -29,8 +30,6 @@ import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:stackwallet/widgets/textfield_icon_button.dart'; import 'package:tuple/tuple.dart'; -import '../../db/isar/main_db.dart'; - class DesktopAllTradesView extends ConsumerStatefulWidget { const DesktopAllTradesView({Key? key}) : super(key: key); diff --git a/lib/pages_desktop_specific/desktop_exchange/desktop_exchange_view.dart b/lib/pages_desktop_specific/desktop_exchange/desktop_exchange_view.dart index 17cd1c778..36006b7c1 100644 --- a/lib/pages_desktop_specific/desktop_exchange/desktop_exchange_view.dart +++ b/lib/pages_desktop_specific/desktop_exchange/desktop_exchange_view.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/pages/exchange_view/exchange_form.dart'; +import 'package:stackwallet/pages_desktop_specific/desktop_exchange/desktop_all_trades_view.dart'; import 'package:stackwallet/pages_desktop_specific/desktop_exchange/subwidgets/desktop_trade_history.dart'; import 'package:stackwallet/providers/exchange/exchange_form_state_provider.dart'; import 'package:stackwallet/providers/global/prefs_provider.dart'; @@ -14,8 +15,6 @@ import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart'; import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; -import 'desktop_all_trades_view.dart'; - class DesktopExchangeView extends ConsumerStatefulWidget { const DesktopExchangeView({Key? key}) : super(key: key); @@ -38,7 +37,8 @@ class _DesktopExchangeViewState extends ConsumerState { ExchangeDataLoadingService.instance.onLoadingComplete = () { WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { await ExchangeDataLoadingService.instance.setCurrenciesIfEmpty( - ref.read(exchangeFormStateProvider), + ref.read(efCurrencyPairProvider), + ref.read(efRateTypeProvider), ); setState(() { _initialCachePopulationUnderway = false; @@ -54,7 +54,8 @@ class _DesktopExchangeViewState extends ConsumerState { ExchangeDataLoadingService.instance.onLoadingComplete = () { WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { await ExchangeDataLoadingService.instance.setCurrenciesIfEmpty( - ref.read(exchangeFormStateProvider), + ref.read(efCurrencyPairProvider), + ref.read(efRateTypeProvider), ); setState(() { _initialCachePopulationUnderway = false; diff --git a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/step_scaffold.dart b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/step_scaffold.dart index 203ae2d9b..e99c63821 100644 --- a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/step_scaffold.dart +++ b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/step_scaffold.dart @@ -84,8 +84,7 @@ class _StepScaffoldState extends ConsumerState { ); final ExchangeResponse response = await ref - .read(exchangeFormStateProvider) - .exchange + .read(efExchangeProvider) .createTrade( from: ref.read(desktopExchangeModelProvider)!.sendTicker, to: ref.read(desktopExchangeModelProvider)!.receiveTicker, @@ -98,24 +97,24 @@ class _StepScaffoldState extends ConsumerState { extraId: null, addressRefund: ref.read(desktopExchangeModelProvider)!.refundAddress!, refundExtraId: "", - rateId: ref.read(desktopExchangeModelProvider)!.rateId, + estimate: ref.read(desktopExchangeModelProvider)!.estimate, reversed: ref.read(desktopExchangeModelProvider)!.reversed, ); if (response.value == null) { if (mounted) { Navigator.of(context).pop(); - } - unawaited( - showDialog( - context: context, - barrierDismissible: true, - builder: (_) => SimpleDesktopDialog( - title: "Failed to create trade", - message: response.exception?.toString() ?? ""), - ), - ); + unawaited( + showDialog( + context: context, + barrierDismissible: true, + builder: (_) => SimpleDesktopDialog( + title: "Failed to create trade", + message: response.exception?.toString() ?? ""), + ), + ); + } return false; } diff --git a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_1.dart b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_1.dart index 16c3b8116..dffe71bb9 100644 --- a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_1.dart +++ b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_1.dart @@ -38,8 +38,7 @@ class DesktopStep1 extends ConsumerWidget { children: [ DesktopStepItem( label: "Swap", - value: ref.watch(exchangeFormStateProvider - .select((value) => value.exchange.name)), + value: ref.watch(efExchangeProviderNameProvider), ), Container( height: 1, diff --git a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_3.dart b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_3.dart index ef72553fa..a5d3fba43 100644 --- a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_3.dart +++ b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_3.dart @@ -36,8 +36,7 @@ class _DesktopStep3State extends ConsumerState { children: [ DesktopStepItem( label: "Swap", - value: ref.watch(exchangeFormStateProvider - .select((value) => value.exchange.name)), + value: ref.watch(efExchangeProviderNameProvider), ), Container( height: 1, diff --git a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_4.dart b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_4.dart index 2654c2cea..bbc3fd30c 100644 --- a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_4.dart +++ b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_4.dart @@ -46,7 +46,7 @@ class _DesktopStep4State extends ConsumerState { } final statusResponse = - await ref.read(exchangeFormStateProvider).exchange.updateTrade(trade); + await ref.read(efExchangeProvider).updateTrade(trade); String status = "Waiting"; if (statusResponse.value != null) { status = statusResponse.value!.status; diff --git a/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_trade_history.dart b/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_trade_history.dart index 707976425..d369a61df 100644 --- a/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_trade_history.dart +++ b/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_trade_history.dart @@ -269,13 +269,6 @@ class _DesktopTradeHistoryState extends ConsumerState { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - "Recent trades", - style: STextStyles.desktopTextExtraExtraSmall(context), - ), - const SizedBox( - height: 16, - ), RoundedWhiteContainer( child: Center( child: Text( diff --git a/lib/providers/exchange/exchange_form_state_provider.dart b/lib/providers/exchange/exchange_form_state_provider.dart index cb7d32bfe..7828e2771 100644 --- a/lib/providers/exchange/exchange_form_state_provider.dart +++ b/lib/providers/exchange/exchange_form_state_provider.dart @@ -1,5 +1,91 @@ +import 'package:decimal/decimal.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:stackwallet/models/exchange/exchange_form_state.dart'; +import 'package:stackwallet/models/exchange/active_pair.dart'; +import 'package:stackwallet/models/exchange/response_objects/estimate.dart'; +import 'package:stackwallet/models/exchange/response_objects/range.dart'; +import 'package:stackwallet/services/exchange/exchange.dart'; +import 'package:stackwallet/services/exchange/exchange_response.dart'; +import 'package:stackwallet/utilities/enums/exchange_rate_type_enum.dart'; +import 'package:tuple/tuple.dart'; -final exchangeFormStateProvider = - ChangeNotifierProvider((ref) => ExchangeFormState()); +final efEstimatesListProvider = StateProvider.family< + Tuple2>, Range?>?, + String>((ref, exchangeName) => null); + +final efRateTypeProvider = + StateProvider((ref) => ExchangeRateType.estimated); + +final efExchangeProvider = + StateProvider((ref) => Exchange.defaultExchange); +final efExchangeProviderNameProvider = + StateProvider((ref) => Exchange.defaultExchange.name); + +final currentCombinedExchangeIdProvider = Provider((ref) { + return "${ref.watch(efExchangeProvider).name}" + " (${ref.watch(efExchangeProviderNameProvider)})"; +}); + +final efSendAmountProvider = StateProvider((ref) => null); +final efReceiveAmountProvider = StateProvider((ref) => null); + +final efSendAmountStringProvider = StateProvider((ref) { + final refreshing = ref.watch(efRefreshingProvider); + final reversed = ref.watch(efReversedProvider); + if (refreshing && reversed) { + return "-"; + } else { + return ref.watch(efSendAmountProvider)?.toStringAsFixed(8) ?? ""; + } +}); +final efReceiveAmountStringProvider = StateProvider((ref) { + final refreshing = ref.watch(efRefreshingProvider); + final reversed = ref.watch(efReversedProvider); + + if (refreshing && reversed == false) { + return "-"; + } else { + return ref.watch(efReceiveAmountProvider)?.toStringAsFixed(8) ?? ""; + } +}); + +final efReversedProvider = StateProvider((ref) => false); + +final efCurrencyPairProvider = ChangeNotifierProvider( + (ref) => ActivePair(), +); + +final efEstimateProvider = StateProvider((ref) { + final exchange = ref.watch(efExchangeProvider); + final provider = ref.watch(efExchangeProviderNameProvider); + final reversed = ref.watch(efReversedProvider); + final fixedRate = ref.watch(efRateTypeProvider) == ExchangeRateType.fixed; + + final matches = ref + .watch(efEstimatesListProvider(exchange.name)) + ?.item1 + .value + ?.where((e) { + return e.exchangeProvider == provider && + e.fixedRate == fixedRate && + e.reversed == reversed; + }); + + Estimate? result; + + if (matches != null && matches.isNotEmpty) { + result = matches.first; + } else { + result = null; + } + + return result; +}); + +final efCanExchangeProvider = StateProvider((ref) { + final Estimate? estimate = ref.watch(efEstimateProvider); + final refreshing = ref.watch(efRefreshingProvider); + + return !refreshing && estimate != null; +}); + +final efRefreshingProvider = StateProvider((ref) => false); diff --git a/lib/services/exchange/TMP.dart b/lib/services/exchange/TMP.dart deleted file mode 100644 index 868ace14e..000000000 --- a/lib/services/exchange/TMP.dart +++ /dev/null @@ -1,389 +0,0 @@ -// import 'package:decimal/decimal.dart'; -// import 'package:flutter/foundation.dart'; -// import 'package:stackwallet/models/exchange/response_objects/currency.dart'; -// import 'package:stackwallet/pages/exchange_view/sub_widgets/exchange_rate_sheet.dart'; -// import 'package:stackwallet/services/exchange/change_now/change_now_exchange.dart'; -// import 'package:stackwallet/services/exchange/exchange.dart'; -// import 'package:stackwallet/utilities/logger.dart'; -// -// class ExchangeFormState extends ChangeNotifier { -// ExchangeFormState(this.exchangeRateType); -// final ExchangeRateType exchangeRateType; -// -// Exchange? _exchange; -// Exchange get exchange => -// _exchange ??= ChangeNowExchange(); // default to change now -// set exchange(Exchange value) { -// _exchange = value; -// _updateRangesAndEstimate( -// shouldNotifyListeners: true, -// ); -// } -// -// bool _reversed = false; -// bool get reversed => _reversed; -// // set reversed(bool reversed) { -// // _reversed = reversed; -// // // -// // } -// -// Decimal? _rate; -// Decimal? get rate => _rate; -// // set rate(Decimal? rate) { -// // _rate = rate; -// // // -// // } -// -// Decimal? _sendAmount; -// Decimal? get sendAmount => _sendAmount; -// // set sendAmount(Decimal? sendAmount) { -// // _sendAmount = sendAmount; -// // // -// // } -// -// Decimal? _receiveAmount; -// Decimal? get receiveAmount => _receiveAmount; -// // set receiveAmount(Decimal? receiveAmount) { -// // _receiveAmount = receiveAmount; -// // // -// // } -// -// Currency? _sendCurrency; -// Currency? get sendCurrency => _sendCurrency; -// // set sendCurrency(Currency? sendCurrency) { -// // _sendCurrency = sendCurrency; -// // // -// // } -// -// Currency? _receiveCurrency; -// Currency? get receiveCurrency => _receiveCurrency; -// // set receiveCurrency(Currency? receiveCurrency) { -// // _receiveCurrency = receiveCurrency; -// // // -// // } -// -// Decimal? _minSendAmount; -// Decimal? get minSendAmount => _minSendAmount; -// // set minSendAmount(Decimal? minSendAmount) { -// // _minSendAmount = minSendAmount; -// // // -// // } -// -// Decimal? _minReceiveAmount; -// Decimal? get minReceiveAmount => _minReceiveAmount; -// // set minReceiveAmount(Decimal? minReceiveAmount) { -// // _minReceiveAmount = minReceiveAmount; -// // // -// // } -// -// Decimal? _maxSendAmount; -// Decimal? get maxSendAmount => _maxSendAmount; -// // set maxSendAmount(Decimal? maxSendAmount) { -// // _maxSendAmount = maxSendAmount; -// // // -// // } -// -// Decimal? _maxReceiveAmount; -// Decimal? get maxReceiveAmount => _maxReceiveAmount; -// // set maxReceiveAmount(Decimal? maxReceiveAmount) { -// // _maxReceiveAmount = maxReceiveAmount; -// // // -// // } -// -// //============================================================================ -// // computed properties -// //============================================================================ -// -// String? get fromTicker => _sendCurrency?.ticker; -// -// String? get toTicker => _receiveCurrency?.ticker; -// -// String get warning { -// if (reversed) { -// if (_receiveCurrency != null && _receiveAmount != null) { -// if (_minReceiveAmount != null && -// _receiveAmount! < _minReceiveAmount! && -// _receiveAmount! > Decimal.zero) { -// return "Minimum amount ${_minReceiveAmount!.toString()} ${_receiveCurrency!.ticker.toUpperCase()}"; -// } else if (_maxReceiveAmount != null && -// _receiveAmount! > _maxReceiveAmount!) { -// return "Maximum amount ${_maxReceiveAmount!.toString()} ${_receiveCurrency!.ticker.toUpperCase()}"; -// } -// } -// } else { -// if (_sendCurrency != null && _sendAmount != null) { -// if (_minSendAmount != null && -// _sendAmount! < _minSendAmount! && -// _sendAmount! > Decimal.zero) { -// return "Minimum amount ${_minSendAmount!.toString()} ${_sendCurrency!.ticker.toUpperCase()}"; -// } else if (_maxSendAmount != null && _sendAmount! > _maxSendAmount!) { -// return "Maximum amount ${_maxSendAmount!.toString()} ${_sendCurrency!.ticker.toUpperCase()}"; -// } -// } -// } -// -// return ""; -// } -// -// //============================================================================ -// // public state updaters -// //============================================================================ -// -// void reset(bool shouldNotifyListeners) { -// _exchange = null; -// _reversed = false; -// _rate = null; -// _sendAmount = null; -// _receiveAmount = null; -// _sendCurrency = null; -// _receiveCurrency = null; -// _minSendAmount = null; -// _minReceiveAmount = null; -// _maxSendAmount = null; -// _maxReceiveAmount = null; -// -// if (shouldNotifyListeners) { -// notifyListeners(); -// } -// } -// -// Future setFromAmountAndCalculateToAmount( -// Decimal? newSendAmount, -// bool shouldNotifyListeners, -// ) async { -// if (newSendAmount == null) { -// // todo: check if this breaks things and stuff -// _receiveAmount = null; -// _sendAmount = null; -// } else { -// if (newSendAmount <= Decimal.zero) { -// _receiveAmount = Decimal.zero; -// } -// -// _sendAmount = newSendAmount; -// _reversed = false; -// -// await _updateRangesAndEstimate( -// shouldNotifyListeners: false, -// ); -// } -// -// if (shouldNotifyListeners) { -// notifyListeners(); -// } -// } -// -// Future setToAmountAndCalculateFromAmount( -// Decimal? newReceiveAmount, -// bool shouldNotifyListeners, -// ) async { -// if (newReceiveAmount == null) { -// // todo: check if this breaks things and stuff -// _receiveAmount = null; -// _sendAmount = null; -// } else { -// if (newReceiveAmount <= Decimal.zero) { -// _sendAmount = Decimal.zero; -// } -// -// _receiveAmount = newReceiveAmount; -// _reversed = true; -// -// await _updateRangesAndEstimate( -// shouldNotifyListeners: false, -// ); -// } -// -// if (shouldNotifyListeners) { -// notifyListeners(); -// } -// } -// -// Future updateFrom( -// Currency sendCurrency, -// bool shouldNotifyListeners, -// ) async { -// try { -// _sendCurrency = sendCurrency; -// if (_receiveCurrency == null) { -// _rate = null; -// } else { -// await _updateRangesAndEstimate( -// shouldNotifyListeners: false, -// ); -// } -// } catch (e, s) { -// Logging.instance.log("$e\n$s", level: LogLevel.Error); -// } -// if (shouldNotifyListeners) { -// notifyListeners(); -// } -// } -// -// Future updateTo( -// Currency receiveCurrency, -// bool shouldNotifyListeners, -// ) async { -// try { -// _receiveCurrency = receiveCurrency; -// -// if (_sendCurrency == null) { -// _rate = null; -// } else { -// await _updateRangesAndEstimate( -// shouldNotifyListeners: false, -// ); -// } -// } catch (e, s) { -// Logging.instance.log("$e\n$s", level: LogLevel.Error); -// } -// if (shouldNotifyListeners) { -// notifyListeners(); -// } -// } -// -// Future swap( -// {required bool shouldNotifyListeners,}) async { -// final Decimal? temp = sendAmount; -// _sendAmount = receiveAmount; -// _receiveAmount = temp; -// -// _minSendAmount = null; -// _maxSendAmount = null; -// _minReceiveAmount = null; -// _maxReceiveAmount = null; -// -// final Currency? tmp = sendCurrency; -// _sendCurrency = receiveCurrency; -// _receiveCurrency = tmp; -// -// await _updateRangesAndEstimate( -// shouldNotifyListeners: false, -// ); -// } -// -// //============================================================================ -// // private state updaters -// //============================================================================ -// -// Future _updateRangesAndEstimate( -// {required bool shouldNotifyListeners,}) async { -// await _updateRanges(shouldNotifyListeners: false); -// await _updateEstimate(shouldNotifyListeners: false); -// if (shouldNotifyListeners) { -// notifyListeners(); -// } -// } -// -// Future _updateRanges({required bool shouldNotifyListeners,}) async { -// // if (exchange?.name == SimpleSwapExchange.exchangeName) { -// // reversed = false; -// // } -// final _send = sendCurrency; -// final _receive = receiveCurrency; -// if (_send == null || _receive == null) { -// Logging.instance.log( -// "Tried to $runtimeType.updateRanges where ( $_send || $_receive) for: $exchange", -// level: LogLevel.Info, -// ); -// return; -// } -// final response = await exchange.getRange( -// _send.ticker, -// _receive.ticker, -// exchangeRateType == ExchangeRateType.fixed, -// ); -// -// if (response.value == null) { -// Logging.instance.log( -// "Tried to $runtimeType.updateRanges for: $exchange where response: $response", -// level: LogLevel.Info, -// ); -// return; -// } -// final responseReversed = await exchange.getRange( -// _receive.ticker, -// _send.ticker, -// exchangeRateType == ExchangeRateType.fixed, -// ); -// -// if (responseReversed.value == null) { -// Logging.instance.log( -// "Tried to $runtimeType.updateRanges for: $exchange where response: $responseReversed", -// level: LogLevel.Info, -// ); -// return; -// } -// -// final range = response.value!; -// final rangeReversed = responseReversed.value!; -// -// _minSendAmount = range.min; -// _maxSendAmount = range.max; -// _minReceiveAmount = rangeReversed.min; -// _maxReceiveAmount = rangeReversed.max; -// -// //todo: check if print needed -// // debugPrint( -// // "updated range for: $exchange for $_fromTicker-$_toTicker: $range"); -// -// if (shouldNotifyListeners) { -// notifyListeners(); -// } -// } -// -// Future _updateEstimate({ -// required bool shouldNotifyListeners, -// }) async { -// // if (exchange?.name == SimpleSwapExchange.exchangeName) { -// // reversed = false; -// // } -// final amount = reversed ? receiveAmount : sendAmount; -// if (sendCurrency == null || -// receiveCurrency == null || -// amount == null || -// amount <= Decimal.zero) { -// Logging.instance.log( -// "Tried to $runtimeType.updateEstimate for: $exchange where (from: $sendCurrency || to: $receiveCurrency || amount: $amount)", -// level: LogLevel.Info, -// ); -// return; -// } -// final response = await exchange.getEstimate( -// sendCurrency!.ticker, -// receiveCurrency!.ticker, -// amount, -// exchangeRateType == ExchangeRateType.fixed, -// reversed, -// ); -// -// if (response.value == null) { -// Logging.instance.log( -// "Tried to $runtimeType.updateEstimate for: $exchange where response: $response", -// level: LogLevel.Info, -// ); -// return; -// } -// -// final estimate = response.value!; -// -// if (reversed) { -// _sendAmount = estimate.estimatedAmount; -// } else { -// _receiveAmount = estimate.estimatedAmount; -// } -// -// _rate = -// (receiveAmount! / sendAmount!).toDecimal(scaleOnInfinitePrecision: 12); -// -// //todo: check if print needed -// // debugPrint( -// // "updated estimate for: $exchange for $fromTicker-$toTicker: $estimate"); -// -// if (shouldNotifyListeners) { -// notifyListeners(); -// } -// } -// -// -// } diff --git a/lib/services/exchange/change_now/change_now_api.dart b/lib/services/exchange/change_now/change_now_api.dart index 57823a813..6f050d2e2 100644 --- a/lib/services/exchange/change_now/change_now_api.dart +++ b/lib/services/exchange/change_now/change_now_api.dart @@ -483,6 +483,7 @@ class ChangeNowAPI { reversed: false, rateId: value.rateId, warningMessage: value.warningMessage, + exchangeProvider: ChangeNowExchange.exchangeName, ), ); } catch (_) { @@ -566,6 +567,7 @@ class ChangeNowAPI { reversed: reversed, rateId: value.rateId, warningMessage: value.warningMessage, + exchangeProvider: ChangeNowExchange.exchangeName, ), ); } catch (_) { diff --git a/lib/services/exchange/change_now/change_now_exchange.dart b/lib/services/exchange/change_now/change_now_exchange.dart index 3189ff84e..a6037b511 100644 --- a/lib/services/exchange/change_now/change_now_exchange.dart +++ b/lib/services/exchange/change_now/change_now_exchange.dart @@ -31,7 +31,7 @@ class ChangeNowExchange extends Exchange { String? extraId, required String addressRefund, required String refundExtraId, - String? rateId, + Estimate? estimate, required bool reversed, }) async { late final ExchangeResponse response; @@ -41,7 +41,7 @@ class ChangeNowExchange extends Exchange { toTicker: to, receivingAddress: addressTo, amount: amount, - rateId: rateId!, + rateId: estimate!.rateId!, extraId: extraId ?? "", refundAddress: addressRefund, refundExtraId: refundExtraId, @@ -128,7 +128,7 @@ class ChangeNowExchange extends Exchange { } @override - Future> getEstimate( + Future>> getEstimates( String from, String to, Decimal amount, @@ -151,7 +151,10 @@ class ChangeNowExchange extends Exchange { fromAmount: amount, ); } - return response; + return ExchangeResponse( + value: response.value == null ? null : [response.value!], + exception: response.exception, + ); } @override diff --git a/lib/services/exchange/exchange.dart b/lib/services/exchange/exchange.dart index e4f5ce8d5..1db451457 100644 --- a/lib/services/exchange/exchange.dart +++ b/lib/services/exchange/exchange.dart @@ -8,6 +8,7 @@ import 'package:stackwallet/services/exchange/change_now/change_now_exchange.dar import 'package:stackwallet/services/exchange/exchange_response.dart'; import 'package:stackwallet/services/exchange/majestic_bank/majestic_bank_exchange.dart'; import 'package:stackwallet/services/exchange/simpleswap/simpleswap_exchange.dart'; +import 'package:stackwallet/services/exchange/trocador/trocador_exchange.dart'; abstract class Exchange { static Exchange get defaultExchange => ChangeNowExchange.instance; @@ -20,7 +21,14 @@ abstract class Exchange { return SimpleSwapExchange.instance; case MajesticBankExchange.exchangeName: return MajesticBankExchange.instance; + case TrocadorExchange.exchangeName: + return TrocadorExchange.instance; default: + final split = name.split(" "); + if (split.length >= 2) { + // silly way to check for 'Trocador ($providerName)' + return fromName(split.first); + } throw ArgumentError("Unknown exchange name"); } } @@ -52,7 +60,7 @@ abstract class Exchange { bool fixedRate, ); - Future> getEstimate( + Future>> getEstimates( String from, String to, Decimal amount, @@ -69,7 +77,7 @@ abstract class Exchange { String? extraId, required String addressRefund, required String refundExtraId, - String? rateId, + Estimate? estimate, required bool reversed, }); } diff --git a/lib/services/exchange/exchange_data_loading_service.dart b/lib/services/exchange/exchange_data_loading_service.dart index 2da6016f4..c0aa2e2b0 100644 --- a/lib/services/exchange/exchange_data_loading_service.dart +++ b/lib/services/exchange/exchange_data_loading_service.dart @@ -1,12 +1,13 @@ import 'package:flutter/foundation.dart'; import 'package:isar/isar.dart'; import 'package:stackwallet/db/hive/db.dart'; +import 'package:stackwallet/models/exchange/active_pair.dart'; import 'package:stackwallet/models/exchange/aggregate_currency.dart'; -import 'package:stackwallet/models/exchange/exchange_form_state.dart'; import 'package:stackwallet/models/isar/exchange_cache/currency.dart'; import 'package:stackwallet/models/isar/exchange_cache/pair.dart'; import 'package:stackwallet/services/exchange/change_now/change_now_exchange.dart'; import 'package:stackwallet/services/exchange/majestic_bank/majestic_bank_exchange.dart'; +import 'package:stackwallet/services/exchange/trocador/trocador_exchange.dart'; import 'package:stackwallet/utilities/enums/exchange_rate_type_enum.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/stack_file_system.dart'; @@ -56,20 +57,29 @@ class ExchangeDataLoadingService { ); } - Future setCurrenciesIfEmpty(ExchangeFormState state) async { - if (state.sendCurrency == null && state.receiveCurrency == null) { + Future setCurrenciesIfEmpty( + ActivePair? pair, + ExchangeRateType rateType, + ) async { + if (pair?.send == null && pair?.receive == null) { if (await isar.currencies.count() > 0) { - final sendCurrency = await getAggregateCurrency( - "BTC", - state.exchangeRateType, - null, + pair?.setSend( + await getAggregateCurrency( + "BTC", + rateType, + null, + ), + notifyListeners: false, ); - final receiveCurrency = await getAggregateCurrency( - "XMR", - state.exchangeRateType, - null, + + pair?.setReceive( + await getAggregateCurrency( + "XMR", + rateType, + null, + ), + notifyListeners: false, ); - state.setCurrencies(sendCurrency, receiveCurrency); } } } @@ -128,6 +138,7 @@ class ExchangeDataLoadingService { // loadSimpleswapFixedRateCurrencies(ref), // loadSimpleswapFloatingRateCurrencies(ref), loadMajesticBankCurrencies(), + loadTrocadorCurrencies(), ]); // quicker to load available currencies on the fly for a specific base currency @@ -302,6 +313,28 @@ class ExchangeDataLoadingService { } } + Future loadTrocadorCurrencies() async { + final exchange = TrocadorExchange.instance; + final responseCurrencies = await exchange.getAllCurrencies(false); + + if (responseCurrencies.value != null) { + await isar.writeTxn(() async { + final idsToDelete = await isar.currencies + .where() + .exchangeNameEqualTo(TrocadorExchange.exchangeName) + .idProperty() + .findAll(); + await isar.currencies.deleteAll(idsToDelete); + await isar.currencies.putAll(responseCurrencies.value!); + }); + } else { + Logging.instance.log( + "loadTrocadorCurrencies: $responseCurrencies", + level: LogLevel.Warning, + ); + } + } + // Future loadMajesticBankPairs() async { // final exchange = MajesticBankExchange.instance; // diff --git a/lib/services/exchange/exchange_service.dart b/lib/services/exchange/exchange_service.dart deleted file mode 100644 index 036c522de..000000000 --- a/lib/services/exchange/exchange_service.dart +++ /dev/null @@ -1 +0,0 @@ -class ExchangeService {} diff --git a/lib/services/exchange/majestic_bank/majestic_bank_exchange.dart b/lib/services/exchange/majestic_bank/majestic_bank_exchange.dart index dcc63c68b..95031490c 100644 --- a/lib/services/exchange/majestic_bank/majestic_bank_exchange.dart +++ b/lib/services/exchange/majestic_bank/majestic_bank_exchange.dart @@ -44,7 +44,7 @@ class MajesticBankExchange extends Exchange { String? extraId, required String addressRefund, required String refundExtraId, - String? rateId, + Estimate? estimate, required bool reversed, }) async { ExchangeResponse? response; @@ -170,7 +170,7 @@ class MajesticBankExchange extends Exchange { } @override - Future> getEstimate( + Future>> getEstimates( String from, String to, Decimal amount, @@ -192,8 +192,9 @@ class MajesticBankExchange extends Exchange { estimatedAmount: reversed ? calc.fromAmount : calc.receiveAmount, fixedRate: fixedRate, reversed: reversed, + exchangeProvider: MajesticBankExchange.exchangeName, ); - return ExchangeResponse(value: estimate); + return ExchangeResponse(value: [estimate]); } @override diff --git a/lib/services/exchange/simpleswap/simpleswap_exchange.dart b/lib/services/exchange/simpleswap/simpleswap_exchange.dart index 1157bd09e..0bfe93a38 100644 --- a/lib/services/exchange/simpleswap/simpleswap_exchange.dart +++ b/lib/services/exchange/simpleswap/simpleswap_exchange.dart @@ -30,7 +30,7 @@ class SimpleSwapExchange extends Exchange { String? extraId, required String addressRefund, required String refundExtraId, - String? rateId, + Estimate? estimate, required bool reversed, }) async { return await SimpleSwapAPI.instance.createNewExchange( @@ -89,7 +89,7 @@ class SimpleSwapExchange extends Exchange { } @override - Future> getEstimate( + Future>> getEstimates( String from, String to, Decimal amount, @@ -109,11 +109,14 @@ class SimpleSwapExchange extends Exchange { } return ExchangeResponse( - value: Estimate( - estimatedAmount: Decimal.parse(response.value!), - fixedRate: fixedRate, - reversed: reversed, - ), + value: [ + Estimate( + estimatedAmount: Decimal.parse(response.value!), + fixedRate: fixedRate, + reversed: reversed, + exchangeProvider: SimpleSwapExchange.exchangeName, + ), + ], ); } diff --git a/lib/services/exchange/trocador/response_objects/trocador_coin.dart b/lib/services/exchange/trocador/response_objects/trocador_coin.dart new file mode 100644 index 000000000..aea01d2f1 --- /dev/null +++ b/lib/services/exchange/trocador/response_objects/trocador_coin.dart @@ -0,0 +1,44 @@ +import 'package:decimal/decimal.dart'; + +class TrocadorCoin { + final String name; + final String ticker; + final String network; + final bool memo; + final String image; + final Decimal minimum; + final Decimal maximum; + + TrocadorCoin({ + required this.name, + required this.ticker, + required this.network, + required this.memo, + required this.image, + required this.minimum, + required this.maximum, + }); + + factory TrocadorCoin.fromMap(Map json) => TrocadorCoin( + name: json['name'] as String, + ticker: json['ticker'] as String, + network: json['network'] as String, + memo: json['memo'] as bool, + image: json['image'] as String, + minimum: Decimal.parse(json['minimum'].toString()), + maximum: Decimal.parse(json['maximum'].toString()), + ); + + @override + String toString() { + return 'TrocadorCoin( ' + 'name: $name, ' + 'ticker: $ticker, ' + 'network: $network, ' + 'memo: $memo, ' + 'image: $image, ' + 'minimum: $minimum, ' + 'maximum: $maximum ' + ')'; + } +} diff --git a/lib/services/exchange/trocador/response_objects/trocador_quote.dart b/lib/services/exchange/trocador/response_objects/trocador_quote.dart new file mode 100644 index 000000000..ada8725f5 --- /dev/null +++ b/lib/services/exchange/trocador/response_objects/trocador_quote.dart @@ -0,0 +1,47 @@ +import 'package:decimal/decimal.dart'; + +class TrocadorQuote { + final String provider; + final String kycRating; + final int insurance; + final bool fixed; + final Decimal? amountTo; + final Decimal? amountFrom; + final Decimal waste; + + TrocadorQuote({ + required this.provider, + required this.kycRating, + required this.insurance, + required this.fixed, + required this.amountTo, + required this.amountFrom, + required this.waste, + }); + + factory TrocadorQuote.fromMap(Map map) { + return TrocadorQuote( + provider: map['provider'] as String, + kycRating: map['kycrating'] as String, + insurance: map['insurance'] as int, + // wtf trocador? + fixed: map['fixed'] == "True", + amountTo: Decimal.tryParse(map['amount_to'].toString()), + amountFrom: Decimal.tryParse(map['amount_from'].toString()), + waste: Decimal.parse(map['waste'].toString()), + ); + } + + @override + String toString() { + return 'TrocadorQuote( ' + 'provider: $provider, ' + 'kycRating: $kycRating, ' + 'insurance: $insurance, ' + 'fixed: $fixed, ' + 'amountTo: $amountTo, ' + 'amountFrom: $amountFrom, ' + 'waste: $waste ' + ')'; + } +} diff --git a/lib/services/exchange/trocador/response_objects/trocador_rate.dart b/lib/services/exchange/trocador/response_objects/trocador_rate.dart new file mode 100644 index 000000000..5c3d18da2 --- /dev/null +++ b/lib/services/exchange/trocador/response_objects/trocador_rate.dart @@ -0,0 +1,84 @@ +import 'package:decimal/decimal.dart'; +import 'package:stackwallet/services/exchange/trocador/response_objects/trocador_quote.dart'; + +class TrocadorRate { + final String tradeId; + final DateTime date; + final String tickerFrom; + final String tickerTo; + final String coinFrom; + final String coinTo; + final String networkFrom; + final String networkTo; + final Decimal amountFrom; + final Decimal amountTo; + final String provider; + final bool fixed; + final bool payment; + final String status; + final List quotes; + + TrocadorRate({ + required this.tradeId, + required this.date, + required this.tickerFrom, + required this.tickerTo, + required this.coinFrom, + required this.coinTo, + required this.networkFrom, + required this.networkTo, + required this.amountFrom, + required this.amountTo, + required this.provider, + required this.fixed, + required this.payment, + required this.status, + required this.quotes, + }); + + factory TrocadorRate.fromMap(Map map) { + final list = + List>.from(map['quotes']['quotes'] as List); + final quotes = list.map((quote) => TrocadorQuote.fromMap(quote)).toList(); + + return TrocadorRate( + tradeId: map['trade_id'] as String, + date: DateTime.parse(map['date'] as String), + tickerFrom: map['ticker_from'] as String, + tickerTo: map['ticker_to'] as String, + coinFrom: map['coin_from'] as String, + coinTo: map['coin_to'] as String, + networkFrom: map['network_from'] as String, + networkTo: map['network_to'] as String, + amountFrom: Decimal.parse(map['amount_from'].toString()), + amountTo: Decimal.parse(map['amount_to'].toString()), + provider: map['provider'] as String, + fixed: map['fixed'] as bool, + payment: map['payment'] as bool, + status: map['status'] as String, + quotes: quotes, + ); + } + + @override + String toString() { + final quotesString = quotes.map((quote) => quote.toString()).join(', '); + return 'TrocadorRate(' + 'tradeId: $tradeId, ' + 'date: $date, ' + 'tickerFrom: $tickerFrom, ' + 'tickerTo: $tickerTo, ' + 'coinFrom: $coinFrom, ' + 'coinTo: $coinTo, ' + 'networkFrom: $networkFrom, ' + 'networkTo: $networkTo, ' + 'amountFrom: $amountFrom, ' + 'amountTo: $amountTo, ' + 'provider: $provider, ' + 'fixed: $fixed, ' + 'payment: $payment, ' + 'status: $status, ' + 'quotes: [$quotesString] ' + ')'; + } +} diff --git a/lib/services/exchange/trocador/response_objects/trocador_trade.dart b/lib/services/exchange/trocador/response_objects/trocador_trade.dart new file mode 100644 index 000000000..5768a9423 --- /dev/null +++ b/lib/services/exchange/trocador/response_objects/trocador_trade.dart @@ -0,0 +1,113 @@ +import 'package:decimal/decimal.dart'; + +class TrocadorTrade { + final String tradeId; + final DateTime date; + final String tickerFrom; + final String tickerTo; + final String coinFrom; + final String coinTo; + final String networkFrom; + final String networkTo; + final Decimal amountFrom; + final Decimal amountTo; + final String provider; + final bool fixed; + final String status; + final String addressProvider; + final String addressProviderMemo; + final String addressUser; + final String addressUserMemo; + final String refundAddress; + final String refundAddressMemo; + final String password; + final String idProvider; + + // dynamic is the devil but this could be anything... because json. + final dynamic quotes; + + final bool payment; + + TrocadorTrade({ + required this.tradeId, + required this.date, + required this.tickerFrom, + required this.tickerTo, + required this.coinFrom, + required this.coinTo, + required this.networkFrom, + required this.networkTo, + required this.amountFrom, + required this.amountTo, + required this.provider, + required this.fixed, + required this.status, + required this.addressProvider, + required this.addressProviderMemo, + required this.addressUser, + required this.addressUserMemo, + required this.refundAddress, + required this.refundAddressMemo, + required this.password, + required this.idProvider, + required this.quotes, + required this.payment, + }); + + factory TrocadorTrade.fromMap(Map map) { + return TrocadorTrade( + tradeId: map['trade_id'] as String, + date: DateTime.parse(map['date'] as String), + tickerFrom: map['ticker_from'] as String, + tickerTo: map['ticker_to'] as String, + coinFrom: map['coin_from'] as String, + coinTo: map['coin_to'] as String, + networkFrom: map['network_from'] as String, + networkTo: map['network_to'] as String, + amountFrom: Decimal.parse(map['amount_from'].toString()), + amountTo: Decimal.parse(map['amount_to'].toString()), + provider: map['provider'] as String, + fixed: map['fixed'] as bool, + status: map['status'] as String, + addressProvider: map['address_provider'] as String, + addressProviderMemo: map['address_provider_memo'] as String, + addressUser: map['address_user'] as String, + addressUserMemo: map['address_user_memo'] as String, + refundAddress: map['refund_address'] as String, + refundAddressMemo: map['refund_address_memo'] as String, + password: map['password'] as String, + idProvider: map['id_provider'] as String, + quotes: map['quotes'], + payment: map['payment'] as bool, + ); + } + + @override + String toString() { + return 'TrocadorTrade( ' + 'tradeId: $tradeId, ' + 'date: $date, ' + 'tickerFrom: $tickerFrom, ' + 'tickerTo: $tickerTo, ' + 'coinFrom: $coinFrom, ' + 'coinTo: $coinTo, ' + 'networkFrom: $networkFrom, ' + 'networkTo: $networkTo, ' + 'amountFrom: $amountFrom, ' + 'amountTo: $amountTo, ' + 'provider: $provider, ' + 'fixed: $fixed, ' + 'status: $status, ' + 'addressProvider: $addressProvider, ' + 'addressProviderMemo: $addressProviderMemo, ' + 'addressUser: $addressUser, ' + 'addressUserMemo: $addressUserMemo, ' + 'refundAddress: $refundAddress, ' + 'refundAddressMemo: $refundAddressMemo, ' + 'password: $password, ' + 'idProvider: $idProvider, ' + 'quotes: $quotes, ' + 'payment: $payment ' + ')'; + } +} diff --git a/lib/services/exchange/trocador/response_objects/trocador_trade_new.dart b/lib/services/exchange/trocador/response_objects/trocador_trade_new.dart new file mode 100644 index 000000000..4dc8ae380 --- /dev/null +++ b/lib/services/exchange/trocador/response_objects/trocador_trade_new.dart @@ -0,0 +1,106 @@ +import 'package:decimal/decimal.dart'; + +class TrocadorTradeNew { + final String tradeId; + final DateTime date; + final String tickerFrom; + final String tickerTo; + final String coinFrom; + final String coinTo; + final String networkFrom; + final String networkTo; + final Decimal amountFrom; + final Decimal amountTo; + final String provider; + final bool fixed; + final String status; + final String addressProvider; + final String addressProviderMemo; + final String addressUser; + final String addressUserMemo; + final String refundAddress; + final String refundAddressMemo; + final String password; + final String idProvider; + final bool payment; + + TrocadorTradeNew({ + required this.tradeId, + required this.date, + required this.tickerFrom, + required this.tickerTo, + required this.coinFrom, + required this.coinTo, + required this.networkFrom, + required this.networkTo, + required this.amountFrom, + required this.amountTo, + required this.provider, + required this.fixed, + required this.status, + required this.addressProvider, + required this.addressProviderMemo, + required this.addressUser, + required this.addressUserMemo, + required this.refundAddress, + required this.refundAddressMemo, + required this.password, + required this.idProvider, + required this.payment, + }); + + factory TrocadorTradeNew.fromMap(Map map) { + return TrocadorTradeNew( + tradeId: map['trade_id'] as String, + date: DateTime.parse(map['date'] as String), + tickerFrom: map['ticker_from'] as String, + tickerTo: map['ticker_to'] as String, + coinFrom: map['coin_from'] as String, + coinTo: map['coin_to'] as String, + networkFrom: map['network_from'] as String, + networkTo: map['network_to'] as String, + amountFrom: Decimal.parse(map['amount_from'].toString()), + amountTo: Decimal.parse(map['amount_to'].toString()), + provider: map['provider'] as String, + fixed: map['fixed'] as bool, + status: map['status'] as String, + addressProvider: map['address_provider'] as String, + addressProviderMemo: map['address_provider_memo'] as String, + addressUser: map['address_user'] as String, + addressUserMemo: map['address_user_memo'] as String, + refundAddress: map['refund_address'] as String, + refundAddressMemo: map['refund_address_memo'] as String, + password: map['password'] as String, + idProvider: map['id_provider'] as String, + payment: map['payment'] as bool, + ); + } + + @override + String toString() { + return 'TrocadorTradeNew( ' + 'tradeId: $tradeId, ' + 'date: $date, ' + 'tickerFrom: $tickerFrom, ' + 'tickerTo: $tickerTo, ' + 'coinFrom: $coinFrom, ' + 'coinTo: $coinTo, ' + 'networkFrom: $networkFrom, ' + 'networkTo: $networkTo, ' + 'amountFrom: $amountFrom, ' + 'amountTo: $amountTo, ' + 'provider: $provider, ' + 'fixed: $fixed, ' + 'status: $status, ' + 'addressProvider: $addressProvider, ' + 'addressProviderMemo: $addressProviderMemo, ' + 'addressUser: $addressUser, ' + 'addressUserMemo: $addressUserMemo, ' + 'refundAddress: $refundAddress, ' + 'refundAddressMemo: $refundAddressMemo, ' + 'password: $password, ' + 'idProvider: $idProvider, ' + 'payment: $payment ' + ')'; + } +} diff --git a/lib/services/exchange/trocador/trocador_api.dart b/lib/services/exchange/trocador/trocador_api.dart new file mode 100644 index 000000000..317ebba53 --- /dev/null +++ b/lib/services/exchange/trocador/trocador_api.dart @@ -0,0 +1,317 @@ +import 'dart:convert'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter_native_splash/cli_commands.dart'; +import 'package:http/http.dart' as http; +import 'package:stackwallet/exceptions/exchange/exchange_exception.dart'; +import 'package:stackwallet/services/exchange/exchange_response.dart'; +import 'package:stackwallet/services/exchange/trocador/response_objects/trocador_coin.dart'; +import 'package:stackwallet/services/exchange/trocador/response_objects/trocador_rate.dart'; +import 'package:stackwallet/services/exchange/trocador/response_objects/trocador_trade.dart'; +import 'package:stackwallet/services/exchange/trocador/response_objects/trocador_trade_new.dart'; +import 'package:stackwallet/utilities/logger.dart'; + +const kTrocadorApiKey = "8rFqf7QLxX1mUBiNPEMaLUpV2biz6n"; +const kTrocadorRefCode = "9eHm9BkQfS"; + +abstract class TrocadorAPI { + static const String authority = "trocador.app"; + static const String onionAuthority = + "trocadorfyhlu27aefre5u7zri66gudtzdyelymftvr4yjwcxhfaqsid.onion"; + + static const String markup = "1"; + static const String minKYCRating = "C"; + + static Uri _buildUri({ + required String method, + required bool isOnion, + Map? params, + }) { + return isOnion + ? Uri.http(onionAuthority, "api/$method", params) + : Uri.https(authority, "api/$method", params); + } + + static Future _makeGetRequest(Uri uri) async { + int code = -1; + try { + debugPrint("URI: $uri"); + final response = await http.get( + uri, + headers: {'Content-Type': 'application/json'}, + ); + + code = response.statusCode; + + debugPrint("CODE: $code"); + debugPrint("BODY: ${response.body}"); + + final json = jsonDecode(response.body); + + return json; + } catch (e, s) { + Logging.instance.log( + "_makeRequest($uri) HTTP:$code threw: $e\n$s", + level: LogLevel.Error, + ); + rethrow; + } + } + + /// fetch all supported coins + static Future>> getCoins({ + required bool isOnion, + }) async { + final uri = _buildUri( + isOnion: isOnion, + method: "coins", + params: { + "api_key": kTrocadorApiKey, + "ref": kTrocadorRefCode, + }, + ); + + try { + final json = await _makeGetRequest(uri); + + if (json is List) { + final list = List>.from(json); + final List coins = list + .map( + (e) => TrocadorCoin.fromMap(e), + ) + .toList(); + + return ExchangeResponse(value: coins); + } else { + throw Exception("unexpected json: $json"); + } + } catch (e, s) { + Logging.instance.log("getCoins exception: $e\n$s", level: LogLevel.Error); + return ExchangeResponse( + exception: ExchangeException( + e.toString(), + ExchangeExceptionType.generic, + ), + ); + } + } + + /// get trade info + static Future> getTrade({ + required bool isOnion, + required String tradeId, + }) async { + final uri = _buildUri( + isOnion: isOnion, + method: "trade", + params: { + "api_key": kTrocadorApiKey, + "ref": kTrocadorRefCode, + "id": tradeId, + }, + ); + + try { + final json = await _makeGetRequest(uri); + final map = Map.from((json as List).first as Map); + + return ExchangeResponse(value: TrocadorTrade.fromMap(map)); + } catch (e, s) { + Logging.instance.log("getTrade exception: $e\n$s", level: LogLevel.Error); + return ExchangeResponse( + exception: ExchangeException( + e.toString(), + ExchangeExceptionType.generic, + ), + ); + } + } + + /// get standard/floating rate + static Future> getNewStandardRate({ + required bool isOnion, + required String fromTicker, + required String fromNetwork, + required String toTicker, + required String toNetwork, + required String fromAmount, + }) async { + final params = { + "api_key": kTrocadorApiKey, + "ref": kTrocadorRefCode, + "ticker_from": fromTicker.toLowerCase(), + "network_from": fromNetwork, + "ticker_to": toTicker.toLowerCase(), + "network_to": toNetwork, + "amount_from": fromAmount, + "payment": "false", + "min_kycrating": minKYCRating, + "markup": markup, + }; + + return await _getNewRate(isOnion: isOnion, params: params); + } + + /// get fixed rate/payment rate + static Future> getNewPaymentRate({ + required bool isOnion, + required String fromTicker, + required String fromNetwork, + required String toTicker, + required String toNetwork, + required String toAmount, + }) async { + final params = { + "api_key": kTrocadorApiKey, + "ref": kTrocadorRefCode, + "ticker_from": fromTicker.toLowerCase(), + "network_from": fromNetwork, + "ticker_to": toTicker.toLowerCase(), + "network_to": toNetwork, + "amount_to": toAmount, + "payment": "true", + "min_kycrating": minKYCRating, + "markup": markup, + }; + + return await _getNewRate(isOnion: isOnion, params: params); + } + + static Future> _getNewRate({ + required bool isOnion, + required Map params, + }) async { + final uri = _buildUri( + isOnion: isOnion, + method: "new_rate", + params: params, + ); + + try { + final json = await _makeGetRequest(uri); + final map = Map.from(json as Map); + + return ExchangeResponse(value: TrocadorRate.fromMap(map)); + } catch (e, s) { + Logging.instance + .log("getNewRate exception: $e\n$s", level: LogLevel.Error); + return ExchangeResponse( + exception: ExchangeException( + e.toString(), + ExchangeExceptionType.generic, + ), + ); + } + } + + /// create new floating rate/standard trade + static Future> createNewStandardRateTrade({ + required bool isOnion, + required String? rateId, + required String fromTicker, + required String fromNetwork, + required String toTicker, + required String toNetwork, + required String fromAmount, + required String receivingAddress, + required String? receivingMemo, + required String refundAddress, + required String? refundMemo, + required String exchangeProvider, + required bool isFixedRate, + }) async { + final Map params = { + "api_key": kTrocadorApiKey, + "ref": kTrocadorRefCode, + "ticker_from": fromTicker.toLowerCase(), + "network_from": fromNetwork, + "ticker_to": toTicker.toLowerCase(), + "network_to": toNetwork, + "amount_from": fromAmount, + "address": receivingAddress, + "address_memo": receivingMemo ?? "0", + "refund": refundAddress, + "refund_memo": refundMemo ?? "0", + "provider": exchangeProvider, + "fixed": isFixedRate.toString().capitalize(), + "payment": "False", + "min_kycrating": minKYCRating, + "markup": markup, + }; + + if (rateId != null) { + params["id"] = rateId; + } + + return await _getNewTrade(isOnion: isOnion, params: params); + } + + static Future> createNewPaymentRateTrade({ + required bool isOnion, + required String? rateId, + required String fromTicker, + required String fromNetwork, + required String toTicker, + required String toNetwork, + required String toAmount, + required String receivingAddress, + required String? receivingMemo, + required String refundAddress, + required String? refundMemo, + required String exchangeProvider, + required bool isFixedRate, + }) async { + final params = { + "api_key": kTrocadorApiKey, + "ref": kTrocadorRefCode, + "ticker_from": fromTicker.toLowerCase(), + "network_from": fromNetwork, + "ticker_to": toTicker.toLowerCase(), + "network_to": toNetwork, + "amount_to": toAmount, + "address": receivingAddress, + "address_memo": receivingMemo ?? "0", + "refund": refundAddress, + "refund_memo": refundMemo ?? "0", + "provider": exchangeProvider, + "fixed": isFixedRate.toString().capitalize(), + "payment": "True", + "min_kycrating": minKYCRating, + "markup": markup, + }; + + if (rateId != null) { + params["id"] = rateId; + } + + return await _getNewTrade(isOnion: isOnion, params: params); + } + + static Future> _getNewTrade({ + required bool isOnion, + required Map params, + }) async { + final uri = _buildUri( + isOnion: isOnion, + method: "new_trade", + params: params, + ); + + try { + final json = await _makeGetRequest(uri); + final map = Map.from(json as Map); + + return ExchangeResponse(value: TrocadorTradeNew.fromMap(map)); + } catch (e, s) { + Logging.instance + .log("_getNewTrade exception: $e\n$s", level: LogLevel.Error); + return ExchangeResponse( + exception: ExchangeException( + e.toString(), + ExchangeExceptionType.generic, + ), + ); + } + } +} diff --git a/lib/services/exchange/trocador/trocador_exchange.dart b/lib/services/exchange/trocador/trocador_exchange.dart new file mode 100644 index 000000000..e0becb11c --- /dev/null +++ b/lib/services/exchange/trocador/trocador_exchange.dart @@ -0,0 +1,394 @@ +import 'dart:math'; + +import 'package:decimal/decimal.dart'; +import 'package:stackwallet/exceptions/exchange/exchange_exception.dart'; +import 'package:stackwallet/models/exchange/response_objects/estimate.dart'; +import 'package:stackwallet/models/exchange/response_objects/range.dart'; +import 'package:stackwallet/models/exchange/response_objects/trade.dart'; +import 'package:stackwallet/models/isar/exchange_cache/currency.dart'; +import 'package:stackwallet/models/isar/exchange_cache/pair.dart'; +import 'package:stackwallet/services/exchange/exchange.dart'; +import 'package:stackwallet/services/exchange/exchange_response.dart'; +import 'package:stackwallet/services/exchange/trocador/response_objects/trocador_coin.dart'; +import 'package:stackwallet/services/exchange/trocador/response_objects/trocador_quote.dart'; +import 'package:stackwallet/services/exchange/trocador/trocador_api.dart'; +import 'package:uuid/uuid.dart'; + +class TrocadorExchange extends Exchange { + TrocadorExchange._(); + + static TrocadorExchange? _instance; + static TrocadorExchange get instance => _instance ??= TrocadorExchange._(); + + static const exchangeName = "Trocador"; + + static const onlySupportedNetwork = "Mainnet"; + + @override + Future> createTrade({ + required String from, + required String to, + required bool fixedRate, + required Decimal amount, + required String addressTo, + String? extraId, + required String addressRefund, + required String refundExtraId, + Estimate? estimate, + required bool reversed, + }) async { + final response = reversed + ? await TrocadorAPI.createNewPaymentRateTrade( + isOnion: false, + rateId: estimate?.rateId, + fromTicker: from.toLowerCase(), + fromNetwork: onlySupportedNetwork, + toTicker: to.toLowerCase(), + toNetwork: onlySupportedNetwork, + toAmount: amount.toString(), + receivingAddress: addressTo, + receivingMemo: null, + refundAddress: addressRefund, + refundMemo: null, + exchangeProvider: estimate!.exchangeProvider!, + isFixedRate: fixedRate, + ) + : await TrocadorAPI.createNewStandardRateTrade( + isOnion: false, + rateId: estimate?.rateId, + fromTicker: from.toLowerCase(), + fromNetwork: onlySupportedNetwork, + toTicker: to.toLowerCase(), + toNetwork: onlySupportedNetwork, + fromAmount: amount.toString(), + receivingAddress: addressTo, + receivingMemo: null, + refundAddress: addressRefund, + refundMemo: null, + exchangeProvider: estimate!.exchangeProvider!, + isFixedRate: fixedRate, + ); + + if (response.value == null) { + return ExchangeResponse(exception: response.exception); + } + + final trade = response.value!; + + return ExchangeResponse( + value: Trade( + uuid: const Uuid().v1(), + tradeId: trade.tradeId, + rateType: fixedRate ? "fixed" : "floating", + direction: reversed ? "reversed" : "direct", + timestamp: trade.date, + updatedAt: trade.date, + payInCurrency: trade.coinFrom, + payInAmount: trade.amountFrom.toString(), + payInAddress: trade.addressProvider, + payInNetwork: trade.networkFrom, + payInExtraId: trade.addressProviderMemo, + payInTxid: "", + payOutCurrency: trade.coinTo, + payOutAmount: trade.amountTo.toString(), + payOutAddress: trade.addressUser, + payOutNetwork: trade.networkTo, + payOutExtraId: trade.addressUserMemo, + payOutTxid: "", + refundAddress: trade.refundAddress, + refundExtraId: trade.refundAddressMemo, + status: trade.status, + exchangeName: "$exchangeName (${trade.provider})", + ), + ); + } + + List? _cachedCurrencies; + + @override + Future>> getAllCurrencies( + bool fixedRate) async { + _cachedCurrencies ??= (await TrocadorAPI.getCoins(isOnion: false)).value; + + _cachedCurrencies?.removeWhere((e) => e.network != onlySupportedNetwork); + + final value = _cachedCurrencies + ?.map( + (e) => Currency( + exchangeName: exchangeName, + ticker: e.ticker, + name: e.name, + network: e.network, + image: e.image, + isFiat: false, + rateType: SupportedRateType.both, + isStackCoin: Currency.checkIsStackCoin(e.ticker), + tokenContract: null, + isAvailable: true, + ), + ) + .toList(); + + if (value == null) { + return ExchangeResponse( + exception: ExchangeException( + "Failed to fetch trocador coins", + ExchangeExceptionType.generic, + ), + ); + } else { + return ExchangeResponse(value: value); + } + } + + @override + Future>> getAllPairs(bool fixedRate) async { + final response = await getAllCurrencies(fixedRate); + + if (response.value == null) { + return ExchangeResponse(exception: response.exception); + } + + final List pairs = []; + + for (int i = 0; i < response.value!.length; i++) { + final a = response.value![i]; + + for (int j = i + 1; j < response.value!.length; j++) { + final b = response.value![j]; + + pairs.add( + Pair( + exchangeName: exchangeName, + from: a.ticker, + to: b.ticker, + rateType: SupportedRateType.both, + ), + ); + pairs.add( + Pair( + exchangeName: exchangeName, + to: a.ticker, + from: b.ticker, + rateType: SupportedRateType.both, + ), + ); + } + } + + return ExchangeResponse(value: pairs); + } + + @override + Future>> getEstimates( + String from, + String to, + Decimal amount, + bool fixedRate, + bool reversed, + ) async { + final response = reversed + ? await TrocadorAPI.getNewPaymentRate( + isOnion: false, + fromTicker: from, + fromNetwork: onlySupportedNetwork, + toTicker: to, + toNetwork: onlySupportedNetwork, + toAmount: amount.toString(), + ) + : await TrocadorAPI.getNewStandardRate( + isOnion: false, + fromTicker: from, + fromNetwork: onlySupportedNetwork, + toTicker: to, + toNetwork: onlySupportedNetwork, + fromAmount: amount.toString(), + ); + + if (response.value == null) { + return ExchangeResponse(exception: response.exception); + } + + final List estimates = []; + final List cOrLowerQuotes = []; + + for (final quote in response.value!.quotes) { + if (quote.fixed == fixedRate && + quote.provider.toLowerCase() != "changenow") { + final rating = quote.kycRating.toLowerCase(); + if (rating == "a" || rating == "b") { + estimates.add( + Estimate( + estimatedAmount: reversed ? quote.amountFrom! : quote.amountTo!, + fixedRate: quote.fixed, + reversed: reversed, + exchangeProvider: quote.provider, + rateId: response.value!.tradeId, + kycRating: quote.kycRating, + ), + ); + } else { + cOrLowerQuotes.add(quote); + } + } + } + + cOrLowerQuotes.sort((a, b) => b.waste.compareTo(a.waste)); + + for (int i = 0; i < min(3, cOrLowerQuotes.length); i++) { + final quote = cOrLowerQuotes[i]; + estimates.add( + Estimate( + estimatedAmount: reversed ? quote.amountFrom! : quote.amountTo!, + fixedRate: quote.fixed, + reversed: reversed, + exchangeProvider: quote.provider, + rateId: response.value!.tradeId, + kycRating: quote.kycRating, + ), + ); + } + + return ExchangeResponse( + value: estimates + ..sort((a, b) => b.estimatedAmount.compareTo(a.estimatedAmount)), + ); + } + + @override + Future>> getPairedCurrencies( + String forCurrency, bool fixedRate) async { + // TODO: implement getPairedCurrencies + throw UnimplementedError(); + } + + @override + Future>> getPairsFor( + String currency, + bool fixedRate, + ) async { + final response = await getAllPairs(fixedRate); + if (response.value == null) { + return ExchangeResponse(exception: response.exception); + } + + final pairs = response.value!.where( + (e) => + e.from.toUpperCase() == currency.toUpperCase() || + e.to.toUpperCase() == currency.toUpperCase(), + ); + + return ExchangeResponse(value: pairs.toList()); + } + + @override + Future> getRange( + String from, + String to, + bool fixedRate, + ) async { + if (_cachedCurrencies == null) { + await getAllCurrencies(fixedRate); + } + if (_cachedCurrencies == null) { + return ExchangeResponse( + exception: ExchangeException( + "Failed to updated trocador cached coins to get min/max range", + ExchangeExceptionType.generic, + ), + ); + } + + final fromCoin = _cachedCurrencies! + .firstWhere((e) => e.ticker.toLowerCase() == from.toLowerCase()); + + return ExchangeResponse( + value: Range( + max: fromCoin.maximum, + min: fromCoin.minimum, + ), + ); + } + + @override + Future> getTrade(String tradeId) async { + // TODO: implement getTrade + throw UnimplementedError(); + } + + @override + Future>> getTrades() async { + // TODO: implement getTrades + throw UnimplementedError(); + } + + @override + String get name => exchangeName; + + @override + Future> updateTrade(Trade trade) async { + final response = await TrocadorAPI.getTrade( + isOnion: false, + tradeId: trade.tradeId, + ); + + if (response.value != null) { + final updated = response.value!; + final updatedTrade = Trade( + uuid: trade.uuid, + tradeId: updated.tradeId, + rateType: trade.rateType, + direction: trade.direction, + timestamp: trade.timestamp, + updatedAt: DateTime.now(), + payInCurrency: updated.coinFrom, + payInAmount: updated.amountFrom.toString(), + payInAddress: updated.addressProvider, + payInNetwork: trade.payInNetwork, + payInExtraId: trade.payInExtraId, + payInTxid: trade.payInTxid, + payOutCurrency: updated.coinTo, + payOutAmount: updated.amountTo.toString(), + payOutAddress: updated.addressUser, + payOutNetwork: trade.payOutNetwork, + payOutExtraId: trade.payOutExtraId, + payOutTxid: trade.payOutTxid, + refundAddress: trade.refundAddress, + refundExtraId: trade.refundExtraId, + status: updated.status, + exchangeName: "$exchangeName (${updated.provider})", + ); + + return ExchangeResponse(value: updatedTrade); + } else { + if (response.exception?.type == ExchangeExceptionType.orderNotFound) { + final updatedTrade = Trade( + uuid: trade.uuid, + tradeId: trade.tradeId, + rateType: trade.rateType, + direction: trade.direction, + timestamp: trade.timestamp, + updatedAt: DateTime.now(), + payInCurrency: trade.payInCurrency, + payInAmount: trade.payInAmount, + payInAddress: trade.payInAddress, + payInNetwork: trade.payInNetwork, + payInExtraId: trade.payInExtraId, + payInTxid: trade.payInTxid, + payOutCurrency: trade.payOutCurrency, + payOutAmount: trade.payOutAmount, + payOutAddress: trade.payOutAddress, + payOutNetwork: trade.payOutNetwork, + payOutExtraId: trade.payOutExtraId, + payOutTxid: trade.payOutTxid, + refundAddress: trade.refundAddress, + refundExtraId: trade.refundExtraId, + status: "Unknown", + exchangeName: trade.exchangeName, + ); + return ExchangeResponse(value: updatedTrade); + } + return ExchangeResponse(exception: response.exception); + } + } +} diff --git a/lib/utilities/assets.dart b/lib/utilities/assets.dart index b6e77f878..6444b2896 100644 --- a/lib/utilities/assets.dart +++ b/lib/utilities/assets.dart @@ -1,5 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:stackwallet/services/exchange/change_now/change_now_exchange.dart'; +import 'package:stackwallet/services/exchange/majestic_bank/majestic_bank_exchange.dart'; +import 'package:stackwallet/services/exchange/simpleswap/simpleswap_exchange.dart'; +import 'package:stackwallet/services/exchange/trocador/trocador_exchange.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/theme/color_theme.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; @@ -63,6 +67,23 @@ class _EXCHANGE { String get simpleSwap => "${_path}simpleswap-icon.svg"; String get majesticBankBlue => "${_path}mb_blue.svg"; String get majesticBankGreen => "${_path}mb_green.svg"; + String get trocador => "${_path}trocador.svg"; + + String getIconFor({required String exchangeName}) { + switch (exchangeName) { + case SimpleSwapExchange.exchangeName: + return simpleSwap; + case ChangeNowExchange.exchangeName: + return changeNow; + case MajesticBankExchange.exchangeName: + return majesticBankBlue; + case TrocadorExchange.exchangeName: + return trocador; + default: + throw ArgumentError("Invalid exchange name passed to " + "Assets.exchange.getIconFor()"); + } + } } class _BUY { @@ -285,6 +306,11 @@ class _SVG { String get list => "assets/svg/list-ul.svg"; String get unclaimedPaynym => "assets/svg/unclaimed.png"; + String get trocadorRatingA => "assets/svg/trocador_rating_a.svg"; + String get trocadorRatingB => "assets/svg/trocador_rating_b.svg"; + String get trocadorRatingC => "assets/svg/trocador_rating_c.svg"; + String get trocadorRatingD => "assets/svg/trocador_rating_d.svg"; + // TODO provide proper assets String get bitcoinTestnet => "assets/svg/coin_icons/Bitcoin.svg"; String get bitcoincashTestnet => "assets/svg/coin_icons/Bitcoincash.svg"; diff --git a/lib/widgets/exchange/trocador/trocador_kyc_icon.dart b/lib/widgets/exchange/trocador/trocador_kyc_icon.dart new file mode 100644 index 000000000..18250932b --- /dev/null +++ b/lib/widgets/exchange/trocador/trocador_kyc_icon.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/exchange/trocador/trocador_rating_type_enum.dart'; + +class TrocadorKYCIcon extends StatelessWidget { + const TrocadorKYCIcon({ + Key? key, + required this.kycType, + this.width = 18, + this.height = 18, + }) : super(key: key); + + final TrocadorKYCType kycType; + final double width; + final double height; + + String _getAssetName(TrocadorKYCType type) { + switch (type) { + case TrocadorKYCType.a: + return Assets.svg.trocadorRatingA; + case TrocadorKYCType.b: + return Assets.svg.trocadorRatingB; + case TrocadorKYCType.c: + return Assets.svg.trocadorRatingC; + case TrocadorKYCType.d: + return Assets.svg.trocadorRatingD; + } + } + + Color _getColor(TrocadorKYCType type, BuildContext context) { + switch (type) { + case TrocadorKYCType.a: + return Theme.of(context).extension()!.accentColorGreen; + case TrocadorKYCType.b: + return const Color(0xFF7AA500); + case TrocadorKYCType.c: + return Theme.of(context).extension()!.accentColorYellow; + case TrocadorKYCType.d: + return const Color(0xFFF37B58); + } + } + + @override + Widget build(BuildContext context) { + return SvgPicture.asset( + _getAssetName(kycType), + width: width, + height: height, + color: _getColor(kycType, context), + ); + } +} diff --git a/lib/widgets/exchange/trocador/trocador_kyc_info_button.dart b/lib/widgets/exchange/trocador/trocador_kyc_info_button.dart new file mode 100644 index 000000000..785ca9457 --- /dev/null +++ b/lib/widgets/exchange/trocador/trocador_kyc_info_button.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; +import 'package:stackwallet/widgets/exchange/trocador/trocador_kyc_icon.dart'; +import 'package:stackwallet/widgets/exchange/trocador/trocador_rating_type_enum.dart'; +import 'package:stackwallet/widgets/trocador_kyc_rating_info.dart'; + +class TrocadorKYCInfoButton extends StatelessWidget { + const TrocadorKYCInfoButton({ + Key? key, + required this.kycType, + }) : super(key: key); + + final TrocadorKYCType kycType; + + @override + Widget build(BuildContext context) { + return IconButton( + onPressed: () { + showDialog( + context: context, + builder: (context) => const TrocadorKYCRatingInfo(), + ); + }, + icon: TrocadorKYCIcon( + kycType: kycType, + ), + ); + } +} diff --git a/lib/widgets/exchange/trocador/trocador_rating_type_enum.dart b/lib/widgets/exchange/trocador/trocador_rating_type_enum.dart new file mode 100644 index 000000000..538ee8c37 --- /dev/null +++ b/lib/widgets/exchange/trocador/trocador_rating_type_enum.dart @@ -0,0 +1,15 @@ +enum TrocadorKYCType { + a, + b, + c, + d; + + static TrocadorKYCType fromString(String type) { + for (final result in values) { + if (result.name == type.toLowerCase()) { + return result; + } + } + throw ArgumentError("Invalid trocador kyc type: $type"); + } +} diff --git a/lib/widgets/trocador_kyc_rating_info.dart b/lib/widgets/trocador_kyc_rating_info.dart new file mode 100644 index 000000000..bfd786e9c --- /dev/null +++ b/lib/widgets/trocador_kyc_rating_info.dart @@ -0,0 +1,122 @@ +import 'package:flutter/material.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/exchange/trocador/trocador_kyc_icon.dart'; +import 'package:stackwallet/widgets/exchange/trocador/trocador_rating_type_enum.dart'; +import 'package:stackwallet/widgets/stack_dialog.dart'; + +class TrocadorKYCRatingInfo extends StatelessWidget { + const TrocadorKYCRatingInfo({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final small = MediaQuery.of(context).size.width <= 500; + return ConditionalParent( + condition: !small, + builder: (child) => child, + child: ConditionalParent( + condition: small, + builder: (child) { + return StackDialogBase( + child: child, + ); + }, + child: Column( + children: [ + Text( + "Trocador KYC Rating", + style: STextStyles.pageTitleH2(context), + ), + const SizedBox( + height: 16, + ), + const _Rating( + kycType: TrocadorKYCType.a, + text: "Never asks for user verification.", + ), + const SizedBox( + height: 16, + ), + const _Rating( + kycType: TrocadorKYCType.b, + text: "Rarely asks for verification. Refunds if refused.", + ), + const SizedBox( + height: 16, + ), + const _Rating( + kycType: TrocadorKYCType.c, + text: + "Rarely asks for verification. Refunds if refused, unless a " + "legal order prevents it.", + ), + const SizedBox( + height: 16, + ), + const _Rating( + kycType: TrocadorKYCType.d, + text: + "Rarely asks for verification. In case of refusal may block " + "funds indefinitely without a legal order.", + ), + if (small) + Padding( + padding: const EdgeInsets.only( + top: 16, + ), + child: Row( + children: [ + const Spacer(), + const SizedBox( + width: 16, + ), + Expanded( + child: SecondaryButton( + label: "Close", + onPressed: Navigator.of(context).pop, + ), + ) + ], + ), + ), + ], + ), + ), + ); + } +} + +class _Rating extends StatelessWidget { + const _Rating({ + Key? key, + required this.kycType, + required this.text, + }) : super(key: key); + + final TrocadorKYCType kycType; + final String text; + + @override + Widget build(BuildContext context) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TrocadorKYCIcon( + kycType: kycType, + width: 20, + height: 20, + ), + const SizedBox( + width: 8, + ), + Flexible( + child: Text( + text, + style: STextStyles.subtitle(context), + ), + ), + ], + ); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 96d1ae623..a754c2143 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -327,6 +327,10 @@ flutter: - assets/svg/framed-gear.svg - assets/svg/list-ul.svg - assets/svg/cc.svg + - assets/svg/trocador_rating_a.svg + - assets/svg/trocador_rating_b.svg + - assets/svg/trocador_rating_c.svg + - assets/svg/trocador_rating_d.svg # coin icons