mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-17 05:41:29 +02:00
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:
@@ -287,6 +287,12 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
|
||||
return CryptoCurrency._rawCurrencyMap[raw]!;
|
||||
}
|
||||
|
||||
static CryptoCurrency? safeDeserialize({int? raw}) {
|
||||
if (raw == null || raw < 0) return null;
|
||||
return _rawCurrencyMap[raw];
|
||||
}
|
||||
|
||||
|
||||
// TODO: refactor this
|
||||
static CryptoCurrency fromString(String name, {CryptoCurrency? walletCurrency}) {
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@ class PolygonWallet extends EVMChainWallet {
|
||||
contractAddress: token.contractAddress,
|
||||
decimal: token.decimal,
|
||||
enabled: token.enabled,
|
||||
tag: token.tag ?? "MATIC",
|
||||
tag: token.tag ?? 'POL',
|
||||
iconPath: iconPath,
|
||||
isPotentialScam: token.isPotentialScam,
|
||||
);
|
||||
|
||||
@@ -385,18 +385,20 @@ class TransactionsPageRobot {
|
||||
|
||||
Future<void> _verifyTradeListItemDisplay(TradeListItem item) async {
|
||||
final keyId = 'trade_list_item_${item.trade.id}_key';
|
||||
final from = item.trade.from?.toString() ?? item.trade.userCurrencyFrom.toString();
|
||||
final to = item.trade.to?.toString() ?? item.trade.userCurrencyTo.toString();
|
||||
|
||||
//* ==============Confirm it has the right key for this item ========
|
||||
commonTestCases.hasValueKey(keyId);
|
||||
|
||||
//* ==============Confirm it displays the correct provider =========================
|
||||
final conversionFlow = '${item.trade.from.toString()} → ${item.trade.to.toString()}';
|
||||
final conversionFlow = '$from → $to';
|
||||
|
||||
commonTestCases.hasText(conversionFlow);
|
||||
|
||||
//* ===========Confirm it displays the properly formatted amount with its crypto tag ========
|
||||
|
||||
final amountCryptoText = item.tradeFormattedAmount + ' ' + item.trade.from.toString();
|
||||
final amountCryptoText = item.tradeFormattedAmount + ' ' + from;
|
||||
|
||||
commonTestCases.hasText(amountCryptoText);
|
||||
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
Reference in New Issue
Block a user