This commit is contained in:
julian 2024-07-01 13:02:08 -06:00
parent 40f8767b11
commit df3935d5d4
20 changed files with 1605 additions and 172 deletions

View file

@ -0,0 +1,69 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="75.209999mm"
height="75.209999mm"
viewBox="0 0 213.19692 213.2"
version="1.1"
id="svg30"
sodipodi:docname="nanswap2.svg"
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)">
<metadata
id="metadata36">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title>nanswap</dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs34" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="2635"
inkscape:window-height="1461"
id="namedview32"
showgrid="false"
inkscape:zoom="0.29390619"
inkscape:cx="-79.957486"
inkscape:cy="142.12913"
inkscape:window-x="0"
inkscape:window-y="27"
inkscape:window-maximized="0"
inkscape:current-layer="svg30" />
<title
id="title2">nanswap</title>
<circle
cx="106.6"
cy="106.6"
r="106.6"
id="circle4"
style="fill:#4a90e2" />
<path
d="m 166.7,66.500006 a 20,20 0 0 1 -20,20 c -15,0 -20,5 -20,19.999994 a 20,20 0 0 1 -20,20 c -15,0 -20,5 -20,20 a 20,20 0 1 1 -20,-20 c 15,0 20,-5 20,-20 a 20,20 0 0 1 20,-19.999994 c 15,0 20,-5 20,-20 a 20,20 0 0 1 40,0 z"
id="path6"
inkscape:connector-curvature="0"
style="fill:#000034" />
<path
d="m 66.5,46.500006 a 20,20 0 0 1 20,20 c 0,15 5,20 20,20 A 20,20 0 0 1 126.5,106.5 c 0,15 5,20 20,20 a 20,20 0 1 1 -20,20 c 0,-15 -5,-20 -20,-20 a 20,20 0 0 1 -20,-20 c 0,-14.999994 -5,-19.999994 -20,-19.999994 a 20,20 0 0 1 0,-40 z"
id="path8"
inkscape:connector-curvature="0"
style="fill:#000034" />
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View file

@ -0,0 +1,69 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="75.209999mm"
height="75.209999mm"
viewBox="0 0 213.19692 213.2"
version="1.1"
id="svg30"
sodipodi:docname="nanswap2.svg"
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)">
<metadata
id="metadata36">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title>nanswap</dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs34" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="2635"
inkscape:window-height="1461"
id="namedview32"
showgrid="false"
inkscape:zoom="0.29390619"
inkscape:cx="-79.957486"
inkscape:cy="142.12913"
inkscape:window-x="0"
inkscape:window-y="27"
inkscape:window-maximized="0"
inkscape:current-layer="svg30" />
<title
id="title2">nanswap</title>
<circle
cx="106.6"
cy="106.6"
r="106.6"
id="circle4"
style="fill:#4a90e2" />
<path
d="m 166.7,66.500006 a 20,20 0 0 1 -20,20 c -15,0 -20,5 -20,19.999994 a 20,20 0 0 1 -20,20 c -15,0 -20,5 -20,20 a 20,20 0 1 1 -20,-20 c 15,0 20,-5 20,-20 a 20,20 0 0 1 20,-19.999994 c 15,0 20,-5 20,-20 a 20,20 0 0 1 40,0 z"
id="path6"
inkscape:connector-curvature="0"
style="fill:#000034" />
<path
d="m 66.5,46.500006 a 20,20 0 0 1 20,20 c 0,15 5,20 20,20 A 20,20 0 0 1 126.5,106.5 c 0,15 5,20 20,20 a 20,20 0 1 1 -20,20 c 0,-15 -5,-20 -20,-20 a 20,20 0 0 1 -20,-20 c 0,-14.999994 -5,-19.999994 -20,-19.999994 a 20,20 0 0 1 0,-40 z"
id="path8"
inkscape:connector-curvature="0"
style="fill:#000034" />
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View file

