@rabbitio/ui-kit 1.0.0-beta.11 → 1.0.0-beta.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,798 @@
1
+ import axios from "axios";
2
+ import { BigNumber } from "bignumber.js";
3
+
4
+ import { AmountUtils } from "../../common/amountUtils.js";
5
+ import { improveAndRethrow } from "../../common/errorUtils.js";
6
+ import { SwapProvider } from "./swapProvider.js";
7
+ import { ExistingSwap } from "../models/existingSwap.js";
8
+ import { Coin } from "../../common/models/coin.js";
9
+ import { Protocol } from "../../common/models/protocol.js";
10
+ import { safeStringify } from "../../common/utils/safeStringify.js";
11
+ import { Logger } from "../../common/utils/logging/logger.js";
12
+
13
+ export const BANNED_PARTNERS = ["stealthex", "changee", "coincraddle"];
14
+ const FALLBACK_ICON_URL = "https://rabbit.io/asset-icons/fallback.svg";
15
+
16
+ export class SwapspaceSwapProvider extends SwapProvider {
17
+ constructor(
18
+ apiKeysProxyUrl,
19
+ cache,
20
+ customCoinBuilder = (coin, network) => null,
21
+ useRestrictedCoinsSet = true
22
+ ) {
23
+ super();
24
+ this._supportedCoins = [];
25
+ this._URL = `${apiKeysProxyUrl}/swapspace`;
26
+ this._maxRateDigits = 20;
27
+ this.useRestrictedCoinsSet = useRestrictedCoinsSet;
28
+ this._customCoinBuilder = customCoinBuilder;
29
+ this._cache = cache;
30
+ }
31
+
32
+ getSwapCreationInfoTtlMs() {
33
+ /* Actually 2 minutes and only relevant for some partners, but we use it
34
+ * (and even a bit smaller value) for better consistency */
35
+ return 110000;
36
+ }
37
+
38
+ async getDepositCurrencies() {
39
+ const loggerSource = "getDepositCurrencies";
40
+ try {
41
+ await this._fetchSupportedCurrenciesIfNeeded();
42
+ Logger.log(
43
+ `We have ${this._supportedCoins?.length} supported coins, getting depositable`,
44
+ loggerSource
45
+ );
46
+ return {
47
+ result: true,
48
+ coins: this._supportedCoins
49
+ .filter((item) => item.deposit)
50
+ .map((item) => item.coin),
51
+ };
52
+ } catch (e) {
53
+ if (e?.response?.status === 429) {
54
+ return {
55
+ result: false,
56
+ reason: SwapProvider.COMMON_ERRORS.REQUESTS_LIMIT_EXCEEDED,
57
+ };
58
+ }
59
+ improveAndRethrow(e, loggerSource);
60
+ }
61
+ }
62
+
63
+ async getWithdrawalCurrencies(exceptCurrency = null) {
64
+ const loggerSource = "getWithdrawalCurrencies";
65
+ try {
66
+ await this._fetchSupportedCurrenciesIfNeeded();
67
+ Logger.log(
68
+ `We have ${this._supportedCoins?.length} supported coins, getting withdrawable`,
69
+ loggerSource
70
+ );
71
+ return {
72
+ result: true,
73
+ coins: this._supportedCoins
74
+ .filter(
75
+ (item) =>
76
+ item.withdrawal &&
77
+ (!exceptCurrency || item.coin !== exceptCurrency)
78
+ )
79
+ .map((item) => item.coin),
80
+ };
81
+ } catch (e) {
82
+ if (e?.response?.status === 429) {
83
+ return {
84
+ result: false,
85
+ reason: SwapProvider.COMMON_ERRORS.REQUESTS_LIMIT_EXCEEDED,
86
+ };
87
+ }
88
+ improveAndRethrow(e, loggerSource);
89
+ }
90
+ }
91
+
92
+ async initialize() {
93
+ await this._fetchSupportedCurrenciesIfNeeded();
94
+ }
95
+
96
+ getIconUrl(coinOrTicker) {
97
+ const loggerSource = "getIconUrl";
98
+ try {
99
+ let coin = coinOrTicker;
100
+ if (!(coinOrTicker instanceof Coin)) {
101
+ coin = this._supportedCoins.find(
102
+ (i) => i.coin.ticker === coinOrTicker
103
+ )?.coin;
104
+ }
105
+ return (
106
+ this._supportedCoins.find((item) => item.coin === coin)
107
+ ?.iconURL ?? FALLBACK_ICON_URL
108
+ );
109
+ } catch (e) {
110
+ improveAndRethrow(e, loggerSource);
111
+ }
112
+ }
113
+
114
+ async _fetchSupportedCurrenciesIfNeeded() {
115
+ const loggerSource = "_fetchSupportedCurrenciesIfNeeded";
116
+ try {
117
+ if (!this._supportedCoins?.length) {
118
+ const rawResponse = await axios.get(
119
+ `${this._URL}/api/v2/currencies`
120
+ );
121
+ Logger.log(
122
+ `Retrieved ${rawResponse?.data?.length} currencies`,
123
+ loggerSource
124
+ );
125
+ this._supportedCoins = (rawResponse?.data ?? [])
126
+ .map((item) => {
127
+ let coin = this._customCoinBuilder(
128
+ item.code,
129
+ item.network
130
+ );
131
+ if (!coin && !this.useRestrictedCoinsSet) {
132
+ /** Building coin object for coin that isn't supported OOB in Rabbit.
133
+ * We are doing this way to be able to use extended coins set for swaps.
134
+ * These temporary built coins are only for in-swap use, and we omit some usual
135
+ * coin details here.
136
+ * Ideally we should add some new abstractions e.g. BaseCoin:
137
+ * Coin will extend BaseCoin, SwapCoin will extend BaseCoin etc.
138
+ * But for now it is reasonable to use this simpler approach.
139
+ */
140
+ const code = item.code.toUpperCase();
141
+ const network = item.network.toUpperCase();
142
+ const ticker = `${code}${
143
+ code === network ? "" : network
144
+ }`;
145
+ const defaultDecimalPlacesForCoinNotSupportedOOB = 8;
146
+ const defaultMinConfirmationsForCoinNotSupportedOOB = 1;
147
+ coin = new Coin(
148
+ item.name,
149
+ ticker,
150
+ code,
151
+ defaultDecimalPlacesForCoinNotSupportedOOB,
152
+ null,
153
+ "",
154
+ null,
155
+ null,
156
+ defaultMinConfirmationsForCoinNotSupportedOOB,
157
+ null,
158
+ [],
159
+ 60000,
160
+ null, // We cannot recognize blockchain from swapspace data
161
+ new Protocol(network), // TODO: [dev] maybe we should recognize standard protocols?
162
+ item.contractAddress || null,
163
+ false
164
+ );
165
+ }
166
+ if (coin) {
167
+ return {
168
+ coin: coin,
169
+ code: item.code,
170
+ network: item.network,
171
+ extraId: item.extraIdName,
172
+ isPopular: !!item?.popular,
173
+ iconURL: item.icon
174
+ ? `https://storage.swapspace.co${item.icon}`
175
+ : FALLBACK_ICON_URL,
176
+ deposit: item.deposit ?? false,
177
+ withdrawal: item.withdrawal ?? false,
178
+ validationRegexp: item.validationRegexp ?? null,
179
+ };
180
+ }
181
+
182
+ return [];
183
+ })
184
+ .flat();
185
+ this._putPopularCoinsFirst();
186
+ }
187
+ } catch (e) {
188
+ improveAndRethrow(e, loggerSource);
189
+ }
190
+ }
191
+
192
+ /**
193
+ * This method sort internal list putting popular (as swapspace thinks) coins to the top.
194
+ * This is just for users of this API if they don't care about the sorting - we just improve a list a bit this way.
195
+ * @private
196
+ */
197
+ _putPopularCoinsFirst() {
198
+ this._supportedCoins.sort((i1, i2) => {
199
+ if (i1.isPopular && !i2.isPopular) return -1;
200
+ if (i2.isPopular && !i1.isPopular) return 1;
201
+ return i1.coin.ticker > i2.coin.ticker
202
+ ? 1
203
+ : i1.coin.ticker < i2.coin.ticker
204
+ ? -1
205
+ : 0;
206
+ });
207
+ }
208
+
209
+ async getCoinToUSDTRate(coin) {
210
+ const loggerSource = "getCoinToUSDTRate";
211
+ try {
212
+ await this._fetchSupportedCurrenciesIfNeeded();
213
+
214
+ // Using USDT TRC20 as usually fee in this network is smaller than ERC20 and this network is widely used for USDT
215
+ const usdtTrc20 = this._supportedCoins.find(
216
+ (i) => i.code === "usdt" && i.network === "trc20"
217
+ )?.coin;
218
+ if (!usdtTrc20) {
219
+ return { result: false };
220
+ }
221
+ const cached = this._cache.get(
222
+ "swapspace_usdt_rate_" + coin.ticker
223
+ );
224
+ if (cached != null) {
225
+ return {
226
+ result: true,
227
+ rate: cached,
228
+ };
229
+ }
230
+ Logger.log(
231
+ "Loading USDT -> coin rate as not found in cache:",
232
+ coin?.ticker
233
+ );
234
+ const result = await this.getSwapInfo(usdtTrc20, coin, "5000");
235
+ if (!result.result) {
236
+ return { result: false };
237
+ }
238
+
239
+ // This calculation is not precise as we cannot recognize the actual fee and network fee. Just approximate.
240
+ const standardSwapspaceFeeMultiplier = 1.002; // usually 0.2%
241
+ const rate = BigNumber(1)
242
+ .div(
243
+ BigNumber(result.rate).times(standardSwapspaceFeeMultiplier)
244
+ )
245
+ .toString();
246
+ this._cache.put(
247
+ "swapspace_usdt_rate_" + coin.ticker,
248
+ rate,
249
+ 15 * 60000
250
+ );
251
+ return {
252
+ result: true,
253
+ rate: rate,
254
+ };
255
+ } catch (e) {
256
+ improveAndRethrow(e, loggerSource);
257
+ }
258
+ }
259
+
260
+ getCoinByTickerIfPresent(ticker) {
261
+ try {
262
+ const item = this._supportedCoins.find(
263
+ (i) => i.coin.ticker === ticker
264
+ );
265
+ return item?.coin ?? null;
266
+ } catch (e) {
267
+ improveAndRethrow(e, "getCoinByTickerIfPresent");
268
+ }
269
+ }
270
+
271
+ async getSwapInfo(fromCoin, toCoin, amountCoins, fromCoinToUsdRate = null) {
272
+ const loggerSource = "getSwapInfo";
273
+ try {
274
+ if (
275
+ !(fromCoin instanceof Coin) ||
276
+ !(toCoin instanceof Coin) ||
277
+ typeof amountCoins !== "string" ||
278
+ BigNumber(amountCoins).lt("0")
279
+ ) {
280
+ throw new Error(
281
+ `Wrong input params: ${amountCoins} ${fromCoin.ticker} -> ${toCoin.ticker}` +
282
+ (fromCoin instanceof Coin) +
283
+ (toCoin instanceof Coin)
284
+ );
285
+ }
286
+ const fromCoinSwapspaceDetails = this._supportedCoins.find(
287
+ (i) => i.coin === fromCoin
288
+ );
289
+ const toCoinSwapspaceDetails = this._supportedCoins.find(
290
+ (i) => i.coin === toCoin
291
+ );
292
+ if (!fromCoinSwapspaceDetails || !toCoinSwapspaceDetails) {
293
+ throw new Error(
294
+ "Failed to find swapspace coin details for: " +
295
+ fromCoin.ticker +
296
+ " -> " +
297
+ toCoin.ticker
298
+ );
299
+ }
300
+ /* Here we use not documented parameter 'estimated=false'. This parameter controls whether we want to use
301
+ * cached rate values stored in swapspace cache. Their support says they store at most for 30 sec.
302
+ * But we are better off using the most actual rates.
303
+ */
304
+ const response = await axios.get(
305
+ `${this._URL}/api/v2/amounts?fromCurrency=${fromCoinSwapspaceDetails.code}&fromNetwork=${fromCoinSwapspaceDetails.network}&toNetwork=${toCoinSwapspaceDetails.network}&toCurrency=${toCoinSwapspaceDetails.code}&amount=${amountCoins}&float=true&estimated=false`
306
+ );
307
+ Logger.log(
308
+ `Retrieved ${response?.data?.length} options`,
309
+ loggerSource
310
+ );
311
+ const options = Array.isArray(response.data) ? response.data : [];
312
+ const exchangesSupportingThePair = options.filter(
313
+ (exchange) =>
314
+ exchange?.exists &&
315
+ !BANNED_PARTNERS.find(
316
+ (bannedPartner) => bannedPartner === exchange?.partner
317
+ ) &&
318
+ exchange?.fixed === false &&
319
+ (exchange.min === 0 ||
320
+ exchange.max === 0 ||
321
+ exchange.max > exchange.min ||
322
+ ((typeof exchange.min !== "number" ||
323
+ typeof exchange.max !== "number") &&
324
+ exchange.toAmount > 0))
325
+ );
326
+ Logger.log(
327
+ `${exchangesSupportingThePair?.length} of them have exist=true`,
328
+ loggerSource
329
+ );
330
+ if (!exchangesSupportingThePair.length) {
331
+ return {
332
+ result: false,
333
+ reason: SwapProvider.NO_SWAPS_REASONS.NOT_SUPPORTED,
334
+ };
335
+ }
336
+ const availableExchanges = exchangesSupportingThePair.filter(
337
+ (exchange) =>
338
+ typeof exchange?.toAmount === "number" &&
339
+ exchange.toAmount > 0
340
+ );
341
+ Logger.log(
342
+ `Available (having amountTo): ${safeStringify(
343
+ availableExchanges
344
+ )}`,
345
+ loggerSource
346
+ );
347
+ // min=0 or max=0 means there is no limit for the partner
348
+ let smallestMin = null;
349
+ if (
350
+ exchangesSupportingThePair.find((ex) =>
351
+ BigNumber(ex.min).isZero()
352
+ ) == null
353
+ ) {
354
+ smallestMin = exchangesSupportingThePair.reduce((prev, cur) => {
355
+ if (
356
+ typeof cur.min === "number" &&
357
+ (prev === null || BigNumber(cur.min).lt(prev))
358
+ )
359
+ return BigNumber(cur.min);
360
+ return prev;
361
+ }, null);
362
+ }
363
+ let greatestMax = null;
364
+ if (
365
+ exchangesSupportingThePair.find((ex) =>
366
+ BigNumber(ex.max).isZero()
367
+ ) == null
368
+ ) {
369
+ greatestMax = exchangesSupportingThePair.reduce((prev, cur) => {
370
+ if (
371
+ typeof cur.max === "number" &&
372
+ (prev === null || BigNumber(cur.max).gt(prev))
373
+ )
374
+ return BigNumber(cur.max);
375
+ return prev;
376
+ }, null);
377
+ }
378
+ let extraCoinsToFitMinMax = "0";
379
+ if (
380
+ typeof fromCoinToUsdRate === "string" &&
381
+ BigNumber(fromCoinToUsdRate).gt("0")
382
+ ) {
383
+ const extraUsdToFitMinMax = BigNumber("1"); // We correct the limits as the exact limit can fluctuate and cause failed swap creation
384
+ extraCoinsToFitMinMax = AmountUtils.trim(
385
+ extraUsdToFitMinMax.div(fromCoinToUsdRate),
386
+ fromCoin.digits
387
+ );
388
+ }
389
+ if (smallestMin instanceof BigNumber) {
390
+ smallestMin = AmountUtils.trim(
391
+ smallestMin.plus(extraCoinsToFitMinMax),
392
+ fromCoin.digits
393
+ );
394
+ }
395
+ if (greatestMax instanceof BigNumber) {
396
+ if (greatestMax > extraCoinsToFitMinMax) {
397
+ greatestMax = AmountUtils.trim(
398
+ greatestMax.minus(extraCoinsToFitMinMax),
399
+ fromCoin.digits
400
+ );
401
+ } else {
402
+ greatestMax = "0";
403
+ }
404
+ }
405
+
406
+ if (availableExchanges.length) {
407
+ const sorted = availableExchanges.sort(
408
+ (op1, op2) => op2.toAmount - op1.toAmount
409
+ );
410
+ const bestOpt = sorted[0];
411
+ Logger.log(
412
+ `Returning first option after sorting: ${safeStringify(
413
+ bestOpt
414
+ )}`,
415
+ loggerSource
416
+ );
417
+ let max = null;
418
+ let min = null;
419
+ if (extraCoinsToFitMinMax != null) {
420
+ if (typeof bestOpt.max === "number" && bestOpt.max !== 0) {
421
+ max = BigNumber(bestOpt.max).minus(
422
+ extraCoinsToFitMinMax
423
+ );
424
+ max = AmountUtils.trim(
425
+ max.lt(0) ? "0" : max,
426
+ fromCoin.digits
427
+ );
428
+ }
429
+ if (typeof bestOpt.min === "number" && bestOpt.min !== 0) {
430
+ min = AmountUtils.trim(
431
+ BigNumber(bestOpt.min).plus(extraCoinsToFitMinMax),
432
+ fromCoin.digits
433
+ );
434
+ }
435
+ }
436
+
437
+ const rate =
438
+ bestOpt.toAmount && bestOpt.fromAmount
439
+ ? BigNumber(bestOpt.toAmount).div(bestOpt.fromAmount)
440
+ : null;
441
+ return {
442
+ result: true,
443
+ min: min,
444
+ max: max,
445
+ smallestMin: smallestMin,
446
+ greatestMax: greatestMax,
447
+ rate:
448
+ rate != null
449
+ ? AmountUtils.trim(rate, this._maxRateDigits)
450
+ : null,
451
+ durationMinutesRange: bestOpt.duration ?? null,
452
+ rawSwapData: bestOpt,
453
+ };
454
+ }
455
+ const result = {
456
+ result: false,
457
+ reason:
458
+ smallestMin && BigNumber(amountCoins).lt(smallestMin)
459
+ ? SwapProvider.NO_SWAPS_REASONS.TOO_LOW
460
+ : greatestMax && BigNumber(amountCoins).gt(greatestMax)
461
+ ? SwapProvider.NO_SWAPS_REASONS.TOO_HIGH
462
+ : SwapProvider.NO_SWAPS_REASONS.NOT_SUPPORTED,
463
+ smallestMin: smallestMin,
464
+ greatestMax: greatestMax,
465
+ };
466
+ Logger.log(
467
+ `Returning result ${safeStringify(result)}`,
468
+ loggerSource
469
+ );
470
+ return result;
471
+ } catch (e) {
472
+ if (e?.response?.status === 429) {
473
+ return {
474
+ result: false,
475
+ reason: SwapProvider.COMMON_ERRORS.REQUESTS_LIMIT_EXCEEDED,
476
+ };
477
+ }
478
+ Logger.log(
479
+ `Internal swapspace/rabbit error when getting swap options ${safeStringify(
480
+ e
481
+ )}`,
482
+ loggerSource
483
+ );
484
+ improveAndRethrow(e, loggerSource);
485
+ }
486
+ }
487
+
488
+ async createSwap(
489
+ fromCoin,
490
+ toCoin,
491
+ amount,
492
+ toAddress,
493
+ refundAddress,
494
+ rawSwapData,
495
+ clientIpAddress
496
+ ) {
497
+ const loggerSource = "createSwap";
498
+ const partner = rawSwapData?.partner;
499
+ try {
500
+ if (
501
+ !(fromCoin instanceof Coin) ||
502
+ !(toCoin instanceof Coin) ||
503
+ typeof amount !== "string" ||
504
+ typeof toAddress !== "string" ||
505
+ typeof refundAddress !== "string"
506
+ ) {
507
+ throw new Error(
508
+ `Invalid input: ${fromCoin} ${toCoin} ${amount} ${toAddress} ${refundAddress}`
509
+ );
510
+ }
511
+ if (
512
+ typeof partner !== "string" ||
513
+ typeof rawSwapData?.fromCurrency !== "string" ||
514
+ typeof rawSwapData?.fromNetwork !== "string" ||
515
+ typeof rawSwapData?.toCurrency !== "string" ||
516
+ typeof rawSwapData?.toNetwork !== "string" ||
517
+ typeof rawSwapData?.id !== "string" // can be just empty
518
+ ) {
519
+ throw new Error(
520
+ `Invalid raw swap data: ${safeStringify(rawSwapData)}`
521
+ );
522
+ }
523
+
524
+ await this._fetchSupportedCurrenciesIfNeeded();
525
+ const toCurrencyExtraId =
526
+ this._supportedCoins.find((item) => item.coin === toCoin)
527
+ ?.extraId ?? "";
528
+ const requestData = {
529
+ partner: partner,
530
+ fromCurrency: rawSwapData?.fromCurrency,
531
+ fromNetwork: rawSwapData?.fromNetwork,
532
+ toCurrency: rawSwapData?.toCurrency,
533
+ toNetwork: rawSwapData?.toNetwork,
534
+ address: toAddress,
535
+ amount: amount,
536
+ fixed: false,
537
+ extraId: toCurrencyExtraId ?? "",
538
+ rateId: rawSwapData?.id,
539
+ userIp: clientIpAddress,
540
+ refund: refundAddress,
541
+ };
542
+
543
+ Logger.log(
544
+ `Sending create request: ${safeStringify(requestData)}`,
545
+ loggerSource
546
+ );
547
+ const response = await axios.post(
548
+ `${this._URL}/api/v2/exchange`,
549
+ requestData
550
+ );
551
+ const result = response.data;
552
+ Logger.log(
553
+ `Creation result ${safeStringify(result)}`,
554
+ loggerSource
555
+ );
556
+
557
+ if (result?.id) {
558
+ if (
559
+ typeof result?.from?.amount !== "number" ||
560
+ typeof result?.from?.address !== "string" ||
561
+ typeof result?.to?.amount !== "number" ||
562
+ typeof result?.to?.address !== "string"
563
+ )
564
+ throw new Error(`Wrong swap creation result ${result}`);
565
+ /* We use the returned rate preferably but if the retrieved
566
+ * rate 0/null/undefined we calculate it manually */
567
+ let rate = result.rate;
568
+ if (typeof rate !== "number" || BigNumber(rate).isZero()) {
569
+ rate = BigNumber(result?.to?.amount).div(
570
+ result?.from?.amount
571
+ );
572
+ } else {
573
+ rate = BigNumber(rate);
574
+ }
575
+
576
+ return {
577
+ result: true,
578
+ swapId: result?.id,
579
+ fromCoin: fromCoin,
580
+ fromAmount: AmountUtils.trim(
581
+ result?.from?.amount,
582
+ fromCoin.digits
583
+ ),
584
+ fromAddress: result?.from?.address,
585
+ toCoin: toCoin,
586
+ toAmount: AmountUtils.trim(
587
+ result?.to?.amount,
588
+ toCoin.digits
589
+ ),
590
+ toAddress: result?.to?.address,
591
+ rate: AmountUtils.trim(rate, this._maxRateDigits),
592
+ };
593
+ }
594
+ const errorMessage = `Swap creation succeeded but the response is wrong: ${safeStringify(
595
+ response
596
+ )}`;
597
+ Logger.log(errorMessage, loggerSource);
598
+ throw new Error(errorMessage);
599
+ } catch (e) {
600
+ Logger.log(
601
+ `Failed to create swap. Error is: ${safeStringify(e)}`,
602
+ loggerSource
603
+ );
604
+ const composeFailResult = (reason) => ({
605
+ result: false,
606
+ reason: reason,
607
+ partner: partner,
608
+ });
609
+ const status = e?.response?.status;
610
+ const data = e?.response?.data;
611
+ if (status === 429) {
612
+ Logger.log(
613
+ `Returning fail - RPS limit exceeded ${data}`,
614
+ loggerSource
615
+ );
616
+ return composeFailResult(
617
+ SwapProvider.COMMON_ERRORS.REQUESTS_LIMIT_EXCEEDED
618
+ );
619
+ }
620
+ const texts422 = [
621
+ "Pair cannot be processed by",
622
+ "Currency not found",
623
+ "Amount maximum is",
624
+ "Amount minimum is",
625
+ ];
626
+ const text403 = "IP address is forbidden";
627
+ if (
628
+ typeof data === "string" &&
629
+ ((status === 403 && data.includes(text403)) ||
630
+ (status === 422 &&
631
+ texts422.find((text) => data.includes(text))))
632
+ ) {
633
+ Logger.log(
634
+ `Returning retriable fail: ${status} - ${data}, ${partner}`,
635
+ loggerSource
636
+ );
637
+ return composeFailResult(
638
+ SwapProvider.CREATION_FAIL_REASONS.RETRIABLE_FAIL
639
+ );
640
+ }
641
+ Logger.log(
642
+ `Internal swapspace/rabbit error for ${partner}: ${safeStringify(
643
+ e
644
+ )}`,
645
+ loggerSource
646
+ );
647
+ improveAndRethrow(e, loggerSource);
648
+ }
649
+ }
650
+
651
+ _mapSwapspaceStatusToRabbitStatus(status) {
652
+ switch (status) {
653
+ case "waiting":
654
+ return SwapProvider.SWAP_STATUSES.WAITING_FOR_PAYMENT;
655
+ case "confirming":
656
+ return SwapProvider.SWAP_STATUSES.CONFIRMING;
657
+ case "exchanging":
658
+ return SwapProvider.SWAP_STATUSES.EXCHANGING;
659
+ case "sending":
660
+ return SwapProvider.SWAP_STATUSES.PAYMENT_RECEIVED;
661
+ case "finished":
662
+ return SwapProvider.SWAP_STATUSES.COMPLETED;
663
+ case "verifying":
664
+ return SwapProvider.SWAP_STATUSES.EXCHANGING;
665
+ case "refunded":
666
+ return SwapProvider.SWAP_STATUSES.REFUNDED;
667
+ case "expired":
668
+ return SwapProvider.SWAP_STATUSES.EXPIRED;
669
+ case "failed":
670
+ return SwapProvider.SWAP_STATUSES.FAILED;
671
+ default:
672
+ throw new Error(`Unknown swapspace status: ${status}`);
673
+ }
674
+ }
675
+
676
+ async getExistingSwapsDetailsAndStatus(swapIds) {
677
+ const loggerSource = "getExistingSwapsDetailsAndStatus";
678
+ try {
679
+ if (swapIds.find((id) => typeof id !== "string")) {
680
+ throw new Error(
681
+ "Swap id is not string: " + safeStringify(swapIds)
682
+ );
683
+ }
684
+ const getNotFailingOn404 = async (swapId) => {
685
+ try {
686
+ return await axios.get(
687
+ `${this._URL}/api/v2/exchange/${swapId}`
688
+ );
689
+ } catch (error) {
690
+ if (error?.response?.status === 404) return [];
691
+ throw error;
692
+ }
693
+ };
694
+ const responses = await Promise.all(
695
+ swapIds.map((swapId) => getNotFailingOn404(swapId))
696
+ );
697
+ const wo404 = responses.flat();
698
+ const swaps = wo404
699
+ .map((r) => r.data)
700
+ .map((swap, index) => {
701
+ const fromCoin = this._supportedCoins.find(
702
+ (i) =>
703
+ i.code === swap.from.code &&
704
+ i.network === swap.from.network
705
+ )?.coin;
706
+ const toCoin = this._supportedCoins.find(
707
+ (i) =>
708
+ i.code === swap.to.code &&
709
+ i.network === swap.to.network
710
+ )?.coin;
711
+ if (!fromCoin || !toCoin) {
712
+ return []; // We skip swaps with not supported coins for now
713
+ }
714
+
715
+ const status = this._mapSwapspaceStatusToRabbitStatus(
716
+ swap.status
717
+ );
718
+ const toDigits =
719
+ status === SwapProvider.SWAP_STATUSES.REFUNDED
720
+ ? fromCoin.digits
721
+ : toCoin.digits;
722
+ const addressToSendCoinsToSwapspace = swap.from.address;
723
+ const toUtcTimestamp = (timeStr) =>
724
+ Date.parse(
725
+ timeStr.match(/.+[Zz]$/) ? timeStr : `${timeStr}Z`
726
+ );
727
+ return new ExistingSwap(
728
+ swapIds[index],
729
+ status,
730
+ toUtcTimestamp(swap.timestamps.createdAt),
731
+ toUtcTimestamp(swap.timestamps.expiresAt),
732
+ swap.confirmations,
733
+ AmountUtils.trim(swap.rate, this._maxRateDigits),
734
+ swap.refundAddress,
735
+ addressToSendCoinsToSwapspace,
736
+ fromCoin,
737
+ AmountUtils.trim(swap.from.amount, fromCoin.digits),
738
+ swap.from.transactionHash,
739
+ swap.blockExplorerTransactionUrl.from,
740
+ toCoin,
741
+ AmountUtils.trim(swap.to.amount, toDigits),
742
+ swap.to.transactionHash,
743
+ swap.blockExplorerTransactionUrl.to,
744
+ swap.to.address,
745
+ swap.partner
746
+ );
747
+ })
748
+ .flat();
749
+ Logger.log(
750
+ `Swap details result ${safeStringify(swaps)}`,
751
+ loggerSource
752
+ );
753
+ return { result: true, swaps: swaps };
754
+ } catch (e) {
755
+ Logger.log(
756
+ `Failed to get swap details. Error is: ${safeStringify(e)}`,
757
+ loggerSource
758
+ );
759
+ const composeFailResult = (reason) => ({
760
+ result: false,
761
+ reason: reason,
762
+ });
763
+ const status = e?.response?.status;
764
+ const data = e?.response?.data;
765
+ if (status === 429) {
766
+ Logger.log(
767
+ `Returning fail - RPS limit exceeded ${data}`,
768
+ loggerSource
769
+ );
770
+ return composeFailResult(
771
+ SwapProvider.COMMON_ERRORS.REQUESTS_LIMIT_EXCEEDED
772
+ );
773
+ }
774
+ improveAndRethrow(e, loggerSource);
775
+ }
776
+ }
777
+
778
+ isAddressValidForAsset(asset, address) {
779
+ try {
780
+ const assetData = this._supportedCoins.find(
781
+ (i) => i.coin === asset
782
+ );
783
+ if (assetData) {
784
+ let corrected = assetData.validationRegexp.trim();
785
+ corrected =
786
+ corrected[0] === "/" ? corrected.slice(1) : corrected;
787
+ corrected =
788
+ corrected[corrected.length - 1] === "/"
789
+ ? corrected.slice(0, corrected.length - 1)
790
+ : corrected;
791
+ return address.match(corrected) != null;
792
+ }
793
+ } catch (e) {
794
+ Logger.logError(e, "isAddressValidForAsset");
795
+ }
796
+ return false;
797
+ }
798
+ }