diff --git a/asset_sources/svg/campfire/exchange_icons/wizard.svg b/asset_sources/svg/campfire/exchange_icons/wizard.svg new file mode 100644 index 0000000000..703b2ba26d --- /dev/null +++ b/asset_sources/svg/campfire/exchange_icons/wizard.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/asset_sources/svg/stack_duo/exchange_icons/wizard.svg b/asset_sources/svg/stack_duo/exchange_icons/wizard.svg new file mode 100644 index 0000000000..703b2ba26d --- /dev/null +++ b/asset_sources/svg/stack_duo/exchange_icons/wizard.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/asset_sources/svg/stack_wallet/exchange_icons/wizard.svg b/asset_sources/svg/stack_wallet/exchange_icons/wizard.svg new file mode 100644 index 0000000000..703b2ba26d --- /dev/null +++ b/asset_sources/svg/stack_wallet/exchange_icons/wizard.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/lib/models/isar/exchange_cache/currency.dart b/lib/models/isar/exchange_cache/currency.dart index f450535cb7..d31c88c310 100644 --- a/lib/models/isar/exchange_cache/currency.dart +++ b/lib/models/isar/exchange_cache/currency.dart @@ -15,6 +15,7 @@ import '../../../services/exchange/change_now/change_now_exchange.dart'; import '../../../services/exchange/exchange.dart'; import '../../../services/exchange/nanswap/nanswap_exchange.dart'; import '../../../services/exchange/trocador/trocador_exchange.dart'; +import '../../../services/exchange/wizard_swap/wizard_swap_exchange.dart'; import 'pair.dart'; part 'currency.g.dart'; @@ -94,6 +95,9 @@ class Currency { const (NanswapExchange) => network.isNotEmpty ? network.toLowerCase() : ticker.toLowerCase(), + // wizard swap's api sucks + const (WizardSwapExchange) => ticker.toLowerCase(), + _ => throw Exception("Unknown exchange: $exchangeName"), }; } diff --git a/lib/pages/exchange_view/exchange_form.dart b/lib/pages/exchange_view/exchange_form.dart index 675cd30cd7..93e6512314 100644 --- a/lib/pages/exchange_view/exchange_form.dart +++ b/lib/pages/exchange_view/exchange_form.dart @@ -32,6 +32,7 @@ import '../../services/exchange/exchange_data_loading_service.dart'; import '../../services/exchange/exchange_response.dart'; import '../../services/exchange/nanswap/nanswap_exchange.dart'; import '../../services/exchange/trocador/trocador_exchange.dart'; +import '../../services/exchange/wizard_swap/wizard_swap_exchange.dart'; import '../../themes/stack_colors.dart'; import '../../utilities/amount/amount_unit.dart'; import '../../utilities/assets.dart'; @@ -82,6 +83,7 @@ class _ExchangeFormState extends ConsumerState { ChangeNowExchange.instance, TrocadorExchange.instance, NanswapExchange.instance, + WizardSwapExchange.instance, ]; } } @@ -104,19 +106,18 @@ class _ExchangeFormState extends ConsumerState { showDialog( context: context, barrierDismissible: false, - builder: - (_) => WillPopScope( - onWillPop: () async => false, - child: Container( - color: Theme.of( - context, - ).extension()!.overlay.withOpacity(0.6), - child: const CustomLoadingOverlay( - message: "Updating exchange rate", - eventBus: null, - ), - ), + builder: (_) => WillPopScope( + onWillPop: () async => false, + child: Container( + color: Theme.of( + context, + ).extension()!.overlay.withOpacity(0.6), + child: const CustomLoadingOverlay( + message: "Updating exchange rate", + eventBus: null, ), + ), + ), ), ); @@ -262,71 +263,68 @@ class _ExchangeFormState extends ConsumerState { _sendFocusNode.unfocus(); _receiveFocusNode.unfocus(); - final result = - isDesktop - ? await showDialog( - context: context, - builder: (context) { - return DesktopDialog( - maxHeight: 700, - maxWidth: 580, - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: const EdgeInsets.only(left: 32), - child: Text( - "Choose a coin to exchange", - style: STextStyles.desktopH3(context), - ), - ), - const DesktopDialogCloseButton(), - ], - ), - Expanded( - child: Padding( - padding: const EdgeInsets.only( - left: 32, - right: 32, - bottom: 32, + final result = isDesktop + ? await showDialog( + context: context, + builder: (context) { + return DesktopDialog( + maxHeight: 700, + maxWidth: 580, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(left: 32), + child: Text( + "Choose a coin to exchange", + style: STextStyles.desktopH3(context), ), - child: Row( - children: [ - Expanded( - child: RoundedWhiteContainer( - padding: const EdgeInsets.all(16), - borderColor: - Theme.of( - context, - ).extension()!.background, - child: ExchangeCurrencySelectionView( - pairedCurrency: paired, - isFixedRate: isFixedRate, - willChangeIsSend: willChangeIsSend, - ), + ), + const DesktopDialogCloseButton(), + ], + ), + Expanded( + child: Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + ), + child: Row( + children: [ + Expanded( + child: RoundedWhiteContainer( + padding: const EdgeInsets.all(16), + borderColor: Theme.of( + context, + ).extension()!.background, + child: ExchangeCurrencySelectionView( + pairedCurrency: paired, + isFixedRate: isFixedRate, + willChangeIsSend: willChangeIsSend, ), ), - ], - ), + ), + ], ), ), - ], - ), - ); - }, - ) - : await Navigator.of(context).push( - MaterialPageRoute( - builder: - (_) => ExchangeCurrencySelectionView( - pairedCurrency: paired, - isFixedRate: isFixedRate, - willChangeIsSend: willChangeIsSend, ), + ], + ), + ); + }, + ) + : await Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => ExchangeCurrencySelectionView( + pairedCurrency: paired, + isFixedRate: isFixedRate, + willChangeIsSend: willChangeIsSend, ), - ); + ), + ); if (mounted && result is AggregateCurrency) { return result; @@ -402,11 +400,10 @@ class _ExchangeFormState extends ConsumerState { if (fromCurrency == null || toCurrency == null) { await showDialog( context: context, - builder: - (context) => const StackOkDialog( - title: "Missing currency!", - message: "This should not happen. Please contact support", - ), + builder: (context) => const StackOkDialog( + title: "Missing currency!", + message: "This should not happen. Please contact support", + ), ); return; @@ -426,12 +423,11 @@ class _ExchangeFormState extends ConsumerState { if (mounted) { await showDialog( context: context, - builder: - (context) => const StackOkDialog( - title: "WOW error", - message: - "Wownero is temporarily disabled as a receiving currency for fixed rate trades due to network issues", - ), + builder: (context) => const StackOkDialog( + title: "WOW error", + message: + "Wownero is temporarily disabled as a receiving currency for fixed rate trades due to network issues", + ), ); } @@ -440,12 +436,12 @@ class _ExchangeFormState extends ConsumerState { String rate; - final amountToSend = - estimate.reversed ? estimate.estimatedAmount : sendAmount; - final amountToReceive = - estimate.reversed - ? ref.read(efReceiveAmountProvider)! - : estimate.estimatedAmount; + final amountToSend = estimate.reversed + ? estimate.estimatedAmount + : sendAmount; + final amountToReceive = estimate.reversed + ? ref.read(efReceiveAmountProvider)! + : estimate.estimatedAmount; switch (rateType) { case ExchangeRateType.estimated: @@ -495,11 +491,10 @@ class _ExchangeFormState extends ConsumerState { child: SecondaryButton( label: "Cancel", buttonHeight: ButtonHeight.l, - onPressed: - () => Navigator.of( - context, - rootNavigator: true, - ).pop(true), + onPressed: () => Navigator.of( + context, + rootNavigator: true, + ).pop(true), ), ), const SizedBox(width: 16), @@ -507,11 +502,10 @@ class _ExchangeFormState extends ConsumerState { child: PrimaryButton( label: "Attempt", buttonHeight: ButtonHeight.l, - onPressed: - () => Navigator.of( - context, - rootNavigator: true, - ).pop(false), + onPressed: () => Navigator.of( + context, + rootNavigator: true, + ).pop(false), ), ), ], @@ -631,10 +625,9 @@ class _ExchangeFormState extends ConsumerState { return false; } - final String? ticker = - isSend - ? ref.read(efCurrencyPairProvider).send?.ticker - : ref.read(efCurrencyPairProvider).receive?.ticker; + final String? ticker = isSend + ? ref.read(efCurrencyPairProvider).send?.ticker + : ref.read(efCurrencyPairProvider).receive?.ticker; if (ticker == null) { return false; @@ -652,10 +645,9 @@ class _ExchangeFormState extends ConsumerState { } final reversed = ref.read(efReversedProvider); - final amount = - reversed - ? ref.read(efReceiveAmountProvider) - : ref.read(efSendAmountProvider); + final amount = reversed + ? ref.read(efReceiveAmountProvider) + : ref.read(efSendAmountProvider); final pair = ref.read(efCurrencyPairProvider); if (amount == null || @@ -683,7 +675,7 @@ class _ExchangeFormState extends ConsumerState { ); Logging.instance.d( - "${exchange.name}: fixedRate=$rateType, RANGE=$rangeResponse", + "${exchange.name}: rateType=$rateType, RANGE=$rangeResponse", ); final estimateResponse = await exchange.getEstimates( @@ -696,6 +688,10 @@ class _ExchangeFormState extends ConsumerState { reversed, ); + Logging.instance.d( + "${exchange.name}: estimateResponse=$estimateResponse", + ); + results.addAll({ exchange.name: Tuple2(estimateResponse, rangeResponse.value), }); @@ -842,8 +838,8 @@ class _ExchangeFormState extends ConsumerState { // if (_swapLock) { _receiveController.text = isEstimated && ref.read(efReceiveAmountStringProvider).isEmpty - ? "-" - : ref.read(efReceiveAmountStringProvider); + ? "-" + : ref.read(efReceiveAmountStringProvider); // } if (_receiveFocusNode.hasFocus) { @@ -891,11 +887,13 @@ class _ExchangeFormState extends ConsumerState { textStyle: STextStyles.smallMed14(context).copyWith( color: Theme.of(context).extension()!.textDark, ), - buttonColor: - Theme.of(context).extension()!.buttonBackSecondary, + buttonColor: Theme.of( + context, + ).extension()!.buttonBackSecondary, borderRadius: Constants.size.circularBorderRadius, - background: - Theme.of(context).extension()!.textFieldDefaultBG, + background: Theme.of( + context, + ).extension()!.textFieldDefaultBG, onTap: () { if (_sendController.text == "-") { _sendController.text = ""; @@ -922,23 +920,18 @@ class _ExchangeFormState extends ConsumerState { ), ConditionalParent( condition: isDesktop, - builder: - (child) => MouseRegion( - cursor: SystemMouseCursors.click, - child: child, - ), + builder: (child) => + MouseRegion(cursor: SystemMouseCursors.click, child: child), child: Semantics( label: "Swap Button. Reverse The Exchange Currencies.", excludeSemantics: true, child: RoundedContainer( - padding: - isDesktop - ? const EdgeInsets.all(6) - : const EdgeInsets.all(2), - color: - Theme.of( - context, - ).extension()!.buttonBackSecondary, + padding: isDesktop + ? const EdgeInsets.all(6) + : const EdgeInsets.all(2), + color: Theme.of( + context, + ).extension()!.buttonBackSecondary, radiusMultiplier: 0.75, child: GestureDetector( onTap: () async { @@ -950,10 +943,9 @@ class _ExchangeFormState extends ConsumerState { Assets.svg.swap, width: 20, height: 20, - color: - Theme.of( - context, - ).extension()!.accentColorDark, + color: Theme.of( + context, + ).extension()!.accentColorDark, ), ), ), @@ -972,19 +964,20 @@ class _ExchangeFormState extends ConsumerState { textStyle: STextStyles.smallMed14(context).copyWith( color: Theme.of(context).extension()!.textDark, ), - buttonColor: - Theme.of(context).extension()!.buttonBackSecondary, + buttonColor: Theme.of( + context, + ).extension()!.buttonBackSecondary, borderRadius: Constants.size.circularBorderRadius, - background: - Theme.of(context).extension()!.textFieldDefaultBG, - onTap: - rateType == ExchangeRateType.estimated - ? null - : () { - if (_sendController.text == "-") { - _sendController.text = ""; - } - }, + background: Theme.of( + context, + ).extension()!.textFieldDefaultBG, + onTap: rateType == ExchangeRateType.estimated + ? null + : () { + if (_sendController.text == "-") { + _sendController.text = ""; + } + }, onChanged: receiveFieldOnChanged, onButtonTap: selectReceiveCurrency, isWalletCoin: isWalletCoin(coin, true), @@ -1002,15 +995,15 @@ class _ExchangeFormState extends ConsumerState { duration: const Duration(milliseconds: 300), child: ref.watch(efSendAmountProvider) == null && - ref.watch(efReceiveAmountProvider) == null - ? const SizedBox(height: 0) - : Padding( - padding: EdgeInsets.only(top: isDesktop ? 20 : 12), - child: ExchangeProviderOptions( - fixedRate: rateType == ExchangeRateType.fixed, - reversed: ref.watch(efReversedProvider), - ), + ref.watch(efReceiveAmountProvider) == null + ? const SizedBox(height: 0) + : Padding( + padding: EdgeInsets.only(top: isDesktop ? 20 : 12), + child: ExchangeProviderOptions( + fixedRate: rateType == ExchangeRateType.fixed, + reversed: ref.watch(efReversedProvider), ), + ), ), SizedBox(height: isDesktop ? 20 : 12), PrimaryButton( 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 e4c264e04b..a846b47ccd 100644 --- a/lib/pages/exchange_view/sub_widgets/exchange_provider_options.dart +++ b/lib/pages/exchange_view/sub_widgets/exchange_provider_options.dart @@ -17,6 +17,7 @@ import '../../../services/exchange/change_now/change_now_exchange.dart'; import '../../../services/exchange/exchange.dart'; import '../../../services/exchange/nanswap/nanswap_exchange.dart'; import '../../../services/exchange/trocador/trocador_exchange.dart'; +import '../../../services/exchange/wizard_swap/wizard_swap_exchange.dart'; import '../../../themes/stack_colors.dart'; import '../../../utilities/prefs.dart'; import '../../../utilities/util.dart'; @@ -91,6 +92,11 @@ class _ExchangeProviderOptionsState sendCurrency: sendCurrency, receiveCurrency: receivingCurrency, ); + final showWizardSwap = exchangeSupported( + exchangeName: WizardSwapExchange.exchangeName, + sendCurrency: sendCurrency, + receiveCurrency: receivingCurrency, + ); return RoundedWhiteContainer( padding: isDesktop ? const EdgeInsets.all(0) : const EdgeInsets.all(12), @@ -102,6 +108,7 @@ class _ExchangeProviderOptionsState if (showChangeNow) ChangeNowExchange.instance, if (showTrocador) TrocadorExchange.instance, if (showNanswap) NanswapExchange.instance, + if (showWizardSwap) WizardSwapExchange.instance, ], fixedRate: widget.fixedRate, reversed: widget.reversed, diff --git a/lib/pages/exchange_view/trade_details_view.dart b/lib/pages/exchange_view/trade_details_view.dart index 39623c3d84..1f0e4e7d91 100644 --- a/lib/pages/exchange_view/trade_details_view.dart +++ b/lib/pages/exchange_view/trade_details_view.dart @@ -32,6 +32,7 @@ import '../../services/exchange/exchange.dart'; import '../../services/exchange/nanswap/nanswap_exchange.dart'; import '../../services/exchange/simpleswap/simpleswap_exchange.dart'; import '../../services/exchange/trocador/trocador_exchange.dart'; +import '../../services/exchange/wizard_swap/wizard_swap_exchange.dart'; import '../../services/wallets.dart'; import '../../themes/stack_colors.dart'; import '../../themes/theme_providers.dart'; @@ -228,130 +229,119 @@ class _TradeDetailsViewState extends ConsumerState { return ConditionalParent( condition: !isDesktop, - builder: - (child) => Background( - child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, - appBar: AppBar( - backgroundColor: - Theme.of(context).extension()!.background, - leading: AppBarBackButton( - onPressed: () async { - Navigator.of(context).pop(); - }, - ), - title: Text( - "Trade details", - style: STextStyles.navBarTitle(context), - ), - ), - body: SafeArea( - child: Padding( - padding: const EdgeInsets.all(12), - child: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(4), - child: child, - ), - ), - ), + builder: (child) => Background( + child: Scaffold( + backgroundColor: Theme.of( + context, + ).extension()!.background, + appBar: AppBar( + backgroundColor: Theme.of( + context, + ).extension()!.background, + leading: AppBarBackButton( + onPressed: () async { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Trade details", + style: STextStyles.navBarTitle(context), + ), + ), + body: SafeArea( + child: Padding( + padding: const EdgeInsets.all(12), + child: SingleChildScrollView( + child: Padding(padding: const EdgeInsets.all(4), child: child), ), ), ), + ), + ), child: Padding( - padding: - isDesktop - ? const EdgeInsets.only(left: 32) - : const EdgeInsets.all(0), + padding: isDesktop + ? const EdgeInsets.only(left: 32) + : const EdgeInsets.all(0), child: BranchedParent( condition: isDesktop, - conditionBranchBuilder: - (children) => Padding( - padding: const EdgeInsets.only(right: 20), - child: Padding( - padding: const EdgeInsets.only(right: 12), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - RoundedWhiteContainer( - borderColor: - Theme.of( - context, - ).extension()!.backgroundAppBar, - padding: const EdgeInsets.all(0), - child: ListView( - primary: false, - shrinkWrap: true, - children: children, - ), - ), - if (showSendFromStackButton) const SizedBox(height: 32), - if (showSendFromStackButton) - SecondaryButton( - label: "Send from ${AppConfig.prefix}", - buttonHeight: ButtonHeight.l, - onPressed: () { - CryptoCurrency coin; - try { - coin = - AppConfig.getCryptoCurrencyForTicker( - trade.payInCurrency, - )!; - } catch (_) { - coin = AppConfig.getCryptoCurrencyByPrettyName( - trade.payInCurrency, - ); - } - final amount = Amount.fromDecimal( - sendAmount, - fractionDigits: coin.fractionDigits, - ); - final address = trade.payInAddress; - - Navigator.of(context).pushNamed( - SendFromView.routeName, - arguments: Tuple4(coin, amount, address, trade), - ); - }, - ), - const SizedBox(height: 32), - ], + conditionBranchBuilder: (children) => Padding( + padding: const EdgeInsets.only(right: 20), + child: Padding( + padding: const EdgeInsets.only(right: 12), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + RoundedWhiteContainer( + borderColor: Theme.of( + context, + ).extension()!.backgroundAppBar, + padding: const EdgeInsets.all(0), + child: ListView( + primary: false, + shrinkWrap: true, + children: children, + ), ), - ), - ), - otherBranchBuilder: - (children) => Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - mainAxisSize: isDesktop ? MainAxisSize.min : MainAxisSize.max, - children: children, + if (showSendFromStackButton) const SizedBox(height: 32), + if (showSendFromStackButton) + SecondaryButton( + label: "Send from ${AppConfig.prefix}", + buttonHeight: ButtonHeight.l, + onPressed: () { + CryptoCurrency coin; + try { + coin = AppConfig.getCryptoCurrencyForTicker( + trade.payInCurrency, + )!; + } catch (_) { + coin = AppConfig.getCryptoCurrencyByPrettyName( + trade.payInCurrency, + ); + } + final amount = Amount.fromDecimal( + sendAmount, + fractionDigits: coin.fractionDigits, + ); + final address = trade.payInAddress; + + Navigator.of(context).pushNamed( + SendFromView.routeName, + arguments: Tuple4(coin, amount, address, trade), + ); + }, + ), + const SizedBox(height: 32), + ], ), + ), + ), + otherBranchBuilder: (children) => Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: isDesktop ? MainAxisSize.min : MainAxisSize.max, + children: children, + ), children: [ RoundedWhiteContainer( - padding: - isDesktop - ? const EdgeInsets.all(0) - : const EdgeInsets.all(12), + padding: isDesktop + ? const EdgeInsets.all(0) + : const EdgeInsets.all(12), child: Container( - decoration: - isDesktop - ? BoxDecoration( - color: - Theme.of( - context, - ).extension()!.backgroundAppBar, - borderRadius: BorderRadius.vertical( - top: Radius.circular( - Constants.size.circularBorderRadius, - ), + decoration: isDesktop + ? BoxDecoration( + color: Theme.of( + context, + ).extension()!.backgroundAppBar, + borderRadius: BorderRadius.vertical( + top: Radius.circular( + Constants.size.circularBorderRadius, ), - ) - : null, + ), + ) + : null, child: Padding( - padding: - isDesktop - ? const EdgeInsets.all(12) - : const EdgeInsets.all(0), + padding: isDesktop + ? const EdgeInsets.all(12) + : const EdgeInsets.all(0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -376,10 +366,9 @@ class _TradeDetailsViewState extends ConsumerState { ], ), Column( - crossAxisAlignment: - isDesktop - ? CrossAxisAlignment.end - : CrossAxisAlignment.start, + crossAxisAlignment: isDesktop + ? CrossAxisAlignment.end + : CrossAxisAlignment.start, children: [ SelectableText( "${trade.payInCurrency.toUpperCase()} → ${trade.payOutCurrency.toUpperCase()}", @@ -443,10 +432,9 @@ class _TradeDetailsViewState extends ConsumerState { ), isDesktop ? const _Divider() : const SizedBox(height: 12), RoundedWhiteContainer( - padding: - isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, @@ -473,98 +461,87 @@ class _TradeDetailsViewState extends ConsumerState { isDesktop ? const _Divider() : const SizedBox(height: 12), if (!sentFromStack && !hasTx) RoundedContainer( - padding: - isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), - color: - isDesktop - ? Theme.of(context).extension()!.popupBG - : Theme.of( - context, - ).extension()!.warningBackground, + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + color: isDesktop + ? Theme.of(context).extension()!.popupBG + : Theme.of( + context, + ).extension()!.warningBackground, child: ConditionalParent( condition: isDesktop, - builder: - (child) => Column( - mainAxisSize: MainAxisSize.min, + builder: (child) => Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Amount", - style: - STextStyles.desktopTextExtraExtraSmall( - context, - ), - ), - const SizedBox(height: 2), - Text( - "${trade.payInAmount} ${trade.payInCurrency.toUpperCase()}", - style: - STextStyles.desktopTextExtraExtraSmall( - context, - ).copyWith( - color: - Theme.of(context) - .extension()! - .textDark, - ), - ), - ], + Text( + "Amount", + style: STextStyles.desktopTextExtraExtraSmall( + context, + ), + ), + const SizedBox(height: 2), + Text( + "${trade.payInAmount} ${trade.payInCurrency.toUpperCase()}", + style: + STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: Theme.of( + context, + ).extension()!.textDark, + ), ), - tdv.IconCopyButton(data: trade.payInAmount), ], ), - const SizedBox(height: 6), - child, + tdv.IconCopyButton(data: trade.payInAmount), ], ), + const SizedBox(height: 6), + child, + ], + ), child: RichText( text: TextSpan( text: "You must send at least ${sendAmount.toStringAsFixed(trade.payInCurrency.toLowerCase() == "xmr" ? 12 : 8)} ${trade.payInCurrency.toUpperCase()}. ", - style: - isDesktop - ? STextStyles.desktopTextExtraExtraSmall( + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: Theme.of( context, - ).copyWith( - color: - Theme.of( - context, - ).extension()!.accentColorRed, - ) - : STextStyles.label(context).copyWith( - color: - Theme.of(context) - .extension()! - .warningForeground, - ), + ).extension()!.accentColorRed, + ) + : STextStyles.label(context).copyWith( + color: Theme.of( + context, + ).extension()!.warningForeground, + ), children: [ TextSpan( text: "If you send less than ${sendAmount.toStringAsFixed(trade.payInCurrency.toLowerCase() == "xmr" ? 12 : 8)} ${trade.payInCurrency.toUpperCase()}, your transaction may not be converted and it may not be refunded.", - style: - isDesktop - ? STextStyles.desktopTextExtraExtraSmall( + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: Theme.of( context, - ).copyWith( - color: - Theme.of(context) - .extension()! - .accentColorRed, - ) - : STextStyles.label(context).copyWith( - color: - Theme.of(context) - .extension()! - .warningForeground, - ), + ).extension()!.accentColorRed, + ) + : STextStyles.label(context).copyWith( + color: Theme.of( + context, + ).extension()!.warningForeground, + ), ), ], ), @@ -575,10 +552,9 @@ class _TradeDetailsViewState extends ConsumerState { isDesktop ? const _Divider() : const SizedBox(height: 12), if (sentFromStack) RoundedWhiteContainer( - padding: - isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -592,10 +568,9 @@ class _TradeDetailsViewState extends ConsumerState { CustomTextButton( text: "View transaction", onTap: () { - final coin = - AppConfig.getCryptoCurrencyForTicker( - trade.payInCurrency, - )!; + final coin = AppConfig.getCryptoCurrencyForTicker( + trade.payInCurrency, + )!; if (isDesktop) { Navigator.of(context).push( @@ -634,10 +609,9 @@ class _TradeDetailsViewState extends ConsumerState { isDesktop ? const _Divider() : const SizedBox(height: 12), if (sentFromStack) RoundedWhiteContainer( - padding: - isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, @@ -672,10 +646,9 @@ class _TradeDetailsViewState extends ConsumerState { isDesktop ? const _Divider() : const SizedBox(height: 12), if (!sentFromStack && !hasTx) RoundedWhiteContainer( - padding: - isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -689,40 +662,39 @@ class _TradeDetailsViewState extends ConsumerState { isDesktop ? tdv.IconCopyButton(data: trade.payInAddress) : GestureDetector( - onTap: () async { - final address = trade.payInAddress; - await Clipboard.setData( - ClipboardData(text: address), - ); - if (context.mounted) { - unawaited( - showFloatingFlushBar( - type: FlushBarType.info, - message: "Copied to clipboard", - context: context, - ), + onTap: () async { + final address = trade.payInAddress; + await Clipboard.setData( + ClipboardData(text: address), ); - } - }, - child: Row( - children: [ - SvgPicture.asset( - Assets.svg.copy, - width: 12, - height: 12, - color: - Theme.of(context) - .extension()! - .infoItemIcons, - ), - const SizedBox(width: 4), - Text( - "Copy", - style: STextStyles.link2(context), - ), - ], + if (context.mounted) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.info, + message: "Copied to clipboard", + context: context, + ), + ); + } + }, + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.copy, + width: 12, + height: 12, + color: Theme.of( + context, + ).extension()!.infoItemIcons, + ), + const SizedBox(width: 4), + Text( + "Copy", + style: STextStyles.link2(context), + ), + ], + ), ), - ), ], ), const SizedBox(height: 4), @@ -785,14 +757,12 @@ class _TradeDetailsViewState extends ConsumerState { ), child: Text( "Cancel", - style: STextStyles.button( - context, - ).copyWith( - color: - Theme.of(context) + style: STextStyles.button(context) + .copyWith( + color: Theme.of(context) .extension()! .accentColorDark, - ), + ), ), ), ), @@ -809,10 +779,9 @@ class _TradeDetailsViewState extends ConsumerState { Assets.svg.qrcode, width: 12, height: 12, - color: - Theme.of( - context, - ).extension()!.infoItemIcons, + color: Theme.of( + context, + ).extension()!.infoItemIcons, ), const SizedBox(width: 4), Text( @@ -828,10 +797,9 @@ class _TradeDetailsViewState extends ConsumerState { isDesktop ? const _Divider() : const SizedBox(height: 12), if (trade.payInExtraId.isNotEmpty && !sentFromStack && !hasTx) RoundedWhiteContainer( - padding: - isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -842,40 +810,39 @@ class _TradeDetailsViewState extends ConsumerState { isDesktop ? tdv.IconCopyButton(data: trade.payInExtraId) : GestureDetector( - onTap: () async { - final address = trade.payInExtraId; - await Clipboard.setData( - ClipboardData(text: address), - ); - if (context.mounted) { - unawaited( - showFloatingFlushBar( - type: FlushBarType.info, - message: "Copied to clipboard", - context: context, - ), + onTap: () async { + final address = trade.payInExtraId; + await Clipboard.setData( + ClipboardData(text: address), ); - } - }, - child: Row( - children: [ - SvgPicture.asset( - Assets.svg.copy, - width: 12, - height: 12, - color: - Theme.of(context) - .extension()! - .infoItemIcons, - ), - const SizedBox(width: 4), - Text( - "Copy", - style: STextStyles.link2(context), - ), - ], + if (context.mounted) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.info, + message: "Copied to clipboard", + context: context, + ), + ); + } + }, + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.copy, + width: 12, + height: 12, + color: Theme.of( + context, + ).extension()!.infoItemIcons, + ), + const SizedBox(width: 4), + Text( + "Copy", + style: STextStyles.link2(context), + ), + ], + ), ), - ), ], ), const SizedBox(height: 4), @@ -889,10 +856,9 @@ class _TradeDetailsViewState extends ConsumerState { if (trade.payInExtraId.isNotEmpty && !sentFromStack && !hasTx) isDesktop ? const _Divider() : const SizedBox(height: 12), RoundedWhiteContainer( - padding: - isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -905,86 +871,6 @@ class _TradeDetailsViewState extends ConsumerState { ), isDesktop ? tdv.IconPencilButton( - onPressed: () { - showDialog( - context: context, - builder: (context) { - return DesktopDialog( - maxWidth: 580, - maxHeight: 360, - child: EditTradeNoteView( - tradeId: tradeId, - note: ref - .read(tradeNoteServiceProvider) - .getNote(tradeId: tradeId), - ), - ); - }, - ); - }, - ) - : GestureDetector( - onTap: () { - Navigator.of(context).pushNamed( - EditTradeNoteView.routeName, - arguments: Tuple2( - tradeId, - ref - .read(tradeNoteServiceProvider) - .getNote(tradeId: tradeId), - ), - ); - }, - child: Row( - children: [ - SvgPicture.asset( - Assets.svg.pencil, - width: 10, - height: 10, - color: - Theme.of( - context, - ).extension()!.infoItemIcons, - ), - const SizedBox(width: 4), - Text("Edit", style: STextStyles.link2(context)), - ], - ), - ), - ], - ), - const SizedBox(height: 4), - SelectableText( - ref.watch( - tradeNoteServiceProvider.select( - (value) => value.getNote(tradeId: tradeId), - ), - ), - style: STextStyles.itemSubtitle12(context), - ), - ], - ), - ), - if (sentFromStack) - isDesktop ? const _Divider() : const SizedBox(height: 12), - if (sentFromStack) - RoundedWhiteContainer( - padding: - isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Transaction note", - style: STextStyles.itemSubtitle(context), - ), - isDesktop - ? tdv.IconPencilButton( onPressed: () { showDialog( context: context, @@ -992,22 +878,26 @@ class _TradeDetailsViewState extends ConsumerState { return DesktopDialog( maxWidth: 580, maxHeight: 360, - child: EditNoteView( - txid: transactionIfSentFromStack!.txid, - walletId: walletId!, + child: EditTradeNoteView( + tradeId: tradeId, + note: ref + .read(tradeNoteServiceProvider) + .getNote(tradeId: tradeId), ), ); }, ); }, ) - : GestureDetector( + : GestureDetector( onTap: () { Navigator.of(context).pushNamed( - EditNoteView.routeName, + EditTradeNoteView.routeName, arguments: Tuple2( - transactionIfSentFromStack!.txid, - walletId, + tradeId, + ref + .read(tradeNoteServiceProvider) + .getNote(tradeId: tradeId), ), ); }, @@ -1017,10 +907,9 @@ class _TradeDetailsViewState extends ConsumerState { Assets.svg.pencil, width: 10, height: 10, - color: - Theme.of(context) - .extension()! - .infoItemIcons, + color: Theme.of( + context, + ).extension()!.infoItemIcons, ), const SizedBox(width: 4), Text( @@ -1030,6 +919,84 @@ class _TradeDetailsViewState extends ConsumerState { ], ), ), + ], + ), + const SizedBox(height: 4), + SelectableText( + ref.watch( + tradeNoteServiceProvider.select( + (value) => value.getNote(tradeId: tradeId), + ), + ), + style: STextStyles.itemSubtitle12(context), + ), + ], + ), + ), + if (sentFromStack) + isDesktop ? const _Divider() : const SizedBox(height: 12), + if (sentFromStack) + RoundedWhiteContainer( + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Transaction note", + style: STextStyles.itemSubtitle(context), + ), + isDesktop + ? tdv.IconPencilButton( + onPressed: () { + showDialog( + context: context, + builder: (context) { + return DesktopDialog( + maxWidth: 580, + maxHeight: 360, + child: EditNoteView( + txid: + transactionIfSentFromStack!.txid, + walletId: walletId!, + ), + ); + }, + ); + }, + ) + : GestureDetector( + onTap: () { + Navigator.of(context).pushNamed( + EditNoteView.routeName, + arguments: Tuple2( + transactionIfSentFromStack!.txid, + walletId, + ), + ); + }, + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.pencil, + width: 10, + height: 10, + color: Theme.of( + context, + ).extension()!.infoItemIcons, + ), + const SizedBox(width: 4), + Text( + "Edit", + style: STextStyles.link2(context), + ), + ], + ), + ), ], ), const SizedBox(height: 4), @@ -1050,10 +1017,9 @@ class _TradeDetailsViewState extends ConsumerState { ), isDesktop ? const _Divider() : const SizedBox(height: 12), RoundedWhiteContainer( - padding: - isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, @@ -1069,14 +1035,12 @@ class _TradeDetailsViewState extends ConsumerState { Format.extractDateFrom( trade.timestamp.millisecondsSinceEpoch ~/ 1000, ), - style: STextStyles.desktopTextExtraExtraSmall( - context, - ).copyWith( - color: - Theme.of( + style: STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: Theme.of( context, ).extension()!.textDark, - ), + ), ), ], ), @@ -1098,10 +1062,9 @@ class _TradeDetailsViewState extends ConsumerState { ), isDesktop ? const _Divider() : const SizedBox(height: 12), RoundedWhiteContainer( - padding: - isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, @@ -1132,10 +1095,9 @@ class _TradeDetailsViewState extends ConsumerState { ), isDesktop ? const _Divider() : const SizedBox(height: 12), RoundedWhiteContainer( - padding: - isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, @@ -1180,10 +1142,9 @@ class _TradeDetailsViewState extends ConsumerState { }, child: SvgPicture.asset( Assets.svg.copy, - color: - Theme.of( - context, - ).extension()!.infoItemIcons, + color: Theme.of( + context, + ).extension()!.infoItemIcons, width: 12, ), ), @@ -1196,10 +1157,9 @@ class _TradeDetailsViewState extends ConsumerState { isDesktop ? const _Divider() : const SizedBox(height: 12), if (trade.exchangeName != "Majestic Bank") RoundedWhiteContainer( - padding: - isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -1221,6 +1181,10 @@ class _TradeDetailsViewState extends ConsumerState { url = "https://nanswap.com/transaction/${trade.tradeId}"; break; + case WizardSwapExchange.exchangeName: + url = + "https://www.wizardswap.io/api/exchange/${trade.tradeId}"; + break; default: if (trade.exchangeName.startsWith( @@ -1232,11 +1196,10 @@ class _TradeDetailsViewState extends ConsumerState { } return ConditionalParent( condition: isDesktop, - builder: - (child) => MouseRegion( - cursor: SystemMouseCursors.click, - child: child, - ), + builder: (child) => MouseRegion( + cursor: SystemMouseCursors.click, + child: child, + ), child: GestureDetector( onTap: () { launchUrl( @@ -1259,10 +1222,9 @@ class _TradeDetailsViewState extends ConsumerState { onPressed: () { CryptoCurrency coin; try { - coin = - AppConfig.getCryptoCurrencyForTicker( - trade.payInCurrency, - )!; + coin = AppConfig.getCryptoCurrencyForTicker( + trade.payInCurrency, + )!; } catch (_) { coin = AppConfig.getCryptoCurrencyByPrettyName( trade.payInCurrency, diff --git a/lib/services/exchange/exchange.dart b/lib/services/exchange/exchange.dart index 05ca33e871..c2c8191827 100644 --- a/lib/services/exchange/exchange.dart +++ b/lib/services/exchange/exchange.dart @@ -16,10 +16,10 @@ import '../../models/exchange/response_objects/trade.dart'; import '../../models/isar/exchange_cache/currency.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'; +import 'wizard_swap/wizard_swap_exchange.dart'; abstract class Exchange { static Exchange get defaultExchange => ChangeNowExchange.instance; @@ -36,6 +36,8 @@ abstract class Exchange { return TrocadorExchange.instance; case NanswapExchange.exchangeName: return NanswapExchange.instance; + case WizardSwapExchange.exchangeName: + return WizardSwapExchange.instance; default: final split = name.split(" "); if (split.length >= 2) { diff --git a/lib/services/exchange/exchange_data_loading_service.dart b/lib/services/exchange/exchange_data_loading_service.dart index 1bf16504d3..29645bfb05 100644 --- a/lib/services/exchange/exchange_data_loading_service.dart +++ b/lib/services/exchange/exchange_data_loading_service.dart @@ -27,6 +27,7 @@ import '../../utilities/stack_file_system.dart'; import 'change_now/change_now_exchange.dart'; import 'nanswap/nanswap_exchange.dart'; import 'trocador/trocador_exchange.dart'; +import 'wizard_swap/wizard_swap_exchange.dart'; class ExchangeDataLoadingService { ExchangeDataLoadingService._(); @@ -124,45 +125,41 @@ class ExchangeDataLoadingService { final List currencies; if (contract != null) { - currencies = - await (await isar).currencies - .filter() - .tokenContractEqualTo(contract) - .and() - .group( - (q) => - rateType == ExchangeRateType.fixed - ? q - .rateTypeEqualTo(SupportedRateType.both) - .or() - .rateTypeEqualTo(SupportedRateType.fixed) - : q - .rateTypeEqualTo(SupportedRateType.both) - .or() - .rateTypeEqualTo(SupportedRateType.estimated), - ) - .findAll(); + currencies = await (await isar).currencies + .filter() + .tokenContractEqualTo(contract) + .and() + .group( + (q) => rateType == ExchangeRateType.fixed + ? q + .rateTypeEqualTo(SupportedRateType.both) + .or() + .rateTypeEqualTo(SupportedRateType.fixed) + : q + .rateTypeEqualTo(SupportedRateType.both) + .or() + .rateTypeEqualTo(SupportedRateType.estimated), + ) + .findAll(); } else { - currencies = - await (await isar).currencies - .filter() - .group( - (q) => - rateType == ExchangeRateType.fixed - ? q - .rateTypeEqualTo(SupportedRateType.both) - .or() - .rateTypeEqualTo(SupportedRateType.fixed) - : q - .rateTypeEqualTo(SupportedRateType.both) - .or() - .rateTypeEqualTo(SupportedRateType.estimated), - ) - .and() - .tickerEqualTo(ticker, caseSensitive: false) - .and() - .tokenContractIsNull() - .findAll(); + currencies = await (await isar).currencies + .filter() + .group( + (q) => rateType == ExchangeRateType.fixed + ? q + .rateTypeEqualTo(SupportedRateType.both) + .or() + .rateTypeEqualTo(SupportedRateType.fixed) + : q + .rateTypeEqualTo(SupportedRateType.both) + .or() + .rateTypeEqualTo(SupportedRateType.estimated), + ) + .and() + .tickerEqualTo(ticker, caseSensitive: false) + .and() + .tokenContractIsNull() + .findAll(); } currencies.retainWhere((e) => e.getFuzzyNet() == fuzzyNet); @@ -211,6 +208,7 @@ class ExchangeDataLoadingService { // loadMajesticBankCurrencies(), loadTrocadorCurrencies(), loadNanswapCurrencies(), + loadWizardSwapCurrencies(), ]; // If using Tor, don't load data for exchanges which don't support Tor. @@ -248,12 +246,11 @@ class ExchangeDataLoadingService { final responseCurrencies = await exchange.getAllCurrencies(false); if (responseCurrencies.value != null) { await (await isar).writeTxn(() async { - final idsToDelete = - await (await isar).currencies - .where() - .exchangeNameEqualTo(ChangeNowExchange.exchangeName) - .idProperty() - .findAll(); + final idsToDelete = await (await isar).currencies + .where() + .exchangeNameEqualTo(ChangeNowExchange.exchangeName) + .idProperty() + .findAll(); await (await isar).currencies.deleteAll(idsToDelete); await (await isar).currencies.putAll(responseCurrencies.value!); }); @@ -405,12 +402,11 @@ class ExchangeDataLoadingService { if (responseCurrencies.value != null) { await (await isar).writeTxn(() async { - final idsToDelete = - await (await isar).currencies - .where() - .exchangeNameEqualTo(TrocadorExchange.exchangeName) - .idProperty() - .findAll(); + final idsToDelete = await (await isar).currencies + .where() + .exchangeNameEqualTo(TrocadorExchange.exchangeName) + .idProperty() + .findAll(); await (await isar).currencies.deleteAll(idsToDelete); await (await isar).currencies.putAll(responseCurrencies.value!); }); @@ -429,12 +425,11 @@ class ExchangeDataLoadingService { if (responseCurrencies.value != null) { await (await isar).writeTxn(() async { - final idsToDelete = - await (await isar).currencies - .where() - .exchangeNameEqualTo(NanswapExchange.exchangeName) - .idProperty() - .findAll(); + final idsToDelete = await (await isar).currencies + .where() + .exchangeNameEqualTo(NanswapExchange.exchangeName) + .idProperty() + .findAll(); await (await isar).currencies.deleteAll(idsToDelete); await (await isar).currencies.putAll(responseCurrencies.value!); }); @@ -443,6 +438,28 @@ class ExchangeDataLoadingService { } } + Future loadWizardSwapCurrencies() async { + if (_isar == null) { + await initDB(); + } + final responseCurrencies = await WizardSwapExchange.instance + .getAllCurrencies(false); + + if (responseCurrencies.value != null) { + await (await isar).writeTxn(() async { + final idsToDelete = await (await isar).currencies + .where() + .exchangeNameEqualTo(WizardSwapExchange.exchangeName) + .idProperty() + .findAll(); + await (await isar).currencies.deleteAll(idsToDelete); + await (await isar).currencies.putAll(responseCurrencies.value!); + }); + } else { + Logging.instance.w("loadWizardSwapCurrencies: $responseCurrencies"); + } + } + // Future loadMajesticBankPairs() async { // final exchange = MajesticBankExchange.instance; // diff --git a/lib/services/exchange/nanswap/nanswap_exchange.dart b/lib/services/exchange/nanswap/nanswap_exchange.dart index 2392199e79..a26a35cfb9 100644 --- a/lib/services/exchange/nanswap/nanswap_exchange.dart +++ b/lib/services/exchange/nanswap/nanswap_exchange.dart @@ -138,24 +138,23 @@ class NanswapExchange extends Exchange { } 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(), + 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); @@ -391,7 +390,7 @@ class NanswapExchange extends Exchange { uuid: trade.uuid, tradeId: t.id, rateType: trade.rateType, - direction: trade.rateType, + direction: trade.direction, timestamp: trade.timestamp, updatedAt: DateTime.now(), payInCurrency: t.from, diff --git a/lib/services/exchange/wizard_swap/wizard_swap_api.dart b/lib/services/exchange/wizard_swap/wizard_swap_api.dart new file mode 100644 index 0000000000..302b113f39 --- /dev/null +++ b/lib/services/exchange/wizard_swap/wizard_swap_api.dart @@ -0,0 +1,172 @@ +import 'dart:convert'; + +import 'package:decimal/decimal.dart'; + +import '../../../app_config.dart'; +import '../../../networking/http.dart'; +import '../../../utilities/logger.dart'; +import '../../../utilities/prefs.dart'; +import '../../tor_service.dart'; + +abstract class WizardSwapApi { + static const _client = HTTP(); + static const baseUrl = "https://www.wizardswap.io/api"; + + static Uri _getUri(String endpoint) => Uri.parse("$baseUrl$endpoint"); + + static Future _makeGetRequest(Uri uri) async { + int code = -1; + try { + final response = await _client.get( + url: uri, + headers: {'Accept': 'application/json'}, + proxyInfo: !AppConfig.hasFeature(AppFeature.tor) + ? null + : Prefs.instance.useTor + ? TorService.sharedInstance.getProxyInfo() + : null, + ); + + code = response.code; + + if (code != 200) { + throw Exception( + "WizardSwapApi GET failed CODE=$code, response body=${response.body}", + ); + } + + return response.body; + } catch (e, s) { + Logging.instance.e("rethrowing", error: e, stackTrace: s); + rethrow; + } + } + + static Future _makePostRequest( + Uri uri, + Map body, + ) async { + int code = -1; + try { + final response = await _client.post( + url: uri, + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + body: jsonEncode(body), + proxyInfo: !AppConfig.hasFeature(AppFeature.tor) + ? null + : Prefs.instance.useTor + ? TorService.sharedInstance.getProxyInfo() + : null, + ); + + code = response.code; + + if (code != 200) { + throw Exception( + "WizardSwapApi POST failed CODE=$code, body=${response.body}", + ); + } + + return response.body; + } catch (e, s) { + Logging.instance.e("rethrowing", error: e, stackTrace: s); + rethrow; + } + } + + static Future>> getCurrencies() async { + final body = await _makeGetRequest(_getUri("/currency")); + final data = jsonDecode(body); + return List>.from(data as List); + } + + /// [symbol] should be lowercase. Example: btc + static Future> getCurrencyInfo(String symbol) async { + final body = await _makeGetRequest(_getUri("/currency/$symbol")); + return Map.from(jsonDecode(body) as Map); + } + + static Future getExchange(String id) async { + final body = await _makeGetRequest(_getUri("/exchange/$id")); + return Map.from(jsonDecode(body) as Map); + } + + static Future postEstimate( + String from, + String to, + Decimal fromAmount, + String apiKey, + ) async { + final body = await _makePostRequest(_getUri("/estimate"), { + "currency_from": from, + "currency_to": to, + "amount_from": fromAmount, + "api_key": apiKey, + }); + + final map = Map.from(jsonDecode(body) as Map); + + // sometimes this json value will contain an error message lol... + final amount = Decimal.tryParse(map["estimated_amount"].toString()); + if (amount == null) { + throw Exception(map["estimated_amount"]); + } + + return WzEstimate( + from: from, + to: to, + amountFrom: fromAmount, + amountTo: amount, + ); + } + + static Future postExchange( + String from, + String to, + String toAddress, + Decimal fromAmount, + String refundAddress, + String? toExtraId, + String? refundExtraId, + String apiKey, + ) async { + final body = await _makePostRequest(_getUri("/exchange"), { + "currency_from": from, + "currency_to": to, + "address_to": toAddress, + "amount_from": fromAmount, + "refund_address": refundAddress, + if (toExtraId != null) "extra_id_to": toExtraId, + if (refundExtraId != null) "refund_extra_id": refundExtraId, + "api_key": apiKey, + }); + return Map.from(jsonDecode(body) as Map); + } +} + +final class WzEstimate { + final String from; + final String to; + final Decimal amountFrom; + final Decimal amountTo; + + WzEstimate({ + required this.from, + required this.to, + required this.amountFrom, + required this.amountTo, + }); + + @override + String toString() { + return 'WzEstimate {' + 'from: $from, ' + 'to: $to, ' + 'amountFrom: $amountFrom, ' + 'amountTo: $amountTo ' + '}'; + } +} diff --git a/lib/services/exchange/wizard_swap/wizard_swap_exchange.dart b/lib/services/exchange/wizard_swap/wizard_swap_exchange.dart new file mode 100644 index 0000000000..b73f9944c3 --- /dev/null +++ b/lib/services/exchange/wizard_swap/wizard_swap_exchange.dart @@ -0,0 +1,327 @@ +import 'package:decimal/decimal.dart'; +import 'package:uuid/uuid.dart'; + +import '../../../app_config.dart'; +import '../../../exceptions/exchange/exchange_exception.dart'; +import '../../../external_api_keys.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 '../exchange.dart'; +import '../exchange_response.dart'; +import 'wizard_swap_api.dart'; + +class WizardSwapExchange extends Exchange { + WizardSwapExchange._(); + + static WizardSwapExchange? _instance; + static WizardSwapExchange get instance => + _instance ??= WizardSwapExchange._(); + + static const exchangeName = "Wizard Swap"; + + @override + String get name => exchangeName; + + @override + Future> createTrade({ + required String from, + required String to, + required String? fromNetwork, + required String? toNetwork, + required Decimal amount, + required String addressTo, + String? extraId, + required String addressRefund, + required String refundExtraId, + Estimate? estimate, + bool fixedRate = false, + bool reversed = false, + }) async { + try { + if (reversed) { + throw ExchangeException( + "$runtimeType does not support reversed trades", + ExchangeExceptionType.generic, + ); + } + if (fixedRate) { + throw ExchangeException( + "$runtimeType does not support fixedRate trades", + ExchangeExceptionType.generic, + ); + } + + final json = await WizardSwapApi.postExchange( + from, + to, + addressTo, + amount, + addressRefund, + extraId, + refundExtraId, + kWizSwapApiKey, + ); + + // since the wizard swap api is somewhat lacking we'll make some + // assumptions regarding date + final timestamp = DateTime.parse( + "${(json["timestamp"] as String).replaceFirst(" ", "T")}Z", + ); + + final trade = Trade( + uuid: const Uuid().v1(), + tradeId: json["id"] as String, + rateType: "estimated", + direction: "normal", + timestamp: timestamp, + updatedAt: timestamp, + payInCurrency: from, + payInAmount: json["expected_amount"] as String, + payInAddress: json["address_from"] as String, + payInNetwork: from, // need something here... + payInExtraId: json["extra_id_from"] as String, + payInTxid: json["tx_from"] as String, + payOutCurrency: to, + payOutAmount: json["amount_to"] as String, + payOutAddress: json["address_to"] as String, + payOutNetwork: to, // need something here... + payOutExtraId: json["extra_id_to"] as String, + payOutTxid: json["tx_to"] as String, + refundAddress: json["refund_address"] as String? ?? addressRefund, + refundExtraId: refundExtraId, + status: json["status"] as String? ?? "unknown", + exchangeName: exchangeName, + ); + + return ExchangeResponse(value: trade); + } 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( + "$runtimeType does not support fixedRate", + ExchangeExceptionType.generic, + ); + } + + final response = await WizardSwapApi.getCurrencies(); + + final List result = []; + + for (final json in response) { + final ticker = json["symbol"] as String; + + // lol why do we even have to do this??? There is less info returned + // by this call than in the json response for all currencies???????? + final info = await WizardSwapApi.getCurrencyInfo(ticker); + + final currency = Currency( + exchangeName: exchangeName, + ticker: json["symbol"] as String, + name: json["name"] as String, + network: json["parent_symbol"] as String? ?? ticker, + image: info["image"] as String, + isFiat: false, + rateType: .estimated, + isStackCoin: AppConfig.isStackCoin(ticker), + tokenContract: null, + isAvailable: json["enabled"] == 1, + ); + + result.add(currency); + } + + return ExchangeResponse(value: result); + } on ExchangeException catch (e) { + return ExchangeResponse(exception: e); + } catch (e) { + return ExchangeResponse( + exception: ExchangeException( + e.toString(), + ExchangeExceptionType.generic, + ), + ); + } + } + + @override + Future>> getEstimates( + String from, + String? fromNetwork, + String to, + String? toNetwork, + Decimal amount, + bool fixedRate, + bool reversed, + ) async { + try { + if (reversed) { + throw ExchangeException( + "$runtimeType does not support reversed trades", + ExchangeExceptionType.generic, + ); + } + if (fixedRate) { + throw ExchangeException( + "$runtimeType does not support fixedRate trades", + ExchangeExceptionType.generic, + ); + } + + final response = await WizardSwapApi.postEstimate( + from, + to, + amount, + kWizSwapApiKey, + ); + + final estimate = Estimate( + estimatedAmount: response.amountTo, + fixedRate: fixedRate, + reversed: reversed, + exchangeProvider: exchangeName, + ); + + return ExchangeResponse(value: [estimate]); + } on ExchangeException catch (e) { + return ExchangeResponse(exception: e); + } catch (e) { + return ExchangeResponse( + exception: ExchangeException( + e.toString(), + ExchangeExceptionType.generic, + ), + ); + } + } + + @override + Future> getRange( + String from, + String? fromNetwork, + String to, + String? toNetwork, + bool fixedRate, + ) async { + try { + if (fixedRate) { + throw ExchangeException( + "$runtimeType does not support fixedRate trades", + ExchangeExceptionType.generic, + ); + } + + /// lol ???? + final all = await WizardSwapApi.getCurrencies(); + final coin = all.firstWhere( + (e) => + e["symbol"].toString().toLowerCase() == + from.toString().toLowerCase(), + ); + + return ExchangeResponse( + value: Range(min: Decimal.tryParse(coin["minamt"].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 { + throw UnimplementedError("Not currently used in this app"); + } on ExchangeException catch (e) { + return ExchangeResponse(exception: e); + } catch (e) { + return ExchangeResponse( + exception: ExchangeException( + e.toString(), + ExchangeExceptionType.generic, + ), + ); + } + } + + @override + Future>> getTrades() async { + try { + throw UnimplementedError("Not currently used in this app"); + } on ExchangeException catch (e) { + return ExchangeResponse(exception: e); + } catch (e) { + return ExchangeResponse( + exception: ExchangeException( + e.toString(), + ExchangeExceptionType.generic, + ), + ); + } + } + + @override + Future> updateTrade(Trade trade) async { + try { + final json = await WizardSwapApi.getExchange(trade.tradeId); + + final updated = Trade( + uuid: trade.uuid, + tradeId: trade.tradeId, + rateType: trade.rateType, + direction: trade.direction, + timestamp: trade.timestamp, + updatedAt: DateTime.now(), + payInCurrency: trade.payInCurrency, + payInAmount: json["expected_amount"] as String, + payInAddress: json["address_from"] as String, + payInNetwork: trade.payInNetwork, + payInExtraId: json["extra_id_from"] as String, + payInTxid: json["tx_from"] as String, + payOutCurrency: trade.payOutCurrency, + payOutAmount: json["amount_to"] as String, + payOutAddress: json["address_to"] as String, + payOutNetwork: trade.payOutNetwork, + payOutExtraId: json["extra_id_to"] as String, + payOutTxid: json["tx_to"] as String, + refundAddress: json["refund_address"] as String? ?? trade.refundAddress, + refundExtraId: trade.refundExtraId, + status: json["status"] as String? ?? "unknown", + exchangeName: exchangeName, + ); + + return ExchangeResponse(value: updated); + } 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 d912567d3d..eebe60e092 100644 --- a/lib/utilities/assets.dart +++ b/lib/utilities/assets.dart @@ -14,6 +14,7 @@ import '../services/exchange/change_now/change_now_exchange.dart'; import '../services/exchange/nanswap/nanswap_exchange.dart'; import '../services/exchange/simpleswap/simpleswap_exchange.dart'; import '../services/exchange/trocador/trocador_exchange.dart'; +import '../services/exchange/wizard_swap/wizard_swap_exchange.dart'; abstract class Assets { static const svg = _SVG(); @@ -47,6 +48,7 @@ class _EXCHANGE { // String get majesticBankGreen => "${_path}mb_green.svg"; String get trocador => "${_path}trocador.svg"; String get nanswap => "${_path}nanswap.svg"; + String get wizard => "${_path}wizard.svg"; String getIconFor({required String exchangeName}) { switch (exchangeName) { @@ -60,6 +62,8 @@ class _EXCHANGE { return trocador; case NanswapExchange.exchangeName: return nanswap; + case WizardSwapExchange.exchangeName: + return wizard; default: throw ArgumentError( "Invalid exchange name passed to " diff --git a/scripts/prebuild.sh b/scripts/prebuild.sh index 6c50fbefd9..44d4e0921c 100755 --- a/scripts/prebuild.sh +++ b/scripts/prebuild.sh @@ -4,7 +4,7 @@ KEYS=../lib/external_api_keys.dart if ! test -f "$KEYS"; then echo 'prebuild.sh: creating template lib/external_api_keys.dart file' - printf 'const kChangeNowApiKey = "";\nconst kSimpleSwapApiKey = "";\nconst kNanswapApiKey = "";\nconst kNanoSwapRpcApiKey = "";\n' > $KEYS + printf 'const kChangeNowApiKey = "";\nconst kSimpleSwapApiKey = "";\nconst kNanswapApiKey = "";\nconst kNanoSwapRpcApiKey = "";\nconst kWizSwapApiKey = "";\n' > $KEYS fi # Create template wallet test parameter files if they don't already exist