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 <omarh.ismail1@gmail.com>

Serhii committed Jan 9, 2026 at 20:32 UTC 9b1a62441c0ae10b8f2b878ef520842d2b4674ca
11 files changed +681 -4
assets/images/near.png
Binary files /dev/null and b/assets/images/near.png differ
lib/bitcoin/cw_bitcoin.dart
+1 -1
@@ -214,7 +214,7 @@ class CWBitcoin extends Bitcoin {
214 return estimatedTx.amount;
215 }
216
217 - final p2shAddr = sk.getPublic().toP2pkhInP2sh();
217 + final p2shAddr = sk.getPublic().toP2pkhAddress();
218 final estimatedTx = await electrumWallet.estimateSendAllTx(
219 [BitcoinOutput(address: p2shAddr, value: BigInt.zero)],
220 getFeeRate(
lib/core/trade_monitor.dart
+3
@@ -1,4 +1,5 @@
1 import 'dart:async';
2 +import 'package:cake_wallet/exchange/provider/near_Intents_exchange_provider.dart';
3 import 'package:cake_wallet/exchange/provider/simpleswap_exchange_provider.dart';
4 import 'package:cake_wallet/exchange/provider/swapsxyz_exchange_provider.dart';
5 import 'package:cake_wallet/exchange/trade.dart';
@@ -65,6 +66,8 @@ class TradeMonitor {
66 return XOSwapExchangeProvider();
67 case ExchangeProviderDescription.swapsXyz:
68 return SwapsXyzExchangeProvider();
69 + case ExchangeProviderDescription.nearIntents:
70 + return NearIntentsExchangeProvider();
71 }
72 return null;
73 }
lib/exchange/exchange_provider_description.dart
+4
@@ -38,6 +38,8 @@ class ExchangeProviderDescription extends EnumerableItem<int> with Serializable<
38 ExchangeProviderDescription(title: 'XOSwap', raw: 13, image: 'assets/images/xoswap.svg');
39 static const swapsXyz =
40 ExchangeProviderDescription(title: 'Swaps.XYZ', raw: 14, image: 'assets/images/swaps_xyz.svg');
41 + static const nearIntents =
42 + ExchangeProviderDescription(title: 'Near Intents', raw: 15, image: 'assets/images/near.png');
43
44 static ExchangeProviderDescription deserialize({required int raw}) {
45 switch (raw) {
@@ -71,6 +73,8 @@ class ExchangeProviderDescription extends EnumerableItem<int> with Serializable<
73 return xoSwap;
74 case 14:
75 return swapsXyz;
76 + case 15:
77 + return nearIntents;
78 default:
79 throw Exception('Unexpected token: $raw for ExchangeProviderDescription deserialize');
80 }
lib/exchange/provider/near_Intents_exchange_provider.dart new
+640
@@ -0,0 +1,640 @@
1 +import 'dart:convert';
2 +
3 +import 'package:cake_wallet/.secrets.g.dart' as secrets;
4 +import 'package:cake_wallet/core/utilities.dart';
5 +import 'package:cake_wallet/exchange/exchange_provider_description.dart';
6 +import 'package:cake_wallet/exchange/limits.dart';
7 +import 'package:cake_wallet/exchange/provider/exchange_provider.dart';
8 +import 'package:cake_wallet/exchange/trade.dart';
9 +import 'package:cake_wallet/exchange/trade_not_created_exception.dart';
10 +import 'package:cake_wallet/exchange/trade_request.dart';
11 +import 'package:cake_wallet/exchange/trade_state.dart';
12 +import 'package:cake_wallet/exchange/utils/currency_pairs_utils.dart';
13 +import 'package:cw_core/amount_converter.dart';
14 +import 'package:cw_core/utils/proxy_wrapper.dart';
15 +import 'package:cw_core/crypto_currency.dart';
16 +import 'package:cw_core/utils/print_verbose.dart';
17 +import 'package:cake_wallet/utils/exchange_provider_logger.dart';
18 +
19 +class NearIntentsExchangeProvider extends ExchangeProvider {
20 + NearIntentsExchangeProvider()
21 + : super(pairList: supportedPairs(_notSupported));
22 +
23 + static const List<CryptoCurrency> _notSupported = [];
24 +
25 + static const apiKey = secrets.nearIntentsBearerToken;
26 + static const _baseUrl = '1click.chaindefuser.com';
27 + static const _versionPath = '/v0';
28 + static const _tokenPath = '/tokens';
29 + static const _quotePath = '/quote';
30 + static const _statusPath = '/status';
31 +
32 + static const _slippageTolerance = 100; // 1%
33 + static const _appFeesNearIntents = secrets.nearIntentsAppFee;
34 + static const _appFeeRecipientNearIntents = secrets.nearIntentsAppFeeRecipient;
35 +
36 + static const _memoRequiredCurrencies = <CryptoCurrency>[
37 + CryptoCurrency.xrp,
38 + CryptoCurrency.xlm,
39 + CryptoCurrency.ton,
40 + ];
41 +
42 + /// Use these only for quote/rate testing (dummy data).
43 + static const Map<String, String> kNearDummyAddresses = {
44 + // UTXO
45 + 'LTC': 'ltc1qhdwz74m3wuuhppv2mckagqk9e2e49z5j4kucnv',
46 + 'BTC': 'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kygt080',
47 + 'ZZEC': 't1Zcash4hPS2bq8rTQkN9bKpZV1X4W3pZxK',
48 + 'DOGE': 'D9t7rGQ9mE3hJ2z1w8pGQxkGmKjYwYc8pQ',
49 + 'BCH': 'qpm2qsznhks23z7629mms6s4cwef74vcwvy22gdx6a',
50 +
51 + // EVM (same address works for all EVM chains)
52 + 'ETH': '0x1111111111111111111111111111111111111111',
53 + 'BSC': '0x1111111111111111111111111111111111111111',
54 + 'POL': '0x1111111111111111111111111111111111111111',
55 + 'AVAXC': '0x1111111111111111111111111111111111111111',
56 + 'ARB': '0x1111111111111111111111111111111111111111',
57 + 'BASE': '0x1111111111111111111111111111111111111111',
58 +
59 + // Others
60 + 'SOL': '11111111111111111111111111111111',
61 + 'XRP': 'rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe',
62 + 'TRX': 'T9yD14Nj9j7xAB4dbGeiX9h8unkKHxuWwb',
63 + 'TON': 'UQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM9c',
64 + 'XLM': 'GA3D5O2W7YQZJQ3H4Y5QW6N7V8X9Z0A1B2C3D4E5F6G7H8I9J0',
65 + 'ADA': 'addr1vyc33hdv5vag52f3d8h0qsngu52vm27x28zkzru333jma9gaxd38v',
66 + };
67 +
68 + String getNearDummyAddress(CryptoCurrency currency) {
69 + final tag = (currency.tag ?? '').trim().toUpperCase();
70 + final title = currency.title.toUpperCase();
71 + final key = tag.isEmpty ? title : tag;
72 + return kNearDummyAddresses[key] ?? '';
73 + }
74 +
75 + static final Map<String, String> _headers = {
76 + 'Accept': 'application/json',
77 + 'Content-Type': 'application/json',
78 + 'Authorization': '$apiKey',
79 + };
80 +
81 + static final _supportedTokensList = <Token>[];
82 +
83 + @override
84 + String get title => 'Near Intents';
85 +
86 + @override
87 + bool get isAvailable => true;
88 +
89 + @override
90 + bool get isEnabled => true;
91 +
92 + @override
93 + bool get supportsFixedRate => true;
94 +
95 + @override
96 + ExchangeProviderDescription get description =>
97 + ExchangeProviderDescription.nearIntents;
98 +
99 + @override
100 + Future<bool> checkIsAvailable() async => true;
101 +
102 + @override
103 + Future<Limits> fetchLimits(
104 + {required CryptoCurrency from,
105 + required CryptoCurrency to,
106 + required bool isFixedRateMode}) async {
107 + final tokens = await _geSupportedTokens();
108 + final originToken = currencyToNearAssetId(from, tokens);
109 + final destinationToken = currencyToNearAssetId(to, tokens);
110 +
111 + if (originToken == null || destinationToken == null) {
112 + throw Exception(
113 + 'fetchLimits: unsupported currency pair: ${from.title} ${from.tag ?? ''} to ${to.title} ${to.tag ?? ''}');
114 + }
115 +
116 + return Limits(min: null, max: null);
117 + }
118 +
119 + @override
120 + Future<double> fetchRate({
121 + required CryptoCurrency from,
122 + required CryptoCurrency to,
123 + required double amount,
124 + required bool isFixedRateMode,
125 + required bool isReceiveAmount,
126 + }) async {
127 + final tokens = await _geSupportedTokens();
128 + final originToken = currencyToNearAssetId(from, tokens);
129 + final destinationToken = currencyToNearAssetId(to, tokens);
130 +
131 + try {
132 + if (originToken == null || destinationToken == null) {
133 + throw Exception('fetchRate: Unsupported currency pair');
134 + }
135 +
136 + final formattedAmount = AmountConverter.toBaseUnits(amount.toString(),
137 + isFixedRateMode ? destinationToken.decimals : originToken.decimals);
138 +
139 + final dummyAddrFrom = getNearDummyAddress(from);
140 + final dummyAddrTo = getNearDummyAddress(to);
141 +
142 + final depositMode = _memoRequiredCurrencies.contains(from) ? "MEMO" : "SIMPLE";
143 +
144 + final quote = await getSwapQuote(
145 + dry: true,
146 + isFixedRateMode: isFixedRateMode,
147 + originAsset: originToken.assetId,
148 + destinationAsset: destinationToken.assetId,
149 + amount: formattedAmount,
150 + depositMode: depositMode,
151 + refundTo: dummyAddrFrom,
152 + recipient: dummyAddrTo,
153 + );
154 +
155 + if (quote == null) {
156 + throw Exception('fetchRate: Quote returned null');
157 + }
158 +
159 + final q = quote['quote'] as Map<String, dynamic>;
160 + final amountIn =
161 + double.tryParse(q['amountInFormatted']?.toString() ?? '0') ?? 0.0;
162 + final amountOut =
163 + double.tryParse(q['amountOutFormatted']?.toString() ?? '0') ?? 0.0;
164 +
165 + if (amountIn == 0) return 0.0;
166 +
167 + final rate = amountOut / amountIn;
168 +
169 + ExchangeProviderLogger.logSuccess(
170 + provider: description,
171 + function: 'fetchRate',
172 + requestData: {
173 + 'from': from.title,
174 + 'to': to.title,
175 + 'amount': amount,
176 + 'formattedAmount': formattedAmount,
177 + 'isFixedRateMode': isFixedRateMode,
178 + 'isReceiveAmount': isReceiveAmount,
179 + 'originAsset': originToken.assetId,
180 + 'destinationAsset': destinationToken.assetId,
181 + },
182 + responseData: {
183 + 'amountIn': amountIn,
184 + 'amountOut': amountOut,
185 + 'rate': rate,
186 + },
187 + );
188 +
189 + return rate;
190 + } catch (e, s) {
191 + ExchangeProviderLogger.logError(
192 + provider: description,
193 + function: 'fetchRate',
194 + error: e,
195 + stackTrace: s,
196 + requestData: {
197 + 'from': from.title,
198 + 'to': to.title,
199 + 'amount': amount,
200 + 'isFixedRateMode': isFixedRateMode,
201 + 'isReceiveAmount': isReceiveAmount,
202 + },
203 + );
204 + printV(e.toString());
205 + return 0.0;
206 + }
207 + }
208 +
209 + @override
210 + Future<Trade> createTrade({
211 + required TradeRequest request,
212 + required bool isFixedRateMode,
213 + required bool isSendAll,
214 + }) async {
215 + try {
216 + final tokens = await _geSupportedTokens();
217 + final originToken = currencyToNearAssetId(request.fromCurrency, tokens);
218 + final destinationToken =
219 + currencyToNearAssetId(request.toCurrency, tokens);
220 +
221 + if (originToken == null || destinationToken == null) {
222 + throw Exception('Unsupported currency pair');
223 + }
224 +
225 + final rawAmountStr =
226 + isFixedRateMode ? request.toAmount : request.fromAmount;
227 +
228 + final baseAmount = AmountConverter.toBaseUnits(
229 + rawAmountStr,
230 + isFixedRateMode
231 + ? request.toCurrency.decimals
232 + : request.fromCurrency.decimals,
233 + );
234 +
235 + final depositMode = _memoRequiredCurrencies.contains(request.fromCurrency) ? "MEMO" : "SIMPLE";
236 +
237 + final quote = await getSwapQuote(
238 + dry: false,
239 + isFixedRateMode: isFixedRateMode,
240 + originAsset: originToken.assetId,
241 + destinationAsset: destinationToken.assetId,
242 + depositMode: depositMode,
243 + amount: baseAmount,
244 + refundTo: request.refundAddress,
245 + recipient: request.toAddress,
246 + );
247 +
248 + if (quote == null) {
249 + throw Exception('Quote request failed');
250 + }
251 +
252 + final quoteObj = quote['quote'] as Map<String, dynamic>;
253 + final depositAddress = quoteObj['depositAddress'] as String;
254 + final depositMemo = quoteObj['depositMemo'] as String?;
255 +
256 + final quoteRequest = quote['quoteRequest'] as Map<String, dynamic>;
257 + final fromAssetId = quoteRequest['originAsset'] as String;
258 + final toAssetId = quoteRequest['destinationAsset'] as String;
259 +
260 + final fromCurrency = _nearAssetIdToCurrency(fromAssetId, tokens);
261 + if (fromCurrency == null) {
262 + throw Exception('Failed to parse from currency from assetId: $fromAssetId');
263 + }
264 +
265 + final toCurrency = _nearAssetIdToCurrency(toAssetId, tokens);
266 + if (toCurrency == null) {
267 + throw Exception('Failed to parse to currency from assetId: $toAssetId');
268 + }
269 +
270 + final from = CryptoCurrency.safeParseCurrencyFromString(fromCurrency.$1,
271 + tag: fromCurrency.$2);
272 + final to = CryptoCurrency.safeParseCurrencyFromString(toCurrency.$1,
273 + tag: toCurrency.$2);
274 +
275 +
276 + final trade = Trade(
277 + id: depositAddress,
278 + // Using deposit address as trade ID
279 + from: from,
280 + to: to,
281 + provider: description,
282 + providerName: title,
283 + state: TradeState.created,
284 + createdAt: DateTime.now(),
285 + inputAddress: depositAddress,
286 + payoutAddress: request.toAddress,
287 + refundAddress: request.refundAddress,
288 + amount: request.fromAmount,
289 + receiveAmount: quoteObj['amountOutFormatted']?.toString(),
290 + memo: depositMemo,
291 + isSendAll: isSendAll,
292 + userCurrencyFromRaw:
293 + '${request.fromCurrency.title}_${request.fromCurrency.tag ?? ''}',
294 + userCurrencyToRaw:
295 + '${request.toCurrency.title}_${request.toCurrency.tag ?? ''}',
296 + );
297 +
298 + ExchangeProviderLogger.logSuccess(
299 + provider: description,
300 + function: 'createTrade',
301 + requestData: {
302 + 'from': request.fromCurrency.title,
303 + 'to': request.toCurrency.title,
304 + 'fromAmount': request.fromAmount,
305 + 'toAmount': request.toAmount,
306 + 'refundAddress': request.refundAddress,
307 + 'recipient': request.toAddress,
308 + 'isFixedRateMode': isFixedRateMode,
309 + 'isSendAll': isSendAll,
310 + 'originAsset': originToken.assetId,
311 + 'destinationAsset': destinationToken.assetId,
312 + },
313 + responseData: {
314 + 'correlationId': quote['correlationId'],
315 + 'depositAddress': depositAddress,
316 + 'depositMemo': depositMemo,
317 + 'quote': quoteObj,
318 + },
319 + );
320 +
321 + return trade;
322 + } catch (e, s) {
323 + ExchangeProviderLogger.logError(
324 + provider: description,
325 + function: 'createTrade',
326 + error: e,
327 + stackTrace: s,
328 + requestData: {
329 + 'from': request.fromCurrency.title,
330 + 'to': request.toCurrency.title,
331 + 'fromAmount': request.fromAmount,
332 + 'toAmount': request.toAmount,
333 + 'refundAddress': request.refundAddress,
334 + 'recipient': request.toAddress,
335 + 'isFixedRateMode': isFixedRateMode,
336 + 'isSendAll': isSendAll,
337 + },
338 + );
339 + throw TradeNotCreatedException(description, description: e.toString());
340 + }
341 + }
342 +
343 + @override
344 + Future<Trade> findTradeById({required String id}) async {
345 + final param = {'depositAddress': id};
346 + final uri = Uri.https(_baseUrl, '$_versionPath$_statusPath', param);
347 +
348 + final response =
349 + await ProxyWrapper().get(clearnetUri: uri, headers: _headers);
350 +
351 + if (response.statusCode != 200) {
352 + throw Exception(
353 + 'Near Intents fetch trade failed: ${response.statusCode} ${response.body}',
354 + );
355 + }
356 +
357 + final data = jsonDecode(response.body) as Map<String, dynamic>;
358 + final statusRaw = (data['status'] as String?) ?? 'UNKNOWN';
359 +
360 + final quoteResponse = data['quoteResponse'] as Map<String, dynamic>? ?? {};
361 + final quoteRequest =
362 + quoteResponse['quoteRequest'] as Map<String, dynamic>? ?? {};
363 +
364 + final refundTo = quoteRequest['refundTo'] as String? ?? '';
365 + final recipient = quoteRequest['recipient'] as String? ?? '';
366 +
367 + // Parsing 'from' currency
368 + final originAssetId = quoteRequest['originAsset'] as String? ?? '';
369 + final from =
370 + _nearAssetIdToCurrency(originAssetId, await _geSupportedTokens());
371 +
372 + CryptoCurrency? coinFrom;
373 + CryptoCurrency? coinTo;
374 +
375 + if (from != null) {
376 + coinFrom = CryptoCurrency.safeParseCurrencyFromString(from.$1, tag: from.$2);
377 + }
378 +
379 + // Parsing 'to' currency
380 + final destinationAssetId =
381 + quoteRequest['destinationAsset'] as String? ?? '';
382 +
383 + final to =
384 + _nearAssetIdToCurrency(destinationAssetId, await _geSupportedTokens());
385 +
386 + if (to != null) {
387 + coinTo = CryptoCurrency.safeParseCurrencyFromString(to.$1, tag: to.$2);
388 + }
389 +
390 + final quote = quoteResponse['quote'] as Map<String, dynamic>? ?? {};
391 + final swap = data['swapDetails'] as Map<String, dynamic>? ?? {};
392 +
393 + final depositAddress = quote['depositAddress'] as String?;
394 + final depositMemo = quote['depositMemo'] as String?;
395 +
396 + final depositAmount = swap['amountInFormatted']?.toString() ??
397 + quote['amountInFormatted']?.toString() ??
398 + '0';
399 +
400 + final receiveAmount = swap['amountOutFormatted']?.toString() ??
401 + quote['amountOutFormatted']?.toString();
402 +
403 + final originTxHash = (swap['originChainTxHashes'] as List?)
404 + ?.firstOrNull?['hash']
405 + ?.toString();
406 +
407 + return Trade(
408 + id: id,
409 + from: coinFrom,
410 + to: coinTo,
411 + provider: description,
412 + inputAddress: depositAddress,
413 + payoutAddress: recipient,
414 + refundAddress: refundTo,
415 + amount: depositAmount,
416 + receiveAmount: receiveAmount,
417 + state: _normalizeStatusToTradeState(statusRaw),
418 + txId: originTxHash,
419 + extraId: depositMemo,
420 + isRefund: statusRaw == 'REFUNDED',
421 + userCurrencyFromRaw: '${from?.$1.toUpperCase()}' + '_' + '${from?.$2?.toUpperCase() ?? ''}',
422 + userCurrencyToRaw: '${to?.$1.toUpperCase()}' + '_' + '${to?.$2?.toUpperCase() ?? ''}',
423 + );
424 + }
425 +
426 + // Load & cache supported tokens
427 + Future<List<Token>> _geSupportedTokens() async {
428 + if (_supportedTokensList.isNotEmpty) return _supportedTokensList;
429 +
430 + try {
431 + final uri = Uri.https(_baseUrl, '$_versionPath$_tokenPath');
432 +
433 + final response =
434 + await ProxyWrapper().get(clearnetUri: uri, headers: _headers);
435 + if (response.statusCode != 200) return [];
436 +
437 + final data = json.decode(response.body) as List<dynamic>;
438 + _supportedTokensList
439 + ..clear()
440 + ..addAll(data.map((e) => Token.fromJson(e as Map<String, dynamic>)));
441 + return _supportedTokensList;
442 + } catch (e) {
443 + printV(e);
444 + return [];
445 + }
446 + }
447 +
448 + Future<Map<String, dynamic>?> getSwapQuote({
449 + required bool dry,
450 + required bool isFixedRateMode,
451 + required String originAsset,
452 + required String destinationAsset,
453 + required String amount,
454 + required String refundTo,
455 + required String recipient,
456 + required String depositMode,
457 + List<String>? connectedWallets,
458 + String? sessionId,
459 + String? virtualChainRecipient,
460 + String? virtualChainRefundRecipient,
461 + String? customRecipientMsg,
462 + String? deadline,
463 + String? referral,
464 + int? quoteWaitingTimeMs,
465 + }) async {
466 + final swapType = isFixedRateMode ? 'EXACT_OUTPUT' : 'EXACT_INPUT';
467 + final _isoUtcDeadline = _buildDeadline();
468 + final appFees = [
469 + {
470 + "recipient": _appFeeRecipientNearIntents,
471 + "fee": _appFeesNearIntents,
472 + }
473 + ];
474 +
475 + final uri = Uri.https(_baseUrl, "$_versionPath$_quotePath");
476 +
477 + final payload = {
478 + "dry": dry,
479 + "depositMode": depositMode,
480 + "swapType": swapType,
481 + "slippageTolerance": _slippageTolerance,
482 + "originAsset": originAsset,
483 + "depositType": 'ORIGIN_CHAIN',
484 + "destinationAsset": destinationAsset,
485 + "amount": amount,
486 + "refundTo": refundTo,
487 + "refundType": 'ORIGIN_CHAIN',
488 + "recipient": recipient,
489 + "recipientType": 'DESTINATION_CHAIN',
490 + "deadline": _isoUtcDeadline,
491 + if (connectedWallets != null) "connectedWallets": connectedWallets,
492 + if (sessionId != null) "sessionId": sessionId,
493 + if (virtualChainRecipient != null)
494 + "virtualChainRecipient": virtualChainRecipient,
495 + if (virtualChainRefundRecipient != null)
496 + "virtualChainRefundRecipient": virtualChainRefundRecipient,
497 + if (customRecipientMsg != null) "customRecipientMsg": customRecipientMsg,
498 + if (deadline != null) "deadline": deadline,
499 + if (referral != null) "referral": referral,
500 + if (quoteWaitingTimeMs != null) "quoteWaitingTimeMs": quoteWaitingTimeMs,
501 + "appFees": appFees,
502 + };
503 +
504 + try {
505 + final response = await ProxyWrapper().post(
506 + clearnetUri: uri,
507 + headers: _headers,
508 + body: jsonEncode(payload),
509 + );
510 +
511 + if (response.statusCode != 201) {
512 + printV("Quote request failed with status: ${response.statusCode}");
513 + return null;
514 + }
515 +
516 + return jsonDecode(response.body) as Map<String, dynamic>;
517 + } catch (e) {
518 + printV("Quote error: $e");
519 + return null;
520 + }
521 + }
522 +
523 + TradeState _normalizeStatusToTradeState(String status) {
524 + return switch (status.toUpperCase()) {
525 + 'PENDING_DEPOSIT' => TradeState.pending,
526 + 'PROCESSING' => TradeState.processing,
527 + 'SUCCESS' => TradeState.success,
528 + 'INCOMPLETE_DEPOSIT' => TradeState.underpaid,
529 + 'REFUNDED' => TradeState.refunded,
530 + 'FAILED' => TradeState.failed,
531 + _ => TradeState.notFound,
532 + };
533 + }
534 +
535 + String _normalizeTitleToNearSymbol(CryptoCurrency currency) {
536 + final title = currency.title.toUpperCase();
537 + return switch (title) {
538 + 'TZEC' => 'ZEC',
539 + _ => title,
540 + };
541 + }
542 +
543 + String? _normalizeTagToNearBlockchain(String? tag) {
544 + return switch (tag) {
545 + 'TRX' => 'tron',
546 + 'AVAXC' => 'avax',
547 + _ => tag?.toLowerCase(),
548 + };
549 + }
550 +
551 + String? _normalizeNearBlockchainToTag(String? blockchain) {
552 + return switch (blockchain) {
553 + 'tron' => 'TRX',
554 + 'avax' => 'AVAXC',
555 + _ => blockchain?.toUpperCase(),
556 + };
557 + }
558 +
559 + Token? currencyToNearAssetId(CryptoCurrency currency, List<Token> supported) {
560 + if (supported.isEmpty) return null;
561 +
562 + final symbol = _normalizeTitleToNearSymbol(currency).toUpperCase();
563 + final blockchain = _normalizeTagToNearBlockchain(currency.tag);
564 +
565 + // Native asset (no contract)
566 + final native = supported.firstWhereOrNull((t) =>
567 + t.symbol.toUpperCase() == symbol &&
568 + (blockchain == null || t.blockchain.toLowerCase() == blockchain) &&
569 + t.contractAddress == null);
570 +
571 + if (native != null) {
572 + return native;
573 + }
574 +
575 + final token = supported.firstWhereOrNull((t) =>
576 + t.symbol.toUpperCase() == symbol &&
577 + (blockchain == null || t.blockchain.toLowerCase() == blockchain));
578 +
579 + return token;
580 + }
581 +
582 + (String, String?)? _nearAssetIdToCurrency(
583 + String assetId, List<Token> supported) {
584 + if (supported.isEmpty) return null;
585 +
586 + final token = supported.firstWhereOrNull((t) => t.assetId == assetId);
587 +
588 + if (token == null) return null;
589 + final title = token.symbol;
590 + final normalizedNetwork = _normalizeNearBlockchainToTag(token.blockchain);
591 + final tag =
592 + normalizedNetwork == title.toUpperCase() ? null : normalizedNetwork;
593 +
594 + return (title, tag);
595 + }
596 +
597 + String _buildDeadline() {
598 + return DateTime.now()
599 + .toUtc()
600 + .add(const Duration(hours: 2))
601 + .toIso8601String()
602 + .replaceFirst(RegExp(r'\.\d+Z$'), 'Z');
603 + }
604 +}
605 +
606 +class Token {
607 + final String assetId;
608 + final int decimals;
609 + final String blockchain;
610 + final String symbol;
611 + final double priceUsd;
612 + final String? priceUpdatedAt;
613 + final String? contractAddress;
614 +
615 + Token({
616 + required this.assetId,
617 + required this.decimals,
618 + required this.blockchain,
619 + required this.symbol,
620 + required this.priceUsd,
621 + required this.priceUpdatedAt,
622 + required this.contractAddress,
623 + });
624 +
625 + factory Token.fromJson(Map<String, dynamic> json) {
626 + final decimals = json['decimals'] as int?;
627 + if (decimals == null) {
628 + throw Exception('Token decimals is null for assetId: ${json['assetId']}');
629 + }
630 + return Token(
631 + assetId: json['assetId'] as String,
632 + decimals: json['decimals'] as int,
633 + blockchain: json['blockchain'] as String,
634 + symbol: json['symbol'] as String,
635 + priceUsd: (json['price'] as num?)?.toDouble() ?? 0.0,
636 + priceUpdatedAt: json['priceUpdatedAt'] as String?,
637 + contractAddress: json['contractAddress'] as String?,
638 + );
639 + }
640 +}
lib/store/dashboard/trade_filter_store.dart
+13 -3
@@ -22,7 +22,8 @@ abstract class TradeFilterStoreBase with Store {
22 displayStealthEx = true,
23 displayXOSwap = true,
24 displaySwapTrade = true,
25 - displaySwapXyz = true;
25 + displaySwapXyz = true,
26 + displayNearIntents = true;
27
28 @observable
29 bool displayXMRTO;
@@ -65,6 +66,8 @@ abstract class TradeFilterStoreBase with Store {
66
67 @observable
68 bool displaySwapXyz;
69 + @observable
70 + bool displayNearIntents;
71
72 @computed
73 bool get displayAllTrades =>
@@ -79,7 +82,8 @@ abstract class TradeFilterStoreBase with Store {
82 displayStealthEx &&
83 displayXOSwap &&
84 displaySwapTrade &&
82 - displaySwapXyz;
85 + displaySwapXyz &&
86 + displayNearIntents;
87
88 @action
89 void toggleDisplayExchange(ExchangeProviderDescription provider) {
@@ -126,6 +130,9 @@ abstract class TradeFilterStoreBase with Store {
130 case ExchangeProviderDescription.swapsXyz:
131 displaySwapXyz = !displaySwapXyz;
132 break;
133 + case ExchangeProviderDescription.nearIntents:
134 + displayNearIntents = !displayNearIntents;
135 + break;
136 case ExchangeProviderDescription.all:
137 if (displayAllTrades) {
138 displayChangeNow = false;
@@ -142,6 +149,7 @@ abstract class TradeFilterStoreBase with Store {
149 displayXOSwap = false;
150 displaySwapTrade = false;
151 displaySwapXyz = false;
152 + displayNearIntents = false;
153 } else {
154 displayChangeNow = true;
155 displaySideShift = true;
@@ -157,6 +165,7 @@ abstract class TradeFilterStoreBase with Store {
165 displayXOSwap = true;
166 displaySwapTrade = true;
167 displaySwapXyz = true;
168 + displayNearIntents = true;
169 }
170 break;
171 }
@@ -192,7 +201,8 @@ abstract class TradeFilterStoreBase with Store {
201 (displayXOSwap && item.trade.provider == ExchangeProviderDescription.xoSwap) ||
202 (displaySwapTrade && item.trade.provider == ExchangeProviderDescription.swapTrade) ||
203 (displaySwapXyz &&
195 - item.trade.provider == ExchangeProviderDescription.swapsXyz))
204 + item.trade.provider == ExchangeProviderDescription.swapsXyz) ||
205 + (displayNearIntents && item.trade.provider == ExchangeProviderDescription.nearIntents))
206 .toList()
207 : _trades;
208 }
lib/view_model/dashboard/dashboard_view_model.dart
+5
@@ -187,6 +187,11 @@ abstract class DashboardViewModelBase with Store {
187 caption: ExchangeProviderDescription.swapsXyz.title,
188 onChanged: () =>
189 tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.swapsXyz)),
190 + FilterItem(
191 + value: () => tradeFilterStore.displayNearIntents,
192 + caption: ExchangeProviderDescription.nearIntents.title,
193 + onChanged: () =>
194 + tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.nearIntents)),
195 ]
196 },
197 subname = '',
lib/view_model/exchange/exchange_trade_view_model.dart
+4
@@ -8,6 +8,7 @@ import 'package:cake_wallet/exchange/provider/chainflip_exchange_provider.dart';
8 import 'package:cake_wallet/exchange/provider/changenow_exchange_provider.dart';
9 import 'package:cake_wallet/exchange/provider/exchange_provider.dart';
10 import 'package:cake_wallet/exchange/provider/exolix_exchange_provider.dart';
11 +import 'package:cake_wallet/exchange/provider/near_Intents_exchange_provider.dart';
12 import 'package:cake_wallet/exchange/provider/swapsxyz_exchange_provider.dart';
13 import 'package:cake_wallet/exchange/provider/swaptrade_exchange_provider.dart';
14 import 'package:cake_wallet/exchange/provider/sideshift_exchange_provider.dart';
@@ -86,6 +87,9 @@ abstract class ExchangeTradeViewModelBase with Store {
87 case ExchangeProviderDescription.swapsXyz:
88 _provider = SwapsXyzExchangeProvider();
89 break;
90 + case ExchangeProviderDescription.nearIntents:
91 + _provider = NearIntentsExchangeProvider();
92 + break;
93 }
94
95 _updateItems();
lib/view_model/exchange/exchange_view_model.dart
+2
@@ -25,6 +25,7 @@ import 'package:cake_wallet/exchange/provider/letsexchange_exchange_provider.dar
25 import 'package:cake_wallet/exchange/provider/changenow_exchange_provider.dart';
26 import 'package:cake_wallet/exchange/provider/exchange_provider.dart';
27 import 'package:cake_wallet/exchange/provider/exolix_exchange_provider.dart';
28 +import 'package:cake_wallet/exchange/provider/near_Intents_exchange_provider.dart';
29 import 'package:cake_wallet/exchange/provider/stealth_ex_exchange_provider.dart';
30 import 'package:cake_wallet/exchange/provider/swapsxyz_exchange_provider.dart';
31 import 'package:cake_wallet/exchange/provider/swaptrade_exchange_provider.dart';
@@ -218,6 +219,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
219 StealthExExchangeProvider(),
220 XOSwapExchangeProvider(),
221 // SwapsXyzExchangeProvider(),
222 + NearIntentsExchangeProvider(),
223 TrocadorExchangeProvider(
224 useTorOnly: _useTorOnly, providerStates: _settingsStore.trocadorProviderStates),
225 ];
lib/view_model/trade_details_view_model.dart
+6
@@ -6,6 +6,7 @@ import 'package:cake_wallet/exchange/provider/changenow_exchange_provider.dart';
6 import 'package:cake_wallet/exchange/provider/exchange_provider.dart';
7 import 'package:cake_wallet/exchange/provider/exolix_exchange_provider.dart';
8 import 'package:cake_wallet/exchange/provider/letsexchange_exchange_provider.dart';
9 +import 'package:cake_wallet/exchange/provider/near_Intents_exchange_provider.dart';
10 import 'package:cake_wallet/exchange/provider/swapsxyz_exchange_provider.dart';
11 import 'package:cake_wallet/exchange/provider/swaptrade_exchange_provider.dart';
12 import 'package:cake_wallet/exchange/provider/sideshift_exchange_provider.dart';
@@ -81,6 +82,9 @@ abstract class TradeDetailsViewModelBase with Store {
82 case ExchangeProviderDescription.swapsXyz:
83 _provider = SwapsXyzExchangeProvider();
84 break;
85 + case ExchangeProviderDescription.nearIntents:
86 + _provider = NearIntentsExchangeProvider();
87 + break;
88 }
89
90 _updateItems();
@@ -115,6 +119,8 @@ abstract class TradeDetailsViewModelBase with Store {
119 return 'https://scan.chainflip.io/channels/${trade.id}';
120 case ExchangeProviderDescription.xoSwap:
121 return 'https://orders.xoswap.com/${trade.id}';
122 + case ExchangeProviderDescription.nearIntents:
123 + return 'https://explorer.near-intents.org/transactions/${trade.id}';
124 }
125 return null;
126 }
tool/utils/secret_key.dart
+3
@@ -85,6 +85,9 @@ class SecretKey {
85 SecretKey('kryptonimApiKey', () => ''),
86 SecretKey('walletGroupSalt', () => hex.encode(encrypt.Key.fromSecureRandom(16).bytes)),
87 SecretKey('swapsXyzApiKey', () => ''),
88 + SecretKey('nearIntentsBearerToken', () => ''),
89 + SecretKey('nearIntentsAppFee', () => ''),
90 + SecretKey('nearIntentsAppFeeRecipient', () => ''),
91 ];
92
93 static final evmChainsSecrets = [