User token swap improvements (#2470)
* Handle null currency in trade * Improve trade network handling and error reporting * update Polygon network tag * Update exchange_trade_view_model.dart * handle nullable network fields * Update lib/exchange/provider/letsexchange_exchange_provider.dart --------- Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>
Serhii committed
Aug 21, 2025 at 21:14 UTC
9292fb54879500da7ef5344fa7582a61c449ee3f
15 files changed
+136
-48
cw_core/lib/crypto_currency.dart
+6
@@ -287,6 +287,12 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
287
return CryptoCurrency._rawCurrencyMap[raw]!;
288
}
289
290
+ static CryptoCurrency? safeDeserialize({int? raw}) {
291
+ if (raw == null || raw < 0) return null;
292
+ return _rawCurrencyMap[raw];
293
+ }
294
+
295
+
296
// TODO: refactor this
297
static CryptoCurrency fromString(String name, {CryptoCurrency? walletCurrency}) {
298
cw_polygon/lib/polygon_wallet.dart
+1
-1
@@ -78,7 +78,7 @@ class PolygonWallet extends EVMChainWallet {
78
contractAddress: token.contractAddress,
79
decimal: token.decimal,
80
enabled: token.enabled,
81
- tag: token.tag ?? "MATIC",
81
+ tag: token.tag ?? 'POL',
82
iconPath: iconPath,
83
isPotentialScam: token.isPotentialScam,
84
);
integration_test/robots/transactions_page_robot.dart
+4
-2
@@ -385,18 +385,20 @@ class TransactionsPageRobot {
385
386
Future<void> _verifyTradeListItemDisplay(TradeListItem item) async {
387
final keyId = 'trade_list_item_${item.trade.id}_key';
388
+ final from = item.trade.from?.toString() ?? item.trade.userCurrencyFrom.toString();
389
+ final to = item.trade.to?.toString() ?? item.trade.userCurrencyTo.toString();
390
391
//* ==============Confirm it has the right key for this item ========
392
commonTestCases.hasValueKey(keyId);
393
394
//* ==============Confirm it displays the correct provider =========================
393
- final conversionFlow = '${item.trade.from.toString()} → ${item.trade.to.toString()}';
395
+ final conversionFlow = '$from → $to';
396
397
commonTestCases.hasText(conversionFlow);
398
399
//* ===========Confirm it displays the properly formatted amount with its crypto tag ========
400
399
- final amountCryptoText = item.tradeFormattedAmount + ' ' + item.trade.from.toString();
401
+ final amountCryptoText = item.tradeFormattedAmount + ' ' + from;
402
403
commonTestCases.hasText(amountCryptoText);
404
lib/exchange/provider/chainflip_exchange_provider.dart
+8
@@ -69,6 +69,8 @@ class ChainflipExchangeProvider extends ExchangeProvider {
69
{required CryptoCurrency from,
70
required CryptoCurrency to,
71
required bool isFixedRateMode}) async {
72
+
73
+ try {
74
final assetId = _normalizeCurrency(from);
75
76
final assetsResponse = await _getAssets();
@@ -78,7 +80,13 @@ class ChainflipExchangeProvider extends ExchangeProvider {
80
(asset) => asset['id'] == assetId,
81
orElse: () => null)?['minimalAmountNative'] ?? '0';
82
83
+ if (minAmount == '0') throw Exception('No rates found for $from to $to');
84
+
85
return Limits(min: _amountFromNative(minAmount.toString(), from));
86
+ } catch (e) {
87
+ printV(e.toString());
88
+ throw Exception('Chainflip failed to fetch limits');
89
+ }
90
}
91
92
@override
lib/exchange/provider/changenow_exchange_provider.dart
+17
-2
@@ -242,8 +242,10 @@ class ChangeNowExchangeProvider extends ExchangeProvider {
242
243
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
244
final fromCurrency = responseJSON['fromCurrency'] as String;
245
+ final fromNetwork = responseJSON['fromNetwork'] as String?;
246
final from = CryptoCurrency.safeParseCurrencyFromString(fromCurrency);
247
final toCurrency = responseJSON['toCurrency'] as String;
248
+ final toNetwork = responseJSON['toNetwork'] as String?;
249
final to = CryptoCurrency.safeParseCurrencyFromString(toCurrency);
250
final inputAddress = responseJSON['payinAddress'] as String;
251
final expectedSendAmount = responseJSON['expectedAmountFrom'].toString();
@@ -255,6 +257,9 @@ class ChangeNowExchangeProvider extends ExchangeProvider {
257
final payoutAddress = responseJSON['payoutAddress'] as String;
258
final expiredAt = DateTime.tryParse(expiredAtRaw ?? '')?.toLocal();
259
260
+ final _normalizedFromNetwork = _normalizeNetworkType(fromNetwork ?? '');
261
+ final _normalizedToNetwork = _normalizeNetworkType(toNetwork ?? '');
262
+
263
return Trade(
264
id: id,
265
from: from,
@@ -267,8 +272,8 @@ class ChangeNowExchangeProvider extends ExchangeProvider {
272
expiredAt: expiredAt,
273
outputTransaction: outputTransaction,
274
payoutAddress: payoutAddress,
270
- userCurrencyFromRaw: '${fromCurrency.toUpperCase()}' + '_',
271
- userCurrencyToRaw: '${toCurrency.toUpperCase()}' + '_',
275
+ userCurrencyFromRaw: '${fromCurrency.toUpperCase()}' + '_' + '${_normalizedFromNetwork.toUpperCase()}',
276
+ userCurrencyToRaw: '${toCurrency.toUpperCase()}' + '_' + '${_normalizedToNetwork.toUpperCase()}',
277
);
278
}
279
@@ -307,4 +312,14 @@ class ChangeNowExchangeProvider extends ExchangeProvider {
312
return tag.toLowerCase();
313
}
314
}
315
+
316
+ String _normalizeNetworkType(String network) {
317
+ return switch (network.toUpperCase()) {
318
+ 'POLY' => 'MATIC',
319
+ 'AVAXC' => 'CCHAIN',
320
+ _ => network,
321
+ };
322
+ }
323
+
324
+
325
}
lib/exchange/provider/exolix_exchange_provider.dart
+4
-2
@@ -241,7 +241,9 @@ class ExolixExchangeProvider extends ExchangeProvider {
241
242
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
243
final coinFrom = responseJSON['coinFrom']['coinCode'] as String;
244
+ final coinFromNetwork = responseJSON['coinFrom']['network'] as String?;
245
final coinTo = responseJSON['coinTo']['coinCode'] as String;
246
+ final coinToNetwork = responseJSON['coinTo']['network'] as String?;
247
final inputAddress = responseJSON['depositAddress'] as String;
248
final amount = responseJSON['amount'].toString();
249
final status = responseJSON['status'] as String;
@@ -260,8 +262,8 @@ class ExolixExchangeProvider extends ExchangeProvider {
262
extraId: extraId,
263
outputTransaction: outputTransaction,
264
payoutAddress: payoutAddress,
263
- userCurrencyFromRaw: '${coinFrom.toUpperCase()}' + '_',
264
- userCurrencyToRaw: '${coinTo.toUpperCase()}' + '_',
265
+ userCurrencyFromRaw: '${coinFrom.toUpperCase()}' + '_' + '${coinFromNetwork ?? ''}',
266
+ userCurrencyToRaw: '${coinTo.toUpperCase()}' + '_' + '${coinToNetwork ?? ''}',
267
);
268
}
269
lib/exchange/provider/letsexchange_exchange_provider.dart
+16
-4
@@ -237,9 +237,9 @@ class LetsExchangeExchangeProvider extends ExchangeProvider {
237
}
238
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
239
final from = responseJSON['coin_from'] as String;
240
- final fromNetwork = responseJSON['coin_from_network'] as String;
240
+ final fromNetwork = responseJSON['coin_from_network'] as String?;
241
final to = responseJSON['coin_to'] as String;
242
- final toNetwork = responseJSON['coin_to_network'] as String;
242
+ final toNetwork = responseJSON['coin_to_network'] as String?;
243
final payoutAddress = responseJSON['withdrawal'] as String;
244
final depositAddress = responseJSON['deposit'] as String;
245
final refundAddress = responseJSON['return'] as String;
@@ -253,6 +253,9 @@ class LetsExchangeExchangeProvider extends ExchangeProvider {
253
final createdAt = DateTime.parse(createdAtString).toLocal();
254
final expiredAt = DateTime.fromMillisecondsSinceEpoch(expiredAtTimestamp * 1000).toLocal();
255
256
+ final normalizedFromNetwork = _normalizeNetworkType(fromNetwork ?? '');
257
+ final normalizedToNetwork = _normalizeNetworkType(toNetwork ?? '');
258
+
259
return Trade(
260
id: id,
261
from: CryptoCurrency.safeParseCurrencyFromString(from),
@@ -268,8 +271,8 @@ class LetsExchangeExchangeProvider extends ExchangeProvider {
271
expiredAt: expiredAt,
272
isRefund: status == 'refund',
273
extraId: extraId,
271
- userCurrencyFromRaw: '$from' + '_' + '$fromNetwork',
272
- userCurrencyToRaw: '$to' + '_' + '$toNetwork',
274
+ userCurrencyFromRaw: '$from' + '_' + normalizedFromNetwork,
275
+ userCurrencyToRaw: '$to' + '_' + '$normalizedToNetwork',
276
);
277
}
278
@@ -313,6 +316,15 @@ class LetsExchangeExchangeProvider extends ExchangeProvider {
316
return currency.title;
317
}
318
319
+ String _normalizeNetworkType(String network) {
320
+ return switch (network.toUpperCase()) {
321
+ 'ERC20' => 'ETH',
322
+ 'TRC20' => 'TRX',
323
+ 'BEP20' => 'BSC',
324
+ _ => network,
325
+ };
326
+ }
327
+
328
String _normalizeBchAddress(String address) =>
329
address.startsWith('bitcoincash:') ? address.substring(12) : address;
330
}
lib/exchange/provider/sideshift_exchange_provider.dart
+17
-3
@@ -254,7 +254,9 @@ class SideShiftExchangeProvider extends ExchangeProvider {
254
255
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
256
final fromCurrency = responseJSON['depositCoin'] as String;
257
+ final fromNetwork = responseJSON['depositNetwork'] as String?;
258
final toCurrency = responseJSON['settleCoin'] as String;
259
+ final toNetwork = responseJSON['settleNetwork'] as String?;
260
final inputAddress = responseJSON['depositAddress'] as String;
261
final expectedSendAmount = responseJSON['depositAmount'] as String?;
262
final status = responseJSON['status'] as String?;
@@ -275,8 +277,8 @@ class SideShiftExchangeProvider extends ExchangeProvider {
277
expiredAt: expiredAt,
278
payoutAddress: settleAddress,
279
extraId: depositMemo,
278
- userCurrencyFromRaw: '${fromCurrency.toUpperCase()}' + '_',
279
- userCurrencyToRaw: '${toCurrency.toUpperCase()}' + '_',
280
+ userCurrencyFromRaw: '${fromCurrency.toUpperCase()}' + '_' + _normalizeNetworkType(fromNetwork ?? ''),
281
+ userCurrencyToRaw: '${toCurrency.toUpperCase()}' + '_' + _normalizeNetworkType(toNetwork ?? ''),
282
);
283
}
284
@@ -335,7 +337,7 @@ class SideShiftExchangeProvider extends ExchangeProvider {
337
return 'tron';
338
case 'LN':
339
return 'lightning';
338
- case 'POLY':
340
+ case 'POL':
341
return 'polygon';
342
case 'ZEC':
343
return 'zcash';
@@ -345,4 +347,16 @@ class SideShiftExchangeProvider extends ExchangeProvider {
347
return tag.toLowerCase();
348
}
349
}
350
+
351
+ String _normalizeNetworkType(String network) {
352
+ return switch (network) {
353
+ 'ethereum' => 'ETH',
354
+ 'tron' => 'TRX',
355
+ 'lightning' => 'LN',
356
+ 'polygon' => 'POL',
357
+ 'zcash' => 'ZEC',
358
+ 'avax' => 'AVAXC',
359
+ _ => network,
360
+ };
361
+ }
362
}
lib/exchange/provider/stealth_ex_exchange_provider.dart
+4
-2
@@ -224,7 +224,9 @@ class StealthExExchangeProvider extends ExchangeProvider {
224
225
final respId = responseJSON['id'] as String;
226
final from = deposit['symbol'] as String;
227
+ final fromNetwork = deposit['network'] as String?;
228
final to = withdrawal['symbol'] as String;
229
+ final toNetwork = withdrawal['network'] as String?;
230
final payoutAddress = withdrawal['address'] as String;
231
final depositAddress = deposit['address'] as String;
232
final refundAddress = responseJSON['refund_address'] as String;
@@ -249,8 +251,8 @@ class StealthExExchangeProvider extends ExchangeProvider {
251
createdAt: createdAt,
252
isRefund: status == 'refunded',
253
extraId: extraId,
252
- userCurrencyFromRaw: '${from.toUpperCase()}' + '_',
253
- userCurrencyToRaw: '${to.toUpperCase()}' + '_',
254
+ userCurrencyFromRaw: '${from.toUpperCase()}' + '_' + '${fromNetwork?.toUpperCase() ?? ''}',
255
+ userCurrencyToRaw: '${to.toUpperCase()}' + '_' + '${toNetwork?.toUpperCase() ?? ''}',
256
);
257
}
258
lib/exchange/provider/swaptrade_exchange_provider.dart
+11
-11
@@ -81,20 +81,20 @@ class SwapTradeExchangeProvider extends ExchangeProvider {
81
82
final coinsInfo = responseJSON['data'] as List<dynamic>;
83
84
- for (var coin in coinsInfo) {
85
- if (coin['id'].toString().toUpperCase() == _normalizeCurrency(from)) {
86
- return Limits(
87
- min: double.parse(coin['min'].toString()),
88
- max: double.parse(coin['max'].toString()),
89
- );
90
- }
91
- }
84
+ final coin = coinsInfo.firstWhere(
85
+ (coin) => coin['id'].toString().toUpperCase() == _normalizeCurrency(from),
86
+ orElse: () => null,
87
+ );
88
93
- // coin not found:
94
- return Limits(min: 0, max: 0);
89
+ if (coin == null) throw Exception('Coin not found: ${_normalizeCurrency(from)}');
90
+
91
+ return Limits(
92
+ min: double.parse(coin['min'].toString()),
93
+ max: double.parse(coin['max'].toString()),
94
+ );
95
} catch (e) {
96
printV(e.toString());
97
- return Limits(min: 0, max: 0);
97
+ throw Exception('Error fetching limits: ${e.toString()}');
98
}
99
}
100
lib/exchange/provider/trocador_exchange_provider.dart
+16
-2
@@ -277,7 +277,9 @@ class TrocadorExchangeProvider extends ExchangeProvider {
277
final addressProviderMemo = responseJSON['address_provider_memo'] as String?;
278
279
final from = responseJSON['ticker_from'] as String;
280
+ final networkFrom = responseJSON['network_from'] as String?;
281
final to = responseJSON['ticker_to'] as String;
282
+ final networkTo = responseJSON['network_to'] as String?;
283
284
return Trade(
285
id: id,
@@ -294,8 +296,8 @@ class TrocadorExchangeProvider extends ExchangeProvider {
296
providerId: providerId,
297
providerName: providerName,
298
extraId: addressProviderMemo,
297
- userCurrencyFromRaw: '${from.toUpperCase()}' + '_',
298
- userCurrencyToRaw: '${to.toUpperCase()}' + '_',
299
+ userCurrencyFromRaw: '${from.toUpperCase()}' + '_' + _normalizeNetworkType(networkFrom ?? ''),
300
+ userCurrencyToRaw: '${to.toUpperCase()}' + '_' + _normalizeNetworkType(networkTo ?? ''), // Handle null network
301
);
302
});
303
}
@@ -362,6 +364,18 @@ class TrocadorExchangeProvider extends ExchangeProvider {
364
}
365
}
366
367
+ String _normalizeNetworkType(String network) {
368
+ return switch (network.toUpperCase()) {
369
+ 'ERC20' => 'ETH',
370
+ 'TRC20' => 'TRX',
371
+ 'BEP20' => 'BSC',
372
+ 'LIGHTNING' => 'LN',
373
+ _ => network,
374
+ };
375
+ }
376
+
377
+
378
+
379
Future<Uri> _getUri(String path, Map<String, String> queryParams) async {
380
final uri = Uri.http(onionApiAuthority, path, queryParams);
381
lib/exchange/trade.dart
+12
-13
@@ -36,9 +36,8 @@ class Trade extends HiveObject {
36
}) {
37
if (provider != null) providerRaw = provider.raw;
38
39
- if (from != null) fromRaw = from.raw;
40
-
41
- if (to != null) toRaw = to.raw;
39
+ fromRaw = from?.raw ?? -1;
40
+ toRaw = to?.raw ?? -1;
41
42
if (state != null) stateRaw = state.raw;
43
}
@@ -56,15 +55,15 @@ class Trade extends HiveObject {
55
ExchangeProviderDescription get provider =>
56
ExchangeProviderDescription.deserialize(raw: providerRaw);
57
59
- @HiveField(2, defaultValue: 0)
60
- late int fromRaw;
58
+ @HiveField(2, defaultValue: -1)
59
+ int fromRaw = -1;
60
62
- CryptoCurrency get from => CryptoCurrency.deserialize(raw: fromRaw);
61
+ CryptoCurrency? get from => CryptoCurrency.safeDeserialize(raw: fromRaw);
62
64
- @HiveField(3, defaultValue: 0)
65
- late int toRaw;
63
+ @HiveField(3, defaultValue: -1)
64
+ int toRaw = -1;
65
67
- CryptoCurrency get to => CryptoCurrency.deserialize(raw: toRaw);
66
+ CryptoCurrency? get to => CryptoCurrency.safeDeserialize(raw: toRaw);
67
68
@HiveField(4, defaultValue: '')
69
late String stateRaw;
@@ -193,8 +192,8 @@ class Trade extends HiveObject {
192
return <String, dynamic>{
193
'id': id,
194
'provider': provider.serialize(),
196
- 'input': from.serialize(),
197
- 'output': to.serialize(),
195
+ 'input': fromRaw,
196
+ 'output': toRaw,
197
'date': createdAt != null ? createdAt!.millisecondsSinceEpoch : null,
198
'amount': amount,
199
'receive_amount': receiveAmount,
@@ -252,8 +251,8 @@ class TradeAdapter extends TypeAdapter<Trade> {
251
userCurrencyToRaw: fields[25] as String?,
252
)
253
..providerRaw = fields[1] == null ? 0 : fields[1] as int
255
- ..fromRaw = fields[2] == null ? 0 : fields[2] as int
256
- ..toRaw = fields[3] == null ? 0 : fields[3] as int
254
+ ..fromRaw = (fields[2] as int?) ?? -1
255
+ ..toRaw = (fields[3] as int?) ?? -1
256
..stateRaw = fields[4] == null ? '' : fields[4] as String;
257
}
258
lib/src/screens/exchange/exchange_page.dart
+6
-2
@@ -368,8 +368,12 @@ class ExchangePage extends BasePage {
368
369
void applyTemplate(
370
BuildContext context, ExchangeViewModel exchangeViewModel, ExchangeTemplate template) async {
371
- final depositCryptoCurrency = CryptoCurrency.fromString(template.depositCurrency);
372
- final receiveCryptoCurrency = CryptoCurrency.fromString(template.receiveCurrency);
371
+ final depositCryptoCurrency = CryptoCurrency.safeParseCurrencyFromString(template.depositCurrency);
372
+ final receiveCryptoCurrency = CryptoCurrency.safeParseCurrencyFromString(template.receiveCurrency);
373
+
374
+ if (depositCryptoCurrency == null || receiveCryptoCurrency == null) { ///TO DO: add support for user tokens
375
+ return;
376
+ }
377
378
exchangeViewModel.changeDepositCurrency(currency: depositCryptoCurrency);
379
exchangeViewModel.changeReceiveCurrency(currency: receiveCryptoCurrency);
lib/src/screens/exchange_trade/exchange_trade_page.dart
+4
-2
@@ -28,10 +28,12 @@ void showInformation(ExchangeTradeViewModel exchangeTradeViewModel, BuildContext
28
final trade = exchangeTradeViewModel.trade;
29
final walletName = exchangeTradeViewModel.wallet.name;
30
31
+ final from = trade.from?.toString() ?? trade.userCurrencyFrom.toString();
32
+
33
final information = exchangeTradeViewModel.isSendable
32
- ? S.current.exchange_trade_result_confirm(trade.amount, trade.from.toString(), walletName) +
34
+ ? S.current.exchange_trade_result_confirm(trade.amount, from, walletName) +
35
exchangeTradeViewModel.extraInfo
34
- : S.current.exchange_result_description(trade.amount, trade.from.toString()) +
36
+ : S.current.exchange_result_description(trade.amount, from) +
37
exchangeTradeViewModel.extraInfo;
38
39
showPopUp<void>(
lib/view_model/exchange/exchange_trade_view_model.dart
+10
-2
@@ -128,7 +128,15 @@ abstract class ExchangeTradeViewModelBase with Store {
128
@action
129
Future<void> confirmSending() async {
130
if (!isSendable) return;
131
- sendViewModel.selectedCryptoCurrency = trade.from;
131
+
132
+ final selected = trade.from ?? trade.userCurrencyFrom;
133
+ if (selected == null) {
134
+ printV('No selectable currency for trade ${trade.id}');
135
+ return;
136
+ }
137
+
138
+ sendViewModel.selectedCryptoCurrency = selected;
139
+
140
final pendingTransaction = await sendViewModel.createTransaction(provider: _provider);
141
if (_provider is ThorChainExchangeProvider) {
142
trade.id = pendingTransaction?.id ?? '';
@@ -251,7 +259,7 @@ abstract class ExchangeTradeViewModelBase with Store {
259
260
bool _isEthToken() =>
261
wallet.currency == CryptoCurrency.eth &&
254
- tradesStore.trade!.from.tag == CryptoCurrency.eth.title;
262
+ tradeFrom?.tag == CryptoCurrency.eth.title;
263
264
bool _isPolygonToken() =>
265
wallet.currency == CryptoCurrency.maticpoly &&