@ -0,0 +1,69 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="75.209999mm"
height="75.209999mm"
viewBox="0 0 213.19692 213.2"
version="1.1"
id="svg30"
sodipodi:docname="nanswap2.svg"
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)">
<metadata
id="metadata36">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title>nanswap</dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs34" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="2635"
inkscape:window-height="1461"
id="namedview32"
showgrid="false"
inkscape:zoom="0.29390619"
inkscape:cx="-79.957486"
inkscape:cy="142.12913"
inkscape:window-x="0"
inkscape:window-y="27"
inkscape:window-maximized="0"
inkscape:current-layer="svg30" />
<title
id="title2">nanswap</title>
<circle
cx="106.6"
cy="106.6"
r="106.6"
id="circle4"
style="fill:#4a90e2" />
<path
d="m 166.7,66.500006 a 20,20 0 0 1 -20,20 c -15,0 -20,5 -20,19.999994 a 20,20 0 0 1 -20,20 c -15,0 -20,5 -20,20 a 20,20 0 1 1 -20,-20 c 15,0 20,-5 20,-20 a 20,20 0 0 1 20,-19.999994 c 15,0 20,-5 20,-20 a 20,20 0 0 1 40,0 z"
id="path6"
inkscape:connector-curvature="0"
style="fill:#000034" />
<path
d="m 66.5,46.500006 a 20,20 0 0 1 20,20 c 0,15 5,20 20,20 A 20,20 0 0 1 126.5,106.5 c 0,15 5,20 20,20 a 20,20 0 1 1 -20,20 c 0,-15 -5,-20 -20,-20 a 20,20 0 0 1 -20,-20 c 0,-14.999994 -5,-19.999994 -20,-19.999994 a 20,20 0 0 1 0,-40 z"
id="path8"
inkscape:connector-curvature="0"
style="fill:#000034" />
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View file

@ -22,6 +22,7 @@ import '../../../services/exchange/change_now/change_now_exchange.dart';
import '../../../services/exchange/exchange.dart'; import '../../../services/exchange/exchange.dart';
import '../../../services/exchange/exchange_data_loading_service.dart'; import '../../../services/exchange/exchange_data_loading_service.dart';
import '../../../services/exchange/majestic_bank/majestic_bank_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 '../../../services/exchange/trocador/trocador_exchange.dart';
import '../../../themes/stack_colors.dart'; import '../../../themes/stack_colors.dart';
import '../../../utilities/assets.dart'; import '../../../utilities/assets.dart';
@ -117,6 +118,8 @@ class _ExchangeCurrencySelectionViewState
.exchangeNameEqualTo(MajesticBankExchange.exchangeName) .exchangeNameEqualTo(MajesticBankExchange.exchangeName)
.or() .or()
.exchangeNameStartsWith(TrocadorExchange.exchangeName) .exchangeNameStartsWith(TrocadorExchange.exchangeName)
.or()
.exchangeNameStartsWith(NanswapExchange.exchangeName)
.findAll(); .findAll();
final cn = await ChangeNowExchange.instance.getPairedCurrencies( final cn = await ChangeNowExchange.instance.getPairedCurrencies(

View file

@ -33,6 +33,7 @@ import '../../services/exchange/exchange.dart';
import '../../services/exchange/exchange_data_loading_service.dart'; import '../../services/exchange/exchange_data_loading_service.dart';
import '../../services/exchange/exchange_response.dart'; import '../../services/exchange/exchange_response.dart';
import '../../services/exchange/majestic_bank/majestic_bank_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 '../../services/exchange/trocador/trocador_exchange.dart';
import '../../themes/stack_colors.dart'; import '../../themes/stack_colors.dart';
import '../../utilities/amount/amount_unit.dart'; import '../../utilities/amount/amount_unit.dart';
@ -87,6 +88,7 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> {
MajesticBankExchange.instance, MajesticBankExchange.instance,
ChangeNowExchange.instance, ChangeNowExchange.instance,
TrocadorExchange.instance, TrocadorExchange.instance,
NanswapExchange.instance,
]; ];
} }
} }

View file

