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 = [