Cw 1294 fix xo swaps bug (#2645)

* Improve currency parsing with tag support

* changenow currency parsing fix

* exolix currency parsing fix

* letsExchange currency parsing fix

* stealth currency parsing fix

* trocador currency parsing fix

---------

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>
This commit is contained in:
Serhii
2025-11-21 22:45:19 +01:00
committed by GitHub
co-authored by Omar Hatem
parent 7172928e1e
commit c552ab86d2
10 changed files with 217 additions and 53 deletions
+16 -1
View File
@@ -1,5 +1,6 @@
import 'package:cw_core/currency.dart';
import 'package:cw_core/enumerable_item.dart';
import 'package:collection/collection.dart';
class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implements Currency {
const CryptoCurrency({
@@ -336,9 +337,23 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
return CryptoCurrency._fullNameCurrencyMap[name.split("(").first.trim().toLowerCase()]!;
}
static CryptoCurrency? safeParseCurrencyFromString(String? raw, {CryptoCurrency? walletCurrency}) {
static CryptoCurrency? safeParseCurrencyFromString(
String? raw, {
String? tag,
CryptoCurrency? walletCurrency,
}) {
if (raw == null || raw.isEmpty) return null;
if (tag != null && tag.isNotEmpty) {
final match = CryptoCurrency.all.firstWhereOrNull(
(e) =>
e.title.toUpperCase() == raw.toUpperCase() &&
e.tag?.toUpperCase() == tag.toUpperCase(),
);
if (match != null) return match;
return null;
}
try {
return CryptoCurrency.fromString(raw, walletCurrency: walletCurrency);
} catch (_) {}
+6 -2
View File
@@ -476,8 +476,12 @@ abstract class EVMChainWalletBase
'0x${opReturnMemo.codeUnits.map((char) => char.toRadixString(16).padLeft(2, '0')).join()}';
}
final CryptoCurrency transactionCurrency =
balance.keys.firstWhere((element) => element.title == _credentials.currency.title);
final transactionCurrency = balance.keys.firstWhere(
(currency) =>
currency.title == _credentials.currency.title &&
currency.tag == _credentials.currency.tag,
orElse: () => throw Exception(
'Currency ${_credentials.currency.title} ${_credentials.currency.tag} is not accessible in the wallet, try to enable it first.'));
final currencyBalance = balance[transactionCurrency]!;
BigInt totalAmount = BigInt.zero;
+6 -2
View File
@@ -226,8 +226,12 @@ abstract class SolanaWalletBase
await _updateBalance();
final CryptoCurrency transactionCurrency =
balance.keys.firstWhere((element) => element.title == solCredentials.currency.title);
final transactionCurrency = balance.keys.firstWhere(
(currency) =>
currency.title == credentials.currency.title &&
currency.tag == credentials.currency.tag,
orElse: () => throw Exception(
'Currency ${credentials.currency.title} ${credentials.currency.tag} is not accessible in the wallet, try to enable it first.'));
final walletBalanceForCurrency = balance[transactionCurrency]!.balance;
+7 -2
View File
@@ -312,8 +312,13 @@ abstract class TronWalletBase
final hasMultiDestination = outputs.length > 1;
final CryptoCurrency transactionCurrency =
balance.keys.firstWhere((element) => element.title == tronCredentials.currency.title);
final transactionCurrency = balance.keys.firstWhere(
(currency) =>
currency.title == tronCredentials.currency.title &&
currency.tag == tronCredentials.currency.tag,
orElse: () => throw Exception(
'Currency ${tronCredentials.currency.title} ${tronCredentials.currency.tag} is not accessible in the wallet, try to enable it first.'));
final walletBalanceForCurrency = balance[transactionCurrency]!.balance;
@@ -278,12 +278,21 @@ class ChangeNowExchangeProvider extends ExchangeProvider {
throw Exception('Unexpected http status: ${response.statusCode}');
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
// Parsing 'from' currency
final fromCurrency = responseJSON['fromCurrency'] as String;
final fromNetwork = responseJSON['fromNetwork'] as String?;
final from = CryptoCurrency.safeParseCurrencyFromString(fromCurrency);
final _normalizedFromNetwork = _normalizeNetworkType(fromNetwork ?? '');
final fromTag = fromCurrency == _normalizedFromNetwork ? null : _normalizedFromNetwork;
final from = CryptoCurrency.safeParseCurrencyFromString(fromCurrency, tag: fromTag);
// Parsing 'to' currency
final toCurrency = responseJSON['toCurrency'] as String;
final toNetwork = responseJSON['toNetwork'] as String?;
final to = CryptoCurrency.safeParseCurrencyFromString(toCurrency);
final _normalizedToNetwork = _normalizeNetworkType(toNetwork ?? '');
final toTag = toCurrency == _normalizedToNetwork ? null : _normalizedToNetwork;
final to = CryptoCurrency.safeParseCurrencyFromString(toCurrency, tag: toTag);
final inputAddress = responseJSON['payinAddress'] as String;
final expectedSendAmount = responseJSON['expectedAmountFrom'].toString();
final status = responseJSON['status'] as String;
@@ -294,9 +303,6 @@ class ChangeNowExchangeProvider extends ExchangeProvider {
final payoutAddress = responseJSON['payoutAddress'] as String;
final expiredAt = DateTime.tryParse(expiredAtRaw ?? '')?.toLocal();
final _normalizedFromNetwork = _normalizeNetworkType(fromNetwork ?? '');
final _normalizedToNetwork = _normalizeNetworkType(toNetwork ?? '');
return Trade(
id: id,
from: from,
@@ -309,8 +315,8 @@ class ChangeNowExchangeProvider extends ExchangeProvider {
expiredAt: expiredAt,
outputTransaction: outputTransaction,
payoutAddress: payoutAddress,
userCurrencyFromRaw: '${fromCurrency.toUpperCase()}' + '_' + '${_normalizedFromNetwork.toUpperCase()}',
userCurrencyToRaw: '${toCurrency.toUpperCase()}' + '_' + '${_normalizedToNetwork.toUpperCase()}',
userCurrencyFromRaw: '${fromCurrency.toUpperCase()}' + '_' + '${fromTag?.toUpperCase() ?? ''}',
userCurrencyToRaw: '${toCurrency.toUpperCase()}' + '_' + '${toTag?.toUpperCase() ?? ''}',
);
}
@@ -360,10 +360,19 @@ class ExolixExchangeProvider extends ExchangeProvider {
throw Exception('Unexpected http status: ${response.statusCode}');
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
// Parsing 'from' currency
final coinFrom = responseJSON['coinFrom']['coinCode'] as String;
final coinFromNetwork = responseJSON['coinFrom']['network'] as String?;
final fromTag = coinFrom == coinFromNetwork ? null : coinFromNetwork;
final from = CryptoCurrency.safeParseCurrencyFromString(coinFrom, tag: fromTag);
// Parsing 'to' currency
final coinTo = responseJSON['coinTo']['coinCode'] as String;
final coinToNetwork = responseJSON['coinTo']['network'] as String?;
final toTag = coinTo == coinToNetwork ? null : coinToNetwork;
final to = CryptoCurrency.safeParseCurrencyFromString(coinTo, tag: toTag);
final inputAddress = responseJSON['depositAddress'] as String;
final amount = responseJSON['amount'].toString();
final status = responseJSON['status'] as String;
@@ -373,8 +382,8 @@ class ExolixExchangeProvider extends ExchangeProvider {
return Trade(
id: id,
from: CryptoCurrency.safeParseCurrencyFromString(coinFrom),
to: CryptoCurrency.safeParseCurrencyFromString(coinTo),
from: from,
to: to,
provider: description,
inputAddress: inputAddress,
amount: amount,
@@ -382,8 +391,8 @@ class ExolixExchangeProvider extends ExchangeProvider {
extraId: extraId,
outputTransaction: outputTransaction,
payoutAddress: payoutAddress,
userCurrencyFromRaw: '${coinFrom.toUpperCase()}' + '_' + '${coinFromNetwork ?? ''}',
userCurrencyToRaw: '${coinTo.toUpperCase()}' + '_' + '${coinToNetwork ?? ''}',
userCurrencyFromRaw: '${coinFrom.toUpperCase()}' + '_' + '${fromTag?.toUpperCase() ?? ''}',
userCurrencyToRaw: '${coinTo.toUpperCase()}' + '_' + '${toTag?.toUpperCase() ?? ''}',
);
}
@@ -348,10 +348,22 @@ class LetsExchangeExchangeProvider extends ExchangeProvider {
throw Exception('LetsExchange fetch trade failed: ${response.body}');
}
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
final from = responseJSON['coin_from'] as String;
// Parsing 'from' currency
final fromCurrency = responseJSON['coin_from'] as String;
final fromNetwork = responseJSON['coin_from_network'] as String?;
final to = responseJSON['coin_to'] as String;
final normalizedFromNetwork = _normalizeNetworkType(fromNetwork ?? '');
final fromTag = fromCurrency == normalizedFromNetwork ? null : normalizedFromNetwork;
final from = CryptoCurrency.safeParseCurrencyFromString(fromCurrency, tag: fromTag);
// Parsing 'to' currency
final toCurrency = responseJSON['coin_to'] as String;
final toNetwork = responseJSON['coin_to_network'] as String?;
final normalizedToNetwork = _normalizeNetworkType(toNetwork ?? '');
final toTag = toCurrency == normalizedToNetwork ? null : normalizedToNetwork;
final to = CryptoCurrency.safeParseCurrencyFromString(toCurrency, tag: toTag);
final payoutAddress = responseJSON['withdrawal'] as String;
final depositAddress = responseJSON['deposit'] as String;
final refundAddress = responseJSON['return'] as String;
@@ -365,13 +377,10 @@ class LetsExchangeExchangeProvider extends ExchangeProvider {
final createdAt = DateTime.parse(createdAtString).toLocal();
final expiredAt = DateTime.fromMillisecondsSinceEpoch(expiredAtTimestamp * 1000).toLocal();
final normalizedFromNetwork = _normalizeNetworkType(fromNetwork ?? '');
final normalizedToNetwork = _normalizeNetworkType(toNetwork ?? '');
return Trade(
id: id,
from: CryptoCurrency.safeParseCurrencyFromString(from),
to: CryptoCurrency.safeParseCurrencyFromString(to),
from: from,
to: to,
provider: description,
inputAddress: depositAddress,
payoutAddress: payoutAddress,
@@ -383,8 +392,8 @@ class LetsExchangeExchangeProvider extends ExchangeProvider {
expiredAt: expiredAt,
isRefund: status == 'refund',
extraId: extraId,
userCurrencyFromRaw: '$from' + '_' + normalizedFromNetwork,
userCurrencyToRaw: '$to' + '_' + '$normalizedToNetwork',
userCurrencyFromRaw: '${fromCurrency.toUpperCase()}' + '_' + '${fromTag?.toUpperCase() ?? ''}',
userCurrencyToRaw: '${toCurrency.toUpperCase()}' + '_' + '${toTag?.toUpperCase() ?? ''}',
);
}
@@ -74,8 +74,8 @@ class StealthExExchangeProvider extends ExchangeProvider {
throw Exception('StealthEx fetch limits failed: ${response.body}');
}
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
final min = toDouble(responseJSON['min_amount']);
final max = responseJSON['max_amount'] as double?;
final min = _toDouble(responseJSON['min_amount']);
final max = _toDouble(responseJSON['max_amount']);
return Limits(min: min, max: max);
} catch (e) {
log(e.toString());
@@ -230,8 +230,8 @@ class StealthExExchangeProvider extends ExchangeProvider {
final payoutAddress = withdrawal['address'] as String;
final depositAddress = deposit['address'] as String;
final refundAddress = responseJSON['refund_address'] as String;
final depositAmount = toDouble(deposit['amount']);
final receiveAmount = toDouble(withdrawal['amount']);
final depositAmount = _toDouble(deposit['amount']);
final receiveAmount = _toDouble(withdrawal['amount']);
final status = responseJSON['status'] as String;
final createdAtString = responseJSON['created_at'] as String;
final extraId = deposit['extra_id'] as String?;
@@ -342,15 +342,24 @@ class StealthExExchangeProvider extends ExchangeProvider {
final withdrawal = responseJSON['withdrawal'] as Map<String, dynamic>;
final respId = responseJSON['id'] as String;
final from = deposit['symbol'] as String;
// Parsing 'from' currency with network tag
final fromCurrency = deposit['symbol'] as String;
final fromNetwork = deposit['network'] as String?;
final to = withdrawal['symbol'] as String;
final fromTag = fromNetwork == 'mainnet' ? null : fromNetwork;
final from = CryptoCurrency.safeParseCurrencyFromString(fromCurrency, tag: fromTag);
// Parsing 'to' currency with network tag
final toCurrency = withdrawal['symbol'] as String;
final toNetwork = withdrawal['network'] as String?;
final toTag = toNetwork == 'mainnet' ? null : toNetwork;
final to = CryptoCurrency.safeParseCurrencyFromString(toCurrency, tag: toTag);
final payoutAddress = withdrawal['address'] as String;
final depositAddress = deposit['address'] as String;
final refundAddress = responseJSON['refund_address'] as String;
final depositAmount = toDouble(deposit['amount']);
final receiveAmount = toDouble(withdrawal['amount']);
final depositAmount = _toDouble(deposit['amount']);
final receiveAmount = _toDouble(withdrawal['amount']);
final status = responseJSON['status'] as String;
final createdAtString = responseJSON['created_at'] as String;
final createdAt = DateTime.parse(createdAtString).toLocal();
@@ -358,8 +367,8 @@ class StealthExExchangeProvider extends ExchangeProvider {
return Trade(
id: respId,
from: CryptoCurrency.safeParseCurrencyFromString(from),
to: CryptoCurrency.safeParseCurrencyFromString(to),
from: from,
to: to,
provider: description,
inputAddress: depositAddress,
payoutAddress: payoutAddress,
@@ -370,8 +379,8 @@ class StealthExExchangeProvider extends ExchangeProvider {
createdAt: createdAt,
isRefund: status == 'refunded',
extraId: extraId,
userCurrencyFromRaw: '${from.toUpperCase()}' + '_' + '${fromNetwork?.toUpperCase() ?? ''}',
userCurrencyToRaw: '${to.toUpperCase()}' + '_' + '${toNetwork?.toUpperCase() ?? ''}',
userCurrencyFromRaw: '${fromCurrency.toUpperCase()}' + '_' + '${fromTag?.toUpperCase() ?? ''}',
userCurrencyToRaw: '${toCurrency.toUpperCase()}' + '_' + '${toTag?.toUpperCase() ?? ''}',
);
}
@@ -414,16 +423,18 @@ class StealthExExchangeProvider extends ExchangeProvider {
}
}
double toDouble(dynamic value) {
static double? _toDouble(dynamic value) {
if (value is int) {
return value.toDouble();
} else if (value is double) {
return value;
} else {
return 0.0;
} else if (value is String) {
return double.tryParse(value);
}
return null;
}
String _getName(CryptoCurrency currency) {
if (currency == CryptoCurrency.usdcEPoly) return 'usdce';
return currency.title.toLowerCase();
@@ -420,15 +420,22 @@ class TrocadorExchangeProvider extends ExchangeProvider {
final providerName = responseJSON['provider'] as String;
final addressProviderMemo = responseJSON['address_provider_memo'] as String?;
final from = responseJSON['ticker_from'] as String;
final networkFrom = responseJSON['network_from'] as String?;
final to = responseJSON['ticker_to'] as String;
final fromCurrency = responseJSON['ticker_from'] as String;
final fromNetwork = responseJSON['network_from'] as String?;
final _normalizedFromNetwork = _normalizeNetworkType(fromNetwork ?? '');
final fromTag = _normalizedFromNetwork.isEmpty || _normalizedFromNetwork == 'Mainnet' ? null : _normalizedFromNetwork;
final from = CryptoCurrency.safeParseCurrencyFromString(fromCurrency, tag: fromTag);
final toCurrency = responseJSON['ticker_to'] as String;
final networkTo = responseJSON['network_to'] as String?;
final _normalizedToNetwork = _normalizeNetworkType(networkTo ?? '');
final toTag = _normalizedToNetwork.isEmpty || _normalizedToNetwork == 'Mainnet' ? null : _normalizedToNetwork;
final to = CryptoCurrency.safeParseCurrencyFromString(toCurrency, tag: toTag);
return Trade(
id: id,
from: CryptoCurrency.safeParseCurrencyFromString(from),
to: CryptoCurrency.safeParseCurrencyFromString(to),
from: from,
to: to,
provider: description,
inputAddress: inputAddress,
refundAddress: refundAddress,
@@ -440,8 +447,8 @@ class TrocadorExchangeProvider extends ExchangeProvider {
providerId: providerId,
providerName: providerName,
extraId: addressProviderMemo,
userCurrencyFromRaw: '${from.toUpperCase()}' + '_' + _normalizeNetworkType(networkFrom ?? ''),
userCurrencyToRaw: '${to.toUpperCase()}' + '_' + _normalizeNetworkType(networkTo ?? ''), // Handle null network
userCurrencyFromRaw: '${fromCurrency.toUpperCase()}' + '_' + '${fromTag?.toUpperCase() ?? ''}',
userCurrencyToRaw: '${toCurrency.toUpperCase()}' + '_' + '${toTag?.toUpperCase() ?? ''}',
);
});
}
@@ -43,8 +43,41 @@ class XOSwapExchangeProvider extends ExchangeProvider {
'LTC': 'litecoin',
'EOS': 'eosio',
'XLM': 'stellar',
'BASE': 'basemainnet',
};
static const supportedTags = [
'POL',
'ETH',
'BTC',
'BSC',
'SOL',
'TRX',
'ZEC',
'ADA',
'DOGE',
'XMR',
'BCH',
'BSV',
'XRP',
'LTC',
'EOS',
'XLM',
'BASE',
];
String _normalizeXOSwapsNetwork(String string) {
final lower = string.toLowerCase();
if (lower.endsWith('matic0a883d9b')) return string.replaceFirst(RegExp(r'matic0a883d9b$', caseSensitive: false), 'POL');
if (lower.endsWith('matic86e249c1')) return string.replaceFirst(RegExp(r'matic86e249c1$', caseSensitive: false), 'POL');
if (lower.endsWith('bscddedf0f8')) return string.replaceFirst(RegExp(r'bscddedf0f8$', caseSensitive: false), 'BSC');
if (lower.endsWith('basemainnetb5a52617')) return string.replaceFirst(RegExp(r'basemainnetb5a52617$', caseSensitive: false), 'BASE');
return string;
}
@override
String get title => 'XOSwap';
@@ -422,9 +455,46 @@ class XOSwapExchangeProvider extends ExchangeProvider {
final pairId = responseJSON['pairId'] as String;
final pairParts = pairId.split('_');
final fromAsset = pairParts.isNotEmpty ? pairParts[0] : '';
final normalizedFromAsset = _normalizeXOSwapsNetwork(fromAsset);
String? fromAssetTag = _extractTagFromAsset(normalizedFromAsset);
String fromAssetBase = fromAssetTag != null
? normalizedFromAsset.substring(0, normalizedFromAsset.length - fromAssetTag.length)
: normalizedFromAsset;
// Special case for USDT defaulting to ETH tag
if (fromAssetBase == 'USDT' && fromAssetTag == null) {
fromAssetTag = 'ETH';
}
// Special case for BASE defaulting to BASE tag
if (fromAssetBase == 'BASE' && fromAssetTag == null) {
fromAssetTag = 'BASE';
fromAssetBase = 'ETH';
}
final toAsset = pairParts.length > 1 ? pairParts[1] : '';
final fromCurrency = CryptoCurrency.safeParseCurrencyFromString(fromAsset);
final toCurrency = CryptoCurrency.safeParseCurrencyFromString(toAsset);
final normalizedToAsset = _normalizeXOSwapsNetwork(toAsset);
String? toAssetTag = _extractTagFromAsset(normalizedToAsset);
String toAssetBase = toAssetTag != null
? normalizedToAsset.substring(0, normalizedToAsset.length - toAssetTag.length)
: normalizedToAsset;
// Special case for USDT defaulting to ETH tag
if (toAssetBase == 'USDT' && toAssetTag == null) {
toAssetTag = 'ETH';
}
// Special case for BASE defaulting to BASE tag
if (toAssetBase == 'BASE' && toAssetTag == null) {
toAssetTag = 'ETH';
toAssetBase = 'BASE';
}
final fromCurrency = CryptoCurrency.safeParseCurrencyFromString(fromAssetBase,tag: fromAssetTag);
final toCurrency = CryptoCurrency.safeParseCurrencyFromString(toAssetBase,tag: toAssetTag);
final amount = responseJSON['amount'] as Map<String, dynamic>;
final toAmount = responseJSON['toAmount'] as Map<String, dynamic>;
@@ -439,6 +509,14 @@ class XOSwapExchangeProvider extends ExchangeProvider {
final createdAt = DateTime.parse(createdAtString).toLocal();
final extraId = responseJSON['payInAddressTag'] as String?;
final userCurrencyFromRaw = fromCurrency != null
? '${fromCurrency.title}' + '_' + '${fromCurrency.tag ?? ''}'
: '${fromAssetBase}' + '_' + '${fromAssetTag ?? ''}';
final userCurrencyToRaw = toCurrency != null
? '${toCurrency.title}' + '_' + '${toCurrency.tag ?? ''}'
: '${toAssetBase}' + '_' + '${toAssetTag ?? ''}';
return Trade(
id: orderId,
from: fromCurrency,
@@ -452,8 +530,8 @@ class XOSwapExchangeProvider extends ExchangeProvider {
receiveAmount: receiveAmount,
payoutAddress: payoutAddress,
extraId: extraId,
userCurrencyFromRaw: '$fromAsset' + '_',
userCurrencyToRaw: '$toAsset' + '_',
userCurrencyFromRaw: userCurrencyFromRaw,
userCurrencyToRaw: userCurrencyToRaw,
);
} catch (e) {
printV(e.toString());
@@ -461,6 +539,22 @@ class XOSwapExchangeProvider extends ExchangeProvider {
}
}
// ensure something remains before tag (at least 2 chars)
String? _extractTagFromAsset(String asset) {
for (final tag in supportedTags) {
if (asset.endsWith(tag)) {
final prefixLength = asset.length - tag.length;
if (prefixLength >= 2) {
return tag;
}
}
}
return null;
}
double _toDouble(dynamic value) {
if (value is int) {
return value.toDouble();