@ -15,7 +15,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../app_config.dart'; import '../../../app_config.dart';
import '../../../models/exchange/incomplete_exchange.dart'; import '../../../models/exchange/incomplete_exchange.dart';
import '../../../providers/providers.dart'; import '../../../providers/providers.dart';
import '../../../services/exchange/majestic_bank/majestic_bank_exchange.dart';
import '../../../themes/stack_colors.dart'; import '../../../themes/stack_colors.dart';
import '../../../utilities/address_utils.dart'; import '../../../utilities/address_utils.dart';
import '../../../utilities/barcode_scanner_interface.dart'; import '../../../utilities/barcode_scanner_interface.dart';
@ -126,8 +125,7 @@ class _Step2ViewState extends ConsumerState<Step2View> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final supportsRefund = final supportsRefund = ref.watch(efExchangeProvider).supportsRefundAddress;
ref.watch(efExchangeProvider).name != MajesticBankExchange.exchangeName;
return Background( return Background(
child: Scaffold( child: Scaffold(

View file

@ -18,7 +18,6 @@ import '../../../models/exchange/response_objects/trade.dart';
import '../../../providers/global/trades_service_provider.dart'; import '../../../providers/global/trades_service_provider.dart';
import '../../../providers/providers.dart'; import '../../../providers/providers.dart';
import '../../../services/exchange/exchange_response.dart'; import '../../../services/exchange/exchange_response.dart';
import '../../../services/exchange/majestic_bank/majestic_bank_exchange.dart';
import '../../../services/notifications_api.dart'; import '../../../services/notifications_api.dart';
import '../../../themes/stack_colors.dart'; import '../../../themes/stack_colors.dart';
import '../../../utilities/assets.dart'; import '../../../utilities/assets.dart';
@ -63,8 +62,7 @@ class _Step3ViewState extends ConsumerState<Step3View> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final supportsRefund = final supportsRefund = ref.watch(efExchangeProvider).supportsRefundAddress;
ref.watch(efExchangeProvider).name != MajesticBankExchange.exchangeName;
return Background( return Background(
child: Scaffold( child: Scaffold(

View file

@ -10,17 +10,19 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../models/exchange/aggregate_currency.dart'; import '../../../models/exchange/aggregate_currency.dart';
import 'exchange_provider_option.dart';
import '../../../providers/providers.dart'; import '../../../providers/providers.dart';
import '../../../services/exchange/change_now/change_now_exchange.dart'; import '../../../services/exchange/change_now/change_now_exchange.dart';
import '../../../services/exchange/exchange.dart'; import '../../../services/exchange/exchange.dart';
import '../../../services/exchange/majestic_bank/majestic_bank_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 '../../../services/exchange/trocador/trocador_exchange.dart';
import '../../../themes/stack_colors.dart'; import '../../../themes/stack_colors.dart';
import '../../../utilities/prefs.dart'; import '../../../utilities/prefs.dart';
import '../../../utilities/util.dart'; import '../../../utilities/util.dart';
import '../../../widgets/rounded_white_container.dart'; import '../../../widgets/rounded_white_container.dart';
import 'exchange_provider_option.dart';
class ExchangeProviderOptions extends ConsumerStatefulWidget { class ExchangeProviderOptions extends ConsumerStatefulWidget {
const ExchangeProviderOptions({ const ExchangeProviderOptions({
@ -88,6 +90,11 @@ class _ExchangeProviderOptionsState
sendCurrency: sendCurrency, sendCurrency: sendCurrency,
receiveCurrency: receivingCurrency, receiveCurrency: receivingCurrency,
); );
final showNanswap = exchangeSupported(
exchangeName: NanswapExchange.exchangeName,
sendCurrency: sendCurrency,
receiveCurrency: receivingCurrency,
);
return RoundedWhiteContainer( return RoundedWhiteContainer(
padding: isDesktop ? const EdgeInsets.all(0) : const EdgeInsets.all(12), padding: isDesktop ? const EdgeInsets.all(0) : const EdgeInsets.all(12),
@ -134,6 +141,23 @@ class _ExchangeProviderOptionsState
reversed: widget.reversed, reversed: widget.reversed,
exchange: TrocadorExchange.instance, exchange: TrocadorExchange.instance,
), ),
if ((showChangeNow || showMajesticBank || showTrocador) &&
showNanswap)
isDesktop
? Container(
height: 1,
color:
Theme.of(context).extension<StackColors>()!.background,
)
: const SizedBox(
height: 16,
),
if (showNanswap)
ExchangeOption(
fixedRate: widget.fixedRate,
reversed: widget.reversed,
exchange: NanswapExchange.instance,
),
], ],
), ),
); );

View file

@ -30,6 +30,7 @@ import '../../route_generator.dart';
import '../../services/exchange/change_now/change_now_exchange.dart'; import '../../services/exchange/change_now/change_now_exchange.dart';
import '../../services/exchange/exchange.dart'; import '../../services/exchange/exchange.dart';
import '../../services/exchange/majestic_bank/majestic_bank_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/simpleswap/simpleswap_exchange.dart';
import '../../services/exchange/trocador/trocador_exchange.dart'; import '../../services/exchange/trocador/trocador_exchange.dart';
import '../../themes/stack_colors.dart'; import '../../themes/stack_colors.dart';
@ -1330,6 +1331,10 @@ class _TradeDetailsViewState extends ConsumerState<TradeDetailsView> {
url = url =
"https://majesticbank.sc/track?trx=${trade.tradeId}"; "https://majesticbank.sc/track?trx=${trade.tradeId}";
break; break;
case NanswapExchange.exchangeName:
url =
"https://nanswap.com/transaction/${trade.tradeId}";
break;
default: default:
if (trade.exchangeName if (trade.exchangeName

View file

@ -15,8 +15,7 @@ import 'package:tuple/tuple.dart';
import '../../../../app_config.dart'; import '../../../../app_config.dart';
import '../../../../models/contact_address_entry.dart'; import '../../../../models/contact_address_entry.dart';
import '../../../../providers/exchange/exchange_send_from_wallet_id_provider.dart'; import '../../../../providers/providers.dart';
import '../../../../providers/global/wallets_provider.dart';
import '../../../../themes/stack_colors.dart'; import '../../../../themes/stack_colors.dart';
import '../../../../utilities/clipboard_interface.dart'; import '../../../../utilities/clipboard_interface.dart';
import '../../../../utilities/constants.dart'; import '../../../../utilities/constants.dart';
@ -88,7 +87,7 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> {
} }
widget.enableNextChanged.call( widget.enableNextChanged.call(
_toController.text.isNotEmpty && _refundController.text.isNotEmpty, _next(),
); );
} }
@ -120,7 +119,7 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> {
Logging.instance.log("$e\n$s", level: LogLevel.Info); Logging.instance.log("$e\n$s", level: LogLevel.Info);
} }
widget.enableNextChanged.call( widget.enableNextChanged.call(
_toController.text.isNotEmpty && _refundController.text.isNotEmpty, _next(),
); );
} }
@ -167,7 +166,7 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> {
_toController.text = entry.address; _toController.text = entry.address;
ref.read(desktopExchangeModelProvider)!.recipientAddress = entry.address; ref.read(desktopExchangeModelProvider)!.recipientAddress = entry.address;
widget.enableNextChanged.call( widget.enableNextChanged.call(
_toController.text.isNotEmpty && _refundController.text.isNotEmpty, _next(),
); );
} }
} }
@ -215,11 +214,21 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> {
_refundController.text = entry.address; _refundController.text = entry.address;
ref.read(desktopExchangeModelProvider)!.refundAddress = entry.address; ref.read(desktopExchangeModelProvider)!.refundAddress = entry.address;
widget.enableNextChanged.call( 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 @override
void initState() { void initState() {
clipboard = widget.clipboard; clipboard = widget.clipboard;
@ -230,6 +239,13 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> {
_toFocusNode = FocusNode(); _toFocusNode = FocusNode();
_refundFocusNode = 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; final tuple = ref.read(exchangeSendFromWalletIdStateProvider.state).state;
if (tuple != null) { if (tuple != null) {
if (ref.read(desktopExchangeModelProvider)!.receiveTicker.toLowerCase() == if (ref.read(desktopExchangeModelProvider)!.receiveTicker.toLowerCase() ==
@ -243,8 +259,9 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> {
ref.read(desktopExchangeModelProvider)!.recipientAddress = ref.read(desktopExchangeModelProvider)!.recipientAddress =
_toController.text; _toController.text;
} else { } else {
if (ref.read(desktopExchangeModelProvider)!.sendTicker.toUpperCase() == if (doesRefundAddress &&
tuple.item2.ticker.toUpperCase()) { ref.read(desktopExchangeModelProvider)!.sendTicker.toUpperCase() ==
tuple.item2.ticker.toUpperCase()) {
_refundController.text = ref _refundController.text = ref
.read(pWallets) .read(pWallets)
.getWallet(tuple.item1) .getWallet(tuple.item1)
@ -341,8 +358,7 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> {
style: STextStyles.field(context), style: STextStyles.field(context),
onChanged: (value) { onChanged: (value) {
widget.enableNextChanged.call( widget.enableNextChanged.call(
_toController.text.isNotEmpty && _next(),
_refundController.text.isNotEmpty,
); );
}, },
decoration: standardInputDecoration( decoration: standardInputDecoration(
@ -376,8 +392,7 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> {
.read(desktopExchangeModelProvider)! .read(desktopExchangeModelProvider)!
.recipientAddress = _toController.text; .recipientAddress = _toController.text;
widget.enableNextChanged.call( widget.enableNextChanged.call(
_toController.text.isNotEmpty && _next(),
_refundController.text.isNotEmpty,
); );
}, },
child: const XIcon(), child: const XIcon(),
@ -397,8 +412,7 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> {
.read(desktopExchangeModelProvider)! .read(desktopExchangeModelProvider)!
.recipientAddress = _toController.text; .recipientAddress = _toController.text;
widget.enableNextChanged.call( widget.enableNextChanged.call(
_toController.text.isNotEmpty && _next(),
_refundController.text.isNotEmpty,
); );
} }
}, },
@ -435,155 +449,158 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> {
style: STextStyles.desktopTextExtraExtraSmall(context), style: STextStyles.desktopTextExtraExtraSmall(context),
), ),
), ),
const SizedBox( if (doesRefundAddress)
height: 24, const SizedBox(
), height: 24,
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Refund Wallet (required)",
style: STextStyles.desktopTextExtraExtraSmall(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.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,
), ),
child: TextField( if (doesRefundAddress)
key: const Key("refundExchangeStep2ViewAddressFieldKey"), Row(
controller: _refundController, mainAxisAlignment: MainAxisAlignment.spaceBetween,
readOnly: false, children: [
autocorrect: false, Text(
enableSuggestions: false, "Refund Wallet (required)",
// inputFormatters: <TextInputFormatter>[ style: STextStyles.desktopTextExtraExtraSmall(context).copyWith(
// FilteringTextInputFormatter.allow(RegExp("[a-zA-Z0-9]{34}")), color: Theme.of(context)
// ], .extension<StackColors>()!
toolbarOptions: const ToolbarOptions( .textFieldActiveSearchIconRight,
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,
), ),
suffixIcon: Padding( if (AppConfig.isStackCoin(
padding: _refundController.text.isEmpty ref.watch(
? const EdgeInsets.only(right: 16) desktopExchangeModelProvider
: const EdgeInsets.only(right: 0), .select((value) => value!.sendTicker),
child: UnconstrainedBox( ),
child: Row( ))
mainAxisAlignment: MainAxisAlignment.spaceAround, CustomTextButton(
children: [ text: "Choose from Stack",
_refundController.text.isNotEmpty onTap: selectRefundAddressFromStack,
? TextFieldIconButton( ),
key: const Key( ],
"sendViewClearAddressFieldButtonKey", ),
), if (doesRefundAddress)
onTap: () { const SizedBox(
_refundController.text = ""; height: 10,
ref ),
.read(desktopExchangeModelProvider)! if (doesRefundAddress)
.refundAddress = _refundController.text; ClipRRect(
borderRadius: BorderRadius.circular(
widget.enableNextChanged.call( Constants.size.circularBorderRadius,
_toController.text.isNotEmpty && ),
_refundController.text.isNotEmpty, child: TextField(
); key: const Key("refundExchangeStep2ViewAddressFieldKey"),
}, controller: _refundController,
child: const XIcon(), readOnly: false,
) autocorrect: false,
: TextFieldIconButton( enableSuggestions: false,
key: const Key( // inputFormatters: <TextInputFormatter>[
"sendViewPasteAddressFieldButtonKey", // FilteringTextInputFormatter.allow(RegExp("[a-zA-Z0-9]{34}")),
), // ],
onTap: () async { toolbarOptions: const ToolbarOptions(
final ClipboardData? data = await clipboard copy: false,
.getData(Clipboard.kTextPlain); cut: false,
if (data?.text != null && paste: true,
data!.text!.isNotEmpty) { selectAll: false,
final content = data.text!.trim(); ),
focusNode: _refundFocusNode,
_refundController.text = content; 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 ref
.read(desktopExchangeModelProvider)! .read(desktopExchangeModelProvider)!
.refundAddress = _refundController.text; .refundAddress = _refundController.text;
widget.enableNextChanged.call( widget.enableNextChanged.call(
_toController.text.isNotEmpty && _next(),
_refundController.text.isNotEmpty,
); );
} },
}, child: const XIcon(),
child: _refundController.text.isEmpty )
? const ClipboardIcon() : TextFieldIconButton(
: const XIcon(), key: const Key(
), "sendViewPasteAddressFieldButtonKey",
if (_refundController.text.isEmpty && ),
AppConfig.isStackCoin( onTap: () async {
ref.watch( final ClipboardData? data = await clipboard
desktopExchangeModelProvider .getData(Clipboard.kTextPlain);
.select((value) => value!.sendTicker), if (data?.text != null &&
), data!.text!.isNotEmpty) {
)) final content = data.text!.trim();
TextFieldIconButton(
key: const Key("sendViewAddressBookButtonKey"), _refundController.text = content;
onTap: selectRefundFromAddressBook, ref
child: const AddressBookIcon(), .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(),
),
],
),
), ),
), ),
), ),
), ),
), ),
), if (doesRefundAddress)
const SizedBox( const SizedBox(
height: 10, height: 10,
), ),
RoundedWhiteContainer( if (doesRefundAddress)
borderColor: Theme.of(context).extension<StackColors>()!.background, RoundedWhiteContainer(
child: Text( borderColor: Theme.of(context).extension<StackColors>()!.background,
"In case something goes wrong during the exchange, we might need a refund address so we can return your coins back to you.", child: Text(
style: STextStyles.desktopTextExtraExtraSmall(context), "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),
),
), ),
),
], ],
); );
} }

