From 0cf80439ecadee4cbb77767ffeb6adf4820ace81 Mon Sep 17 00:00:00 2001 From: Serhii <17529954+serhii-bor@users.noreply.github.com> Date: Mon, 13 Jul 2026 04:23:10 +0300 Subject: [PATCH 1/3] refactor send validation and near token parsing (#3351) * refactor send validation and near token parsing * fix exchange send validation[skip ci] --- .../near_Intents_exchange_provider.dart | 21 ++-- .../exchange/exchange_trade_view_model.dart | 98 +++++++++++-------- 2 files changed, 72 insertions(+), 47 deletions(-) diff --git a/lib/exchange/provider/near_Intents_exchange_provider.dart b/lib/exchange/provider/near_Intents_exchange_provider.dart index 2a6a86143..f2ebe1006 100644 --- a/lib/exchange/provider/near_Intents_exchange_provider.dart +++ b/lib/exchange/provider/near_Intents_exchange_provider.dart @@ -271,17 +271,15 @@ class NearIntentsExchangeProvider extends ExchangeProvider { throw Exception('Failed to parse to currency from assetId: $toAssetId'); } - final from = CryptoCurrency.safeParseCurrencyFromString(fromCurrency.$1, - tag: fromCurrency.$2); - final to = CryptoCurrency.safeParseCurrencyFromString(toCurrency.$1, - tag: toCurrency.$2); + final from = CryptoCurrency.safeParseCurrencyFromString(fromCurrency.$1, tag: fromCurrency.$2); + final to = CryptoCurrency.safeParseCurrencyFromString(toCurrency.$1, tag: toCurrency.$2); final trade = Trade( id: depositAddress, // Using deposit address as trade ID from: request.fromCurrency, - to: request.toCurrency, + to: request.fromCurrency, provider: description, providerName: title, state: TradeState.created, @@ -575,10 +573,17 @@ class NearIntentsExchangeProvider extends ExchangeProvider { final token = supported.firstWhereOrNull((t) => t.assetId == assetId); if (token == null) return null; - final title = token.symbol; + + final title = token.symbol.toUpperCase() + .replaceAll(RegExp(r'\s*\([^)]*\)'), ''); + final normalizedNetwork = _normalizeNearBlockchainToTag(token.blockchain); - final tag = - normalizedNetwork == title.toUpperCase() ? null : normalizedNetwork; + + final isNativeAsset = assetId.contains(':native:coin'); + + final tag = isNativeAsset || normalizedNetwork == title + ? null + : normalizedNetwork; return (title, tag); } diff --git a/lib/view_model/exchange/exchange_trade_view_model.dart b/lib/view_model/exchange/exchange_trade_view_model.dart index 2579b4644..58540f8fc 100644 --- a/lib/view_model/exchange/exchange_trade_view_model.dart +++ b/lib/view_model/exchange/exchange_trade_view_model.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:cake_wallet/core/amount_parsing_proxy.dart'; +import 'package:cake_wallet/core/execution_state.dart'; import 'package:cake_wallet/entities/calculate_fiat_amount.dart'; import 'package:cake_wallet/entities/fiat_currency.dart'; import 'package:cake_wallet/exchange/exchange_provider_description.dart'; @@ -24,6 +25,7 @@ import 'package:cake_wallet/reactions/wallet_connect.dart'; import 'package:cake_wallet/src/screens/exchange_trade/exchange_trade_item.dart'; import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; import 'package:cake_wallet/store/dashboard/trades_store.dart'; +import 'package:cake_wallet/utils/exchange_provider_logger.dart'; import 'package:cake_wallet/utils/qr_util.dart'; import 'package:cake_wallet/utils/token_utilities.dart'; import 'package:cake_wallet/view_model/send/fees_view_model.dart'; @@ -48,7 +50,6 @@ abstract class ExchangeTradeViewModelBase with Store { required this.feesViewModel, required this.fiatConversionStore, }) : trade = tradesStore.trade!, - isSendable = _checkIfCanSend(tradesStore, wallet), isSwapsXYZCanSendFromExternal = _checkIfSwapsXYZCanSendFromExternal(tradesStore.trade!, wallet), items = ObservableList() { setUpOutput(); @@ -115,12 +116,10 @@ abstract class ExchangeTradeViewModelBase with Store { @observable Trade trade; - @observable - bool isSendable; - - bool isSwapsXYZCanSendFromExternal; + bool get isSendable => checkIfCanSend(trade, wallet) == null; + /// Providers that should hide the "send from external" button static const List _providersThatHideExternalSend = [ JupiterExchangeProvider, @@ -212,7 +211,13 @@ abstract class ExchangeTradeViewModelBase with Store { @action Future confirmSending() async { - if (!isSendable) return; + final canSendError = checkIfCanSend(trade, wallet); + + if (canSendError != null) { + sendViewModel.state = FailureState(canSendError); + return; + } + final selected = trade.from; if (selected == null) { @@ -348,48 +353,63 @@ abstract class ExchangeTradeViewModelBase with Store { ); } - static bool _checkIfCanSend(TradesStore tradesStore, WalletBase wallet) { - final trade = tradesStore.trade!; - final tradeFrom = trade.from; + String? checkIfCanSend(Trade? trade, WalletBase wallet) { + try { - bool _sameCurrency(CryptoCurrency? a, CryptoCurrency? b) => - a != null && b != null && a.titleAndTagEqual(b); + if (trade == null) throw Exception('Trade is null'); - bool _isEthToken() => - wallet.currency == CryptoCurrency.eth && tradeFrom?.tag == CryptoCurrency.eth.title; + final tradeFrom = trade.from; + if (tradeFrom == null) throw Exception('Trade from currency is null'); - bool _isPolygonToken() => - wallet.currency == CryptoCurrency.maticpoly && - tradeFrom?.tag == CryptoCurrency.maticpoly.tag; + bool _sameCurrency(CryptoCurrency a, CryptoCurrency b) => a.titleAndTagEqual(b); - bool _isBaseToken() => - wallet.currency == CryptoCurrency.baseEth && tradeFrom?.tag == CryptoCurrency.baseEth.tag; + bool _isTokenBelongingToWallet(CryptoCurrency cur) { + final chainTag = cur.tag ?? cur.title; + return wallet.currency == cur && + tradeFrom.tag?.toUpperCase() == chainTag.toUpperCase(); + } - bool _isArbitrumToken() => - wallet.currency == CryptoCurrency.arbEth && - (tradeFrom?.tag == CryptoCurrency.arbEth.tag || - tradeFrom?.title == CryptoCurrency.arbEth.tag); // This is to handle the CryptoCurrency.arb that doesn't have a tag but fully belongs to the Arbitrum chain + final canSend = _sameCurrency(tradeFrom, wallet.currency) || + (_sameCurrency(tradeFrom, CryptoCurrency.btcln) && + wallet.currency == CryptoCurrency.btc) || + trade.provider == ExchangeProviderDescription.xmrto || + _isTokenBelongingToWallet(CryptoCurrency.eth) || + _isTokenBelongingToWallet(CryptoCurrency.maticpoly) || + _isTokenBelongingToWallet(CryptoCurrency.baseEth) || + _isTokenBelongingToWallet(CryptoCurrency.arbEth) || + _isTokenBelongingToWallet(CryptoCurrency.trx) || + _isTokenBelongingToWallet(CryptoCurrency.sol) || + _isTokenBelongingToWallet(CryptoCurrency.bnb); - bool _isTronToken() => - wallet.currency == CryptoCurrency.trx && tradeFrom?.tag == CryptoCurrency.trx.title; + if (!canSend) { + throw Exception( + 'Wallet currency ${wallet.currency.title} does not match trade from currency ${tradeFrom.title} or is not a supported token for this wallet.', + ); + } - bool _isSplToken() => - wallet.currency == CryptoCurrency.sol && tradeFrom?.tag == CryptoCurrency.sol.title; + return null; + } catch (e, s) { + final trade = tradesStore.trade; - bool _isBscToken() => - wallet.currency == CryptoCurrency.bnb && tradeFrom?.tag == CryptoCurrency.bnb.tag; + ExchangeProviderLogger.logError( + provider: trade?.provider, + function: '_checkIfCanSend', + error: e, + stackTrace: s, + requestData: { + 'tradeId': trade?.id, + 'tradeFrom': trade?.from?.title, + 'tradeFromTag': trade?.from?.tag, + 'tradeTo': trade?.to?.title, + 'tradeToTag': trade?.to?.tag, + 'walletName': wallet.name, + 'walletCurrency': wallet.currency.title, + 'walletCurrencyTag': wallet.currency.tag, + }, + ); - return _sameCurrency(tradeFrom, wallet.currency) || - (_sameCurrency(tradeFrom, CryptoCurrency.btcln) && - wallet.currency == CryptoCurrency.btc) || - tradesStore.trade!.provider == ExchangeProviderDescription.xmrto || - _isEthToken() || - _isPolygonToken() || - _isSplToken() || - _isTronToken() || - _isBaseToken() || - _isArbitrumToken() || - _isBscToken(); + return e.toString(); + } } static bool _checkIfSwapsXYZCanSendFromExternal(Trade trade, WalletBase wallet) { From 7403aec38d243625563f0cb083e9ede3de77b474 Mon Sep 17 00:00:00 2001 From: Serhii <17529954+serhii-bor@users.noreply.github.com> Date: Mon, 13 Jul 2026 04:24:12 +0300 Subject: [PATCH 2/3] handle lightning balance fetch failures (#3382) --- cw_bitcoin/lib/bitcoin_wallet.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index 51f48571d..a0d88ce9a 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -345,12 +345,16 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { return balance; } + try { final lBalance = await lightningWallet!.getBalance(); this.balance[CryptoCurrency.btcln] = ElectrumBalance( confirmed: lBalance, unconfirmed: Money.zero(CryptoCurrency.btcln), frozen: Money.zero(CryptoCurrency.btcln)); + } catch (e) { + printV("Error fetching lightning balance: $e"); + } return ElectrumBalance( confirmed: balance.confirmed, From bff9b2fa9648b22c2fcc84062caba58861e9d24f Mon Sep 17 00:00:00 2001 From: Omar Date: Mon, 13 Jul 2026 05:49:27 +0300 Subject: [PATCH 3/3] minor fix [skip ci] --- .../near_Intents_exchange_provider.dart | 8 +- .../exchange/exchange_trade_view_model.dart | 99 +++++++++---------- 2 files changed, 51 insertions(+), 56 deletions(-) diff --git a/lib/exchange/provider/near_Intents_exchange_provider.dart b/lib/exchange/provider/near_Intents_exchange_provider.dart index f2ebe1006..2ac942def 100644 --- a/lib/exchange/provider/near_Intents_exchange_provider.dart +++ b/lib/exchange/provider/near_Intents_exchange_provider.dart @@ -271,15 +271,17 @@ class NearIntentsExchangeProvider extends ExchangeProvider { throw Exception('Failed to parse to currency from assetId: $toAssetId'); } - final from = CryptoCurrency.safeParseCurrencyFromString(fromCurrency.$1, tag: fromCurrency.$2); - final to = CryptoCurrency.safeParseCurrencyFromString(toCurrency.$1, tag: toCurrency.$2); + final from = CryptoCurrency.safeParseCurrencyFromString(fromCurrency.$1, + tag: fromCurrency.$2); + final to = CryptoCurrency.safeParseCurrencyFromString(toCurrency.$1, + tag: toCurrency.$2); final trade = Trade( id: depositAddress, // Using deposit address as trade ID from: request.fromCurrency, - to: request.fromCurrency, + to: request.toCurrency, provider: description, providerName: title, state: TradeState.created, diff --git a/lib/view_model/exchange/exchange_trade_view_model.dart b/lib/view_model/exchange/exchange_trade_view_model.dart index 58540f8fc..154c1ab96 100644 --- a/lib/view_model/exchange/exchange_trade_view_model.dart +++ b/lib/view_model/exchange/exchange_trade_view_model.dart @@ -214,11 +214,11 @@ abstract class ExchangeTradeViewModelBase with Store { final canSendError = checkIfCanSend(trade, wallet); if (canSendError != null) { + _logCanSendError(trade, wallet, canSendError); sendViewModel.state = FailureState(canSendError); return; } - final selected = trade.from; if (selected == null) { printV('No selectable currency for trade ${trade.id}'); @@ -354,62 +354,55 @@ abstract class ExchangeTradeViewModelBase with Store { } String? checkIfCanSend(Trade? trade, WalletBase wallet) { - try { + if (trade == null) return 'Trade is null'; - if (trade == null) throw Exception('Trade is null'); + final tradeFrom = trade.from; + if (tradeFrom == null) return 'Trade from currency is null'; - final tradeFrom = trade.from; - if (tradeFrom == null) throw Exception('Trade from currency is null'); + bool _sameCurrency(CryptoCurrency a, CryptoCurrency b) => a.titleAndTagEqual(b); - bool _sameCurrency(CryptoCurrency a, CryptoCurrency b) => a.titleAndTagEqual(b); - - bool _isTokenBelongingToWallet(CryptoCurrency cur) { - final chainTag = cur.tag ?? cur.title; - return wallet.currency == cur && - tradeFrom.tag?.toUpperCase() == chainTag.toUpperCase(); - } - - final canSend = _sameCurrency(tradeFrom, wallet.currency) || - (_sameCurrency(tradeFrom, CryptoCurrency.btcln) && - wallet.currency == CryptoCurrency.btc) || - trade.provider == ExchangeProviderDescription.xmrto || - _isTokenBelongingToWallet(CryptoCurrency.eth) || - _isTokenBelongingToWallet(CryptoCurrency.maticpoly) || - _isTokenBelongingToWallet(CryptoCurrency.baseEth) || - _isTokenBelongingToWallet(CryptoCurrency.arbEth) || - _isTokenBelongingToWallet(CryptoCurrency.trx) || - _isTokenBelongingToWallet(CryptoCurrency.sol) || - _isTokenBelongingToWallet(CryptoCurrency.bnb); - - if (!canSend) { - throw Exception( - 'Wallet currency ${wallet.currency.title} does not match trade from currency ${tradeFrom.title} or is not a supported token for this wallet.', - ); - } - - return null; - } catch (e, s) { - final trade = tradesStore.trade; - - ExchangeProviderLogger.logError( - provider: trade?.provider, - function: '_checkIfCanSend', - error: e, - stackTrace: s, - requestData: { - 'tradeId': trade?.id, - 'tradeFrom': trade?.from?.title, - 'tradeFromTag': trade?.from?.tag, - 'tradeTo': trade?.to?.title, - 'tradeToTag': trade?.to?.tag, - 'walletName': wallet.name, - 'walletCurrency': wallet.currency.title, - 'walletCurrencyTag': wallet.currency.tag, - }, - ); - - return e.toString(); + bool _isTokenBelongingToWallet(CryptoCurrency cur) { + final chainTag = cur.tag ?? cur.title; + return wallet.currency == cur && + (tradeFrom.tag?.toUpperCase() == chainTag.toUpperCase() || + tradeFrom.title.toUpperCase() == chainTag.toUpperCase()); } + + final canSend = _sameCurrency(tradeFrom, wallet.currency) || + (_sameCurrency(tradeFrom, CryptoCurrency.btcln) && + wallet.currency == CryptoCurrency.btc) || + trade.provider == ExchangeProviderDescription.xmrto || + _isTokenBelongingToWallet(CryptoCurrency.eth) || + _isTokenBelongingToWallet(CryptoCurrency.maticpoly) || + _isTokenBelongingToWallet(CryptoCurrency.baseEth) || + _isTokenBelongingToWallet(CryptoCurrency.arbEth) || + _isTokenBelongingToWallet(CryptoCurrency.trx) || + _isTokenBelongingToWallet(CryptoCurrency.sol) || + _isTokenBelongingToWallet(CryptoCurrency.bnb); + + if (!canSend) { + return 'Wallet currency ${wallet.currency.title} does not match trade from currency ${tradeFrom.title} or is not a supported token for this wallet.'; + } + + return null; + } + + void _logCanSendError(Trade? trade, WalletBase wallet, String error) { + ExchangeProviderLogger.logError( + provider: trade?.provider, + function: '_checkIfCanSend', + error: error, + requestData: { + 'tradeId': trade?.id, + 'tradeFrom': trade?.from?.title, + 'tradeFromTag': trade?.from?.tag, + 'tradeTo': trade?.to?.title, + 'tradeToTag': trade?.to?.tag, + 'walletName': wallet.name, + 'walletCurrency': wallet.currency.title, + 'walletCurrencyTag': wallet.currency.tag, + }, + ); } static bool _checkIfSwapsXYZCanSendFromExternal(Trade trade, WalletBase wallet) {