fix: Minor swap fixes (#3046)
What changed, and why it matters
This commit is a large bundle of mostly routine UI and feature work for a new design in Cake Wallet, but the actual code changes in the diff are small, defensive fixes in the exchange/swap module. They replace unsafe number parsing (double.parse) with safe parsing (double.tryParse), add null checks for API responses, make provider logging tolerate unknown providers, and add a Lightning invoice fallback for Bitcoin Lightning swaps. These changes reduce the chance of the app crashing or misbehaving when an exchange API returns unexpected data, but they are not a fix for a known active attack.
Review the new Lightning invoice fallback path to ensure amount=BigInt.zero invoices are safe and cannot be confused with on-chain deposit addresses. Confirm that all exchange providers handle the new nullable Limits contract downstream (e.g., callers that previously assumed non-null). Otherwise, treat as routine defensive hardening and include in normal QA/release notes.
Security signals we found
Defensive parsing: double.parse replaced with double.tryParse in fiat calculation and multiple exchange providers
Null-safety improvements in API response handling (Limits?, safe string extraction, empty/null provider lists)
Logging hardening: unknown exchange providers no longer silently mapped to ChangeNow
Swap flow now generates a Lightning invoice for BTC LN deposit/receive addresses
No evidence of memory corruption, injection, or authentication bypass in the diff
Evidence from the diff
The diff touches 21 files in lib/core, lib/entities, lib/exchange, lib/src/screens/dev, lib/utils, and lib/view_model. Key changes: (1) calculateFiatAmount now uses double.tryParse and returns ‘0.00’ on invalid input instead of throwing; (2) ExchangeProvider.fetchLimits signature changed from Future
Changed components
lib/entities/calculate_fiat_amount.dartlib/exchange/provider/*_exchange_provider.dartlib/exchange/provider/exchange_provider.dartlib/utils/exchange_provider_logger.dartlib/view_model/exchange/exchange_view_model.dartlib/view_model/dev/exchange_provider_logs_view_model.dartlib/src/screens/dev/exchange_provider_logs_page.dartlib/core/trade_monitor.dartInspect captured patch +230 / −114
diff --git a/lib/core/trade_monitor.dart b/lib/core/trade_monitor.dart
index f97b47c..07cd3ee 100644
--- a/lib/core/trade_monitor.dart
+++ b/lib/core/trade_monitor.dart
@@ -127,7 +127,6 @@ class TradeMonitor {
bool _shouldSkipTrade(Trade trade, String walletId, ExchangeProvider? provider) {
if (trade.walletId != walletId) {
- printV('Skipping trade ${trade.id} because it\'s not for this wallet');
return true;
}
diff --git a/lib/entities/calculate_fiat_amount.dart b/lib/entities/calculate_fiat_amount.dart
index 574600b..e4e4b29 100644
--- a/lib/entities/calculate_fiat_amount.dart
+++ b/lib/entities/calculate_fiat_amount.dart
@@ -5,7 +5,8 @@ String calculateFiatAmount({double? price, String? cryptoAmount, bool raw = fals
cryptoAmount = cryptoAmount.replaceAll(',', '.');
- final _amount = double.parse(cryptoAmount);
+ final _amount = double.tryParse(cryptoAmount);
+ if (_amount == null || _amount.isNaN) return '0.00';
final _result = price * _amount;
final result = _result < 0 ? _result * -1 : _result;
diff --git a/lib/exchange/provider/chainflip_exchange_provider.dart b/lib/exchange/provider/chainflip_exchange_provider.dart
index 787070b..7110922 100644
--- a/lib/exchange/provider/chainflip_exchange_provider.dart
+++ b/lib/exchange/provider/chainflip_exchange_provider.dart
@@ -63,7 +63,7 @@ class ChainflipExchangeProvider extends ExchangeProvider {
Future<bool> checkIsAvailable() async => true;
@override
- Future<Limits> fetchLimits(
+ Future<Limits?> fetchLimits(
{required CryptoCurrency from,
required CryptoCurrency to,
required bool isFixedRateMode}) async {
diff --git a/lib/exchange/provider/changenow_exchange_provider.dart b/lib/exchange/provider/changenow_exchange_provider.dart
index 24f1ec8..69b906d 100644
--- a/lib/exchange/provider/changenow_exchange_provider.dart
+++ b/lib/exchange/provider/changenow_exchange_provider.dart
@@ -23,7 +23,6 @@ class ChangeNowExchangeProvider extends ExchangeProvider {
: _settingsStore = settingsStore,
_lastUsedRateId = '';
-
static final apiKey =
isMoneroOnly ? secrets.changeNowMoneroApiKey : secrets.changeNowCakeWalletApiKey;
static const apiAuthority = 'api.changenow.io';
@@ -55,7 +54,7 @@ class ChangeNowExchangeProvider extends ExchangeProvider {
Future<bool> checkIsAvailable() async => true;
@override
- Future<Limits> fetchLimits(
+ Future<Limits?> fetchLimits(
{required CryptoCurrency from,
required CryptoCurrency to,
required bool isFixedRateMode}) async {
@@ -81,18 +80,19 @@ class ChangeNowExchangeProvider extends ExchangeProvider {
throw Exception('Unexpected http status: ${response.statusCode}');
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
- return Limits(
- min: responseJSON['minAmount'] as double?, max: responseJSON['maxAmount'] as double?);
+ final min = double.tryParse(responseJSON['minAmount']?.toString() ?? '');
+ final max = double.tryParse(responseJSON['maxAmount']?.toString() ?? '');
+ if (max == 0) return null;
+ return Limits(min: min, max: max);
}
@override
- Future<double> fetchRate({
- required CryptoCurrency from,
- required CryptoCurrency to,
- required double amount,
- required bool isFixedRateMode,
- required bool isReceiveAmount
- }) async {
+ Future<double> fetchRate(
+ {required CryptoCurrency from,
+ required CryptoCurrency to,
+ required double amount,
+ required bool isFixedRateMode,
+ required bool isReceiveAmount}) async {
try {
if (amount == 0) return 0.0;
@@ -117,8 +117,9 @@ class ChangeNowExchangeProvider extends ExchangeProvider {
final response = await ProxyWrapper().get(clearnetUri: uri, headers: headers);
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
- final fromAmount = double.parse(responseJSON['fromAmount'].toString());
- final toAmount = double.parse(responseJSON['toAmount'].toString());
+ final fromAmount = double.tryParse(responseJSON['fromAmount']?.toString() ?? '') ?? 0.0;
+ final toAmount = double.tryParse(responseJSON['toAmount']?.toString() ?? '') ?? 0.0;
+ if (fromAmount <= 0 || toAmount <= 0) return 0.0;
final rateId = responseJSON['rateId'] as String? ?? '';
if (rateId.isNotEmpty) _lastUsedRateId = rateId;
@@ -278,14 +279,18 @@ class ChangeNowExchangeProvider extends ExchangeProvider {
final fromCurrency = responseJSON['fromCurrency'] as String;
final fromNetwork = responseJSON['fromNetwork'] as String?;
final _normalizedFromNetwork = _normalizeNetworkType(fromNetwork ?? '');
- final fromTag = fromCurrency.toUpperCase() == _normalizedFromNetwork.toUpperCase() ? null : _normalizedFromNetwork;
+ final fromTag = fromCurrency.toUpperCase() == _normalizedFromNetwork.toUpperCase()
+ ? 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 _normalizedToNetwork = _normalizeNetworkType(toNetwork ?? '');
- final toTag = toCurrency.toUpperCase() == _normalizedToNetwork.toUpperCase() ? null : _normalizedToNetwork;
+ final toTag = toCurrency.toUpperCase() == _normalizedToNetwork.toUpperCase()
+ ? null
+ : _normalizedToNetwork;
final to = CryptoCurrency.safeParseCurrencyFromString(toCurrency, tag: toTag);
final inputAddress = responseJSON['payinAddress'] as String;
@@ -310,7 +315,8 @@ class ChangeNowExchangeProvider extends ExchangeProvider {
expiredAt: expiredAt,
outputTransaction: outputTransaction,
payoutAddress: payoutAddress,
- userCurrencyFromRaw: '${fromCurrency.toUpperCase()}' + '_' + '${fromTag?.toUpperCase() ?? ''}',
+ userCurrencyFromRaw:
+ '${fromCurrency.toUpperCase()}' + '_' + '${fromTag?.toUpperCase() ?? ''}',
userCurrencyToRaw: '${toCurrency.toUpperCase()}' + '_' + '${toTag?.toUpperCase() ?? ''}',
);
}
@@ -363,6 +369,4 @@ class ChangeNowExchangeProvider extends ExchangeProvider {
_ => network,
};
}
-
-
}
diff --git a/lib/exchange/provider/exchange_provider.dart b/lib/exchange/provider/exchange_provider.dart
index 2f2bfaf..513af4c 100644
--- a/lib/exchange/provider/exchange_provider.dart
+++ b/lib/exchange/provider/exchange_provider.dart
@@ -23,7 +23,7 @@ abstract class ExchangeProvider {
@override
String toString() => title;
- Future<Limits> fetchLimits(
+ Future<Limits?> fetchLimits(
{required CryptoCurrency from, required CryptoCurrency to, required bool isFixedRateMode});
Future<Trade> createTrade(
diff --git a/lib/exchange/provider/exolix_exchange_provider.dart b/lib/exchange/provider/exolix_exchange_provider.dart
index 7b8234b..caa1c33 100644
--- a/lib/exchange/provider/exolix_exchange_provider.dart
+++ b/lib/exchange/provider/exolix_exchange_provider.dart
@@ -42,7 +42,7 @@ class ExolixExchangeProvider extends ExchangeProvider {
Future<bool> checkIsAvailable() async => true;
@override
- Future<Limits> fetchLimits({
+ Future<Limits?> fetchLimits({
required CryptoCurrency from,
required CryptoCurrency to,
required bool isFixedRateMode,
@@ -143,7 +143,7 @@ class ExolixExchangeProvider extends ExchangeProvider {
throw Exception(message);
}
- final rate = responseJSON['rate'] as double;
+ final rate = double.tryParse(responseJSON['rate']?.toString() ?? '') ?? 0.0;
ExchangeProviderLogger.logSuccess(
provider: description,
diff --git a/lib/exchange/provider/jupiter_exchange_provider.dart b/lib/exchange/provider/jupiter_exchange_provider.dart
index 379bd42..ff9b529 100644
--- a/lib/exchange/provider/jupiter_exchange_provider.dart
+++ b/lib/exchange/provider/jupiter_exchange_provider.dart
@@ -72,7 +72,7 @@ class JupiterExchangeProvider extends ExchangeProvider {
}
@override
- Future<Limits> fetchLimits({
+ Future<Limits?> fetchLimits({
required CryptoCurrency from,
required CryptoCurrency to,
required bool isFixedRateMode,
diff --git a/lib/exchange/provider/letsexchange_exchange_provider.dart b/lib/exchange/provider/letsexchange_exchange_provider.dart
index b579123..d2c9191 100644
--- a/lib/exchange/provider/letsexchange_exchange_provider.dart
+++ b/lib/exchange/provider/letsexchange_exchange_provider.dart
@@ -47,7 +47,7 @@ class LetsExchangeExchangeProvider extends ExchangeProvider {
Future<bool> checkIsAvailable() async => true;
@override
- Future<Limits> fetchLimits(
+ Future<Limits?> fetchLimits(
{required CryptoCurrency from,
required CryptoCurrency to,
required bool isFixedRateMode}) async {
diff --git a/lib/exchange/provider/near_Intents_exchange_provider.dart b/lib/exchange/provider/near_Intents_exchange_provider.dart
index b36a420..0f52062 100644
--- a/lib/exchange/provider/near_Intents_exchange_provider.dart
+++ b/lib/exchange/provider/near_Intents_exchange_provider.dart
@@ -97,7 +97,7 @@ class NearIntentsExchangeProvider extends ExchangeProvider {
Future<bool> checkIsAvailable() async => true;
@override
- Future<Limits> fetchLimits(
+ Future<Limits?> fetchLimits(
{required CryptoCurrency from,
required CryptoCurrency to,
required bool isFixedRateMode}) async {
diff --git a/lib/exchange/provider/sideshift_exchange_provider.dart b/lib/exchange/provider/sideshift_exchange_provider.dart
index 3408422..cd91421 100644
--- a/lib/exchange/provider/sideshift_exchange_provider.dart
+++ b/lib/exchange/provider/sideshift_exchange_provider.dart
@@ -60,7 +60,7 @@ class SideShiftExchangeProvider extends ExchangeProvider {
}
@override
- Future<Limits> fetchLimits(
+ Future<Limits?> fetchLimits(
{required CryptoCurrency from,
required CryptoCurrency to,
required bool isFixedRateMode}) async {
diff --git a/lib/exchange/provider/simpleswap_exchange_provider.dart b/lib/exchange/provider/simpleswap_exchange_provider.dart
index 3d14c69..211aeff 100644
--- a/lib/exchange/provider/simpleswap_exchange_provider.dart
+++ b/lib/exchange/provider/simpleswap_exchange_provider.dart
@@ -51,7 +51,7 @@ class SimpleSwapExchangeProvider extends ExchangeProvider {
}
@override
- Future<Limits> fetchLimits(
+ Future<Limits?> fetchLimits(
{required CryptoCurrency from,
required CryptoCurrency to,
required bool isFixedRateMode}) async {
diff --git a/lib/exchange/provider/stealth_ex_exchange_provider.dart b/lib/exchange/provider/stealth_ex_exchange_provider.dart
index a10affa..d8d8831 100644
--- a/lib/exchange/provider/stealth_ex_exchange_provider.dart
+++ b/lib/exchange/provider/stealth_ex_exchange_provider.dart
@@ -43,7 +43,7 @@ class StealthExExchangeProvider extends ExchangeProvider {
Future<bool> checkIsAvailable() async => true;
@override
- Future<Limits> fetchLimits(
+ Future<Limits?> fetchLimits(
{required CryptoCurrency from,
required CryptoCurrency to,
required bool isFixedRateMode}) async {
diff --git a/lib/exchange/provider/swapsxyz_exchange_provider.dart b/lib/exchange/provider/swapsxyz_exchange_provider.dart
index 68f4cb0..10e8eb2 100644
--- a/lib/exchange/provider/swapsxyz_exchange_provider.dart
+++ b/lib/exchange/provider/swapsxyz_exchange_provider.dart
@@ -62,7 +62,7 @@ class SwapsXyzExchangeProvider extends ExchangeProvider {
Future<bool> checkIsAvailable() async => true;
@override
- Future<Limits> fetchLimits({
+ Future<Limits?> fetchLimits({
required CryptoCurrency from,
required CryptoCurrency to,
required bool isFixedRateMode,
diff --git a/lib/exchange/provider/swaptrade_exchange_provider.dart b/lib/exchange/provider/swaptrade_exchange_provider.dart
index 65f2968..e78fc66 100644
--- a/lib/exchange/provider/swaptrade_exchange_provider.dart
+++ b/lib/exchange/provider/swaptrade_exchange_provider.dart
@@ -9,7 +9,6 @@ import 'package:cake_wallet/exchange/trade_not_created_exception.dart';
import 'package:cake_wallet/exchange/trade_not_found_exception.dart';
import 'package:cake_wallet/exchange/trade_request.dart';
import 'package:cake_wallet/exchange/trade_state.dart';
-import 'package:cake_wallet/exchange/utils/currency_pairs_utils.dart';
import 'package:cw_core/utils/proxy_wrapper.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/utils/print_verbose.dart';
@@ -47,7 +46,7 @@ class SwapTradeExchangeProvider extends ExchangeProvider {
Future<bool> checkIsAvailable() async => true;
@override
- Future<Limits> fetchLimits({
+ Future<Limits?> fetchLimits({
required CryptoCurrency from,
required CryptoCurrency to,
required bool isFixedRateMode,
@@ -55,29 +54,33 @@ class SwapTradeExchangeProvider extends ExchangeProvider {
try {
final uri = Uri.https(apiAuthority, getCoins);
final response = await ProxyWrapper().get(clearnetUri: uri);
-
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
if (response.statusCode != 200)
throw Exception('Unexpected http status: ${response.statusCode}');
- final coinsInfo = responseJSON['data'] as List<dynamic>;
+ final coinsInfoRaw = responseJSON['data'];
+ final coinsInfo = coinsInfoRaw is List<dynamic> ? coinsInfoRaw : <dynamic>[];
- final coin = coinsInfo.firstWhere(
- (coin) => coin['id'].toString().toUpperCase() == _normalizeCurrency(from),
- orElse: () => null,
+ final normalized = _normalizeCurrency(from);
+ final coin = coinsInfo.cast<Map<String, dynamic>>().firstWhere(
+ (c) => (c['id']?.toString().toUpperCase() ?? '') == normalized,
+ orElse: () => <String, dynamic>{},
);
- if (coin == null) throw Exception('Coin not found: ${_normalizeCurrency(from)}');
+ if (coin.isEmpty) {
+ // Currency not supported by SwapTrade (e.g. USDC, DOGE).
+ return null;
+ }
- return Limits(
- min: double.parse(coin['min'].toString()),
- max: double.parse(coin['max'].toString()),
- );
+ final min = double.tryParse(coin['min']?.toString() ?? '') ?? 0.0;
+ final max = double.tryParse(coin['max']?.toString() ?? '') ?? 0.0;
+ if (max == 0) return null;
+ return Limits(min: min, max: max);
} catch (e) {
printV(e.toString());
- throw Exception('Error fetching limits: ${e.toString()}');
+ return null;
}
}
@@ -189,7 +192,7 @@ class SwapTradeExchangeProvider extends ExchangeProvider {
'amount_send': request.fromAmount,
'recipient': request.toAddress,
'ref': 'cake',
- 'markup': markup,
+ 'markup': int.tryParse(markup.toString()) ?? 0,
'refund_address': request.refundAddress,
};
@@ -394,7 +397,15 @@ class SwapTradeExchangeProvider extends ExchangeProvider {
CryptoCurrency.bnb => 'BNB_BSC',
CryptoCurrency.usdterc20 => 'USDT_ERC20',
CryptoCurrency.usdttrc20 => 'TRX_USDT_S2UZ',
- _ => '',
+ CryptoCurrency.usdtbsc => 'USDT_BSC',
+ CryptoCurrency.sol => 'SOL',
+ CryptoCurrency.btc => 'BTC',
+ CryptoCurrency.xmr => 'XMR',
+ CryptoCurrency.ltc => 'LTC',
+ CryptoCurrency.ada => 'ADA',
+ CryptoCurrency.bch => 'BCH',
+ CryptoCurrency.zec => 'ZEC',
+ _ => currency.title.toUpperCase(),
};
return network;
}
diff --git a/lib/exchange/provider/thorchain_exchange.provider.dart b/lib/exchange/provider/thorchain_exchange.provider.dart
index 69b11ad..c3f09d5 100644
--- a/lib/exchange/provider/thorchain_exchange.provider.dart
+++ b/lib/exchange/provider/thorchain_exchange.provider.dart
@@ -109,7 +109,7 @@ class ThorChainExchangeProvider extends ExchangeProvider {
}
@override
- Future<Limits> fetchLimits(
+ Future<Limits?> fetchLimits(
{required CryptoCurrency from,
required CryptoCurrency to,
required bool isFixedRateMode}) async {
diff --git a/lib/exchange/provider/trocador_exchange_provider.dart b/lib/exchange/provider/trocador_exchange_provider.dart
index 179e824..060575d 100644
--- a/lib/exchange/provider/trocador_exchange_provider.dart
+++ b/lib/exchange/provider/trocador_exchange_provider.dart
@@ -82,7 +82,7 @@ class TrocadorExchangeProvider extends ExchangeProvider {
Future<bool> checkIsAvailable() async => true;
@override
- Future<Limits> fetchLimits(
+ Future<Limits?> fetchLimits(
{required CryptoCurrency from,
required CryptoCurrency to,
required bool isFixedRateMode}) async {
@@ -103,10 +103,10 @@ class TrocadorExchangeProvider extends ExchangeProvider {
final coinJson = responseJSON.first as Map<String, dynamic>;
- return Limits(
- min: coinJson['minimum'] as double?,
- max: coinJson['maximum'] as double?,
- );
+ final min = double.tryParse(coinJson['minimum']?.toString() ?? '');
+ final max = double.tryParse(coinJson['maximum']?.toString() ?? '');
+ if (max == 0) return null;
+ return Limits(min: min, max: max);
}
@override
@@ -135,11 +135,18 @@ class TrocadorExchangeProvider extends ExchangeProvider {
final response = await ProxyWrapper().get(clearnetUri: uri, headers: {'API-Key': apiKey});
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
- final fromAmount = double.parse(responseJSON['amount_from'].toString());
- final toAmount = double.parse(responseJSON['amount_to'].toString());
- final rateId = responseJSON['trade_id'] as String? ?? '';
+ final fromAmount = double.tryParse(responseJSON['amount_from']?.toString() ?? '') ?? 0.0;
+ final toAmount = double.tryParse(responseJSON['amount_to']?.toString() ?? '') ?? 0.0;
+
+ if (fromAmount <= 0 || toAmount <= 0) return 0.0;
- var quotes = responseJSON['quotes']['quotes'] as List;
+ final quotesData = responseJSON['quotes'];
+ final rateId = _safeString(responseJSON, 'trade_id');
+
+ if (quotesData == null || quotesData is! Map<String, dynamic>) {
+ return 0.0;
+ }
+ final quotes = (quotesData['quotes'] as List?) ?? [];
_provider = quotes
.where((quote) => providerStates[quote['provider']] != false)
.map((quote) => quote['provider'])
@@ -243,7 +250,7 @@ class TrocadorExchangeProvider extends ExchangeProvider {
params['id'] = _lastUsedRateId;
}
- if (_provider.isEmpty) {
+ if (_provider.isEmpty || _provider.first == null || _provider.first.toString().isEmpty) {
ExchangeProviderLogger.logError(
provider: description,
function: 'createTrade',
@@ -264,15 +271,15 @@ class TrocadorExchangeProvider extends ExchangeProvider {
throw Exception('No available provider is enabled');
}
- params['provider'] = _provider.first as String;
+ params['provider'] = _provider.first.toString();
final uri = await _getUri(createTradePath, params);
final response = await ProxyWrapper().get(clearnetUri: uri, headers: {'API-Key': apiKey});
if (response.statusCode == 400) {
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
- final error = responseJSON['error'] as String;
- final message = responseJSON['message'] as String;
+ final error = _safeString(responseJSON, 'error', 'Unknown error');
+ final message = _safeString(responseJSON, 'message');
ExchangeProviderLogger.logError(
provider: description,
@@ -319,18 +326,18 @@ class TrocadorExchangeProvider extends ExchangeProvider {
}
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
- final id = responseJSON['trade_id'] as String;
- final inputAddress = responseJSON['address_provider'] as String;
- final refundAddress = responseJSON['refund_address'] as String;
- final status = responseJSON['status'] as String;
- final payoutAddress = responseJSON['address_user'] as String;
- final date = responseJSON['date'] as String;
- final password = responseJSON['password'] as String;
- final providerId = responseJSON['id_provider'] as String;
- final providerName = responseJSON['provider'] as String;
- final amount = responseJSON['amount_from']?.toString();
- final receiveAmount = responseJSON['amount_to']?.toString();
- final addressProviderMemo = responseJSON['address_provider_memo'] as String?;
+ final id = _safeString(responseJSON, 'trade_id');
+ final inputAddress = _safeString(responseJSON, 'address_provider');
+ final refundAddress = _safeString(responseJSON, 'refund_address');
+ final status = _safeString(responseJSON, 'status');
+ final payoutAddress = _safeString(responseJSON, 'address_user');
+ final date = _safeString(responseJSON, 'date');
+ final password = _safeString(responseJSON, 'password');
+ final providerId = _safeString(responseJSON, 'id_provider');
+ final providerName = _safeString(responseJSON, 'provider');
+ final amount = _safeString(responseJSON, 'amount_from');
+ final receiveAmount = _safeString(responseJSON, 'amount_to');
+ final addressProviderMemo = _safeString(responseJSON, 'address_provider_memo');
ExchangeProviderLogger.logSuccess(
provider: description,
@@ -365,6 +372,9 @@ class TrocadorExchangeProvider extends ExchangeProvider {
},
);
+ final amountToTrade = amount.isEmpty ? request.fromAmount : amount;
+ final receiveAmountToTrade = receiveAmount.isEmpty ? request.toAmount : receiveAmount;
+
return Trade(
id: id,
from: request.fromCurrency,
@@ -377,8 +387,8 @@ class TrocadorExchangeProvider extends ExchangeProvider {
providerId: providerId,
providerName: providerName,
createdAt: DateTime.tryParse(date)?.toLocal(),
- amount: amount ?? request.fromAmount,
- receiveAmount: receiveAmount ?? request.toAmount,
+ amount: amountToTrade,
+ receiveAmount: receiveAmountToTrade,
payoutAddress: payoutAddress,
isSendAll: isSendAll,
extraId: addressProviderMemo,
@@ -396,19 +406,21 @@ class TrocadorExchangeProvider extends ExchangeProvider {
throw Exception('Unexpected http status: ${response.statusCode}');
final responseListJson = json.decode(response.body) as List;
- final responseJSON = responseListJson.first;
- final id = responseJSON['trade_id'] as String;
- final payoutAddress = responseJSON['address_user'] as String;
- final refundAddress = responseJSON['refund_address'] as String;
- final inputAddress = responseJSON['address_provider'] as String;
+ final responseJSON = responseListJson.first as Map<String, dynamic>;
+ final id = _safeString(responseJSON, 'trade_id');
+ final payoutAddress = _safeString(responseJSON, 'address_user');
+ final refundAddress = _safeString(responseJSON, 'refund_address');
+ final inputAddress = _safeString(responseJSON, 'address_provider');
final fromAmount = responseJSON['amount_from']?.toString() ?? '0';
- final password = responseJSON['password'] as String;
- final providerId = responseJSON['id_provider'] as String;
- final providerName = responseJSON['provider'] as String;
- final addressProviderMemo = responseJSON['address_provider_memo'] as String?;
-
- final fromCurrency = responseJSON['ticker_from'] as String;
- final fromNetwork = responseJSON['network_from'] as String?;
+ final password = _safeString(responseJSON, 'password');
+ final providerId = _safeString(responseJSON, 'id_provider');
+ final providerName = _safeString(responseJSON, 'provider');
+ final memoVal = _safeString(responseJSON, 'address_provider_memo');
+ final addressProviderMemo = memoVal.isEmpty ? null : memoVal;
+
+ final fromCurrency = _safeString(responseJSON, 'ticker_from');
+ final fromNetworkVal = _safeString(responseJSON, 'network_from');
+ final fromNetwork = fromNetworkVal.isEmpty ? null : fromNetworkVal;
final _normalizedFromNetwork = _normalizeNetworkType(fromNetwork ?? '');
final fromTag = _normalizedFromNetwork.isEmpty ||
_normalizedFromNetwork == fromCurrency.toUpperCase() ||
@@ -418,8 +430,9 @@ class TrocadorExchangeProvider extends ExchangeProvider {
final from = CryptoCurrency.safeParseCurrencyFromString(fromCurrency, tag: fromTag);
- final toCurrency = responseJSON['ticker_to'] as String;
- final networkTo = responseJSON['network_to'] as String?;
+ final toCurrency = _safeString(responseJSON, 'ticker_to');
+ final networkToVal = _safeString(responseJSON, 'network_to');
+ final networkTo = networkToVal.isEmpty ? null : networkToVal;
final _normalizedToNetwork = _normalizeNetworkType(networkTo ?? '');
final toTag = _normalizedToNetwork.isEmpty ||
_normalizedToNetwork == toCurrency.toUpperCase() ||
@@ -435,9 +448,9 @@ class TrocadorExchangeProvider extends ExchangeProvider {
provider: description,
inputAddress: inputAddress,
refundAddress: refundAddress,
- createdAt: DateTime.parse(responseJSON['date'] as String),
+ createdAt: DateTime.tryParse(_safeString(responseJSON, 'date')),
amount: fromAmount,
- state: TradeState.deserialize(raw: responseJSON['status'] as String),
+ state: TradeState.deserialize(raw: _safeString(responseJSON, 'status')),
payoutAddress: payoutAddress,
password: password,
providerId: providerId,
@@ -541,6 +554,14 @@ class TrocadorExchangeProvider extends ExchangeProvider {
return Uri.https(clearNetAuthority, path, queryParams);
}
}
+
+ /// Safe string extraction from API response. Handles different data types.
+ static String _safeString(Map<String, dynamic> m, String key, [String nullError = '']) {
+ final v = m[key];
+ if (v == null) return nullError;
+ if (v is String) return v;
+ return v.toString();
+ }
}
class TrocadorPartners {
diff --git a/lib/exchange/provider/xoswap_exchange_provider.dart b/lib/exchange/provider/xoswap_exchange_provider.dart
index 7e7eeeb..34bf923 100644
--- a/lib/exchange/provider/xoswap_exchange_provider.dart
+++ b/lib/exchange/provider/xoswap_exchange_provider.dart
@@ -150,7 +150,7 @@ class XOSwapExchangeProvider extends ExchangeProvider {
}
}
- Future<Limits> fetchLimits({
+ Future<Limits?> fetchLimits({
required CryptoCurrency from,
required CryptoCurrency to,
required bool isFixedRateMode,
diff --git a/lib/src/screens/dev/exchange_provider_logs_page.dart b/lib/src/screens/dev/exchange_provider_logs_page.dart
index 26aa5e8..ae71c98 100644
--- a/lib/src/screens/dev/exchange_provider_logs_page.dart
+++ b/lib/src/screens/dev/exchange_provider_logs_page.dart
@@ -130,7 +130,7 @@ class _StatsCardState extends State<_StatsCard> {
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
- Text(entry.key.title),
+ Text(entry.key?.title ?? 'Unknown'),
Text(entry.value.toString()),
],
),
@@ -310,7 +310,7 @@ class _LogEntryCard extends StatelessWidget {
SizedBox(width: 8),
Expanded(
child: Text(
- "${log.provider.title} - ${log.function}",
+ "${log.provider?.title ?? 'Unknown/Empty'} - ${log.function}",
style: Theme.of(context).textTheme.titleMedium,
),
),
diff --git a/lib/utils/exchange_provider_logger.dart b/lib/utils/exchange_provider_logger.dart
index 6e7c553..4c28244 100644
--- a/lib/utils/exchange_provider_logger.dart
+++ b/lib/utils/exchange_provider_logger.dart
@@ -1,9 +1,10 @@
import 'dart:convert';
+import 'package:cake_wallet/core/utilities.dart';
import 'package:cake_wallet/exchange/exchange_provider_description.dart';
class ExchangeProviderLogEntry {
final DateTime timestamp;
- final ExchangeProviderDescription provider;
+ final ExchangeProviderDescription? provider;
final String function;
final String? error;
final String? stackTrace;
@@ -14,8 +15,8 @@ class ExchangeProviderLogEntry {
ExchangeProviderLogEntry({
required this.timestamp,
- required this.provider,
required this.function,
+ this.provider,
this.error,
this.stackTrace,
this.callStack,
@@ -26,7 +27,7 @@ class ExchangeProviderLogEntry {
String toLogString() {
final buffer = StringBuffer();
- buffer.writeln('Provider: ${provider.title}');
+ buffer.writeln('Provider: ${provider?.title ?? 'Unknown/Empty'}');
buffer.writeln('Function: $function');
buffer.writeln('Timestamp: ${timestamp.toIso8601String()}');
buffer.writeln('Success: $isSuccess');
@@ -58,7 +59,7 @@ class ExchangeProviderLogEntry {
Map<String, dynamic> toJson() {
return {
'timestamp': timestamp.toIso8601String(),
- 'provider': provider.title,
+ 'provider': provider?.title,
'function': function,
'error': error,
'stackTrace': stackTrace,
@@ -85,14 +86,14 @@ class ExchangeProviderLogEntry {
ExchangeProviderDescription.stealthEx,
ExchangeProviderDescription.chainflip,
ExchangeProviderDescription.xoSwap,
+ ExchangeProviderDescription.swapsXyz,
+ ExchangeProviderDescription.nearIntents,
+ ExchangeProviderDescription.jupiter,
];
return ExchangeProviderLogEntry(
timestamp: DateTime.parse(json['timestamp'] as String),
- provider: allProviders.firstWhere(
- (p) => p.title == json['provider'] as String,
- orElse: () => ExchangeProviderDescription.changeNow,
- ),
+ provider: allProviders.firstWhereOrNull((p) => p.title == json['provider'] as String),
function: json['function'] as String,
error: json['error'] as String?,
stackTrace: json['stackTrace'] as String?,
@@ -131,9 +132,9 @@ class ExchangeProviderLogger {
}
static void logError({
- required ExchangeProviderDescription provider,
required String function,
required dynamic error,
+ ExchangeProviderDescription? provider,
StackTrace? stackTrace,
Map<String, dynamic>? requestData,
String? callStack,
diff --git a/lib/view_model/dev/exchange_provider_logs_view_model.dart b/lib/view_model/dev/exchange_provider_logs_view_model.dart
index 7c4ac16..f72e756 100644
--- a/lib/view_model/dev/exchange_provider_logs_view_model.dart
+++ b/lib/view_model/dev/exchange_provider_logs_view_model.dart
@@ -43,10 +43,11 @@ abstract class ExchangeProviderLogsViewModelBase with Store {
int get errorLogs => logs.where((log) => !log.isSuccess).length;
@computed
- Map<ExchangeProviderDescription, int> get logsByProvider {
+ Map<ExchangeProviderDescription?, int> get logsByProvider {
final Map<ExchangeProviderDescription, int> counts = {};
for (final log in logs) {
- counts[log.provider] = (counts[log.provider] ?? 0) + 1;
+ if (log.provider == null) continue;
+ counts[log.provider!] = (counts[log.provider!] ?? 0) + 1;
}
return counts;
}
diff --git a/lib/view_model/exchange/exchange_view_model.dart b/lib/view_model/exchange/exchange_view_model.dart
index d6854ac..defdd83 100644
--- a/lib/view_model/exchange/exchange_view_model.dart
+++ b/lib/view_model/exchange/exchange_view_model.dart
@@ -40,6 +40,7 @@ import 'package:cake_wallet/exchange/trade_request.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker.dart';
import 'package:cake_wallet/store/app_store.dart';
+import 'package:cake_wallet/utils/exchange_provider_logger.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cw_core/currency.dart';
import "package:cw_core/wallet_info.dart";
@@ -321,7 +322,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
final List<ExchangeProvider> _tradeAvailableProviders = [];
- Map<ExchangeProvider, Limits> _providerLimits = {};
+ Map<ExchangeProvider, Limits?> _providerLimits = {};
@observable
ObservableList<ExchangeProvider> selectedProviders;
@@ -681,14 +682,16 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
if (receiveAmountFiat.split(".").last.length <= digits) {
return receiveAmountFiat;
}
- return double.parse(receiveAmountFiat).toStringAsPrecision(digits);
+
+ return double.tryParse(receiveAmountFiat)?.toStringAsPrecision(digits) ?? '0.00';
}
String roundedDepositAmountFiat(int digits) {
if (depositAmountFiat.split(".").last.length <= digits) {
return depositAmountFiat;
}
- return double.parse(depositAmountFiat).toStringAsPrecision(digits);
+
+ return double.tryParse(depositAmountFiat)?.toStringAsPrecision(digits) ?? '0.00';
}
@action
@@ -875,6 +878,8 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
// will throw "Concurrent modification during iteration" error if modified at the same
// time [createTrade] is called, as this is not a normal map, but a sorted map
}
+ } else {
+ printV('calculateBestRate: ${_providers[i].title} returned rate=0');
}
}
@@ -914,10 +919,10 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
to: to,
isFixedRateMode: isFixedRateMode,
)
- .onError((error, stackTrace) => Limits(max: 0.0, min: double.maxFinite))
+ .onError((error, stackTrace) => null)
.timeout(
Duration(seconds: 7),
- onTimeout: () => Limits(max: 0.0, min: double.maxFinite),
+ onTimeout: () => null,
);
return MapEntry(provider, limits);
}).toList();
@@ -925,7 +930,9 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
final entries = await Future.wait(futures);
_providerLimits = Map.fromEntries(entries);
- _providerLimits.values.forEach((tempLimits) {
+ _providerLimits.values
+ .whereType<Limits>()
+ .forEach((tempLimits) {
if (lowestMin != null && (tempLimits.min ?? -1) < lowestMin!) {
lowestMin = tempLimits.min;
}
@@ -968,18 +975,58 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
}
}
- late final Map<double, ExchangeProvider> providers;
+ if (depositCurrency == CryptoCurrency.btcln &&
+ depositAddress == wallet.walletAddresses.addressForExchange) {
+ final invoice = await bitcoin!.getLightningInvoice(wallet, BigInt.zero);
+ if (invoice != null) {
+ depositAddress = invoice;
+ }
+ }
+
+ if (receiveCurrency == CryptoCurrency.btcln &&
+ receiveAddress == wallet.walletAddresses.addressForExchange) {
+ final invoice = await bitcoin!.getLightningInvoice(wallet, BigInt.zero);
+ if (invoice != null) {
+ receiveAddress = invoice;
+ }
+ }
+
+ Map<double, ExchangeProvider> providers;
if (forcedProvider != null) {
providers = {forcedProviderRate: forcedProvider!};
} else {
- providers = _sortedAvailableProviders;
+ providers = Map.fromEntries(
+ _sortedAvailableProviders.entries.where(
+ (e) => selectedProviders.contains(e.value),
+ ),
+ );
}
// Ensure we have providers available before attempting to create trade
if (providers.isEmpty) {
await calculateBestRate();
+ if (forcedProvider != null) {
+ providers = {forcedProviderRate: forcedProvider!};
+ } else {
+ providers = Map.fromEntries(
+ _sortedAvailableProviders.entries.where(
+ (e) => selectedProviders.contains(e.value),
+ ),
+ );
+ }
if (providers.isEmpty) {
+ ExchangeProviderLogger.logError(
+ provider: null,
+ function: 'createTrade',
+ error: 'No providers available for $depositCurrency->$receiveCurrency',
+ requestData: {
+ 'from': depositCurrency.title,
+ 'to': receiveCurrency.title,
+ 'fromAmount': _depositAmount,
+ 'toAmount': _receiveAmount,
+ },
+ );
tradeState = TradeIsCreatedFailure(
title: S.current.trade_not_created,
error: S.current.none_of_selected_providers_can_exchange);
@@ -1022,6 +1069,8 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
final provider = providersSnapshot[i];
final providerRate = ratesSnapshot[i];
+ printV('createTrade: trying provider=${provider.title}');
+
// Skip Swaps.xyz when sending from external
if (isSendFromExternal &&
provider.description == ExchangeProviderDescription.swapsXyz) {
@@ -1055,7 +1104,10 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
amount = amount.replaceAll(',', '.');
if (limitsState is LimitsLoadedSuccessfully) {
- if (double.tryParse(amount) == null) continue;
+ if (double.tryParse(amount) == null) {
+ printV('createTrade: ${provider.title} amount parse failed: "$amount"');
+ continue;
+ }
if (limits.min != null && double.parse(amount) < limits.min!) {
continue;
@@ -1075,6 +1127,17 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
final canCreateTrade = await isCanCreateTrade(trade);
if (!canCreateTrade.result) {
+ ExchangeProviderLogger.logError(
+ provider: provider.description,
+ function: 'createTrade',
+ error: canCreateTrade.errorMessage ?? 'isCanCreateTrade returned false',
+ requestData: {
+ 'from': depositCurrency.title,
+ 'to': receiveCurrency.title,
+ 'fromAmount': _depositAmount,
+ 'toAmount': _receiveAmount,
+ },
+ );
continue;
}
@@ -1084,7 +1147,21 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
/// return after the first successful trade
return;
- } catch (e) {
+ } catch (e, s) {
+ ExchangeProviderLogger.logError(
+ provider: provider.description,
+ function: 'createTrade',
+ error: e,
+ stackTrace: s,
+ requestData: {
+ 'from': depositCurrency.title,
+ 'to': receiveCurrency.title,
+ 'fromAmount': _depositAmount,
+ 'toAmount': _receiveAmount,
+ 'toAddress': receiveAddress,
+ 'refundAddress': depositAddress,
+ },
+ );
continue;
}
}
@@ -1206,6 +1283,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
_receiveAmount = '';
bestRate = 0.0;
bestRateProvider = null;
+ _sortedAvailableProviders.clear();
loadLimits();
_setAvailableProviders();
}
Why this scored 36/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.