What changed, and why it matters
This commit fixes a network-labeling bug in the Nanswap cryptocurrency exchange integration. Before the fix, the code sometimes mixed up the currency/network a user was supposed to send in with the one they would receive out. That could mislead users into depositing funds on the wrong blockchain or network, which in the worst case could mean lost or stuck funds. The patch adds helper getters so the correct network is always chosen, and adds a unit test to confirm the behavior.
Review whether any persisted trade records created by the buggy code could still be displayed or acted upon with swapped networks, and consider a migration or warning. Verify the fix against Nanswap API documentation for fromNetwork/toNetwork semantics. Ensure the new unit test runs in CI.
Security signals we found
Incorrect mapping of pay-in/pay-out networks in exchange trade construction
Potential user funds misrouted to wrong network/address
Regression test added to prevent future source/destination swap
Dependency injection added to enable unit testing of exchange adapter
Evidence from the diff
The Nanswap exchange adapter was mapping payInNetwork and payOutNetwork incorrectly in several places. In createTrade(), getTrade(), and updateTrade(), it used t.toNetwork ?? t.to for payInNetwork and t.fromNetwork ?? t.from for payOutNetwork, which swaps source/destination. The patch introduces NTrade.payInNetwork (fromNetwork ?? from) and NTrade.payOutNetwork (toNetwork ?? to) getters and uses them consistently. It also injects the order lookup dependency for testability and adds a unit test verifying that getTrade and updateTrade return the expected (BTC, XNO) network pair.
Changed components
lib/services/exchange/nanswap/nanswap_exchange.dartlib/services/exchange/nanswap/api_response_models/n_trade.darttest/services/exchange/nanswap_exchange_test.dartInspect captured patch +61 / −9
diff --git a/lib/services/exchange/nanswap/api_response_models/n_trade.dart b/lib/services/exchange/nanswap/api_response_models/n_trade.dart
index f26e19f..b9355cd 100644
--- a/lib/services/exchange/nanswap/api_response_models/n_trade.dart
+++ b/lib/services/exchange/nanswap/api_response_models/n_trade.dart
@@ -17,6 +17,9 @@ class NTrade {
final String? fromNetwork;
final String? toNetwork;
+ String get payInNetwork => fromNetwork ?? from;
+ String get payOutNetwork => toNetwork ?? to;
+
NTrade({
required this.id,
required this.from,
diff --git a/lib/services/exchange/nanswap/nanswap_exchange.dart b/lib/services/exchange/nanswap/nanswap_exchange.dart
index a26a35c..6ef4873 100644
--- a/lib/services/exchange/nanswap/nanswap_exchange.dart
+++ b/lib/services/exchange/nanswap/nanswap_exchange.dart
@@ -1,4 +1,5 @@
import 'package:decimal/decimal.dart';
+import 'package:flutter/foundation.dart';
import 'package:uuid/uuid.dart';
import '../../../app_config.dart';
@@ -11,14 +12,25 @@ import '../../../models/isar/exchange_cache/pair.dart';
import '../exchange.dart';
import '../exchange_response.dart';
import 'api_response_models/n_estimate.dart';
+import 'api_response_models/n_trade.dart';
import 'nanswap_api.dart';
+typedef NanswapOrderLookup =
+ Future<ExchangeResponse<NTrade>> Function({required String id});
+
class NanswapExchange extends Exchange {
- NanswapExchange._();
+ NanswapExchange._({NanswapOrderLookup? getOrder})
+ : _getOrder = getOrder ?? NanswapAPI.instance.getOrder;
+
+ @visibleForTesting
+ NanswapExchange.forTesting({required NanswapOrderLookup getOrder})
+ : this._(getOrder: getOrder);
static NanswapExchange? _instance;
static NanswapExchange get instance => _instance ??= NanswapExchange._();
+ final NanswapOrderLookup _getOrder;
+
static const exchangeName = "Nanswap";
static const filter = ["BTC", "BAN", "XNO"];
@@ -92,13 +104,13 @@ class NanswapExchange extends Exchange {
payInCurrency: from,
payInAmount: t.expectedAmountFrom.toString(),
payInAddress: t.payinAddress,
- payInNetwork: t.toNetwork ?? t.to,
+ payInNetwork: t.payInNetwork,
payInExtraId: t.payinExtraId ?? "",
payInTxid: t.payinHash ?? "",
payOutCurrency: to,
payOutAmount: t.expectedAmountTo.toString(),
payOutAddress: t.payoutAddress,
- payOutNetwork: t.fromNetwork ?? t.from,
+ payOutNetwork: t.payOutNetwork,
payOutExtraId: "",
payOutTxid: t.payoutHash ?? "",
refundAddress: "",
@@ -319,7 +331,7 @@ class NanswapExchange extends Exchange {
@override
Future<ExchangeResponse<Trade>> getTrade(String tradeId) async {
try {
- final response = await NanswapAPI.instance.getOrder(id: tradeId);
+ final response = await _getOrder(id: tradeId);
if (response.exception != null) {
return ExchangeResponse(exception: response.exception);
@@ -338,13 +350,13 @@ class NanswapExchange extends Exchange {
payInCurrency: t.from,
payInAmount: t.expectedAmountFrom.toString(),
payInAddress: t.payinAddress,
- payInNetwork: t.toNetwork ?? t.to,
+ payInNetwork: t.payInNetwork,
payInExtraId: t.payinExtraId ?? "",
payInTxid: t.payinHash ?? "",
payOutCurrency: t.to,
payOutAmount: t.expectedAmountTo.toString(),
payOutAddress: t.payoutAddress,
- payOutNetwork: t.fromNetwork ?? t.from,
+ payOutNetwork: t.payOutNetwork,
payOutExtraId: "",
payOutTxid: t.payoutHash ?? "",
refundAddress: "",
@@ -377,7 +389,7 @@ class NanswapExchange extends Exchange {
@override
Future<ExchangeResponse<Trade>> updateTrade(Trade trade) async {
try {
- final response = await NanswapAPI.instance.getOrder(id: trade.tradeId);
+ final response = await _getOrder(id: trade.tradeId);
if (response.exception != null) {
return ExchangeResponse(exception: response.exception);
@@ -396,13 +408,13 @@ class NanswapExchange extends Exchange {
payInCurrency: t.from,
payInAmount: t.expectedAmountFrom.toString(),
payInAddress: t.payinAddress,
- payInNetwork: t.toNetwork ?? trade.payInNetwork,
+ payInNetwork: t.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,
+ payOutNetwork: t.payOutNetwork,
payOutExtraId: trade.payOutExtraId,
payOutTxid: t.payoutHash ?? trade.payOutTxid,
refundAddress: trade.refundAddress,
diff --git a/test/services/exchange/nanswap_exchange_test.dart b/test/services/exchange/nanswap_exchange_test.dart
new file mode 100644
index 0000000..ee0bcd5
--- /dev/null
+++ b/test/services/exchange/nanswap_exchange_test.dart
@@ -0,0 +1,37 @@
+import 'package:flutter_test/flutter_test.dart';
+import 'package:stackwallet/services/exchange/exchange_response.dart';
+import 'package:stackwallet/services/exchange/nanswap/api_response_models/n_trade.dart';
+import 'package:stackwallet/services/exchange/nanswap/nanswap_exchange.dart';
+
+void main() {
+ test('maps Nanswap source and destination networks', () async {
+ final nTrade = NTrade(
+ id: 'trade-id',
+ from: 'BTC',
+ to: 'XNO',
+ expectedAmountFrom: 1,
+ expectedAmountTo: 2,
+ payinAddress: 'pay-in',
+ payoutAddress: 'pay-out',
+ );
+ final exchange = NanswapExchange.forTesting(
+ getOrder: ({required String id}) async {
+ expect(id, nTrade.id);
+ return ExchangeResponse(value: nTrade);
+ },
+ );
+
+ final trade = (await exchange.getTrade(nTrade.id)).value!;
+ final staleTrade = trade.copyWith(
+ payInNetwork: 'XNO',
+ payOutNetwork: 'BTC',
+ );
+ final updatedTrade = (await exchange.updateTrade(staleTrade)).value!;
+
+ expect((trade.payInNetwork, trade.payOutNetwork), ('BTC', 'XNO'));
+ expect(
+ (updatedTrade.payInNetwork, updatedTrade.payOutNetwork),
+ ('BTC', 'XNO'),
+ );
+ });
+}
Why this scored 33/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.