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>
This commit is contained in:
Serhii
2025-08-21 21:14:15 +03:00
committed by GitHub
co-authored by Omar Hatem
parent 13a74133f5
commit 9292fb5487
15 changed files with 136 additions and 48 deletions
@@ -69,6 +69,8 @@ class ChainflipExchangeProvider extends ExchangeProvider {
{required CryptoCurrency from,
required CryptoCurrency to,
required bool isFixedRateMode}) async {
try {
final assetId = _normalizeCurrency(from);
final assetsResponse = await _getAssets();
@@ -78,7 +80,13 @@ class ChainflipExchangeProvider extends ExchangeProvider {
(asset) => asset['id'] == assetId,
orElse: () => null)?['minimalAmountNative'] ?? '0';
if (minAmount == '0') throw Exception('No rates found for $from to $to');
return Limits(min: _amountFromNative(minAmount.toString(), from));
} catch (e) {
printV(e.toString());
throw Exception('Chainflip failed to fetch limits');
}
}
@override
@@ -242,8 +242,10 @@ class ChangeNowExchangeProvider extends ExchangeProvider {
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
final fromCurrency = responseJSON['fromCurrency'] as String;
final fromNetwork = responseJSON['fromNetwork'] as String?;
final from = CryptoCurrency.safeParseCurrencyFromString(fromCurrency);
final toCurrency = responseJSON['toCurrency'] as String;
final toNetwork = responseJSON['toNetwork'] as String?;
final to = CryptoCurrency.safeParseCurrencyFromString(toCurrency);
final inputAddress = responseJSON['payinAddress'] as String;
final expectedSendAmount = responseJSON['expectedAmountFrom'].toString();
@@ -255,6 +257,9 @@ 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,
@@ -267,8 +272,8 @@ class ChangeNowExchangeProvider extends ExchangeProvider {
expiredAt: expiredAt,
outputTransaction: outputTransaction,
payoutAddress: payoutAddress,
userCurrencyFromRaw: '${fromCurrency.toUpperCase()}' + '_',
userCurrencyToRaw: '${toCurrency.toUpperCase()}' + '_',
userCurrencyFromRaw: '${fromCurrency.toUpperCase()}' + '_' + '${_normalizedFromNetwork.toUpperCase()}',
userCurrencyToRaw: '${toCurrency.toUpperCase()}' + '_' + '${_normalizedToNetwork.toUpperCase()}',
);
}
@@ -307,4 +312,14 @@ class ChangeNowExchangeProvider extends ExchangeProvider {
return tag.toLowerCase();
}
}
String _normalizeNetworkType(String network) {
return switch (network.toUpperCase()) {
'POLY' => 'MATIC',
'AVAXC' => 'CCHAIN',
_ => network,
};
}
}
@@ -241,7 +241,9 @@ class ExolixExchangeProvider extends ExchangeProvider {
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
final coinFrom = responseJSON['coinFrom']['coinCode'] as String;
final coinFromNetwork = responseJSON['coinFrom']['network'] as String?;
final coinTo = responseJSON['coinTo']['coinCode'] as String;
final coinToNetwork = responseJSON['coinTo']['network'] as String?;
final inputAddress = responseJSON['depositAddress'] as String;
final amount = responseJSON['amount'].toString();
final status = responseJSON['status'] as String;
@@ -260,8 +262,8 @@ class ExolixExchangeProvider extends ExchangeProvider {
extraId: extraId,
outputTransaction: outputTransaction,
payoutAddress: payoutAddress,
userCurrencyFromRaw: '${coinFrom.toUpperCase()}' + '_',
userCurrencyToRaw: '${coinTo.toUpperCase()}' + '_',
userCurrencyFromRaw: '${coinFrom.toUpperCase()}' + '_' + '${coinFromNetwork ?? ''}',
userCurrencyToRaw: '${coinTo.toUpperCase()}' + '_' + '${coinToNetwork ?? ''}',
);
}
@@ -237,9 +237,9 @@ class LetsExchangeExchangeProvider extends ExchangeProvider {
}
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
final from = responseJSON['coin_from'] as String;
final fromNetwork = responseJSON['coin_from_network'] as String;
final fromNetwork = responseJSON['coin_from_network'] as String?;
final to = responseJSON['coin_to'] as String;
final toNetwork = responseJSON['coin_to_network'] as String;
final toNetwork = responseJSON['coin_to_network'] as String?;
final payoutAddress = responseJSON['withdrawal'] as String;
final depositAddress = responseJSON['deposit'] as String;
final refundAddress = responseJSON['return'] as String;
@@ -253,6 +253,9 @@ 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),
@@ -268,8 +271,8 @@ class LetsExchangeExchangeProvider extends ExchangeProvider {
expiredAt: expiredAt,
isRefund: status == 'refund',
extraId: extraId,
userCurrencyFromRaw: '$from' + '_' + '$fromNetwork',
userCurrencyToRaw: '$to' + '_' + '$toNetwork',
userCurrencyFromRaw: '$from' + '_' + normalizedFromNetwork,
userCurrencyToRaw: '$to' + '_' + '$normalizedToNetwork',
);
}
@@ -313,6 +316,15 @@ class LetsExchangeExchangeProvider extends ExchangeProvider {
return currency.title;
}
String _normalizeNetworkType(String network) {
return switch (network.toUpperCase()) {
'ERC20' => 'ETH',
'TRC20' => 'TRX',
'BEP20' => 'BSC',
_ => network,
};
}
String _normalizeBchAddress(String address) =>
address.startsWith('bitcoincash:') ? address.substring(12) : address;
}
@@ -254,7 +254,9 @@ class SideShiftExchangeProvider extends ExchangeProvider {
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
final fromCurrency = responseJSON['depositCoin'] as String;
final fromNetwork = responseJSON['depositNetwork'] as String?;
final toCurrency = responseJSON['settleCoin'] as String;
final toNetwork = responseJSON['settleNetwork'] as String?;
final inputAddress = responseJSON['depositAddress'] as String;
final expectedSendAmount = responseJSON['depositAmount'] as String?;
final status = responseJSON['status'] as String?;
@@ -275,8 +277,8 @@ class SideShiftExchangeProvider extends ExchangeProvider {
expiredAt: expiredAt,
payoutAddress: settleAddress,
extraId: depositMemo,
userCurrencyFromRaw: '${fromCurrency.toUpperCase()}' + '_',
userCurrencyToRaw: '${toCurrency.toUpperCase()}' + '_',
userCurrencyFromRaw: '${fromCurrency.toUpperCase()}' + '_' + _normalizeNetworkType(fromNetwork ?? ''),
userCurrencyToRaw: '${toCurrency.toUpperCase()}' + '_' + _normalizeNetworkType(toNetwork ?? ''),
);
}
@@ -335,7 +337,7 @@ class SideShiftExchangeProvider extends ExchangeProvider {
return 'tron';
case 'LN':
return 'lightning';
case 'POLY':
case 'POL':
return 'polygon';
case 'ZEC':
return 'zcash';
@@ -345,4 +347,16 @@ class SideShiftExchangeProvider extends ExchangeProvider {
return tag.toLowerCase();
}
}
String _normalizeNetworkType(String network) {
return switch (network) {
'ethereum' => 'ETH',
'tron' => 'TRX',
'lightning' => 'LN',
'polygon' => 'POL',
'zcash' => 'ZEC',
'avax' => 'AVAXC',
_ => network,
};
}
}
@@ -224,7 +224,9 @@ class StealthExExchangeProvider extends ExchangeProvider {
final respId = responseJSON['id'] as String;
final from = deposit['symbol'] as String;
final fromNetwork = deposit['network'] as String?;
final to = withdrawal['symbol'] as String;
final toNetwork = withdrawal['network'] as String?;
final payoutAddress = withdrawal['address'] as String;
final depositAddress = deposit['address'] as String;
final refundAddress = responseJSON['refund_address'] as String;
@@ -249,8 +251,8 @@ class StealthExExchangeProvider extends ExchangeProvider {
createdAt: createdAt,
isRefund: status == 'refunded',
extraId: extraId,
userCurrencyFromRaw: '${from.toUpperCase()}' + '_',
userCurrencyToRaw: '${to.toUpperCase()}' + '_',
userCurrencyFromRaw: '${from.toUpperCase()}' + '_' + '${fromNetwork?.toUpperCase() ?? ''}',
userCurrencyToRaw: '${to.toUpperCase()}' + '_' + '${toNetwork?.toUpperCase() ?? ''}',
);
}
@@ -81,20 +81,20 @@ class SwapTradeExchangeProvider extends ExchangeProvider {
final coinsInfo = responseJSON['data'] as List<dynamic>;
for (var coin in coinsInfo) {
if (coin['id'].toString().toUpperCase() == _normalizeCurrency(from)) {
return Limits(
min: double.parse(coin['min'].toString()),
max: double.parse(coin['max'].toString()),
);
}
}
final coin = coinsInfo.firstWhere(
(coin) => coin['id'].toString().toUpperCase() == _normalizeCurrency(from),
orElse: () => null,
);
// coin not found:
return Limits(min: 0, max: 0);
if (coin == null) throw Exception('Coin not found: ${_normalizeCurrency(from)}');
return Limits(
min: double.parse(coin['min'].toString()),
max: double.parse(coin['max'].toString()),
);
} catch (e) {
printV(e.toString());
return Limits(min: 0, max: 0);
throw Exception('Error fetching limits: ${e.toString()}');
}
}
@@ -277,7 +277,9 @@ class TrocadorExchangeProvider extends ExchangeProvider {
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 networkTo = responseJSON['network_to'] as String?;
return Trade(
id: id,
@@ -294,8 +296,8 @@ class TrocadorExchangeProvider extends ExchangeProvider {
providerId: providerId,
providerName: providerName,
extraId: addressProviderMemo,
userCurrencyFromRaw: '${from.toUpperCase()}' + '_',
userCurrencyToRaw: '${to.toUpperCase()}' + '_',
userCurrencyFromRaw: '${from.toUpperCase()}' + '_' + _normalizeNetworkType(networkFrom ?? ''),
userCurrencyToRaw: '${to.toUpperCase()}' + '_' + _normalizeNetworkType(networkTo ?? ''), // Handle null network
);
});
}
@@ -362,6 +364,18 @@ class TrocadorExchangeProvider extends ExchangeProvider {
}
}
String _normalizeNetworkType(String network) {
return switch (network.toUpperCase()) {
'ERC20' => 'ETH',
'TRC20' => 'TRX',
'BEP20' => 'BSC',
'LIGHTNING' => 'LN',
_ => network,
};
}
Future<Uri> _getUri(String path, Map<String, String> queryParams) async {
final uri = Uri.http(onionApiAuthority, path, queryParams);
+12 -13
View File
@@ -36,9 +36,8 @@ class Trade extends HiveObject {
}) {
if (provider != null) providerRaw = provider.raw;
if (from != null) fromRaw = from.raw;
if (to != null) toRaw = to.raw;
fromRaw = from?.raw ?? -1;
toRaw = to?.raw ?? -1;
if (state != null) stateRaw = state.raw;
}
@@ -56,15 +55,15 @@ class Trade extends HiveObject {
ExchangeProviderDescription get provider =>
ExchangeProviderDescription.deserialize(raw: providerRaw);
@HiveField(2, defaultValue: 0)
late int fromRaw;
@HiveField(2, defaultValue: -1)
int fromRaw = -1;
CryptoCurrency get from => CryptoCurrency.deserialize(raw: fromRaw);
CryptoCurrency? get from => CryptoCurrency.safeDeserialize(raw: fromRaw);
@HiveField(3, defaultValue: 0)
late int toRaw;
@HiveField(3, defaultValue: -1)
int toRaw = -1;
CryptoCurrency get to => CryptoCurrency.deserialize(raw: toRaw);
CryptoCurrency? get to => CryptoCurrency.safeDeserialize(raw: toRaw);
@HiveField(4, defaultValue: '')
late String stateRaw;
@@ -193,8 +192,8 @@ class Trade extends HiveObject {
return <String, dynamic>{
'id': id,
'provider': provider.serialize(),
'input': from.serialize(),
'output': to.serialize(),
'input': fromRaw,
'output': toRaw,
'date': createdAt != null ? createdAt!.millisecondsSinceEpoch : null,
'amount': amount,
'receive_amount': receiveAmount,
@@ -252,8 +251,8 @@ class TradeAdapter extends TypeAdapter<Trade> {
userCurrencyToRaw: fields[25] as String?,
)
..providerRaw = fields[1] == null ? 0 : fields[1] as int
..fromRaw = fields[2] == null ? 0 : fields[2] as int
..toRaw = fields[3] == null ? 0 : fields[3] as int
..fromRaw = (fields[2] as int?) ?? -1
..toRaw = (fields[3] as int?) ?? -1
..stateRaw = fields[4] == null ? '' : fields[4] as String;
}
+6 -2
View File
@@ -368,8 +368,12 @@ class ExchangePage extends BasePage {
void applyTemplate(
BuildContext context, ExchangeViewModel exchangeViewModel, ExchangeTemplate template) async {
final depositCryptoCurrency = CryptoCurrency.fromString(template.depositCurrency);
final receiveCryptoCurrency = CryptoCurrency.fromString(template.receiveCurrency);
final depositCryptoCurrency = CryptoCurrency.safeParseCurrencyFromString(template.depositCurrency);
final receiveCryptoCurrency = CryptoCurrency.safeParseCurrencyFromString(template.receiveCurrency);
if (depositCryptoCurrency == null || receiveCryptoCurrency == null) { ///TO DO: add support for user tokens
return;
}
exchangeViewModel.changeDepositCurrency(currency: depositCryptoCurrency);
exchangeViewModel.changeReceiveCurrency(currency: receiveCryptoCurrency);
@@ -28,10 +28,12 @@ void showInformation(ExchangeTradeViewModel exchangeTradeViewModel, BuildContext
final trade = exchangeTradeViewModel.trade;
final walletName = exchangeTradeViewModel.wallet.name;
final from = trade.from?.toString() ?? trade.userCurrencyFrom.toString();
final information = exchangeTradeViewModel.isSendable
? S.current.exchange_trade_result_confirm(trade.amount, trade.from.toString(), walletName) +
? S.current.exchange_trade_result_confirm(trade.amount, from, walletName) +
exchangeTradeViewModel.extraInfo
: S.current.exchange_result_description(trade.amount, trade.from.toString()) +
: S.current.exchange_result_description(trade.amount, from) +
exchangeTradeViewModel.extraInfo;
showPopUp<void>(
@@ -128,7 +128,15 @@ abstract class ExchangeTradeViewModelBase with Store {
@action
Future<void> confirmSending() async {
if (!isSendable) return;
sendViewModel.selectedCryptoCurrency = trade.from;
final selected = trade.from ?? trade.userCurrencyFrom;
if (selected == null) {
printV('No selectable currency for trade ${trade.id}');
return;
}
sendViewModel.selectedCryptoCurrency = selected;
final pendingTransaction = await sendViewModel.createTransaction(provider: _provider);
if (_provider is ThorChainExchangeProvider) {
trade.id = pendingTransaction?.id ?? '';
@@ -251,7 +259,7 @@ abstract class ExchangeTradeViewModelBase with Store {
bool _isEthToken() =>
wallet.currency == CryptoCurrency.eth &&
tradesStore.trade!.from.tag == CryptoCurrency.eth.title;
tradeFrom?.tag == CryptoCurrency.eth.title;
bool _isPolygonToken() =>
wallet.currency == CryptoCurrency.maticpoly &&