refactor send validation and near token parsing (#3351)

* refactor send validation and near token parsing * fix exchange send validation[skip ci]

Serhii committed Jul 13, 2026 at 04:23 UTC 0cf80439ecadee4cbb77767ffeb6adf4820ace81
2 files changed +74 -49
lib/exchange/provider/near_Intents_exchange_provider.dart
+13 -8
@@ -271,17 +271,15 @@ class NearIntentsExchangeProvider extends ExchangeProvider {
271 throw Exception('Failed to parse to currency from assetId: $toAssetId');
272 }
273
274 - final from = CryptoCurrency.safeParseCurrencyFromString(fromCurrency.$1,
275 - tag: fromCurrency.$2);
276 - final to = CryptoCurrency.safeParseCurrencyFromString(toCurrency.$1,
277 - tag: toCurrency.$2);
274 + final from = CryptoCurrency.safeParseCurrencyFromString(fromCurrency.$1, tag: fromCurrency.$2);
275 + final to = CryptoCurrency.safeParseCurrencyFromString(toCurrency.$1, tag: toCurrency.$2);
276
277
278 final trade = Trade(
279 id: depositAddress,
280 // Using deposit address as trade ID
281 from: request.fromCurrency,
284 - to: request.toCurrency,
282 + to: request.fromCurrency,
283 provider: description,
284 providerName: title,
285 state: TradeState.created,
@@ -575,10 +573,17 @@ class NearIntentsExchangeProvider extends ExchangeProvider {
573 final token = supported.firstWhereOrNull((t) => t.assetId == assetId);
574
575 if (token == null) return null;
578 - final title = token.symbol;
576 +
577 + final title = token.symbol.toUpperCase()
578 + .replaceAll(RegExp(r'\s*\([^)]*\)'), '');
579 +
580 final normalizedNetwork = _normalizeNearBlockchainToTag(token.blockchain);
580 - final tag =
581 - normalizedNetwork == title.toUpperCase() ? null : normalizedNetwork;
581 +
582 + final isNativeAsset = assetId.contains(':native:coin');
583 +
584 + final tag = isNativeAsset || normalizedNetwork == title
585 + ? null
586 + : normalizedNetwork;
587
588 return (title, tag);
589 }
lib/view_model/exchange/exchange_trade_view_model.dart
+61 -41
@@ -1,6 +1,7 @@
1 import 'dart:async';
2
3 import 'package:cake_wallet/core/amount_parsing_proxy.dart';
4 +import 'package:cake_wallet/core/execution_state.dart';
5 import 'package:cake_wallet/entities/calculate_fiat_amount.dart';
6 import 'package:cake_wallet/entities/fiat_currency.dart';
7 import 'package:cake_wallet/exchange/exchange_provider_description.dart';
@@ -24,6 +25,7 @@ import 'package:cake_wallet/reactions/wallet_connect.dart';
25 import 'package:cake_wallet/src/screens/exchange_trade/exchange_trade_item.dart';
26 import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart';
27 import 'package:cake_wallet/store/dashboard/trades_store.dart';
28 +import 'package:cake_wallet/utils/exchange_provider_logger.dart';
29 import 'package:cake_wallet/utils/qr_util.dart';
30 import 'package:cake_wallet/utils/token_utilities.dart';
31 import 'package:cake_wallet/view_model/send/fees_view_model.dart';
@@ -48,7 +50,6 @@ abstract class ExchangeTradeViewModelBase with Store {
50 required this.feesViewModel,
51 required this.fiatConversionStore,
52 }) : trade = tradesStore.trade!,
51 - isSendable = _checkIfCanSend(tradesStore, wallet),
53 isSwapsXYZCanSendFromExternal = _checkIfSwapsXYZCanSendFromExternal(tradesStore.trade!, wallet),
54 items = ObservableList<ExchangeTradeItem>() {
55 setUpOutput();
@@ -115,12 +116,10 @@ abstract class ExchangeTradeViewModelBase with Store {
116 @observable
117 Trade trade;
118
118 - @observable
119 - bool isSendable;
120 -
121 -
119 bool isSwapsXYZCanSendFromExternal;
120
121 + bool get isSendable => checkIfCanSend(trade, wallet) == null;
122 +
123 /// Providers that should hide the "send from external" button
124 static const List<Type> _providersThatHideExternalSend = [
125 JupiterExchangeProvider,
@@ -212,7 +211,13 @@ abstract class ExchangeTradeViewModelBase with Store {
211
212 @action
213 Future<void> confirmSending() async {
215 - if (!isSendable) return;
214 + final canSendError = checkIfCanSend(trade, wallet);
215 +
216 + if (canSendError != null) {
217 + sendViewModel.state = FailureState(canSendError);
218 + return;
219 + }
220 +
221
222 final selected = trade.from;
223 if (selected == null) {
@@ -348,48 +353,63 @@ abstract class ExchangeTradeViewModelBase with Store {
353 );
354 }
355
351 - static bool _checkIfCanSend(TradesStore tradesStore, WalletBase wallet) {
352 - final trade = tradesStore.trade!;
353 - final tradeFrom = trade.from;
354 -
355 - bool _sameCurrency(CryptoCurrency? a, CryptoCurrency? b) =>
356 - a != null && b != null && a.titleAndTagEqual(b);
357 -
358 - bool _isEthToken() =>
359 - wallet.currency == CryptoCurrency.eth && tradeFrom?.tag == CryptoCurrency.eth.title;
356 + String? checkIfCanSend(Trade? trade, WalletBase wallet) {
357 + try {
358
361 - bool _isPolygonToken() =>
362 - wallet.currency == CryptoCurrency.maticpoly &&
363 - tradeFrom?.tag == CryptoCurrency.maticpoly.tag;
359 + if (trade == null) throw Exception('Trade is null');
360
365 - bool _isBaseToken() =>
366 - wallet.currency == CryptoCurrency.baseEth && tradeFrom?.tag == CryptoCurrency.baseEth.tag;
361 + final tradeFrom = trade.from;
362 + if (tradeFrom == null) throw Exception('Trade from currency is null');
363
368 - bool _isArbitrumToken() =>
369 - wallet.currency == CryptoCurrency.arbEth &&
370 - (tradeFrom?.tag == CryptoCurrency.arbEth.tag ||
371 - 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
364 + bool _sameCurrency(CryptoCurrency a, CryptoCurrency b) => a.titleAndTagEqual(b);
365
373 - bool _isTronToken() =>
374 - wallet.currency == CryptoCurrency.trx && tradeFrom?.tag == CryptoCurrency.trx.title;
366 + bool _isTokenBelongingToWallet(CryptoCurrency cur) {
367 + final chainTag = cur.tag ?? cur.title;
368 + return wallet.currency == cur &&
369 + tradeFrom.tag?.toUpperCase() == chainTag.toUpperCase();
370 + }
371
376 - bool _isSplToken() =>
377 - wallet.currency == CryptoCurrency.sol && tradeFrom?.tag == CryptoCurrency.sol.title;
372 + final canSend = _sameCurrency(tradeFrom, wallet.currency) ||
373 + (_sameCurrency(tradeFrom, CryptoCurrency.btcln) &&
374 + wallet.currency == CryptoCurrency.btc) ||
375 + trade.provider == ExchangeProviderDescription.xmrto ||
376 + _isTokenBelongingToWallet(CryptoCurrency.eth) ||
377 + _isTokenBelongingToWallet(CryptoCurrency.maticpoly) ||
378 + _isTokenBelongingToWallet(CryptoCurrency.baseEth) ||
379 + _isTokenBelongingToWallet(CryptoCurrency.arbEth) ||
380 + _isTokenBelongingToWallet(CryptoCurrency.trx) ||
381 + _isTokenBelongingToWallet(CryptoCurrency.sol) ||
382 + _isTokenBelongingToWallet(CryptoCurrency.bnb);
383 +
384 + if (!canSend) {
385 + throw Exception(
386 + 'Wallet currency ${wallet.currency.title} does not match trade from currency ${tradeFrom.title} or is not a supported token for this wallet.',
387 + );
388 + }
389
379 - bool _isBscToken() =>
380 - wallet.currency == CryptoCurrency.bnb && tradeFrom?.tag == CryptoCurrency.bnb.tag;
390 + return null;
391 + } catch (e, s) {
392 + final trade = tradesStore.trade;
393 +
394 + ExchangeProviderLogger.logError(
395 + provider: trade?.provider,
396 + function: '_checkIfCanSend',
397 + error: e,
398 + stackTrace: s,
399 + requestData: {
400 + 'tradeId': trade?.id,
401 + 'tradeFrom': trade?.from?.title,
402 + 'tradeFromTag': trade?.from?.tag,
403 + 'tradeTo': trade?.to?.title,
404 + 'tradeToTag': trade?.to?.tag,
405 + 'walletName': wallet.name,
406 + 'walletCurrency': wallet.currency.title,
407 + 'walletCurrencyTag': wallet.currency.tag,
408 + },
409 + );
410
382 - return _sameCurrency(tradeFrom, wallet.currency) ||
383 - (_sameCurrency(tradeFrom, CryptoCurrency.btcln) &&
384 - wallet.currency == CryptoCurrency.btc) ||
385 - tradesStore.trade!.provider == ExchangeProviderDescription.xmrto ||
386 - _isEthToken() ||
387 - _isPolygonToken() ||
388 - _isSplToken() ||
389 - _isTronToken() ||
390 - _isBaseToken() ||
391 - _isArbitrumToken() ||
392 - _isBscToken();
411 + return e.toString();
412 + }
413 }
414
415 static bool _checkIfSwapsXYZCanSendFromExternal(Trade trade, WalletBase wallet) {