View file

@ -10,13 +10,14 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../step_scaffold.dart';
import 'desktop_step_item.dart';
import '../../../../providers/providers.dart'; import '../../../../providers/providers.dart';
import '../../../../themes/stack_colors.dart'; import '../../../../themes/stack_colors.dart';
import '../../../../utilities/enums/exchange_rate_type_enum.dart'; import '../../../../utilities/enums/exchange_rate_type_enum.dart';
import '../../../../utilities/text_styles.dart'; import '../../../../utilities/text_styles.dart';
import '../../../../widgets/rounded_white_container.dart'; import '../../../../widgets/rounded_white_container.dart';
import '../step_scaffold.dart';
import 'desktop_step_item.dart';
class DesktopStep3 extends ConsumerStatefulWidget { class DesktopStep3 extends ConsumerStatefulWidget {
const DesktopStep3({ const DesktopStep3({
@ -97,20 +98,22 @@ class _DesktopStep3State extends ConsumerState<DesktopStep3> {
) ?? ) ??
"Error", "Error",
), ),
Container( if (ref.watch(efExchangeProvider).supportsRefundAddress)
height: 1, Container(
color: Theme.of(context).extension<StackColors>()!.background, height: 1,
), color: Theme.of(context).extension<StackColors>()!.background,
DesktopStepItem( ),
vertical: true, if (ref.watch(efExchangeProvider).supportsRefundAddress)
label: DesktopStepItem(
"Refund ${ref.watch(desktopExchangeModelProvider.select((value) => value!.sendTicker.toUpperCase()))} address", vertical: true,
value: ref.watch( label:
desktopExchangeModelProvider "Refund ${ref.watch(desktopExchangeModelProvider.select((value) => value!.sendTicker.toUpperCase()))} address",
.select((value) => value!.refundAddress), value: ref.watch(
) ?? desktopExchangeModelProvider
"Error", .select((value) => value!.refundAddress),
), ) ??
"Error",
),
], ],
), ),
), ),

