From df3935d5d4d0b0dc93514b59ee1ad85f1de5a1b5 Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 1 Jul 2024 13:02:08 -0600 Subject: [PATCH] nanswap --- .../svg/campfire/exchange_icons/nanswap.svg | 69 +++ .../svg/stack_duo/exchange_icons/nanswap.svg | 69 +++ .../stack_wallet/exchange_icons/nanswap.svg | 69 +++ .../exchange_currency_selection_view.dart | 3 + lib/pages/exchange_view/exchange_form.dart | 2 + .../exchange_step_views/step_2_view.dart | 4 +- .../exchange_step_views/step_3_view.dart | 4 +- .../exchange_provider_options.dart | 26 +- .../exchange_view/trade_details_view.dart | 5 + .../subwidgets/desktop_step_2.dart | 315 ++++++----- .../subwidgets/desktop_step_3.dart | 35 +- lib/services/exchange/exchange.dart | 7 + .../exchange_data_loading_service.dart | 27 + .../majestic_bank/majestic_bank_exchange.dart | 3 + .../api_response_models/n_currency.dart | 43 ++ .../api_response_models/n_estimate.dart | 32 ++ .../nanswap/api_response_models/n_trade.dart | 81 +++ .../exchange/nanswap/nanswap_api.dart | 517 ++++++++++++++++++ .../exchange/nanswap/nanswap_exchange.dart | 461 ++++++++++++++++ lib/utilities/assets.dart | 5 + 20 files changed, 1605 insertions(+), 172 deletions(-) create mode 100644 asset_sources/svg/campfire/exchange_icons/nanswap.svg create mode 100644 asset_sources/svg/stack_duo/exchange_icons/nanswap.svg create mode 100644 asset_sources/svg/stack_wallet/exchange_icons/nanswap.svg create mode 100644 lib/services/exchange/nanswap/api_response_models/n_currency.dart create mode 100644 lib/services/exchange/nanswap/api_response_models/n_estimate.dart create mode 100644 lib/services/exchange/nanswap/api_response_models/n_trade.dart create mode 100644 lib/services/exchange/nanswap/nanswap_api.dart create mode 100644 lib/services/exchange/nanswap/nanswap_exchange.dart diff --git a/asset_sources/svg/campfire/exchange_icons/nanswap.svg b/asset_sources/svg/campfire/exchange_icons/nanswap.svg new file mode 100644 index 000000000..caceb9a29 --- /dev/null +++ b/asset_sources/svg/campfire/exchange_icons/nanswap.svg @@ -0,0 +1,69 @@ + + + + + + image/svg+xml + + nanswap + + + + + + nanswap + + + + diff --git a/asset_sources/svg/stack_duo/exchange_icons/nanswap.svg b/asset_sources/svg/stack_duo/exchange_icons/nanswap.svg new file mode 100644 index 000000000..caceb9a29 --- /dev/null +++ b/asset_sources/svg/stack_duo/exchange_icons/nanswap.svg @@ -0,0 +1,69 @@ + + + + + + image/svg+xml + + nanswap + + + + + + nanswap + + + + diff --git a/asset_sources/svg/stack_wallet/exchange_icons/nanswap.svg b/asset_sources/svg/stack_wallet/exchange_icons/nanswap.svg new file mode 100644 index 000000000..caceb9a29 --- /dev/null +++ b/asset_sources/svg/stack_wallet/exchange_icons/nanswap.svg @@ -0,0 +1,69 @@ + + + + + + image/svg+xml + + nanswap + + + + + + nanswap + + + + diff --git a/lib/pages/exchange_view/exchange_coin_selection/exchange_currency_selection_view.dart b/lib/pages/exchange_view/exchange_coin_selection/exchange_currency_selection_view.dart index 109bee3ba..e3ae98acd 100644 --- a/lib/pages/exchange_view/exchange_coin_selection/exchange_currency_selection_view.dart +++ b/lib/pages/exchange_view/exchange_coin_selection/exchange_currency_selection_view.dart @@ -22,6 +22,7 @@ import '../../../services/exchange/change_now/change_now_exchange.dart'; import '../../../services/exchange/exchange.dart'; import '../../../services/exchange/exchange_data_loading_service.dart'; import '../../../services/exchange/majestic_bank/majestic_bank_exchange.dart'; +import '../../../services/exchange/nanswap/nanswap_exchange.dart'; import '../../../services/exchange/trocador/trocador_exchange.dart'; import '../../../themes/stack_colors.dart'; import '../../../utilities/assets.dart'; @@ -117,6 +118,8 @@ class _ExchangeCurrencySelectionViewState .exchangeNameEqualTo(MajesticBankExchange.exchangeName) .or() .exchangeNameStartsWith(TrocadorExchange.exchangeName) + .or() + .exchangeNameStartsWith(NanswapExchange.exchangeName) .findAll(); final cn = await ChangeNowExchange.instance.getPairedCurrencies( diff --git a/lib/pages/exchange_view/exchange_form.dart b/lib/pages/exchange_view/exchange_form.dart index 9d227f231..111d38448 100644 --- a/lib/pages/exchange_view/exchange_form.dart +++ b/lib/pages/exchange_view/exchange_form.dart @@ -33,6 +33,7 @@ import '../../services/exchange/exchange.dart'; import '../../services/exchange/exchange_data_loading_service.dart'; import '../../services/exchange/exchange_response.dart'; import '../../services/exchange/majestic_bank/majestic_bank_exchange.dart'; +import '../../services/exchange/nanswap/nanswap_exchange.dart'; import '../../services/exchange/trocador/trocador_exchange.dart'; import '../../themes/stack_colors.dart'; import '../../utilities/amount/amount_unit.dart'; @@ -87,6 +88,7 @@ class _ExchangeFormState extends ConsumerState { MajesticBankExchange.instance, ChangeNowExchange.instance, TrocadorExchange.instance, + NanswapExchange.instance, ]; } } 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 433984627..111c93240 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 @@ -15,7 +15,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../app_config.dart'; import '../../../models/exchange/incomplete_exchange.dart'; import '../../../providers/providers.dart'; -import '../../../services/exchange/majestic_bank/majestic_bank_exchange.dart'; import '../../../themes/stack_colors.dart'; import '../../../utilities/address_utils.dart'; import '../../../utilities/barcode_scanner_interface.dart'; @@ -126,8 +125,7 @@ class _Step2ViewState extends ConsumerState { @override Widget build(BuildContext context) { - final supportsRefund = - ref.watch(efExchangeProvider).name != MajesticBankExchange.exchangeName; + final supportsRefund = ref.watch(efExchangeProvider).supportsRefundAddress; 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 545e2d41a..2cd014fc3 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 @@ -18,7 +18,6 @@ import '../../../models/exchange/response_objects/trade.dart'; import '../../../providers/global/trades_service_provider.dart'; import '../../../providers/providers.dart'; import '../../../services/exchange/exchange_response.dart'; -import '../../../services/exchange/majestic_bank/majestic_bank_exchange.dart'; import '../../../services/notifications_api.dart'; import '../../../themes/stack_colors.dart'; import '../../../utilities/assets.dart'; @@ -63,8 +62,7 @@ class _Step3ViewState extends ConsumerState { @override Widget build(BuildContext context) { - final supportsRefund = - ref.watch(efExchangeProvider).name != MajesticBankExchange.exchangeName; + final supportsRefund = ref.watch(efExchangeProvider).supportsRefundAddress; return Background( child: Scaffold( 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 436e724ea..1ffa12275 100644 --- a/lib/pages/exchange_view/sub_widgets/exchange_provider_options.dart +++ b/lib/pages/exchange_view/sub_widgets/exchange_provider_options.dart @@ -10,17 +10,19 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; + import '../../../models/exchange/aggregate_currency.dart'; -import 'exchange_provider_option.dart'; import '../../../providers/providers.dart'; import '../../../services/exchange/change_now/change_now_exchange.dart'; import '../../../services/exchange/exchange.dart'; import '../../../services/exchange/majestic_bank/majestic_bank_exchange.dart'; +import '../../../services/exchange/nanswap/nanswap_exchange.dart'; import '../../../services/exchange/trocador/trocador_exchange.dart'; import '../../../themes/stack_colors.dart'; import '../../../utilities/prefs.dart'; import '../../../utilities/util.dart'; import '../../../widgets/rounded_white_container.dart'; +import 'exchange_provider_option.dart'; class ExchangeProviderOptions extends ConsumerStatefulWidget { const ExchangeProviderOptions({ @@ -88,6 +90,11 @@ class _ExchangeProviderOptionsState sendCurrency: sendCurrency, receiveCurrency: receivingCurrency, ); + final showNanswap = exchangeSupported( + exchangeName: NanswapExchange.exchangeName, + sendCurrency: sendCurrency, + receiveCurrency: receivingCurrency, + ); return RoundedWhiteContainer( padding: isDesktop ? const EdgeInsets.all(0) : const EdgeInsets.all(12), @@ -134,6 +141,23 @@ class _ExchangeProviderOptionsState reversed: widget.reversed, exchange: TrocadorExchange.instance, ), + if ((showChangeNow || showMajesticBank || showTrocador) && + showNanswap) + isDesktop + ? Container( + height: 1, + color: + Theme.of(context).extension()!.background, + ) + : const SizedBox( + height: 16, + ), + if (showNanswap) + ExchangeOption( + fixedRate: widget.fixedRate, + reversed: widget.reversed, + exchange: NanswapExchange.instance, + ), ], ), ); diff --git a/lib/pages/exchange_view/trade_details_view.dart b/lib/pages/exchange_view/trade_details_view.dart index fd56c4a59..7c2317768 100644 --- a/lib/pages/exchange_view/trade_details_view.dart +++ b/lib/pages/exchange_view/trade_details_view.dart @@ -30,6 +30,7 @@ import '../../route_generator.dart'; import '../../services/exchange/change_now/change_now_exchange.dart'; import '../../services/exchange/exchange.dart'; import '../../services/exchange/majestic_bank/majestic_bank_exchange.dart'; +import '../../services/exchange/nanswap/nanswap_exchange.dart'; import '../../services/exchange/simpleswap/simpleswap_exchange.dart'; import '../../services/exchange/trocador/trocador_exchange.dart'; import '../../themes/stack_colors.dart'; @@ -1330,6 +1331,10 @@ class _TradeDetailsViewState extends ConsumerState { url = "https://majesticbank.sc/track?trx=${trade.tradeId}"; break; + case NanswapExchange.exchangeName: + url = + "https://nanswap.com/transaction/${trade.tradeId}"; + break; default: if (trade.exchangeName diff --git a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart index 64f4e4471..6c84996da 100644 --- a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart +++ b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart @@ -15,8 +15,7 @@ import 'package:tuple/tuple.dart'; import '../../../../app_config.dart'; import '../../../../models/contact_address_entry.dart'; -import '../../../../providers/exchange/exchange_send_from_wallet_id_provider.dart'; -import '../../../../providers/global/wallets_provider.dart'; +import '../../../../providers/providers.dart'; import '../../../../themes/stack_colors.dart'; import '../../../../utilities/clipboard_interface.dart'; import '../../../../utilities/constants.dart'; @@ -88,7 +87,7 @@ class _DesktopStep2State extends ConsumerState { } widget.enableNextChanged.call( - _toController.text.isNotEmpty && _refundController.text.isNotEmpty, + _next(), ); } @@ -120,7 +119,7 @@ class _DesktopStep2State extends ConsumerState { Logging.instance.log("$e\n$s", level: LogLevel.Info); } widget.enableNextChanged.call( - _toController.text.isNotEmpty && _refundController.text.isNotEmpty, + _next(), ); } @@ -167,7 +166,7 @@ class _DesktopStep2State extends ConsumerState { _toController.text = entry.address; ref.read(desktopExchangeModelProvider)!.recipientAddress = entry.address; widget.enableNextChanged.call( - _toController.text.isNotEmpty && _refundController.text.isNotEmpty, + _next(), ); } } @@ -215,11 +214,21 @@ class _DesktopStep2State extends ConsumerState { _refundController.text = entry.address; ref.read(desktopExchangeModelProvider)!.refundAddress = entry.address; widget.enableNextChanged.call( - _toController.text.isNotEmpty && _refundController.text.isNotEmpty, + _next(), ); } } + bool _next() { + if (doesRefundAddress) { + return _toController.text.isNotEmpty && _refundController.text.isNotEmpty; + } else { + return _toController.text.isNotEmpty; + } + } + + late final bool doesRefundAddress; + @override void initState() { clipboard = widget.clipboard; @@ -230,6 +239,13 @@ class _DesktopStep2State extends ConsumerState { _toFocusNode = FocusNode(); _refundFocusNode = FocusNode(); + doesRefundAddress = ref.read(efExchangeProvider).supportsRefundAddress; + + if (!doesRefundAddress) { + // hack: set to empty to not throw null unwrap error later + ref.read(desktopExchangeModelProvider)!.refundAddress = ""; + } + final tuple = ref.read(exchangeSendFromWalletIdStateProvider.state).state; if (tuple != null) { if (ref.read(desktopExchangeModelProvider)!.receiveTicker.toLowerCase() == @@ -243,8 +259,9 @@ class _DesktopStep2State extends ConsumerState { ref.read(desktopExchangeModelProvider)!.recipientAddress = _toController.text; } else { - if (ref.read(desktopExchangeModelProvider)!.sendTicker.toUpperCase() == - tuple.item2.ticker.toUpperCase()) { + if (doesRefundAddress && + ref.read(desktopExchangeModelProvider)!.sendTicker.toUpperCase() == + tuple.item2.ticker.toUpperCase()) { _refundController.text = ref .read(pWallets) .getWallet(tuple.item1) @@ -341,8 +358,7 @@ class _DesktopStep2State extends ConsumerState { style: STextStyles.field(context), onChanged: (value) { widget.enableNextChanged.call( - _toController.text.isNotEmpty && - _refundController.text.isNotEmpty, + _next(), ); }, decoration: standardInputDecoration( @@ -376,8 +392,7 @@ class _DesktopStep2State extends ConsumerState { .read(desktopExchangeModelProvider)! .recipientAddress = _toController.text; widget.enableNextChanged.call( - _toController.text.isNotEmpty && - _refundController.text.isNotEmpty, + _next(), ); }, child: const XIcon(), @@ -397,8 +412,7 @@ class _DesktopStep2State extends ConsumerState { .read(desktopExchangeModelProvider)! .recipientAddress = _toController.text; widget.enableNextChanged.call( - _toController.text.isNotEmpty && - _refundController.text.isNotEmpty, + _next(), ); } }, @@ -435,155 +449,158 @@ class _DesktopStep2State extends ConsumerState { style: STextStyles.desktopTextExtraExtraSmall(context), ), ), - const SizedBox( - height: 24, - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Refund Wallet (required)", - style: STextStyles.desktopTextExtraExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveSearchIconRight, - ), - ), - if (AppConfig.isStackCoin( - ref.watch( - desktopExchangeModelProvider - .select((value) => value!.sendTicker), - ), - )) - CustomTextButton( - text: "Choose from Stack", - onTap: selectRefundAddressFromStack, - ), - ], - ), - const SizedBox( - height: 10, - ), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + if (doesRefundAddress) + const SizedBox( + height: 24, ), - child: TextField( - key: const Key("refundExchangeStep2ViewAddressFieldKey"), - controller: _refundController, - readOnly: false, - autocorrect: false, - enableSuggestions: false, - // inputFormatters: [ - // FilteringTextInputFormatter.allow(RegExp("[a-zA-Z0-9]{34}")), - // ], - toolbarOptions: const ToolbarOptions( - copy: false, - cut: false, - paste: true, - selectAll: false, - ), - focusNode: _refundFocusNode, - style: STextStyles.field(context), - onChanged: (value) { - widget.enableNextChanged.call( - _toController.text.isNotEmpty && - _refundController.text.isNotEmpty, - ); - }, - decoration: standardInputDecoration( - "Enter ${ref.watch(desktopExchangeModelProvider.select((value) => value!.sendTicker.toUpperCase()))} refund address", - _refundFocusNode, - context, - desktopMed: true, - ).copyWith( - contentPadding: const EdgeInsets.only( - left: 16, - top: 6, - bottom: 8, - right: 5, + if (doesRefundAddress) + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Refund Wallet (required)", + style: STextStyles.desktopTextExtraExtraSmall(context).copyWith( + color: Theme.of(context) + .extension()! + .textFieldActiveSearchIconRight, + ), ), - suffixIcon: Padding( - padding: _refundController.text.isEmpty - ? const EdgeInsets.only(right: 16) - : const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - _refundController.text.isNotEmpty - ? TextFieldIconButton( - key: const Key( - "sendViewClearAddressFieldButtonKey", - ), - onTap: () { - _refundController.text = ""; - ref - .read(desktopExchangeModelProvider)! - .refundAddress = _refundController.text; - - widget.enableNextChanged.call( - _toController.text.isNotEmpty && - _refundController.text.isNotEmpty, - ); - }, - child: const XIcon(), - ) - : TextFieldIconButton( - key: const Key( - "sendViewPasteAddressFieldButtonKey", - ), - onTap: () async { - final ClipboardData? data = await clipboard - .getData(Clipboard.kTextPlain); - if (data?.text != null && - data!.text!.isNotEmpty) { - final content = data.text!.trim(); - - _refundController.text = content; + if (AppConfig.isStackCoin( + ref.watch( + desktopExchangeModelProvider + .select((value) => value!.sendTicker), + ), + )) + CustomTextButton( + text: "Choose from Stack", + onTap: selectRefundAddressFromStack, + ), + ], + ), + if (doesRefundAddress) + const SizedBox( + height: 10, + ), + if (doesRefundAddress) + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: const Key("refundExchangeStep2ViewAddressFieldKey"), + controller: _refundController, + readOnly: false, + autocorrect: false, + enableSuggestions: false, + // inputFormatters: [ + // FilteringTextInputFormatter.allow(RegExp("[a-zA-Z0-9]{34}")), + // ], + toolbarOptions: const ToolbarOptions( + copy: false, + cut: false, + paste: true, + selectAll: false, + ), + focusNode: _refundFocusNode, + style: STextStyles.field(context), + onChanged: (value) { + widget.enableNextChanged.call( + _next(), + ); + }, + decoration: standardInputDecoration( + "Enter ${ref.watch(desktopExchangeModelProvider.select((value) => value!.sendTicker.toUpperCase()))} refund address", + _refundFocusNode, + context, + desktopMed: true, + ).copyWith( + contentPadding: const EdgeInsets.only( + left: 16, + top: 6, + bottom: 8, + right: 5, + ), + suffixIcon: Padding( + padding: _refundController.text.isEmpty + ? const EdgeInsets.only(right: 16) + : const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _refundController.text.isNotEmpty + ? TextFieldIconButton( + key: const Key( + "sendViewClearAddressFieldButtonKey", + ), + onTap: () { + _refundController.text = ""; ref .read(desktopExchangeModelProvider)! .refundAddress = _refundController.text; widget.enableNextChanged.call( - _toController.text.isNotEmpty && - _refundController.text.isNotEmpty, + _next(), ); - } - }, - child: _refundController.text.isEmpty - ? const ClipboardIcon() - : const XIcon(), - ), - if (_refundController.text.isEmpty && - AppConfig.isStackCoin( - ref.watch( - desktopExchangeModelProvider - .select((value) => value!.sendTicker), - ), - )) - TextFieldIconButton( - key: const Key("sendViewAddressBookButtonKey"), - onTap: selectRefundFromAddressBook, - child: const AddressBookIcon(), - ), - ], + }, + child: const XIcon(), + ) + : TextFieldIconButton( + key: const Key( + "sendViewPasteAddressFieldButtonKey", + ), + onTap: () async { + final ClipboardData? data = await clipboard + .getData(Clipboard.kTextPlain); + if (data?.text != null && + data!.text!.isNotEmpty) { + final content = data.text!.trim(); + + _refundController.text = content; + ref + .read(desktopExchangeModelProvider)! + .refundAddress = _refundController.text; + + widget.enableNextChanged.call( + _next(), + ); + } + }, + child: _refundController.text.isEmpty + ? const ClipboardIcon() + : const XIcon(), + ), + if (_refundController.text.isEmpty && + AppConfig.isStackCoin( + ref.watch( + desktopExchangeModelProvider + .select((value) => value!.sendTicker), + ), + )) + TextFieldIconButton( + key: const Key("sendViewAddressBookButtonKey"), + onTap: selectRefundFromAddressBook, + child: const AddressBookIcon(), + ), + ], + ), ), ), ), ), ), - ), - const SizedBox( - height: 10, - ), - RoundedWhiteContainer( - borderColor: Theme.of(context).extension()!.background, - child: Text( - "In case something goes wrong during the exchange, we might need a refund address so we can return your coins back to you.", - style: STextStyles.desktopTextExtraExtraSmall(context), + if (doesRefundAddress) + const SizedBox( + height: 10, + ), + if (doesRefundAddress) + RoundedWhiteContainer( + borderColor: Theme.of(context).extension()!.background, + child: Text( + "In case something goes wrong during the exchange, we might need a refund address so we can return your coins back to you.", + style: STextStyles.desktopTextExtraExtraSmall(context), + ), ), - ), ], ); } 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 8dfb82734..98c4daaf5 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 @@ -10,13 +10,14 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../step_scaffold.dart'; -import 'desktop_step_item.dart'; + import '../../../../providers/providers.dart'; import '../../../../themes/stack_colors.dart'; import '../../../../utilities/enums/exchange_rate_type_enum.dart'; import '../../../../utilities/text_styles.dart'; import '../../../../widgets/rounded_white_container.dart'; +import '../step_scaffold.dart'; +import 'desktop_step_item.dart'; class DesktopStep3 extends ConsumerStatefulWidget { const DesktopStep3({ @@ -97,20 +98,22 @@ class _DesktopStep3State extends ConsumerState { ) ?? "Error", ), - Container( - height: 1, - color: Theme.of(context).extension()!.background, - ), - DesktopStepItem( - vertical: true, - label: - "Refund ${ref.watch(desktopExchangeModelProvider.select((value) => value!.sendTicker.toUpperCase()))} address", - value: ref.watch( - desktopExchangeModelProvider - .select((value) => value!.refundAddress), - ) ?? - "Error", - ), + if (ref.watch(efExchangeProvider).supportsRefundAddress) + Container( + height: 1, + color: Theme.of(context).extension()!.background, + ), + if (ref.watch(efExchangeProvider).supportsRefundAddress) + DesktopStepItem( + vertical: true, + label: + "Refund ${ref.watch(desktopExchangeModelProvider.select((value) => value!.sendTicker.toUpperCase()))} address", + value: ref.watch( + desktopExchangeModelProvider + .select((value) => value!.refundAddress), + ) ?? + "Error", + ), ], ), ), diff --git a/lib/services/exchange/exchange.dart b/lib/services/exchange/exchange.dart index b5a2c4180..bf592fab3 100644 --- a/lib/services/exchange/exchange.dart +++ b/lib/services/exchange/exchange.dart @@ -9,6 +9,7 @@ */ import 'package:decimal/decimal.dart'; + import '../../models/exchange/response_objects/estimate.dart'; import '../../models/exchange/response_objects/range.dart'; import '../../models/exchange/response_objects/trade.dart'; @@ -17,6 +18,7 @@ import '../../models/isar/exchange_cache/pair.dart'; import 'change_now/change_now_exchange.dart'; import 'exchange_response.dart'; import 'majestic_bank/majestic_bank_exchange.dart'; +import 'nanswap/nanswap_exchange.dart'; import 'simpleswap/simpleswap_exchange.dart'; import 'trocador/trocador_exchange.dart'; @@ -33,6 +35,8 @@ abstract class Exchange { return MajesticBankExchange.instance; case TrocadorExchange.exchangeName: return TrocadorExchange.instance; + case NanswapExchange.exchangeName: + return NanswapExchange.instance; default: final split = name.split(" "); if (split.length >= 2) { @@ -45,6 +49,8 @@ abstract class Exchange { String get name; + bool get supportsRefundAddress => true; + Future>> getAllCurrencies(bool fixedRate); Future>> getPairedCurrencies( @@ -97,6 +103,7 @@ abstract class Exchange { static List get exchangesWithTorSupport => [ MajesticBankExchange.instance, TrocadorExchange.instance, + NanswapExchange.instance, // Maybe?? ]; /// List of exchange names which support Tor. diff --git a/lib/services/exchange/exchange_data_loading_service.dart b/lib/services/exchange/exchange_data_loading_service.dart index e500ed5cf..cfb01e150 100644 --- a/lib/services/exchange/exchange_data_loading_service.dart +++ b/lib/services/exchange/exchange_data_loading_service.dart @@ -23,6 +23,7 @@ import '../../utilities/prefs.dart'; import '../../utilities/stack_file_system.dart'; import 'change_now/change_now_exchange.dart'; import 'majestic_bank/majestic_bank_exchange.dart'; +import 'nanswap/nanswap_exchange.dart'; import 'trocador/trocador_exchange.dart'; class ExchangeDataLoadingService { @@ -170,6 +171,7 @@ class ExchangeDataLoadingService { final futures = [ loadMajesticBankCurrencies(), loadTrocadorCurrencies(), + loadNanswapCurrencies(), ]; // If using Tor, don't load data for exchanges which don't support Tor. @@ -382,6 +384,31 @@ class ExchangeDataLoadingService { } } + Future loadNanswapCurrencies() async { + if (_isar == null) { + await initDB(); + } + final responseCurrencies = + await NanswapExchange.instance.getAllCurrencies(false); + + if (responseCurrencies.value != null) { + await isar.writeTxn(() async { + final idsToDelete = await isar.currencies + .where() + .exchangeNameEqualTo(NanswapExchange.exchangeName) + .idProperty() + .findAll(); + await isar.currencies.deleteAll(idsToDelete); + await isar.currencies.putAll(responseCurrencies.value!); + }); + } else { + Logging.instance.log( + "loadNanswapCurrencies: $responseCurrencies", + level: LogLevel.Warning, + ); + } + } + // Future loadMajesticBankPairs() async { // final exchange = MajesticBankExchange.instance; // diff --git a/lib/services/exchange/majestic_bank/majestic_bank_exchange.dart b/lib/services/exchange/majestic_bank/majestic_bank_exchange.dart index e23490e0a..2d283e69e 100644 --- a/lib/services/exchange/majestic_bank/majestic_bank_exchange.dart +++ b/lib/services/exchange/majestic_bank/majestic_bank_exchange.dart @@ -46,6 +46,9 @@ class MajesticBankExchange extends Exchange { "XMR": "Monero", }; + @override + bool get supportsRefundAddress => false; + @override Future> createTrade({ required String from, diff --git a/lib/services/exchange/nanswap/api_response_models/n_currency.dart b/lib/services/exchange/nanswap/api_response_models/n_currency.dart new file mode 100644 index 000000000..87f42de12 --- /dev/null +++ b/lib/services/exchange/nanswap/api_response_models/n_currency.dart @@ -0,0 +1,43 @@ +class NCurrency { + final String id; + final String ticker; + final String name; + final String image; + final String network; + final bool hasExternalId; + final bool feeLess; + + NCurrency({ + required this.id, + required this.ticker, + required this.name, + required this.image, + required this.network, + required this.hasExternalId, + required this.feeLess, + }); + + factory NCurrency.fromJson(Map json) { + return NCurrency( + id: json["id"] as String, + ticker: json['ticker'] as String, + name: json['name'] as String, + image: json['image'] as String, + network: json['network'] as String, + hasExternalId: json['hasExternalId'] as bool, + feeLess: json['feeless'] as bool, + ); + } + + @override + String toString() { + return 'NCurrency {' + 'ticker: $ticker, ' + 'name: $name, ' + 'image: $image, ' + 'network: $network, ' + 'hasExternalId: $hasExternalId, ' + 'feeless: $feeLess' + '}'; + } +} diff --git a/lib/services/exchange/nanswap/api_response_models/n_estimate.dart b/lib/services/exchange/nanswap/api_response_models/n_estimate.dart new file mode 100644 index 000000000..4ee2f51f8 --- /dev/null +++ b/lib/services/exchange/nanswap/api_response_models/n_estimate.dart @@ -0,0 +1,32 @@ +class NEstimate { + final String from; + final String to; + final num amountFrom; + final num amountTo; + + NEstimate({ + required this.from, + required this.to, + required this.amountFrom, + required this.amountTo, + }); + + factory NEstimate.fromJson(Map json) { + return NEstimate( + from: json['from'] as String, + to: json['to'] as String, + amountFrom: json['amountFrom'] as num, + amountTo: json['amountTo'] as num, + ); + } + + @override + String toString() { + return 'NEstimate {' + 'from: $from, ' + 'to: $to, ' + 'amountFrom: $amountFrom, ' + 'amountTo: $amountTo ' + '}'; + } +} diff --git a/lib/services/exchange/nanswap/api_response_models/n_trade.dart b/lib/services/exchange/nanswap/api_response_models/n_trade.dart new file mode 100644 index 000000000..f26e19f3f --- /dev/null +++ b/lib/services/exchange/nanswap/api_response_models/n_trade.dart @@ -0,0 +1,81 @@ +class NTrade { + final String id; + final String from; + final String to; + final num expectedAmountFrom; + final num expectedAmountTo; + final String payinAddress; + final String payoutAddress; + + final String? payinExtraId; + final String? fullLink; + final String? status; + final String? payinHash; + final String? payoutHash; + final num? fromAmount; + final num? toAmount; + final String? fromNetwork; + final String? toNetwork; + + NTrade({ + required this.id, + required this.from, + required this.to, + required this.expectedAmountFrom, + required this.expectedAmountTo, + required this.payinAddress, + required this.payoutAddress, + this.payinExtraId, + this.fullLink, + this.status, + this.payinHash, + this.payoutHash, + this.fromAmount, + this.toAmount, + this.fromNetwork, + this.toNetwork, + }); + + factory NTrade.fromJson(Map json) { + return NTrade( + id: json['id'] as String, + from: json['from'] as String, + to: json['to'] as String, + expectedAmountFrom: num.parse(json['expectedAmountFrom'].toString()), + expectedAmountTo: json['expectedAmountTo'] as num, + payinAddress: json['payinAddress'] as String, + payoutAddress: json['payoutAddress'] as String, + fullLink: json['fullLink'] as String?, + payinExtraId: json['payinExtraId'] as String?, + status: json['status'] as String?, + payinHash: json['payinHash'] as String?, + payoutHash: json['payoutHash'] as String?, + fromAmount: json['fromAmount'] as num?, + toAmount: json['toAmount'] as num?, + fromNetwork: json['fromNetwork'] as String?, + toNetwork: json['toNetwork'] as String?, + ); + } + + @override + String toString() { + return 'NTrade {' + ' id: $id, ' + ' from: $from, ' + ' to: $to, ' + ' expectedAmountFrom: $expectedAmountFrom, ' + ' expectedAmountTo: $expectedAmountTo, ' + ' payinAddress: $payinAddress, ' + ' payoutAddress: $payoutAddress, ' + ' fullLink: $fullLink, ' + ' payinExtraId: $payinExtraId, ' + ' status: $status, ' + ' payinHash: $payinHash, ' + ' payoutHash: $payoutHash ' + ' fromAmount: $fromAmount, ' + ' toAmount: $toAmount, ' + ' fromNetwork: $fromNetwork, ' + ' toNetwork: $toNetwork, ' + '}'; + } +} diff --git a/lib/services/exchange/nanswap/nanswap_api.dart b/lib/services/exchange/nanswap/nanswap_api.dart new file mode 100644 index 000000000..41f06f19a --- /dev/null +++ b/lib/services/exchange/nanswap/nanswap_api.dart @@ -0,0 +1,517 @@ +import 'dart:convert'; + +import 'package:flutter/foundation.dart'; + +import '../../../exceptions/exchange/exchange_exception.dart'; +import '../../../external_api_keys.dart'; +import '../../../networking/http.dart'; +import '../../../utilities/logger.dart'; +import '../../../utilities/prefs.dart'; +import '../../tor_service.dart'; +import '../exchange_response.dart'; +import 'api_response_models/n_currency.dart'; +import 'api_response_models/n_estimate.dart'; +import 'api_response_models/n_trade.dart'; + +class NanswapAPI { + NanswapAPI._(); + + static const authority = "api.nanswap.com"; + static const version = "v1"; + + static NanswapAPI? _instance; + static NanswapAPI get instance => _instance ??= NanswapAPI._(); + + final _client = HTTP(); + + Uri _buildUri({required String endpoint, Map? params}) { + return Uri.https(authority, "/$version/$endpoint", params); + } + + Future _makeGetRequest(Uri uri) async { + int code = -1; + try { + final response = await _client.get( + url: uri, + headers: { + 'Accept': 'application/json', + }, + proxyInfo: Prefs.instance.useTor + ? TorService.sharedInstance.getProxyInfo() + : null, + ); + + code = response.code; + + final parsed = jsonDecode(response.body); + + return parsed; + } catch (e, s) { + Logging.instance.log( + "NanswapAPI._makeRequest($uri) HTTP:$code threw: $e\n$s", + level: LogLevel.Error, + ); + rethrow; + } + } + + Future _makePostRequest( + Uri uri, + Map body, + ) async { + int code = -1; + try { + final response = await _client.post( + url: uri, + headers: { + 'nanswap-api-key': kNanswapApiKey, + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + body: jsonEncode(body), + proxyInfo: Prefs.instance.useTor + ? TorService.sharedInstance.getProxyInfo() + : null, + ); + + code = response.code; + + final data = response.body; + final parsed = jsonDecode(data); + + return parsed; + } catch (e, s) { + Logging.instance.log( + "NanswapAPI._makePostRequest($uri) HTTP:$code threw: $e\n$s", + level: LogLevel.Error, + ); + rethrow; + } + } + + // ============= API =================================================== + + // GET List of supported currencies + // https://api.nanswap.com/v1/all-currencies + // + // Returns a Key => Value map of available currencies. + // + // The Key is the ticker, that can be used in the from and to params of the /get-estimate, /get-limit, /create-order. + // + // The Value is the currency info: + // + // name + // + // logo + // + // network Network of the crypto. + // + // hasExternalId Boolean. If the crypto require a memo/id. + // + // feeless Boolean. If crypto has 0 network fees. + // + // HEADERS + // Accept + // + // application/json + Future>> getSupportedCurrencies() async { + final uri = _buildUri( + endpoint: "all-currencies", + ); + + try { + final json = await _makeGetRequest(uri); + + final List result = []; + for (final key in (json as Map).keys) { + final _map = json[key] as Map; + _map["id"] = key; + result.add( + NCurrency.fromJson( + Map.from(_map), + ), + ); + } + + return ExchangeResponse(value: result); + } catch (e, s) { + Logging.instance.log( + "Nanswap.getSupportedCurrencies() exception: $e\n$s", + level: LogLevel.Error, + ); + return ExchangeResponse( + exception: ExchangeException( + e.toString(), + ExchangeExceptionType.generic, + ), + ); + } + } + + // GET Get estimate + // https://api.nanswap.com/v1/get-estimate?from=XNO&to=BAN&amount=10 + // + // Get estimated exchange amount. + // HEADERS + // Accept + // + // application/json + // PARAMS + // + // from + // XNO + // Ticker from + // + // to + // BAN + // Ticker to + // + // amount + // 10 + // Amount from + Future> getEstimate({ + required String amountFrom, + required String from, + required String to, + }) async { + final uri = _buildUri( + endpoint: "get-estimate", + params: { + "to": to.toUpperCase(), + "from": from.toUpperCase(), + "amount": amountFrom, + }, + ); + + try { + final json = await _makeGetRequest(uri); + + try { + final map = Map.from(json as Map); + + // not sure why the api responds without these sometimes... + map["to"] ??= to.toUpperCase(); + map["from"] ??= from.toUpperCase(); + + return ExchangeResponse( + value: NEstimate.fromJson( + map, + ), + ); + } catch (_) { + Logging.instance.log( + "Nanswap.getEstimate() response was: $json", + level: LogLevel.Error, + ); + rethrow; + } + } catch (e, s) { + Logging.instance.log( + "Nanswap.getEstimate() exception: $e\n$s", + level: LogLevel.Error, + ); + return ExchangeResponse( + exception: ExchangeException( + e.toString(), + ExchangeExceptionType.generic, + ), + ); + } + } + + // GET Get estimate reverse + // https://api.nanswap.com/v1/get-estimate-reverse?from=XNO&to=BAN&amount=1650 + // + // (Only available for feeless crypto) + // + // Get estimate but reversed, it takes toAmount and returns the fromAmount + // estimation. Allows to let user input directly their toAmount wanted. + // HEADERS + // Accept + // + // application/json + // PARAMS + // from + // XNO + // Ticker from + // + // to + // BAN + // Ticker to + // + // amount + // 1650 + // Amount to + Future> getEstimateReversed({ + required String amountTo, + required String from, + required String to, + }) async { + final uri = _buildUri( + endpoint: "get-estimate-reverse", + params: { + "to": to.toUpperCase(), + "from": from.toUpperCase(), + "amount": amountTo, + }, + ); + + try { + final json = await _makeGetRequest(uri); + + final map = Map.from(json as Map); + + // not sure why the api responds without these sometimes... + map["to"] ??= to.toUpperCase(); + map["from"] ??= from.toUpperCase(); + + return ExchangeResponse( + value: NEstimate.fromJson( + map, + ), + ); + } catch (e, s) { + Logging.instance.log( + "Nanswap.getEstimateReverse() exception: $e\n$s", + level: LogLevel.Error, + ); + return ExchangeResponse( + exception: ExchangeException( + e.toString(), + ExchangeExceptionType.generic, + ), + ); + } + } + + // GET Get order limit amount + // https://api.nanswap.com/v1/get-limits?from=XNO&to=BAN + // + // Returns minimum and maximum from amount for a given pair. Maximum amount depends of current liquidity. + // HEADERS + // Accept + // + // application/json + // PARAMS + // from + // XNO + // Ticker from + // + // to + // BAN + // Ticker to + Future> getOrderLimits({ + required String from, + required String to, + }) async { + final uri = _buildUri( + endpoint: "get-limits", + params: { + "to": to.toUpperCase(), + "from": from.toUpperCase(), + }, + ); + + try { + final json = await _makeGetRequest(uri); + + return ExchangeResponse( + value: ( + minFrom: json["min"] as num, + maxFrom: json["max"] as num, + ), + ); + } catch (e, s) { + Logging.instance.log( + "Nanswap.getOrderLimits() exception: $e\n$s", + level: LogLevel.Error, + ); + return ExchangeResponse( + exception: ExchangeException( + e.toString(), + ExchangeExceptionType.generic, + ), + ); + } + } + + // POST Create a new order + // https://api.nanswap.com/v1/create-order + // + // Create a new order and returns order data. You need to send the request body as JSON. + // A valid API key is required in nanswap-api-key header for this request. + // You can get one at https://nanswap.com/API + // Request: + // + // * from ticker of currency you want to exchange + // * to ticker of currency you want to receive + // * amount The amount you want to send + // * toAddress The address that will recieve the exchanged funds + // * extraId (optional) Memo/Id of the toAddress + // + // * itemName (optional) An item name that will be displayed on transaction + // page. Can be used by merchant to provide a better UX to users. Max 128 char. + // * maxDurationSeconds (optional) Maximum seconds after what transaction + // expires. Min: 30s Max: 259200s. Default to 72h or 5min if itemName is set + // Reponse: + // + // * id Order id. + // * from ticker of currency you want to exchange + // * to ticker of currency you want to receive + // * expectedAmountFrom The amount you want to send + // * expectedAmountTo Estimated value that you will get based on the field expectedAmountFrom + // * payinAddress Nanswap's address you need to send the funds to + // * payinExtraId If present, the extra/memo id required for the payinAddress + // * payoutAddress The address that will recieve the exchanged funds + // * fullLink URL of the transaction + // AUTHORIZATIONAPI Key + // Key + // + // nanswap-api-key + // Value + // + // + // HEADERS + // nanswap-api-key + // + // API_KEY + // + // (Required) + // Content-Type + // + // application/json + // Accept + // + // application/json + Future> createOrder({ + required String from, + required String to, + required num fromAmount, + required String toAddress, + String? extraIdOrMemo, + }) async { + final uri = _buildUri( + endpoint: "create-order", + ); + + final body = { + "from": from.toUpperCase(), + "to": to.toUpperCase(), + "amount": fromAmount, + "toAddress": toAddress, + }; + + if (extraIdOrMemo != null) { + body["extraId"] = extraIdOrMemo; + } + + try { + final json = await _makePostRequest(uri, body); + + try { + return ExchangeResponse( + value: NTrade.fromJson( + Map.from(json as Map), + ), + ); + } catch (_) { + debugPrint(json.toString()); + rethrow; + } + } catch (e, s) { + Logging.instance.log( + "Nanswap.createOrder() exception: $e\n$s", + level: LogLevel.Error, + ); + return ExchangeResponse( + exception: ExchangeException( + e.toString(), + ExchangeExceptionType.generic, + ), + ); + } + } + + // GET Get order id data + // https://api.nanswap.com/v1/get-order?id=zYkxDxfmYRM + // + // Returns data of an order id. + // Response: + // + // id Order id. + // + // status Order status, can be one of the following : [waiting, exchanging, sending, completed, error] + // + // from ticker of currency you want to exchange + // + // fromNetwork network of the currency you want to exchange. + // + // to ticker of currency you want to receive + // + // toNetwork network of the currency you want to receive. + // + // expectedAmountFrom The amount you want to send + // + // expectedAmountTo Estimated value that you will get based on the field expectedAmountFrom + // + // amountFrom From Amount Exchanged + // + // amountTo To Amount Exchanged + // + // payinAddress Nanswap's address you need to send the funds to + // + // payinExtraId If present, the extra/memo id required for the payinAddress + // + // payoutAddress The address that will recieve the exchanged funds + // + // payinHash Hash of the transaction you sent us + // + // senderAddress Address which sent us the funds + // + // payoutHash Hash of the transaction we sent to you + // + // HEADERS + // Accept + // + // application/json + // PARAMS + // id + // + // zYkxDxfmYRM + // + // The order id + Future> getOrder({required String id}) async { + final uri = _buildUri( + endpoint: "get-order", + params: { + "id": id, + }, + ); + + try { + final json = await _makeGetRequest(uri); + + try { + return ExchangeResponse( + value: NTrade.fromJson( + Map.from(json as Map), + ), + ); + } catch (_) { + debugPrint(json.toString()); + rethrow; + } + } catch (e, s) { + Logging.instance.log( + "Nanswap.getOrder($id) exception: $e\n$s", + level: LogLevel.Error, + ); + return ExchangeResponse( + exception: ExchangeException( + e.toString(), + ExchangeExceptionType.generic, + ), + ); + } + } +} diff --git a/lib/services/exchange/nanswap/nanswap_exchange.dart b/lib/services/exchange/nanswap/nanswap_exchange.dart new file mode 100644 index 000000000..a2de87c33 --- /dev/null +++ b/lib/services/exchange/nanswap/nanswap_exchange.dart @@ -0,0 +1,461 @@ +import 'package:decimal/decimal.dart'; +import 'package:uuid/uuid.dart'; + +import '../../../app_config.dart'; +import '../../../exceptions/exchange/exchange_exception.dart'; +import '../../../models/exchange/response_objects/estimate.dart'; +import '../../../models/exchange/response_objects/range.dart'; +import '../../../models/exchange/response_objects/trade.dart'; +import '../../../models/isar/exchange_cache/currency.dart'; +import '../../../models/isar/exchange_cache/pair.dart'; +import '../exchange.dart'; +import '../exchange_response.dart'; +import 'api_response_models/n_estimate.dart'; +import 'nanswap_api.dart'; + +class NanswapExchange extends Exchange { + NanswapExchange._(); + + static NanswapExchange? _instance; + static NanswapExchange get instance => _instance ??= NanswapExchange._(); + + static const exchangeName = "Nanswap"; + + static const filter = ["BTC", "BAN", "XNO"]; + + @override + bool get supportsRefundAddress => false; + + @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 { + try { + if (fixedRate) { + throw ExchangeException( + "Nanswap fixedRate not available", + ExchangeExceptionType.generic, + ); + } + if (refundExtraId.isNotEmpty) { + throw ExchangeException( + "Nanswap refundExtraId not available", + ExchangeExceptionType.generic, + ); + } + if (addressRefund.isNotEmpty) { + throw ExchangeException( + "Nanswap addressRefund not available", + ExchangeExceptionType.generic, + ); + } + if (reversed) { + throw ExchangeException( + "Nanswap reversed not available", + ExchangeExceptionType.generic, + ); + } + + final response = await NanswapAPI.instance.createOrder( + from: from, + to: to, + fromAmount: amount.toDouble(), + toAddress: addressTo, + extraIdOrMemo: extraId, + ); + + if (response.exception != null) { + return ExchangeResponse( + exception: response.exception, + ); + } + + final t = response.value!; + print(t); + + return ExchangeResponse( + value: Trade( + uuid: const Uuid().v1(), + tradeId: t.id, + rateType: "estimated", + direction: "normal", + timestamp: DateTime.now(), + updatedAt: DateTime.now(), + payInCurrency: from, + payInAmount: t.expectedAmountFrom.toString(), + payInAddress: t.payinAddress, + payInNetwork: t.toNetwork ?? t.to, + payInExtraId: t.payinExtraId ?? "", + payInTxid: t.payinHash ?? "", + payOutCurrency: to, + payOutAmount: t.expectedAmountTo.toString(), + payOutAddress: t.payoutAddress, + payOutNetwork: t.fromNetwork ?? t.from, + payOutExtraId: "", + payOutTxid: t.payoutHash ?? "", + refundAddress: "", + refundExtraId: "", + status: "waiting", + exchangeName: exchangeName, + ), + ); + } on ExchangeException catch (e) { + return ExchangeResponse( + exception: e, + ); + } catch (e) { + return ExchangeResponse( + exception: ExchangeException( + e.toString(), + ExchangeExceptionType.generic, + ), + ); + } + } + + @override + Future>> getAllCurrencies( + bool fixedRate, + ) async { + try { + if (fixedRate) { + throw ExchangeException( + "Nanswap fixedRate not available", + ExchangeExceptionType.generic, + ); + } + + final response = await NanswapAPI.instance.getSupportedCurrencies(); + + if (response.exception != null) { + return ExchangeResponse( + exception: response.exception, + ); + } + + return ExchangeResponse( + value: response.value! + .where((e) => filter.contains(e.id)) + .map( + (e) => Currency( + exchangeName: exchangeName, + ticker: e.id, + name: e.name, + network: e.network, + image: e.image, + isFiat: false, + rateType: SupportedRateType.estimated, + isStackCoin: AppConfig.isStackCoin(e.id), + tokenContract: null, + isAvailable: true, + ), + ) + .toList(), + ); + } on ExchangeException catch (e) { + return ExchangeResponse( + exception: e, + ); + } catch (e) { + return ExchangeResponse( + exception: ExchangeException( + e.toString(), + ExchangeExceptionType.generic, + ), + ); + } + } + + @override + Future>> getAllPairs(bool fixedRate) async { + throw UnimplementedError(); + } + + @override + Future>> getEstimates( + String from, + String to, + Decimal amount, + bool fixedRate, + bool reversed, + ) async { + try { + if (fixedRate) { + throw ExchangeException( + "Nanswap fixedRate not available", + ExchangeExceptionType.generic, + ); + } + + final ExchangeResponse response; + if (reversed) { + response = await NanswapAPI.instance.getEstimateReversed( + from: from, + to: to, + amountTo: amount.toString(), + ); + } else { + response = await NanswapAPI.instance.getEstimate( + from: from, + to: to, + amountFrom: amount.toString(), + ); + } + + if (response.exception != null) { + return ExchangeResponse( + exception: response.exception, + ); + } + + final t = response.value!; + + return ExchangeResponse( + value: [ + Estimate( + estimatedAmount: Decimal.parse( + (reversed ? t.amountFrom : t.amountTo).toString(), + ), + fixedRate: fixedRate, + reversed: reversed, + exchangeProvider: exchangeName, + ), + ], + ); + } on ExchangeException catch (e) { + return ExchangeResponse( + exception: e, + ); + } catch (e) { + return ExchangeResponse( + exception: ExchangeException( + e.toString(), + ExchangeExceptionType.generic, + ), + ); + } + } + + @override + Future>> getPairedCurrencies( + String forCurrency, + bool fixedRate, + ) async { + try { + if (fixedRate) { + throw ExchangeException( + "Nanswap fixedRate not available", + ExchangeExceptionType.generic, + ); + } + + final response = await getAllCurrencies( + fixedRate, + ); + + if (response.exception != null) { + return ExchangeResponse( + exception: response.exception, + ); + } + + return ExchangeResponse( + value: response.value!..removeWhere((e) => e.ticker == forCurrency), + ); + } on ExchangeException catch (e) { + return ExchangeResponse( + exception: e, + ); + } catch (e) { + return ExchangeResponse( + exception: ExchangeException( + e.toString(), + ExchangeExceptionType.generic, + ), + ); + } + } + + @override + Future>> getPairsFor( + String currency, + bool fixedRate, + ) async { + throw UnsupportedError("Not used"); + } + + @override + Future> getRange( + String from, + String to, + bool fixedRate, + ) async { + try { + if (fixedRate) { + throw ExchangeException( + "Nanswap fixedRate not available", + ExchangeExceptionType.generic, + ); + } + + final response = await NanswapAPI.instance.getOrderLimits( + from: from, + to: to, + ); + + if (response.exception != null) { + return ExchangeResponse( + exception: response.exception, + ); + } + + final t = response.value!; + + return ExchangeResponse( + value: Range( + min: Decimal.parse(t.minFrom.toString()), + max: Decimal.parse(t.maxFrom.toString()), + ), + ); + } on ExchangeException catch (e) { + return ExchangeResponse( + exception: e, + ); + } catch (e) { + return ExchangeResponse( + exception: ExchangeException( + e.toString(), + ExchangeExceptionType.generic, + ), + ); + } + } + + @override + Future> getTrade(String tradeId) async { + try { + final response = await NanswapAPI.instance.getOrder( + id: tradeId, + ); + + if (response.exception != null) { + return ExchangeResponse( + exception: response.exception, + ); + } + + final t = response.value!; + + return ExchangeResponse( + value: Trade( + uuid: const Uuid().v1(), + tradeId: t.id, + rateType: "estimated", + direction: "normal", + timestamp: DateTime.now(), + updatedAt: DateTime.now(), + payInCurrency: t.from, + payInAmount: t.expectedAmountFrom.toString(), + payInAddress: t.payinAddress, + payInNetwork: t.toNetwork ?? t.to, + payInExtraId: t.payinExtraId ?? "", + payInTxid: t.payinHash ?? "", + payOutCurrency: t.to, + payOutAmount: t.expectedAmountTo.toString(), + payOutAddress: t.payoutAddress, + payOutNetwork: t.fromNetwork ?? t.from, + payOutExtraId: "", + payOutTxid: t.payoutHash ?? "", + refundAddress: "", + refundExtraId: "", + status: t.status ?? "unknown", + exchangeName: exchangeName, + ), + ); + } on ExchangeException catch (e) { + return ExchangeResponse( + exception: e, + ); + } catch (e) { + return ExchangeResponse( + exception: ExchangeException( + e.toString(), + ExchangeExceptionType.generic, + ), + ); + } + } + + @override + Future>> getTrades() async { + // TODO: implement getTrades + throw UnimplementedError(); + } + + @override + String get name => exchangeName; + + @override + Future> updateTrade(Trade trade) async { + try { + final response = await NanswapAPI.instance.getOrder( + id: trade.tradeId, + ); + + if (response.exception != null) { + return ExchangeResponse( + exception: response.exception, + ); + } + + final t = response.value!; + + return ExchangeResponse( + value: Trade( + uuid: trade.uuid, + tradeId: t.id, + rateType: trade.rateType, + direction: trade.rateType, + timestamp: trade.timestamp, + updatedAt: DateTime.now(), + payInCurrency: t.from, + payInAmount: t.expectedAmountFrom.toString(), + payInAddress: t.payinAddress, + payInNetwork: t.toNetwork ?? trade.payInNetwork, + payInExtraId: t.payinExtraId ?? trade.payInExtraId, + payInTxid: t.payinHash ?? trade.payInTxid, + payOutCurrency: t.to, + payOutAmount: t.expectedAmountTo.toString(), + payOutAddress: t.payoutAddress, + payOutNetwork: t.fromNetwork ?? trade.payOutNetwork, + payOutExtraId: trade.payOutExtraId, + payOutTxid: t.payoutHash ?? trade.payOutTxid, + refundAddress: trade.refundAddress, + refundExtraId: trade.refundExtraId, + status: t.status ?? "unknown", + exchangeName: exchangeName, + ), + ); + } on ExchangeException catch (e) { + return ExchangeResponse( + exception: e, + ); + } catch (e) { + return ExchangeResponse( + exception: ExchangeException( + e.toString(), + ExchangeExceptionType.generic, + ), + ); + } + } +} diff --git a/lib/utilities/assets.dart b/lib/utilities/assets.dart index 84dad9ae6..c8bfaf1fb 100644 --- a/lib/utilities/assets.dart +++ b/lib/utilities/assets.dart @@ -9,8 +9,10 @@ */ import 'package:flutter/material.dart'; + import '../services/exchange/change_now/change_now_exchange.dart'; import '../services/exchange/majestic_bank/majestic_bank_exchange.dart'; +import '../services/exchange/nanswap/nanswap_exchange.dart'; import '../services/exchange/simpleswap/simpleswap_exchange.dart'; import '../services/exchange/trocador/trocador_exchange.dart'; @@ -45,6 +47,7 @@ class _EXCHANGE { String get majesticBankBlue => "${_path}mb_blue.svg"; String get majesticBankGreen => "${_path}mb_green.svg"; String get trocador => "${_path}trocador.svg"; + String get nanswap => "${_path}nanswap.svg"; String getIconFor({required String exchangeName}) { switch (exchangeName) { @@ -56,6 +59,8 @@ class _EXCHANGE { return majesticBankBlue; case TrocadorExchange.exchangeName: return trocador; + case NanswapExchange.exchangeName: + return nanswap; default: throw ArgumentError("Invalid exchange name passed to " "Assets.exchange.getIconFor()");