From 9b1a62441c0ae10b8f2b878ef520842d2b4674ca Mon Sep 17 00:00:00 2001 From: Serhii Date: Fri, 9 Jan 2026 20:32:56 +0200 Subject: [PATCH] Cw 1329 implement near intents dex (#2742) * add Near Intents exchange provider integration * refactor for improved trade handling * add explorer link for trades provider. * refactor currency parsing in near Intents exchange * fix: address type for estimating send all transactions in swaps * add token and app fee config * Update near_Intents_exchange_provider.dart * add Near Intents app fee secrets and update usage * add NEAR intents app fee secrets to workflows * Update lib/view_model/exchange/exchange_view_model.dart [skip ci] * making priceUpdatedAt nullable [skip ci] --------- Co-authored-by: Serhii-Borodenko <17529954+Serhii-Borodenko@users.noreply.github.com> Co-authored-by: Omar Hatem --- assets/images/near.png | Bin 0 -> 18584 bytes lib/bitcoin/cw_bitcoin.dart | 2 +- lib/core/trade_monitor.dart | 3 + .../exchange_provider_description.dart | 4 + .../near_Intents_exchange_provider.dart | 640 ++++++++++++++++++ lib/store/dashboard/trade_filter_store.dart | 16 +- .../dashboard/dashboard_view_model.dart | 5 + .../exchange/exchange_trade_view_model.dart | 4 + .../exchange/exchange_view_model.dart | 2 + lib/view_model/trade_details_view_model.dart | 6 + tool/utils/secret_key.dart | 3 + 11 files changed, 681 insertions(+), 4 deletions(-) create mode 100644 assets/images/near.png create mode 100644 lib/exchange/provider/near_Intents_exchange_provider.dart diff --git a/assets/images/near.png b/assets/images/near.png new file mode 100644 index 0000000000000000000000000000000000000000..fa0630df09bd25fc9557b4f80ed518a116244109 GIT binary patch literal 18584 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 literal 0 HcmV?d00001 diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index c80032d78..90ca4f791 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -214,7 +214,7 @@ class CWBitcoin extends Bitcoin { return estimatedTx.amount; } - final p2shAddr = sk.getPublic().toP2pkhInP2sh(); + final p2shAddr = sk.getPublic().toP2pkhAddress(); final estimatedTx = await electrumWallet.estimateSendAllTx( [BitcoinOutput(address: p2shAddr, value: BigInt.zero)], getFeeRate( diff --git a/lib/core/trade_monitor.dart b/lib/core/trade_monitor.dart index cacc8ef0b..8dbb6407d 100644 --- a/lib/core/trade_monitor.dart +++ b/lib/core/trade_monitor.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'package:cake_wallet/exchange/provider/near_Intents_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/simpleswap_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/swapsxyz_exchange_provider.dart'; import 'package:cake_wallet/exchange/trade.dart'; @@ -65,6 +66,8 @@ class TradeMonitor { return XOSwapExchangeProvider(); case ExchangeProviderDescription.swapsXyz: return SwapsXyzExchangeProvider(); + case ExchangeProviderDescription.nearIntents: + return NearIntentsExchangeProvider(); } return null; } diff --git a/lib/exchange/exchange_provider_description.dart b/lib/exchange/exchange_provider_description.dart index c4ddee3c2..de2ff28b4 100644 --- a/lib/exchange/exchange_provider_description.dart +++ b/lib/exchange/exchange_provider_description.dart @@ -38,6 +38,8 @@ class ExchangeProviderDescription extends EnumerableItem with Serializable< ExchangeProviderDescription(title: 'XOSwap', raw: 13, image: 'assets/images/xoswap.svg'); static const swapsXyz = ExchangeProviderDescription(title: 'Swaps.XYZ', raw: 14, image: 'assets/images/swaps_xyz.svg'); + static const nearIntents = + ExchangeProviderDescription(title: 'Near Intents', raw: 15, image: 'assets/images/near.png'); static ExchangeProviderDescription deserialize({required int raw}) { switch (raw) { @@ -71,6 +73,8 @@ class ExchangeProviderDescription extends EnumerableItem with Serializable< return xoSwap; case 14: return swapsXyz; + case 15: + return nearIntents; default: throw Exception('Unexpected token: $raw for ExchangeProviderDescription deserialize'); } diff --git a/lib/exchange/provider/near_Intents_exchange_provider.dart b/lib/exchange/provider/near_Intents_exchange_provider.dart new file mode 100644 index 000000000..71a4005c0 --- /dev/null +++ b/lib/exchange/provider/near_Intents_exchange_provider.dart @@ -0,0 +1,640 @@ +import 'dart:convert'; + +import 'package:cake_wallet/.secrets.g.dart' as secrets; +import 'package:cake_wallet/core/utilities.dart'; +import 'package:cake_wallet/exchange/exchange_provider_description.dart'; +import 'package:cake_wallet/exchange/limits.dart'; +import 'package:cake_wallet/exchange/provider/exchange_provider.dart'; +import 'package:cake_wallet/exchange/trade.dart'; +import 'package:cake_wallet/exchange/trade_not_created_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/amount_converter.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:cake_wallet/utils/exchange_provider_logger.dart'; + +class NearIntentsExchangeProvider extends ExchangeProvider { + NearIntentsExchangeProvider() + : super(pairList: supportedPairs(_notSupported)); + + static const List _notSupported = []; + + static const apiKey = secrets.nearIntentsBearerToken; + static const _baseUrl = '1click.chaindefuser.com'; + static const _versionPath = '/v0'; + static const _tokenPath = '/tokens'; + static const _quotePath = '/quote'; + static const _statusPath = '/status'; + + static const _slippageTolerance = 100; // 1% + static const _appFeesNearIntents = secrets.nearIntentsAppFee; + static const _appFeeRecipientNearIntents = secrets.nearIntentsAppFeeRecipient; + + static const _memoRequiredCurrencies = [ + CryptoCurrency.xrp, + CryptoCurrency.xlm, + CryptoCurrency.ton, + ]; + + /// Use these only for quote/rate testing (dummy data). + static const Map kNearDummyAddresses = { + // UTXO + 'LTC': 'ltc1qhdwz74m3wuuhppv2mckagqk9e2e49z5j4kucnv', + 'BTC': 'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kygt080', + 'ZZEC': 't1Zcash4hPS2bq8rTQkN9bKpZV1X4W3pZxK', + 'DOGE': 'D9t7rGQ9mE3hJ2z1w8pGQxkGmKjYwYc8pQ', + 'BCH': 'qpm2qsznhks23z7629mms6s4cwef74vcwvy22gdx6a', + + // EVM (same address works for all EVM chains) + 'ETH': '0x1111111111111111111111111111111111111111', + 'BSC': '0x1111111111111111111111111111111111111111', + 'POL': '0x1111111111111111111111111111111111111111', + 'AVAXC': '0x1111111111111111111111111111111111111111', + 'ARB': '0x1111111111111111111111111111111111111111', + 'BASE': '0x1111111111111111111111111111111111111111', + + // Others + 'SOL': '11111111111111111111111111111111', + 'XRP': 'rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe', + 'TRX': 'T9yD14Nj9j7xAB4dbGeiX9h8unkKHxuWwb', + 'TON': 'UQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM9c', + 'XLM': 'GA3D5O2W7YQZJQ3H4Y5QW6N7V8X9Z0A1B2C3D4E5F6G7H8I9J0', + 'ADA': 'addr1vyc33hdv5vag52f3d8h0qsngu52vm27x28zkzru333jma9gaxd38v', + }; + + String getNearDummyAddress(CryptoCurrency currency) { + final tag = (currency.tag ?? '').trim().toUpperCase(); + final title = currency.title.toUpperCase(); + final key = tag.isEmpty ? title : tag; + return kNearDummyAddresses[key] ?? ''; + } + + static final Map _headers = { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Authorization': '$apiKey', + }; + + static final _supportedTokensList = []; + + @override + String get title => 'Near Intents'; + + @override + bool get isAvailable => true; + + @override + bool get isEnabled => true; + + @override + bool get supportsFixedRate => true; + + @override + ExchangeProviderDescription get description => + ExchangeProviderDescription.nearIntents; + + @override + Future checkIsAvailable() async => true; + + @override + Future fetchLimits( + {required CryptoCurrency from, + required CryptoCurrency to, + required bool isFixedRateMode}) async { + final tokens = await _geSupportedTokens(); + final originToken = currencyToNearAssetId(from, tokens); + final destinationToken = currencyToNearAssetId(to, tokens); + + if (originToken == null || destinationToken == null) { + throw Exception( + 'fetchLimits: unsupported currency pair: ${from.title} ${from.tag ?? ''} to ${to.title} ${to.tag ?? ''}'); + } + + return Limits(min: null, max: null); + } + + @override + Future fetchRate({ + required CryptoCurrency from, + required CryptoCurrency to, + required double amount, + required bool isFixedRateMode, + required bool isReceiveAmount, + }) async { + final tokens = await _geSupportedTokens(); + final originToken = currencyToNearAssetId(from, tokens); + final destinationToken = currencyToNearAssetId(to, tokens); + + try { + if (originToken == null || destinationToken == null) { + throw Exception('fetchRate: Unsupported currency pair'); + } + + final formattedAmount = AmountConverter.toBaseUnits(amount.toString(), + isFixedRateMode ? destinationToken.decimals : originToken.decimals); + + final dummyAddrFrom = getNearDummyAddress(from); + final dummyAddrTo = getNearDummyAddress(to); + + final depositMode = _memoRequiredCurrencies.contains(from) ? "MEMO" : "SIMPLE"; + + final quote = await getSwapQuote( + dry: true, + isFixedRateMode: isFixedRateMode, + originAsset: originToken.assetId, + destinationAsset: destinationToken.assetId, + amount: formattedAmount, + depositMode: depositMode, + refundTo: dummyAddrFrom, + recipient: dummyAddrTo, + ); + + if (quote == null) { + throw Exception('fetchRate: Quote returned null'); + } + + final q = quote['quote'] as Map; + final amountIn = + double.tryParse(q['amountInFormatted']?.toString() ?? '0') ?? 0.0; + final amountOut = + double.tryParse(q['amountOutFormatted']?.toString() ?? '0') ?? 0.0; + + if (amountIn == 0) return 0.0; + + final rate = amountOut / amountIn; + + ExchangeProviderLogger.logSuccess( + provider: description, + function: 'fetchRate', + requestData: { + 'from': from.title, + 'to': to.title, + 'amount': amount, + 'formattedAmount': formattedAmount, + 'isFixedRateMode': isFixedRateMode, + 'isReceiveAmount': isReceiveAmount, + 'originAsset': originToken.assetId, + 'destinationAsset': destinationToken.assetId, + }, + responseData: { + 'amountIn': amountIn, + 'amountOut': amountOut, + 'rate': rate, + }, + ); + + return rate; + } catch (e, s) { + ExchangeProviderLogger.logError( + provider: description, + function: 'fetchRate', + error: e, + stackTrace: s, + requestData: { + 'from': from.title, + 'to': to.title, + 'amount': amount, + 'isFixedRateMode': isFixedRateMode, + 'isReceiveAmount': isReceiveAmount, + }, + ); + printV(e.toString()); + return 0.0; + } + } + + @override + Future createTrade({ + required TradeRequest request, + required bool isFixedRateMode, + required bool isSendAll, + }) async { + try { + final tokens = await _geSupportedTokens(); + final originToken = currencyToNearAssetId(request.fromCurrency, tokens); + final destinationToken = + currencyToNearAssetId(request.toCurrency, tokens); + + if (originToken == null || destinationToken == null) { + throw Exception('Unsupported currency pair'); + } + + final rawAmountStr = + isFixedRateMode ? request.toAmount : request.fromAmount; + + final baseAmount = AmountConverter.toBaseUnits( + rawAmountStr, + isFixedRateMode + ? request.toCurrency.decimals + : request.fromCurrency.decimals, + ); + + final depositMode = _memoRequiredCurrencies.contains(request.fromCurrency) ? "MEMO" : "SIMPLE"; + + final quote = await getSwapQuote( + dry: false, + isFixedRateMode: isFixedRateMode, + originAsset: originToken.assetId, + destinationAsset: destinationToken.assetId, + depositMode: depositMode, + amount: baseAmount, + refundTo: request.refundAddress, + recipient: request.toAddress, + ); + + if (quote == null) { + throw Exception('Quote request failed'); + } + + final quoteObj = quote['quote'] as Map; + final depositAddress = quoteObj['depositAddress'] as String; + final depositMemo = quoteObj['depositMemo'] as String?; + + final quoteRequest = quote['quoteRequest'] as Map; + final fromAssetId = quoteRequest['originAsset'] as String; + final toAssetId = quoteRequest['destinationAsset'] as String; + + final fromCurrency = _nearAssetIdToCurrency(fromAssetId, tokens); + if (fromCurrency == null) { + throw Exception('Failed to parse from currency from assetId: $fromAssetId'); + } + + final toCurrency = _nearAssetIdToCurrency(toAssetId, tokens); + if (toCurrency == null) { + 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 trade = Trade( + id: depositAddress, + // Using deposit address as trade ID + from: from, + to: to, + provider: description, + providerName: title, + state: TradeState.created, + createdAt: DateTime.now(), + inputAddress: depositAddress, + payoutAddress: request.toAddress, + refundAddress: request.refundAddress, + amount: request.fromAmount, + receiveAmount: quoteObj['amountOutFormatted']?.toString(), + memo: depositMemo, + isSendAll: isSendAll, + userCurrencyFromRaw: + '${request.fromCurrency.title}_${request.fromCurrency.tag ?? ''}', + userCurrencyToRaw: + '${request.toCurrency.title}_${request.toCurrency.tag ?? ''}', + ); + + ExchangeProviderLogger.logSuccess( + provider: description, + function: 'createTrade', + requestData: { + 'from': request.fromCurrency.title, + 'to': request.toCurrency.title, + 'fromAmount': request.fromAmount, + 'toAmount': request.toAmount, + 'refundAddress': request.refundAddress, + 'recipient': request.toAddress, + 'isFixedRateMode': isFixedRateMode, + 'isSendAll': isSendAll, + 'originAsset': originToken.assetId, + 'destinationAsset': destinationToken.assetId, + }, + responseData: { + 'correlationId': quote['correlationId'], + 'depositAddress': depositAddress, + 'depositMemo': depositMemo, + 'quote': quoteObj, + }, + ); + + return trade; + } catch (e, s) { + ExchangeProviderLogger.logError( + provider: description, + function: 'createTrade', + error: e, + stackTrace: s, + requestData: { + 'from': request.fromCurrency.title, + 'to': request.toCurrency.title, + 'fromAmount': request.fromAmount, + 'toAmount': request.toAmount, + 'refundAddress': request.refundAddress, + 'recipient': request.toAddress, + 'isFixedRateMode': isFixedRateMode, + 'isSendAll': isSendAll, + }, + ); + throw TradeNotCreatedException(description, description: e.toString()); + } + } + + @override + Future findTradeById({required String id}) async { + final param = {'depositAddress': id}; + final uri = Uri.https(_baseUrl, '$_versionPath$_statusPath', param); + + final response = + await ProxyWrapper().get(clearnetUri: uri, headers: _headers); + + if (response.statusCode != 200) { + throw Exception( + 'Near Intents fetch trade failed: ${response.statusCode} ${response.body}', + ); + } + + final data = jsonDecode(response.body) as Map; + final statusRaw = (data['status'] as String?) ?? 'UNKNOWN'; + + final quoteResponse = data['quoteResponse'] as Map? ?? {}; + final quoteRequest = + quoteResponse['quoteRequest'] as Map? ?? {}; + + final refundTo = quoteRequest['refundTo'] as String? ?? ''; + final recipient = quoteRequest['recipient'] as String? ?? ''; + + // Parsing 'from' currency + final originAssetId = quoteRequest['originAsset'] as String? ?? ''; + final from = + _nearAssetIdToCurrency(originAssetId, await _geSupportedTokens()); + + CryptoCurrency? coinFrom; + CryptoCurrency? coinTo; + + if (from != null) { + coinFrom = CryptoCurrency.safeParseCurrencyFromString(from.$1, tag: from.$2); + } + + // Parsing 'to' currency + final destinationAssetId = + quoteRequest['destinationAsset'] as String? ?? ''; + + final to = + _nearAssetIdToCurrency(destinationAssetId, await _geSupportedTokens()); + + if (to != null) { + coinTo = CryptoCurrency.safeParseCurrencyFromString(to.$1, tag: to.$2); + } + + final quote = quoteResponse['quote'] as Map? ?? {}; + final swap = data['swapDetails'] as Map? ?? {}; + + final depositAddress = quote['depositAddress'] as String?; + final depositMemo = quote['depositMemo'] as String?; + + final depositAmount = swap['amountInFormatted']?.toString() ?? + quote['amountInFormatted']?.toString() ?? + '0'; + + final receiveAmount = swap['amountOutFormatted']?.toString() ?? + quote['amountOutFormatted']?.toString(); + + final originTxHash = (swap['originChainTxHashes'] as List?) + ?.firstOrNull?['hash'] + ?.toString(); + + return Trade( + id: id, + from: coinFrom, + to: coinTo, + provider: description, + inputAddress: depositAddress, + payoutAddress: recipient, + refundAddress: refundTo, + amount: depositAmount, + receiveAmount: receiveAmount, + state: _normalizeStatusToTradeState(statusRaw), + txId: originTxHash, + extraId: depositMemo, + isRefund: statusRaw == 'REFUNDED', + userCurrencyFromRaw: '${from?.$1.toUpperCase()}' + '_' + '${from?.$2?.toUpperCase() ?? ''}', + userCurrencyToRaw: '${to?.$1.toUpperCase()}' + '_' + '${to?.$2?.toUpperCase() ?? ''}', + ); + } + + // Load & cache supported tokens + Future> _geSupportedTokens() async { + if (_supportedTokensList.isNotEmpty) return _supportedTokensList; + + try { + final uri = Uri.https(_baseUrl, '$_versionPath$_tokenPath'); + + final response = + await ProxyWrapper().get(clearnetUri: uri, headers: _headers); + if (response.statusCode != 200) return []; + + final data = json.decode(response.body) as List; + _supportedTokensList + ..clear() + ..addAll(data.map((e) => Token.fromJson(e as Map))); + return _supportedTokensList; + } catch (e) { + printV(e); + return []; + } + } + + Future?> getSwapQuote({ + required bool dry, + required bool isFixedRateMode, + required String originAsset, + required String destinationAsset, + required String amount, + required String refundTo, + required String recipient, + required String depositMode, + List? connectedWallets, + String? sessionId, + String? virtualChainRecipient, + String? virtualChainRefundRecipient, + String? customRecipientMsg, + String? deadline, + String? referral, + int? quoteWaitingTimeMs, + }) async { + final swapType = isFixedRateMode ? 'EXACT_OUTPUT' : 'EXACT_INPUT'; + final _isoUtcDeadline = _buildDeadline(); + final appFees = [ + { + "recipient": _appFeeRecipientNearIntents, + "fee": _appFeesNearIntents, + } + ]; + + final uri = Uri.https(_baseUrl, "$_versionPath$_quotePath"); + + final payload = { + "dry": dry, + "depositMode": depositMode, + "swapType": swapType, + "slippageTolerance": _slippageTolerance, + "originAsset": originAsset, + "depositType": 'ORIGIN_CHAIN', + "destinationAsset": destinationAsset, + "amount": amount, + "refundTo": refundTo, + "refundType": 'ORIGIN_CHAIN', + "recipient": recipient, + "recipientType": 'DESTINATION_CHAIN', + "deadline": _isoUtcDeadline, + if (connectedWallets != null) "connectedWallets": connectedWallets, + if (sessionId != null) "sessionId": sessionId, + if (virtualChainRecipient != null) + "virtualChainRecipient": virtualChainRecipient, + if (virtualChainRefundRecipient != null) + "virtualChainRefundRecipient": virtualChainRefundRecipient, + if (customRecipientMsg != null) "customRecipientMsg": customRecipientMsg, + if (deadline != null) "deadline": deadline, + if (referral != null) "referral": referral, + if (quoteWaitingTimeMs != null) "quoteWaitingTimeMs": quoteWaitingTimeMs, + "appFees": appFees, + }; + + try { + final response = await ProxyWrapper().post( + clearnetUri: uri, + headers: _headers, + body: jsonEncode(payload), + ); + + if (response.statusCode != 201) { + printV("Quote request failed with status: ${response.statusCode}"); + return null; + } + + return jsonDecode(response.body) as Map; + } catch (e) { + printV("Quote error: $e"); + return null; + } + } + + TradeState _normalizeStatusToTradeState(String status) { + return switch (status.toUpperCase()) { + 'PENDING_DEPOSIT' => TradeState.pending, + 'PROCESSING' => TradeState.processing, + 'SUCCESS' => TradeState.success, + 'INCOMPLETE_DEPOSIT' => TradeState.underpaid, + 'REFUNDED' => TradeState.refunded, + 'FAILED' => TradeState.failed, + _ => TradeState.notFound, + }; + } + + String _normalizeTitleToNearSymbol(CryptoCurrency currency) { + final title = currency.title.toUpperCase(); + return switch (title) { + 'TZEC' => 'ZEC', + _ => title, + }; + } + + String? _normalizeTagToNearBlockchain(String? tag) { + return switch (tag) { + 'TRX' => 'tron', + 'AVAXC' => 'avax', + _ => tag?.toLowerCase(), + }; + } + + String? _normalizeNearBlockchainToTag(String? blockchain) { + return switch (blockchain) { + 'tron' => 'TRX', + 'avax' => 'AVAXC', + _ => blockchain?.toUpperCase(), + }; + } + + Token? currencyToNearAssetId(CryptoCurrency currency, List supported) { + if (supported.isEmpty) return null; + + final symbol = _normalizeTitleToNearSymbol(currency).toUpperCase(); + final blockchain = _normalizeTagToNearBlockchain(currency.tag); + + // Native asset (no contract) + final native = supported.firstWhereOrNull((t) => + t.symbol.toUpperCase() == symbol && + (blockchain == null || t.blockchain.toLowerCase() == blockchain) && + t.contractAddress == null); + + if (native != null) { + return native; + } + + final token = supported.firstWhereOrNull((t) => + t.symbol.toUpperCase() == symbol && + (blockchain == null || t.blockchain.toLowerCase() == blockchain)); + + return token; + } + + (String, String?)? _nearAssetIdToCurrency( + String assetId, List supported) { + if (supported.isEmpty) return null; + + final token = supported.firstWhereOrNull((t) => t.assetId == assetId); + + if (token == null) return null; + final title = token.symbol; + final normalizedNetwork = _normalizeNearBlockchainToTag(token.blockchain); + final tag = + normalizedNetwork == title.toUpperCase() ? null : normalizedNetwork; + + return (title, tag); + } + + String _buildDeadline() { + return DateTime.now() + .toUtc() + .add(const Duration(hours: 2)) + .toIso8601String() + .replaceFirst(RegExp(r'\.\d+Z$'), 'Z'); + } +} + +class Token { + final String assetId; + final int decimals; + final String blockchain; + final String symbol; + final double priceUsd; + final String? priceUpdatedAt; + final String? contractAddress; + + Token({ + required this.assetId, + required this.decimals, + required this.blockchain, + required this.symbol, + required this.priceUsd, + required this.priceUpdatedAt, + required this.contractAddress, + }); + + factory Token.fromJson(Map json) { + final decimals = json['decimals'] as int?; + if (decimals == null) { + throw Exception('Token decimals is null for assetId: ${json['assetId']}'); + } + return Token( + assetId: json['assetId'] as String, + decimals: json['decimals'] as int, + blockchain: json['blockchain'] as String, + symbol: json['symbol'] as String, + priceUsd: (json['price'] as num?)?.toDouble() ?? 0.0, + priceUpdatedAt: json['priceUpdatedAt'] as String?, + contractAddress: json['contractAddress'] as String?, + ); + } +} diff --git a/lib/store/dashboard/trade_filter_store.dart b/lib/store/dashboard/trade_filter_store.dart index 16be01b62..74536806a 100644 --- a/lib/store/dashboard/trade_filter_store.dart +++ b/lib/store/dashboard/trade_filter_store.dart @@ -22,7 +22,8 @@ abstract class TradeFilterStoreBase with Store { displayStealthEx = true, displayXOSwap = true, displaySwapTrade = true, - displaySwapXyz = true; + displaySwapXyz = true, + displayNearIntents = true; @observable bool displayXMRTO; @@ -65,6 +66,8 @@ abstract class TradeFilterStoreBase with Store { @observable bool displaySwapXyz; + @observable + bool displayNearIntents; @computed bool get displayAllTrades => @@ -79,7 +82,8 @@ abstract class TradeFilterStoreBase with Store { displayStealthEx && displayXOSwap && displaySwapTrade && - displaySwapXyz; + displaySwapXyz && + displayNearIntents; @action void toggleDisplayExchange(ExchangeProviderDescription provider) { @@ -126,6 +130,9 @@ abstract class TradeFilterStoreBase with Store { case ExchangeProviderDescription.swapsXyz: displaySwapXyz = !displaySwapXyz; break; + case ExchangeProviderDescription.nearIntents: + displayNearIntents = !displayNearIntents; + break; case ExchangeProviderDescription.all: if (displayAllTrades) { displayChangeNow = false; @@ -142,6 +149,7 @@ abstract class TradeFilterStoreBase with Store { displayXOSwap = false; displaySwapTrade = false; displaySwapXyz = false; + displayNearIntents = false; } else { displayChangeNow = true; displaySideShift = true; @@ -157,6 +165,7 @@ abstract class TradeFilterStoreBase with Store { displayXOSwap = true; displaySwapTrade = true; displaySwapXyz = true; + displayNearIntents = true; } break; } @@ -192,7 +201,8 @@ abstract class TradeFilterStoreBase with Store { (displayXOSwap && item.trade.provider == ExchangeProviderDescription.xoSwap) || (displaySwapTrade && item.trade.provider == ExchangeProviderDescription.swapTrade) || (displaySwapXyz && - item.trade.provider == ExchangeProviderDescription.swapsXyz)) + item.trade.provider == ExchangeProviderDescription.swapsXyz) || + (displayNearIntents && item.trade.provider == ExchangeProviderDescription.nearIntents)) .toList() : _trades; } diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index 793048a6b..7923d67c1 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -187,6 +187,11 @@ abstract class DashboardViewModelBase with Store { caption: ExchangeProviderDescription.swapsXyz.title, onChanged: () => tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.swapsXyz)), + FilterItem( + value: () => tradeFilterStore.displayNearIntents, + caption: ExchangeProviderDescription.nearIntents.title, + onChanged: () => + tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.nearIntents)), ] }, subname = '', diff --git a/lib/view_model/exchange/exchange_trade_view_model.dart b/lib/view_model/exchange/exchange_trade_view_model.dart index fec47f3f3..b4e53c562 100644 --- a/lib/view_model/exchange/exchange_trade_view_model.dart +++ b/lib/view_model/exchange/exchange_trade_view_model.dart @@ -8,6 +8,7 @@ import 'package:cake_wallet/exchange/provider/chainflip_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/changenow_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/exolix_exchange_provider.dart'; +import 'package:cake_wallet/exchange/provider/near_Intents_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/swapsxyz_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/swaptrade_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/sideshift_exchange_provider.dart'; @@ -86,6 +87,9 @@ abstract class ExchangeTradeViewModelBase with Store { case ExchangeProviderDescription.swapsXyz: _provider = SwapsXyzExchangeProvider(); break; + case ExchangeProviderDescription.nearIntents: + _provider = NearIntentsExchangeProvider(); + break; } _updateItems(); diff --git a/lib/view_model/exchange/exchange_view_model.dart b/lib/view_model/exchange/exchange_view_model.dart index b598bf8bc..6f740ff3f 100644 --- a/lib/view_model/exchange/exchange_view_model.dart +++ b/lib/view_model/exchange/exchange_view_model.dart @@ -25,6 +25,7 @@ import 'package:cake_wallet/exchange/provider/letsexchange_exchange_provider.dar import 'package:cake_wallet/exchange/provider/changenow_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/exolix_exchange_provider.dart'; +import 'package:cake_wallet/exchange/provider/near_Intents_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/stealth_ex_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/swapsxyz_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/swaptrade_exchange_provider.dart'; @@ -218,6 +219,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with StealthExExchangeProvider(), XOSwapExchangeProvider(), // SwapsXyzExchangeProvider(), + NearIntentsExchangeProvider(), TrocadorExchangeProvider( useTorOnly: _useTorOnly, providerStates: _settingsStore.trocadorProviderStates), ]; diff --git a/lib/view_model/trade_details_view_model.dart b/lib/view_model/trade_details_view_model.dart index 3b42b1232..8c034cdec 100644 --- a/lib/view_model/trade_details_view_model.dart +++ b/lib/view_model/trade_details_view_model.dart @@ -6,6 +6,7 @@ import 'package:cake_wallet/exchange/provider/changenow_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/exolix_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/letsexchange_exchange_provider.dart'; +import 'package:cake_wallet/exchange/provider/near_Intents_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/swapsxyz_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/swaptrade_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/sideshift_exchange_provider.dart'; @@ -81,6 +82,9 @@ abstract class TradeDetailsViewModelBase with Store { case ExchangeProviderDescription.swapsXyz: _provider = SwapsXyzExchangeProvider(); break; + case ExchangeProviderDescription.nearIntents: + _provider = NearIntentsExchangeProvider(); + break; } _updateItems(); @@ -115,6 +119,8 @@ abstract class TradeDetailsViewModelBase with Store { return 'https://scan.chainflip.io/channels/${trade.id}'; case ExchangeProviderDescription.xoSwap: return 'https://orders.xoswap.com/${trade.id}'; + case ExchangeProviderDescription.nearIntents: + return 'https://explorer.near-intents.org/transactions/${trade.id}'; } return null; } diff --git a/tool/utils/secret_key.dart b/tool/utils/secret_key.dart index 61ccea60b..3599b5d84 100644 --- a/tool/utils/secret_key.dart +++ b/tool/utils/secret_key.dart @@ -85,6 +85,9 @@ class SecretKey { SecretKey('kryptonimApiKey', () => ''), SecretKey('walletGroupSalt', () => hex.encode(encrypt.Key.fromSecureRandom(16).bytes)), SecretKey('swapsXyzApiKey', () => ''), + SecretKey('nearIntentsBearerToken', () => ''), + SecretKey('nearIntentsAppFee', () => ''), + SecretKey('nearIntentsAppFeeRecipient', () => ''), ]; static final evmChainsSecrets = [