View file

@ -9,6 +9,7 @@
*/ */
import 'package:decimal/decimal.dart'; import 'package:decimal/decimal.dart';
import '../../models/exchange/response_objects/estimate.dart'; import '../../models/exchange/response_objects/estimate.dart';
import '../../models/exchange/response_objects/range.dart'; import '../../models/exchange/response_objects/range.dart';
import '../../models/exchange/response_objects/trade.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 'change_now/change_now_exchange.dart';
import 'exchange_response.dart'; import 'exchange_response.dart';
import 'majestic_bank/majestic_bank_exchange.dart'; import 'majestic_bank/majestic_bank_exchange.dart';
import 'nanswap/nanswap_exchange.dart';
import 'simpleswap/simpleswap_exchange.dart'; import 'simpleswap/simpleswap_exchange.dart';
import 'trocador/trocador_exchange.dart'; import 'trocador/trocador_exchange.dart';
@ -33,6 +35,8 @@ abstract class Exchange {
return MajesticBankExchange.instance; return MajesticBankExchange.instance;
case TrocadorExchange.exchangeName: case TrocadorExchange.exchangeName:
return TrocadorExchange.instance; return TrocadorExchange.instance;
case NanswapExchange.exchangeName:
return NanswapExchange.instance;
default: default:
final split = name.split(" "); final split = name.split(" ");
if (split.length >= 2) { if (split.length >= 2) {
@ -45,6 +49,8 @@ abstract class Exchange {
String get name; String get name;
bool get supportsRefundAddress => true;
Future<ExchangeResponse<List<Currency>>> getAllCurrencies(bool fixedRate); Future<ExchangeResponse<List<Currency>>> getAllCurrencies(bool fixedRate);
Future<ExchangeResponse<List<Currency>>> getPairedCurrencies( Future<ExchangeResponse<List<Currency>>> getPairedCurrencies(
@ -97,6 +103,7 @@ abstract class Exchange {
static List<Exchange> get exchangesWithTorSupport => [ static List<Exchange> get exchangesWithTorSupport => [
MajesticBankExchange.instance, MajesticBankExchange.instance,
TrocadorExchange.instance, TrocadorExchange.instance,
NanswapExchange.instance, // Maybe??
]; ];
/// List of exchange names which support Tor. /// List of exchange names which support Tor.

View file

@ -23,6 +23,7 @@ import '../../utilities/prefs.dart';
import '../../utilities/stack_file_system.dart'; import '../../utilities/stack_file_system.dart';
import 'change_now/change_now_exchange.dart'; import 'change_now/change_now_exchange.dart';
import 'majestic_bank/majestic_bank_exchange.dart'; import 'majestic_bank/majestic_bank_exchange.dart';
import 'nanswap/nanswap_exchange.dart';
import 'trocador/trocador_exchange.dart'; import 'trocador/trocador_exchange.dart';
class ExchangeDataLoadingService { class ExchangeDataLoadingService {
@ -170,6 +171,7 @@ class ExchangeDataLoadingService {
final futures = [ final futures = [
loadMajesticBankCurrencies(), loadMajesticBankCurrencies(),
loadTrocadorCurrencies(), loadTrocadorCurrencies(),
loadNanswapCurrencies(),
]; ];
// If using Tor, don't load data for exchanges which don't support Tor. // If using Tor, don't load data for exchanges which don't support Tor.
@ -382,6 +384,31 @@ class ExchangeDataLoadingService {
} }
} }
Future<void> 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<void> loadMajesticBankPairs() async { // Future<void> loadMajesticBankPairs() async {
// final exchange = MajesticBankExchange.instance; // final exchange = MajesticBankExchange.instance;
// //

View file

@ -46,6 +46,9 @@ class MajesticBankExchange extends Exchange {
"XMR": "Monero", "XMR": "Monero",
}; };
@override
bool get supportsRefundAddress => false;
@override @override
Future<ExchangeResponse<Trade>> createTrade({ Future<ExchangeResponse<Trade>> createTrade({
required String from, required String from,

View file

@ -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<String, dynamic> 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'
'}';
}
}

View file

@ -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<String, dynamic> 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 '
'}';
}
}

View file

@ -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<String, dynamic> 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, '
'}';
}
}

View file

@ -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<String, String>? params}) {
return Uri.https(authority, "/$version/$endpoint", params);
}
Future<dynamic> _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<dynamic> _makePostRequest(
Uri uri,
Map<String, dynamic> 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<ExchangeResponse<List<NCurrency>>> getSupportedCurrencies() async {
final uri = _buildUri(
endpoint: "all-currencies",
);
try {
final json = await _makeGetRequest(uri);
final List<NCurrency> result = [];
for (final key in (json as Map).keys) {
final _map = json[key] as Map;
_map["id"] = key;
result.add(
NCurrency.fromJson(
Map<String, dynamic>.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<ExchangeResponse<NEstimate>> 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<String, dynamic>.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<ExchangeResponse<NEstimate>> 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<String, dynamic>.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<ExchangeResponse<({num minFrom, num maxFrom})>> 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
//
// <value>
// HEADERS
// nanswap-api-key
//
// API_KEY
//
// (Required)
// Content-Type
//
// application/json
// Accept
//
// application/json
Future<ExchangeResponse<NTrade>> 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<String, dynamic>.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<ExchangeResponse<NTrade>> 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<String, dynamic>.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,
),
);
}
}
}

View file

@ -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<ExchangeResponse<Trade>> 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<ExchangeResponse<List<Currency>>> 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<ExchangeResponse<List<Pair>>> getAllPairs(bool fixedRate) async {
throw UnimplementedError();
}
@override
Future<ExchangeResponse<List<Estimate>>> 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<NEstimate> 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<ExchangeResponse<List<Currency>>> 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<ExchangeResponse<List<Pair>>> getPairsFor(
String currency,
bool fixedRate,
) async {
throw UnsupportedError("Not used");
}
@override
Future<ExchangeResponse<Range>> 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<ExchangeResponse<Trade>> 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<ExchangeResponse<List<Trade>>> getTrades() async {
// TODO: implement getTrades
throw UnimplementedError();
}
@override
String get name => exchangeName;
@override
Future<ExchangeResponse<Trade>> 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,
),
);
}
}
}

