diff --git a/cw_evm/lib/evm_chain_wallet.dart b/cw_evm/lib/evm_chain_wallet.dart index 0db8781c7a..c5fe0a01f8 100644 --- a/cw_evm/lib/evm_chain_wallet.dart +++ b/cw_evm/lib/evm_chain_wallet.dart @@ -622,11 +622,43 @@ abstract class EVMChainWalletBase 42161 => CryptoCurrency.arbEth, _ => CryptoCurrency.eth, }; + final nativeBal = balance[nativeCurrency]?.balance ?? BigInt.zero; + final requiredNative = valueWei + BigInt.from(gas.estimatedGasFee); + + if (requiredNative > nativeBal) { + throw EVMChainTransactionFeesException(nativeCurrency.title); + } + + + bool _startsWith(String s, String p) => s.length >= p.length && s.substring(0, p.length).toLowerCase() == p.toLowerCase(); + + if (_startsWith(dataHex, '0xa9059cbb') && to.isNotEmpty) { + // Try find the token by contract address == `to` + Erc20Token? tokenObj; + for (final c in balance.keys) { + if (c is Erc20Token && c.contractAddress.toLowerCase() == to.toLowerCase()) { + tokenObj = c; + break; + } + } + + if (tokenObj != null) { + final hex = dataHex.startsWith('0x') ? dataHex.substring(2) : dataHex; + if (hex.length >= 8 + 64 + 64) { + final amountHex = hex.substring(hex.length - 64); + final requiredToken = BigInt.parse(amountHex, radix: 16); + final tokenBal = balance[tokenObj]?.balance ?? BigInt.zero; + + if (tokenBal < requiredToken) { + throw EVMChainTransactionCreationException(tokenObj); + } + } + } + } + - // Fallback for nodes that fail estimate (non-zero) final gasUnits = gas.estimatedGasUnits == 0 ? 65000 : gas.estimatedGasUnits; - // Sign raw (native) tx with callData return _client.signTransaction( privateKey: _evmChainPrivateKey, toAddress: to, diff --git a/lib/exchange/provider/swapsxyz_exchange_provider.dart b/lib/exchange/provider/swapsxyz_exchange_provider.dart index 8b39bda977..0354c305bf 100644 --- a/lib/exchange/provider/swapsxyz_exchange_provider.dart +++ b/lib/exchange/provider/swapsxyz_exchange_provider.dart @@ -43,7 +43,7 @@ class SwapsXyzExchangeProvider extends ExchangeProvider { bool get isEnabled => true; @override - bool get supportsFixedRate => true; + bool get supportsFixedRate => false; @override ExchangeProviderDescription get description => @@ -62,22 +62,23 @@ class SwapsXyzExchangeProvider extends ExchangeProvider { final chains = await _geSupportedChain(); if (chains.isEmpty) throw Exception('Failed to fetch supported chains'); - final srcChain = _findChainByCurrency(from, chains); - final dstChain = _findChainByCurrency(to, chains); + final fromToUse = isFixedRateMode ? to : from; + final toToUse = isFixedRateMode ? from : to; + + final srcChain = _findChainByCurrency(fromToUse, chains); + final dstChain = _findChainByCurrency(toToUse, chains); await _ensureTokensCached( - fromChain: srcChain, toChain: dstChain, from: from, to: to); + fromChain: srcChain, toChain: dstChain, from: fromToUse, to: toToUse); - final srcToken = _getTokenAddress(currency: from, chain: srcChain); - final dstToken = _getTokenAddress(currency: to, chain: dstChain); + final srcToken = _getTokenAddress(currency: fromToUse, chain: srcChain); + final dstToken = _getTokenAddress(currency: toToUse, chain: dstChain); - // parameters to and from are swapped in this endpoint - // i.e. to get limits for FROM -> TO, we request paths for TO -> FROM final params = { - 'srcChainId': '${dstChain.chainId}', - 'srcToken': dstToken, - 'dstChainId': '${srcChain.chainId}', - 'dstToken': srcToken, + 'srcChainId': '${srcChain.chainId}', + 'srcToken': srcToken, + 'dstChainId': '${dstChain.chainId}', + 'dstToken': dstToken, }; final uri = Uri.https(_baseUrl, _getPaths, params); @@ -88,37 +89,75 @@ class SwapsXyzExchangeProvider extends ExchangeProvider { final body = json.decode(res.body) as Map; - final paths = (body['paths'] as List?) ?? const []; + final paths = + (body['paths'] as List? ?? const []).cast>(); if (paths.isEmpty) { - throw Exception('No paths for ${from.title} -> ${to.title}'); + throw Exception('No paths for ${fromToUse.title} -> ${toToUse.title}'); } - final path0 = paths.first as Map; - final tokens = path0['tokens']; - - // If tokens == "all", use path-level amountLimits - if (tokens is String && tokens == 'all') { - final amountLimits = path0['amountLimits'] as Map?; + final int requestedDstId = dstChain.chainId; - final min = - double.tryParse(amountLimits?['minAmount']?.toString() ?? ''); - final max = - double.tryParse(amountLimits?['maxAmount']?.toString() ?? ''); + Map path = paths.firstWhere( + (p) => p['chainId'] == requestedDstId, + orElse: () => {}, + ); - return Limits(min: min, max: max); + if (path.isEmpty) { + path = paths.firstWhere( + (p) => (p['tokens'] is List) || p['amountLimits'] != null, + orElse: () => paths.first, + ); } - // Otherwise tokens is a list -> find the specific token entry - final tokenList = (tokens as List?) ?? const []; - final token = findTokenBySymbol(title: from.title, tokens: tokenList); + final supportsExactAmountIn = + path['supportsExactAmountIn'] as bool? ?? false; + final supportsExactAmountOut = + path['supportsExactAmountOut'] as bool? ?? false; - if (token == null) { - throw Exception('Token info not found for ${from.title}'); + if (isFixedRateMode && !supportsExactAmountOut) { + throw Exception( + 'This route does not support fixed receive (exact-amount-out)'); + } + if (!isFixedRateMode && !supportsExactAmountIn) { + throw Exception( + 'This route does not support exact send (exact-amount-in)'); } - final min = double.tryParse(token['minAmount']?.toString() ?? ''); - final max = double.tryParse(token['maxAmount']?.toString() ?? ''); + Map? useLimits; + + if (isFixedRateMode) { + final tokensField = path['tokens']; + useLimits = null; + + if (tokensField is List && tokensField.isNotEmpty) { + final tokens = tokensField.cast>(); + String norm(String s) => s.toUpperCase(); + final wantSym = norm(_normalizeCakeNativeTokenName(toToUse.title)); + final wantAddr = (dstToken).toLowerCase(); + + final match = tokens.firstWhere( + (t) { + final sym = norm(t['symbol']?.toString() ?? ''); + final addr = (t['address']?.toString() ?? '').toLowerCase(); + return sym == wantSym || (addr.isNotEmpty && addr == wantAddr); + }, + orElse: () => const {}, + ); + + if (match.isNotEmpty) { + useLimits = { + 'minAmount': match['minAmount'], + 'maxAmount': match['maxAmount'], + }; + } + } + } else { + // Floating/Exact-in: use the route-level limits + useLimits = path['amountLimits'] as Map?; + } + final min = double.tryParse((useLimits?['minAmount'])?.toString() ?? ''); + final max = double.tryParse((useLimits?['maxAmount'])?.toString() ?? ''); return Limits(min: min, max: max); } catch (e) { printV('fetchLimits error: $e'); @@ -250,20 +289,6 @@ class SwapsXyzExchangeProvider extends ExchangeProvider { final dstToken = _getTokenAddress(currency: request.toCurrency, chain: dstChain); - // Optional: ensure path supports exact-out before attempting fixed rate. - if (isFixedRateMode) { - final path = await _pickPath( - srcChainId: srcChain.chainId, - srcToken: srcToken, - dstChainId: dstChain.chainId, - dstToken: dstToken, - ); - if (path == null || !path.supportsExactOut) { - throw Exception( - 'This route does not support fixed receive (exact-amount-out)'); - } - } - final amountStr = isFixedRateMode ? request.toAmount : request.fromAmount; final rawAmount = double.tryParse(amountStr) ?? 0.0; if (rawAmount <= 0) throw Exception('Invalid amount'); @@ -299,6 +324,31 @@ class SwapsXyzExchangeProvider extends ExchangeProvider { final data = json.decode(res.body) as Map; final txId = data['txId'] as String? ?? ''; + + final requestedOut = isFixedRateMode + ? BigInt.tryParse(formattedAmount.replaceAll('n', '')) ?? BigInt.zero + : BigInt.zero; + + if (isFixedRateMode) { + final amountOut = data['amountOut'] as Map?; + + if (amountOut == null || + (amountOut['symbol'] as String?)?.toUpperCase() != + _normalizeCakeNativeTokenName(request.toCurrency.title)) { + throw Exception( + 'No amountOut info in getAction response for fixed rate'); + } + + final amountOutStr = amountOut['amount']?.toString() ?? '0'; + final amountOutBigInt = + BigInt.tryParse(amountOutStr.replaceAll('n', '')) ?? BigInt.zero; + + if (amountOutBigInt != requestedOut) { + throw SwapXyzProviderException( + 'Requested fixed receive amount unavailable for Swaps.XYZ. Try adjusting the amount or using floating rate.'); + } + } + final vmId = data['vmId'] as String? ?? ''; final txObj = (data['tx'] as Map?) ?? const {}; @@ -355,6 +405,8 @@ class SwapsXyzExchangeProvider extends ExchangeProvider { ); return trade; + } on SwapXyzProviderException { + rethrow; } catch (e) { printV('createTrade error: $e'); throw TradeNotCreatedException(description, description: e.toString()); @@ -539,57 +591,36 @@ class SwapsXyzExchangeProvider extends ExchangeProvider { } } - // Ensure tokens for this pair are cached (fills both src & dst chain entries) - // Replace your _ensureTokensCached with this one Future _ensureTokensCached({ required Chain fromChain, required Chain toChain, required CryptoCurrency from, required CryptoCurrency to, }) async { - bool needSrc = !_tokensCache.containsKey(fromChain.chainId) || + final needSrc = !_tokensCache.containsKey(fromChain.chainId) || (_tokensCache[fromChain.chainId]?.isEmpty ?? true); - bool needDst = !_tokensCache.containsKey(toChain.chainId) || + + final needDst = !_tokensCache.containsKey(toChain.chainId) || (_tokensCache[toChain.chainId]?.isEmpty ?? true); if (!needSrc && !needDst) return; - // First try: from -> to - await _fetchAndCacheTokens( - srcChainId: fromChain.chainId, - srcToken: from.title.toUpperCase(), - dstChainId: toChain.chainId, - dstToken: to.title.toUpperCase(), - ); - - // Re-check after first call - needSrc = !_tokensCache.containsKey(fromChain.chainId) || - (_tokensCache[fromChain.chainId]?.isEmpty ?? true); - needDst = !_tokensCache.containsKey(toChain.chainId) || - (_tokensCache[toChain.chainId]?.isEmpty ?? true); - - // If any still missing, try swapped: to -> from - if (needSrc || needDst) { - await _fetchAndCacheTokens( - srcChainId: toChain.chainId, - srcToken: to.title.toUpperCase(), - dstChainId: fromChain.chainId, - dstToken: from.title.toUpperCase(), - ); + if (needSrc) { + await _fetchAndCacheTokens(srcChainId: fromChain.chainId); + } + if (needDst) { + await _fetchAndCacheTokens(srcChainId: toChain.chainId); } } // call getPaths and merge tokens into cache keyed by the chainId Future _fetchAndCacheTokens({ required int srcChainId, - required String srcToken, - required int dstChainId, - required String dstToken, }) async { final params = { 'srcChainId': '$srcChainId', - 'srcToken': srcToken, - 'dstChainId': '$dstChainId', + 'srcToken': '0x0000000000000000000000000000000000000000', + // Native placeholder }; final uri = Uri.https(_baseUrl, _getPaths, params); @@ -599,21 +630,50 @@ class SwapsXyzExchangeProvider extends ExchangeProvider { return; } - final body = json.decode(res.body) as Map; + Map body; + try { + body = json.decode(res.body) as Map; + } catch (e) { + printV('getPaths JSON decode error: $e'); + return; + } + final paths = (body['paths'] as List?) ?? const []; if (paths.isEmpty) return; - final path0 = paths.first as Map; - final pathChainId = (path0['chainId'] as num?)?.toInt(); - final tokens = (path0['tokens'] as List?) ?? const []; + for (final path in paths) { + final map = path as Map; + final pathChainId = (map['chainId'] as num?)?.toInt(); + if (pathChainId == null) continue; - if (pathChainId == null || tokens.isEmpty) return; + final tokensField = map['tokens']; - final parsed = tokens - .map((t) => TokenPathInfo.fromJson(t as Map)) - .toList(); + // Case 1: String "all" -> cache empty list to indicate all tokens supported + if (tokensField is String) { + if (tokensField.toLowerCase() == 'all') { + _tokensCache[pathChainId] = + _tokensCache[pathChainId] ?? []; + } + continue; + } - _mergeCache(pathChainId, parsed); + // Case 2: List -> parse and merge + if (tokensField is List) { + final parsed = []; + for (final token in tokensField) { + if (token is Map) { + try { + parsed.add(TokenPathInfo.fromJson(token)); + } catch (e) { + printV('Token parse error on chain $pathChainId: $e : $token'); + } + } + } + if (parsed.isNotEmpty) { + _mergeCache(pathChainId, parsed); + } + } + } } // Merge by symbol, prefer entries that have a non-empty address/decimals @@ -641,52 +701,19 @@ class SwapsXyzExchangeProvider extends ExchangeProvider { _tokensCache[chainId] = bySymbol.values.toList(); } - String _nativeSymbolForChain(Chain chain) { - final network = chain.name.toUpperCase(); - switch (network) { - case 'BTC': - case 'BITCOIN': - return 'BTC'; - case 'XMR': - case 'MONERO': - return 'XMR'; - case 'TRX': - case 'TRON': - return 'TRX'; - case 'ETH': - case 'ETHEREUM': - return 'ETH'; - case 'LTC': - case 'LITECOIN': - return 'LTC'; - case 'DOGE': - case 'DOGECOIN': - return 'DOGE'; - case 'XRP': - case 'RIPPLE': - return 'XRP'; - case 'XLM': - case 'STELLAR': - return 'XLM'; - case 'SOL': - case 'SOLANA': - return 'SOL'; - case 'CARDANO': - return 'ADA'; - case 'Bitcoin Cash': - return 'BCH'; - case 'POLYGON': - return 'POL'; - default: - return network; - } + String _normalizeCakeNativeTokenName(String title) { + final name = title.toUpperCase(); + return switch (name) { + 'ZZEC' => 'ZEC', + _ => name, + }; } String _getTokenAddress({ required CryptoCurrency currency, required Chain chain, }) { - final symbol = currency.title.toUpperCase(); + final symbol = _normalizeCakeNativeTokenName(currency.title); final list = _tokensCache[chain.chainId]; // Try cache hit @@ -694,31 +721,17 @@ class SwapsXyzExchangeProvider extends ExchangeProvider { for (final t in list) { if (t.symbol == symbol && t.address != null && t.address!.isNotEmpty) { return t.address!; + } else if (t.symbol == symbol && (t.address == null)) { + // Native token on this chain + return '0x0000000000000000000000000000000000000000'; } } } - // If it's the native coin for this chain, return native placeholder - final nativeSym = _nativeSymbolForChain(chain); - if (nativeSym == symbol) { - return _nativePlaceholderForVm(chain.vmId); - } - // May fail for non-native Alt-VM assets return symbol; } - String _nativePlaceholderForVm(String vmId) { - switch (vmId.toLowerCase()) { - case 'evm': - return '0x0000000000000000000000000000000000000000'; - case 'alt-vm': - return '0x0000000000000000000000000000000000000000'; - default: - return '0x0000000000000000000000000000000000000000'; - } - } - Map? findTokenBySymbol( {required String title, required List tokens}) { final reqSymbol = title.toUpperCase(); @@ -821,3 +834,12 @@ class _PathInfo { _PathInfo({required this.supportsExactOut, required this.minToAmountHuman}); } + +class SwapXyzProviderException implements Exception { + final String message; + + const SwapXyzProviderException([this.message = '']); + + @override + String toString() => 'SwapXyzFixedAmountNotAvailable: $message'; +} diff --git a/lib/src/screens/exchange_trade/exchange_trade_page.dart b/lib/src/screens/exchange_trade/exchange_trade_page.dart index 67f05c74bd..bd4e039531 100644 --- a/lib/src/screens/exchange_trade/exchange_trade_page.dart +++ b/lib/src/screens/exchange_trade/exchange_trade_page.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/entities/parsed_address.dart'; import 'package:cake_wallet/exchange/exchange_provider_description.dart'; import 'package:cake_wallet/reactions/wallet_connect.dart'; import 'package:cake_wallet/routes.dart'; @@ -9,6 +10,7 @@ import 'package:cake_wallet/src/widgets/bottom_sheet/confirm_sending_bottom_shee import 'package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart'; import 'package:cake_wallet/utils/request_review_handler.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; +import 'package:cake_wallet/view_model/send/output.dart'; import 'package:mobx/mobx.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:flutter/material.dart'; @@ -162,19 +164,21 @@ class ExchangeTradeState extends State { bottomSectionPadding: EdgeInsets.fromLTRB(24, 0, 24, 24), bottomSection: Column( children: [ - PrimaryButton( - key: ValueKey('exchange_trade_page_send_from_external_button_key'), - text: S.current.send_from_external_wallet, - onPressed: () async { - Navigator.of(context).pushNamed(Routes.exchangeTradeExternalSendPage); - }, - color: widget.exchangeTradeViewModel.isSendable - ? Theme.of(context).colorScheme.surfaceContainer - : Theme.of(context).colorScheme.primary, - textColor: widget.exchangeTradeViewModel.isSendable - ? Theme.of(context).colorScheme.onSecondaryContainer - : Theme.of(context).colorScheme.onPrimary, - isDisabled: widget.exchangeTradeViewModel.isSwapsXyzSendingEVMTokenSwap, + Offstage( + offstage: !widget.exchangeTradeViewModel.isSwapsXYZContractCall, + child: PrimaryButton( + key: ValueKey('exchange_trade_page_send_from_external_button_key'), + text: S.current.send_from_external_wallet, + onPressed: () async { + Navigator.of(context).pushNamed(Routes.exchangeTradeExternalSendPage); + }, + color: widget.exchangeTradeViewModel.isSendable + ? Theme.of(context).colorScheme.surfaceContainer + : Theme.of(context).colorScheme.primary, + textColor: widget.exchangeTradeViewModel.isSendable + ? Theme.of(context).colorScheme.onSecondaryContainer + : Theme.of(context).colorScheme.onPrimary, + ), ), SizedBox(height: 16), Observer( @@ -268,10 +272,10 @@ class ExchangeTradeState extends State { if (state is ExecutedSuccessfullyState) { WidgetsBinding.instance.addPostFrameCallback((_) async { final trade = widget.exchangeTradeViewModel.trade; - final isSwapsXyz = trade.provider == ExchangeProviderDescription.swapsXyz; - final isEVMWallet = widget.exchangeTradeViewModel.sendViewModel.isEVMWallet; - final amountValue = isSwapsXyz && isEVMWallet + final isSwapsXYZContractCall = !widget.exchangeTradeViewModel.isSwapsXYZContractCall; + + final amountValue = isSwapsXYZContractCall ? trade.amount : widget.exchangeTradeViewModel.sendViewModel.pendingTransaction!.amountFormatted; @@ -302,6 +306,7 @@ class ExchangeTradeState extends State { feeFiatAmount: widget.exchangeTradeViewModel.sendViewModel .pendingTransactionFeeFiatAmountFormatted, outputs: widget.exchangeTradeViewModel.sendViewModel.outputs, + hideAddresses: isSwapsXYZContractCall, onSlideActionComplete: () async { if (bottomSheetContext.mounted) { Navigator.of(bottomSheetContext).pop(true); diff --git a/lib/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart b/lib/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart index 4bac63a239..b48422901f 100644 --- a/lib/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart +++ b/lib/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart @@ -37,6 +37,7 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet { this.change, this.explanation, this.isOpenCryptoPay = false, + this.hideAddresses = false, this.cakePayBuyCardViewModel, this.quantity, Key? key, @@ -66,6 +67,7 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet { final WalletType walletType; final PendingChange? change; final bool isOpenCryptoPay; + final bool hideAddresses; final CakePayBuyCardViewModel? cakePayBuyCardViewModel; final String? quantity; final String? explanation; @@ -159,30 +161,36 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet { final _address = item.isParsedAddress ? item.extractedAddress : item.address; final _amount = item.cryptoAmount.replaceAll(',', '.') + ' ${currency.title}'; return isBatchSending || (contactName.isNotEmpty && !isCakePayName) - ? ExpansionAddressTile( - contactType: isOpenCryptoPay ? 'Open CryptoPay' : S.of(context).contact, - name: isBatchSending ? batchContactTitle : contactName, - address: _address, - amount: _amount, - walletType: walletType, - isBatchSending: isBatchSending, - itemTitleTextStyle: itemTitleTextStyle, - itemSubTitleTextStyle: itemSubTitleTextStyle, - tileBackgroundColor: tileBackgroundColor, - ) - : AddressTile( - itemTitle: isCakePayName - ? item.parsedAddress.profileName - : S.of(context).address, - imagePath: isCakePayName ? item.parsedAddress.profileImageUrl : null, - itemTitleTextStyle: itemTitleTextStyle, - walletType: walletType, - amount: isCakePayName ? item.fiatAmount : _amount, - address: _address, - itemSubTitle: isCakePayName ? quantity : null, - itemSubTitleTextStyle: itemSubTitleTextStyle, - tileBackgroundColor: tileBackgroundColor, - ); + ? Offstage( + offstage: hideAddresses, + child: ExpansionAddressTile( + contactType: isOpenCryptoPay ? 'Open CryptoPay' : S.of(context).contact, + name: isBatchSending ? batchContactTitle : contactName, + address: _address, + amount: _amount, + walletType: walletType, + isBatchSending: isBatchSending, + itemTitleTextStyle: itemTitleTextStyle, + itemSubTitleTextStyle: itemSubTitleTextStyle, + tileBackgroundColor: tileBackgroundColor, + ), + ) + : Offstage( + offstage: hideAddresses, + child: AddressTile( + itemTitle: isCakePayName + ? item.parsedAddress.profileName + : S.of(context).address, + imagePath: isCakePayName ? item.parsedAddress.profileImageUrl : null, + itemTitleTextStyle: itemTitleTextStyle, + walletType: walletType, + amount: isCakePayName ? item.fiatAmount : _amount, + address: _address, + itemSubTitle: isCakePayName ? quantity : null, + itemSubTitleTextStyle: itemSubTitleTextStyle, + tileBackgroundColor: tileBackgroundColor, + ), + ); }, ), if (change != null) diff --git a/lib/view_model/exchange/exchange_trade_view_model.dart b/lib/view_model/exchange/exchange_trade_view_model.dart index fec47f3f35..eb21c22758 100644 --- a/lib/view_model/exchange/exchange_trade_view_model.dart +++ b/lib/view_model/exchange/exchange_trade_view_model.dart @@ -49,6 +49,7 @@ abstract class ExchangeTradeViewModelBase with Store { required this.fiatConversionStore, }) : trade = tradesStore.trade!, isSendable = _checkIfCanSend(tradesStore, wallet), + isSwapsXYZContractCall = _checkIfSwapsXYZCanSendFromExternal(tradesStore.trade!, wallet), items = ObservableList() { setUpOutput(); switch (trade.provider) { @@ -110,10 +111,8 @@ abstract class ExchangeTradeViewModelBase with Store { @observable bool isSendable; - bool get isSwapsXyzSendingEVMTokenSwap => - (_provider is SwapsXyzExchangeProvider) && - isEVMCompatibleChain(wallet.type) && - wallet.currency != trade.from; + + bool isSwapsXYZContractCall; String get extraInfo => trade.extraId != null && trade.extraId!.isNotEmpty ? '\n\n' + S.current.exchange_extra_info @@ -280,14 +279,24 @@ abstract class ExchangeTradeViewModelBase with Store { isReceiveDetail: true, isExternalSendDetail: false, ), - ExchangeTradeItem( - title: S.current.send_to_this_address('${tradeFrom}', tagFrom) + ':', - data: trade.inputAddress ?? '', - isCopied: false, - isReceiveDetail: false, - isExternalSendDetail: true, - ), ]); + + items.add( + isSwapsXYZContractCall + ? ExchangeTradeItem( + title: S.current.send_to_this_address('${tradeFrom}', tagFrom) + + ':', + data: trade.inputAddress ?? '', + isCopied: false, + isReceiveDetail: false, + isExternalSendDetail: true) + : ExchangeTradeItem( + title: 'Smart contract call (no address required)', + data: 'Wallet will execute a contract call. On-chain transaction', + isCopied: false, + isReceiveDetail: false, + isExternalSendDetail: true), + ); } final isExtraIdExist = trade.extraId != null && trade.extraId!.isNotEmpty; @@ -353,6 +362,24 @@ abstract class ExchangeTradeViewModelBase with Store { _isArbitrumToken(); } + static bool _checkIfSwapsXYZCanSendFromExternal(Trade trade, WalletBase wallet) { + final provider = trade.provider; + + if (provider == ExchangeProviderDescription.swapsXyz && + isEVMCompatibleChain(wallet.type)) { + + final tradeFrom = trade.fromRaw >= 0 ? trade.from : trade.userCurrencyFrom; + + if (tradeFrom == null) return false; + + final isNativeSupportedToken = + walletTypes.contains(cryptoCurrencyToWalletType(tradeFrom)); + + return isNativeSupportedToken; + } + return true; + } + Future registerSwapsXyzTransaction() async { try { if (!(_provider is SwapsXyzExchangeProvider)) return; diff --git a/lib/view_model/exchange/exchange_view_model.dart b/lib/view_model/exchange/exchange_view_model.dart index 8e471804b6..55687f52a1 100644 --- a/lib/view_model/exchange/exchange_view_model.dart +++ b/lib/view_model/exchange/exchange_view_model.dart @@ -726,6 +726,11 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with /// return after the first successful trade return; + } on SwapXyzProviderException catch (e) { + tradeState = TradeIsCreatedFailure( + title: S.current.trade_not_created, + error: e.message); + return; } catch (e) { continue; } @@ -991,6 +996,50 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with int get receiveMaxDigits => receiveCurrency.decimals; Future isCanCreateTrade(Trade trade) async { + + if (trade.provider == ExchangeProviderDescription.swapsXyz) { + + final tradeFrom = trade.fromRaw >= 0 ? trade.from : trade.userCurrencyFrom; + + if (tradeFrom == null) { + return CreateTradeResult( + result: false, + errorMessage: 'From currency is null', + ); + } + + final isNativeSupportedToken = walletTypes.contains(cryptoCurrencyToWalletType(tradeFrom)); + + if (!isNativeSupportedToken) { + + + + bool _isEthToken() => + wallet.currency == CryptoCurrency.eth && tradeFrom.tag == CryptoCurrency.eth.title; + + bool _isPolygonToken() => + wallet.currency == CryptoCurrency.maticpoly && + tradeFrom.tag == CryptoCurrency.maticpoly.tag; + + bool _isBaseToken() => + wallet.currency == CryptoCurrency.baseEth && tradeFrom.tag == CryptoCurrency.baseEth.tag; + + bool _isTronToken() => + wallet.currency == CryptoCurrency.trx && tradeFrom.tag == CryptoCurrency.trx.title; + + bool _isSplToken() => + wallet.currency == CryptoCurrency.sol && tradeFrom.tag == CryptoCurrency.sol.title; + + if(!(_isEthToken() || _isPolygonToken() || _isBaseToken() || _isTronToken() || _isSplToken())) { + return CreateTradeResult( + result: false, + errorMessage: 'This token isn’t supported on the current wallet/network for Swaps.xyz. Switch to a supported wallet or asset', + ); + } + } + + } + if (trade.provider == ExchangeProviderDescription.thorChain) { final payoutAddress = trade.payoutAddress ?? ''; final fromWalletAddress = trade.fromWalletAddress ?? '';