View file

@ -9,8 +9,10 @@
*/ */
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../services/exchange/change_now/change_now_exchange.dart'; import '../services/exchange/change_now/change_now_exchange.dart';
import '../services/exchange/majestic_bank/majestic_bank_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/simpleswap/simpleswap_exchange.dart';
import '../services/exchange/trocador/trocador_exchange.dart'; import '../services/exchange/trocador/trocador_exchange.dart';
@ -45,6 +47,7 @@ class _EXCHANGE {
String get majesticBankBlue => "${_path}mb_blue.svg"; String get majesticBankBlue => "${_path}mb_blue.svg";
String get majesticBankGreen => "${_path}mb_green.svg"; String get majesticBankGreen => "${_path}mb_green.svg";
String get trocador => "${_path}trocador.svg"; String get trocador => "${_path}trocador.svg";
String get nanswap => "${_path}nanswap.svg";
String getIconFor({required String exchangeName}) { String getIconFor({required String exchangeName}) {
switch (exchangeName) { switch (exchangeName) {
@ -56,6 +59,8 @@ class _EXCHANGE {
return majesticBankBlue; return majesticBankBlue;
case TrocadorExchange.exchangeName: case TrocadorExchange.exchangeName:
return trocador; return trocador;
case NanswapExchange.exchangeName:
return nanswap;
default: default:
throw ArgumentError("Invalid exchange name passed to " throw ArgumentError("Invalid exchange name passed to "
"Assets.exchange.getIconFor()"); "Assets.exchange.getIconFor()");