@rabbitio/ui-kit 1.0.0-beta.12 → 1.0.0-beta.14

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,633 @@
1
+ import { BigNumber } from "bignumber.js";
2
+ import EventBusInstance from "eventbusjs";
3
+
4
+ import { Cache } from "../../common/utils/cache.js";
5
+ import { FiatCurrenciesService } from "../../common/fiatCurrenciesService.js";
6
+ import { improveAndRethrow } from "../../common/errorUtils.js";
7
+ import { safeStringify } from "../../common/utils/safeStringify.js";
8
+ import { Logger } from "../../common/utils/logging/logger.js";
9
+ import { Coin } from "../../common/models/coin.js";
10
+ import { AmountUtils } from "../../common/amountUtils.js";
11
+ import { PublicSwapCreationInfo } from "../models/publicSwapCreationInfo.js";
12
+ import { SwapUtils } from "../utils/swapUtils.js";
13
+ import { SwapspaceSwapProvider } from "../external-apis/swapspaceSwapProvider.js";
14
+ import { SwapProvider } from "../external-apis/swapProvider.js";
15
+
16
+ const API_KEYS_PROXY_URL = `${
17
+ window.location.protocol + "//" + window.location.host
18
+ }/api/v1/proxy`;
19
+ const cache = new Cache(EventBusInstance);
20
+
21
+ export class PublicSwapService {
22
+ static PUBLIC_SWAP_CREATED_EVENT = "publicSwapCreatedEvent";
23
+
24
+ static _swapProvider = new SwapspaceSwapProvider(
25
+ API_KEYS_PROXY_URL,
26
+ cache,
27
+ () => null,
28
+ false
29
+ );
30
+
31
+ static PUBLIC_SWAPS_COMMON_ERRORS = {
32
+ REQUESTS_LIMIT_EXCEEDED: "requestsLimitExceeded",
33
+ };
34
+
35
+ static PUBLIC_SWAP_DETAILS_FAIL_REASONS = {
36
+ AMOUNT_LESS_THAN_MIN_SWAPPABLE: "amountLessThanMinSwappable",
37
+ AMOUNT_HIGHER_THAN_MAX_SWAPPABLE: "amountHigherThanMaxSwappable",
38
+ PAIR_NOT_SUPPORTED: "pairNotSupported",
39
+ };
40
+
41
+ static _fiatDecimalsCount =
42
+ FiatCurrenciesService.getCurrencyDecimalCountByCode("USD");
43
+
44
+ static async initialize() {
45
+ try {
46
+ await this._swapProvider.initialize();
47
+ SwapUtils.safeHandleRequestsLimitExceeding();
48
+ } catch (e) {
49
+ Logger.logError(e, "PublicSwapService.initialize");
50
+ }
51
+ }
52
+
53
+ static async getCurrenciesListForPublicSwap(
54
+ currencyThatShouldNotBeFirst = null
55
+ ) {
56
+ const loggerSource = "getCurrenciesListForPublicSwap";
57
+ try {
58
+ // TODO: [dev] this is temporary hack, change it to use dedicated lists inside UI and also here
59
+ const result = currencyThatShouldNotBeFirst
60
+ ? await this._swapProvider.getWithdrawalCurrencies(
61
+ currencyThatShouldNotBeFirst
62
+ )
63
+ : await this._swapProvider.getDepositCurrencies();
64
+ if (
65
+ result.reason ===
66
+ SwapProvider.COMMON_ERRORS.REQUESTS_LIMIT_EXCEEDED
67
+ ) {
68
+ SwapUtils.safeHandleRequestsLimitExceeding();
69
+ return {
70
+ result: false,
71
+ reason: this.PUBLIC_SWAPS_COMMON_ERRORS
72
+ .REQUESTS_LIMIT_EXCEEDED,
73
+ };
74
+ }
75
+ Logger.log(
76
+ `Retrieved ${result?.coins?.length} supported currencies for swap`,
77
+ loggerSource
78
+ );
79
+ if (
80
+ result.coins[0] === currencyThatShouldNotBeFirst &&
81
+ result.coins.length > 1
82
+ ) {
83
+ let temp = result.coins[0];
84
+ result.coins[0] = result.coins[1];
85
+ result.coins[1] = temp;
86
+ }
87
+ return { result: true, coins: result.coins };
88
+ } catch (e) {
89
+ improveAndRethrow(e, loggerSource);
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Retrieves initial data for swapping two coins.
95
+ *
96
+ * @param fromCoin {Coin}
97
+ * @param toCoin {Coin}
98
+ * @return {Promise<{
99
+ * result: true,
100
+ * min: string,
101
+ * fiatMin: (number|null),
102
+ * max: string,
103
+ * fiatMax: (number|null),
104
+ * rate: (string|null)
105
+ * }|{
106
+ * result: false,
107
+ * reason: string
108
+ * }>}
109
+ */
110
+ static async getInitialPublicSwapData(fromCoin, toCoin) {
111
+ try {
112
+ const result = await SwapUtils.getInitialSwapData(
113
+ this._swapProvider,
114
+ fromCoin,
115
+ toCoin
116
+ );
117
+ if (!result.result) {
118
+ if (
119
+ result.reason ===
120
+ SwapProvider.COMMON_ERRORS.REQUESTS_LIMIT_EXCEEDED
121
+ ) {
122
+ SwapUtils.safeHandleRequestsLimitExceeding();
123
+ return {
124
+ result: false,
125
+ reason: this.PUBLIC_SWAPS_COMMON_ERRORS
126
+ .REQUESTS_LIMIT_EXCEEDED,
127
+ };
128
+ }
129
+ if (
130
+ result.reason ===
131
+ SwapProvider.NO_SWAPS_REASONS.NOT_SUPPORTED
132
+ ) {
133
+ return {
134
+ result: false,
135
+ reason: this.PUBLIC_SWAP_DETAILS_FAIL_REASONS
136
+ .PAIR_NOT_SUPPORTED,
137
+ };
138
+ }
139
+ }
140
+ return result;
141
+ } catch (e) {
142
+ improveAndRethrow(e, "getInitialPublicSwapData");
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Retrieves swap details that can be used to create swap.
148
+ *
149
+ * @param fromCoin {Coin}
150
+ * @param toCoin {Coin}
151
+ * @param fromAmountCoins {string}
152
+ * @return {Promise<{
153
+ * result: false,
154
+ * reason: string,
155
+ * min: (string|null),
156
+ * max: (string|null),
157
+ * rate: (string|undefined),
158
+ * fiatMin: (number|null),
159
+ * fiatMax: (number|null)
160
+ * }|{
161
+ * result: true,
162
+ * swapCreationInfo: PublicSwapCreationInfo
163
+ * }>}
164
+ */
165
+ static async getPublicSwapDetails(fromCoin, toCoin, fromAmountCoins) {
166
+ const loggerSource = "getPublicSwapDetails";
167
+ try {
168
+ const coinUsdtRate =
169
+ (await this._swapProvider.getCoinToUSDTRate(fromCoin))?.rate ??
170
+ null;
171
+ const details = await this._swapProvider.getSwapInfo(
172
+ fromCoin,
173
+ toCoin,
174
+ fromAmountCoins,
175
+ coinUsdtRate
176
+ );
177
+ const min = details.result ? details.min : details.smallestMin;
178
+ const max = details.result ? details.max : details.greatestMax;
179
+ let fiatMin = null,
180
+ fiatMax = null;
181
+ if (coinUsdtRate != null) {
182
+ if (min != null) {
183
+ fiatMin = BigNumber(min)
184
+ .times(coinUsdtRate)
185
+ .toFixed(this._fiatDecimalsCount);
186
+ }
187
+ if (max != null) {
188
+ fiatMax = BigNumber(max)
189
+ .times(coinUsdtRate)
190
+ .toFixed(this._fiatDecimalsCount);
191
+ }
192
+ }
193
+
194
+ const composeFailResult = (reason) => ({
195
+ result: false,
196
+ reason: reason,
197
+ min: min ?? null,
198
+ fiatMin: fiatMin,
199
+ max: max ?? null,
200
+ fiatMax: fiatMax,
201
+ rate: details.rate ?? null,
202
+ });
203
+
204
+ if (!details.result) {
205
+ if (
206
+ details?.reason ===
207
+ SwapProvider.NO_SWAPS_REASONS.NOT_SUPPORTED
208
+ )
209
+ return composeFailResult(
210
+ this.PUBLIC_SWAP_DETAILS_FAIL_REASONS.PAIR_NOT_SUPPORTED
211
+ );
212
+ else if (
213
+ details?.reason ===
214
+ SwapProvider.COMMON_ERRORS.REQUESTS_LIMIT_EXCEEDED
215
+ ) {
216
+ SwapUtils.safeHandleRequestsLimitExceeding();
217
+ return composeFailResult(
218
+ this.PUBLIC_SWAPS_COMMON_ERRORS.REQUESTS_LIMIT_EXCEEDED
219
+ );
220
+ }
221
+ }
222
+
223
+ const fromAmountBigNumber = BigNumber(fromAmountCoins);
224
+ if (typeof min === "string" && fromAmountBigNumber.lt(min)) {
225
+ return composeFailResult(
226
+ this.PUBLIC_SWAP_DETAILS_FAIL_REASONS
227
+ .AMOUNT_LESS_THAN_MIN_SWAPPABLE
228
+ );
229
+ } else if (typeof max === "string" && fromAmountBigNumber.gt(max)) {
230
+ return composeFailResult(
231
+ this.PUBLIC_SWAP_DETAILS_FAIL_REASONS
232
+ .AMOUNT_HIGHER_THAN_MAX_SWAPPABLE
233
+ );
234
+ }
235
+
236
+ const toAmountCoins = AmountUtils.trim(
237
+ fromAmountBigNumber.times(details.rate),
238
+ fromCoin.digits
239
+ );
240
+ const result = {
241
+ result: true,
242
+ swapCreationInfo: new PublicSwapCreationInfo(
243
+ fromCoin,
244
+ toCoin,
245
+ fromAmountCoins,
246
+ toAmountCoins,
247
+ details.rate,
248
+ details.rawSwapData,
249
+ min,
250
+ fiatMin,
251
+ max,
252
+ fiatMax,
253
+ details.durationMinutesRange
254
+ ),
255
+ };
256
+ Logger.log(
257
+ `Result: ${safeStringify({
258
+ result: result.result,
259
+ swapCreationInfo: {
260
+ ...result.swapCreationInfo,
261
+ fromCoin: result?.swapCreationInfo?.fromCoin?.ticker,
262
+ toCoin: result?.swapCreationInfo?.toCoin?.ticker,
263
+ },
264
+ })}`,
265
+ loggerSource
266
+ );
267
+
268
+ return result;
269
+ } catch (e) {
270
+ improveAndRethrow(e, loggerSource);
271
+ }
272
+ }
273
+
274
+ /**
275
+ * Creates swap by given params.
276
+ *
277
+ * @param fromCoin {Coin}
278
+ * @param toCoin {Coin}
279
+ * @param fromAmount {string}
280
+ * @param swapCreationInfo {PublicSwapCreationInfo}
281
+ * @param toAddress {string}
282
+ * @param refundAddress {string}
283
+ * @return {Promise<{
284
+ * result: true,
285
+ * fiatCurrencyCode: string,
286
+ * toCoin: Coin,
287
+ * fromAmountFiat: (number|null),
288
+ * address: string,
289
+ * durationMinutesRange: string,
290
+ * fromAmount: string,
291
+ * toAmount: string,
292
+ * toAmountFiat: (number|null),
293
+ * fiatCurrencyDecimals: number,
294
+ * fromCoin: Coin,
295
+ * rate: string,
296
+ * swapId: string
297
+ * }|{
298
+ * result: false,
299
+ * reason: string
300
+ * }>}
301
+ */
302
+ static async createPublicSwap(
303
+ fromCoin,
304
+ toCoin,
305
+ fromAmount,
306
+ swapCreationInfo,
307
+ toAddress,
308
+ refundAddress,
309
+ clientIp
310
+ ) {
311
+ const loggerSource = "createPublicSwap";
312
+ try {
313
+ if (
314
+ !(fromCoin instanceof Coin) ||
315
+ !(toCoin instanceof Coin) ||
316
+ typeof fromAmount !== "string" ||
317
+ typeof toAddress !== "string" ||
318
+ typeof refundAddress !== "string" ||
319
+ !(swapCreationInfo instanceof PublicSwapCreationInfo)
320
+ ) {
321
+ throw new Error(
322
+ `Wrong input: ${fromCoin.ticker} ${toCoin.ticker} ${fromAmount} ${swapCreationInfo}`
323
+ );
324
+ }
325
+ Logger.log(
326
+ `Start: ${fromAmount} ${fromCoin.ticker} -> ${
327
+ toCoin.ticker
328
+ }. Details: ${safeStringify({
329
+ ...swapCreationInfo,
330
+ fromCoin: swapCreationInfo?.fromCoin?.ticker,
331
+ toCoin: swapCreationInfo?.toCoin?.ticker,
332
+ })}`,
333
+ loggerSource
334
+ );
335
+
336
+ const result = await this._swapProvider.createSwap(
337
+ fromCoin,
338
+ toCoin,
339
+ fromAmount,
340
+ toAddress,
341
+ refundAddress,
342
+ swapCreationInfo.rawSwapData,
343
+ clientIp
344
+ );
345
+ Logger.log(
346
+ `Created:${safeStringify({
347
+ ...result,
348
+ fromCoin: fromCoin?.ticker,
349
+ toCoin: toCoin?.ticker,
350
+ })}`,
351
+ loggerSource
352
+ );
353
+ if (!result?.result) {
354
+ if (
355
+ result?.reason ===
356
+ SwapProvider.COMMON_ERRORS.REQUESTS_LIMIT_EXCEEDED
357
+ ) {
358
+ SwapUtils.safeHandleRequestsLimitExceeding();
359
+ return {
360
+ result: false,
361
+ reason: this.PUBLIC_SWAPS_COMMON_ERRORS
362
+ .REQUESTS_LIMIT_EXCEEDED,
363
+ };
364
+ }
365
+ if (
366
+ result?.reason ===
367
+ SwapProvider.CREATION_FAIL_REASONS.RETRIABLE_FAIL
368
+ ) {
369
+ // TODO: [feature, high] implement retrying if one partner fail and we have another partners task_id=a07e367e488f4a4899613ac9056fa359
370
+ // return {
371
+ // result: false,
372
+ // reason: this.SWAP_CREATION_FAIL_REASONS.RETRIABLE_FAIL,
373
+ // };
374
+ }
375
+ }
376
+ if (result.result && result?.swapId) {
377
+ let fromAmountFiat = null,
378
+ toAmountFiat = null;
379
+ try {
380
+ const fromCoinUsdtRate =
381
+ (await this._swapProvider.getCoinToUSDTRate(fromCoin))
382
+ ?.rate ?? null;
383
+ const toCoinUsdtRate =
384
+ (await this._swapProvider.getCoinToUSDTRate(fromCoin))
385
+ ?.rate ?? null;
386
+ if (fromCoinUsdtRate != null && result.fromAmount != null) {
387
+ fromAmountFiat = BigNumber(result.fromAmount)
388
+ .times(fromCoinUsdtRate)
389
+ .toFixed(this._fiatDecimalsCount);
390
+ }
391
+ if (toCoinUsdtRate != null && result.toAmount != null) {
392
+ toAmountFiat = BigNumber(result.toAmount)
393
+ .times(toCoinUsdtRate)
394
+ .toFixed(this._fiatDecimalsCount);
395
+ }
396
+ } catch (e) {
397
+ Logger.logError(
398
+ e,
399
+ loggerSource,
400
+ "Failed to calculate fiat amounts for result"
401
+ );
402
+ }
403
+
404
+ EventBusInstance.dispatch(
405
+ this.PUBLIC_SWAP_CREATED_EVENT,
406
+ null,
407
+ fromCoin.ticker,
408
+ toCoin.ticker,
409
+ fromAmountFiat
410
+ );
411
+
412
+ const toReturn = {
413
+ result: true,
414
+ swapId: result.swapId,
415
+ fromCoin: fromCoin,
416
+ toCoin: toCoin,
417
+ fromAmount: result.fromAmount,
418
+ toAmount: result.toAmount,
419
+ fromAmountFiat: fromAmountFiat,
420
+ toAmountFiat: toAmountFiat,
421
+ fiatCurrencyCode: "USD",
422
+ fiatCurrencyDecimals: this._fiatDecimalsCount,
423
+ rate: result.rate,
424
+ durationMinutesRange: swapCreationInfo.durationMinutesRange,
425
+ address: result.fromAddress, // CRITICAL: this is the address to send coins to swaps provider
426
+ };
427
+
428
+ this._savePublicSwapIdLocally(result.swapId);
429
+
430
+ Logger.log(
431
+ `Returning: ${safeStringify({
432
+ ...toReturn,
433
+ fromCoin: fromCoin?.ticker,
434
+ toCoin: toCoin?.ticker,
435
+ })}`,
436
+ loggerSource
437
+ );
438
+ return toReturn;
439
+ }
440
+
441
+ throw new Error(
442
+ `Unexpected result from provider ${safeStringify(result)}`
443
+ );
444
+ } catch (e) {
445
+ improveAndRethrow(e, loggerSource);
446
+ }
447
+ }
448
+
449
+ /**
450
+ * Retrieves swap details and status for existing swaps by their ids.
451
+ *
452
+ * @param swapIds {string[]}
453
+ * @return {Promise<{
454
+ * result: true,
455
+ * swaps: ExistingSwapWithFiatData[]
456
+ * }|{
457
+ * result: false,
458
+ * reason: string
459
+ * }>}
460
+ * error reason is one of PUBLIC_SWAPS_COMMON_ERRORS
461
+ */
462
+ static async getPublicExistingSwapDetailsAndStatus(swapIds) {
463
+ const loggerSource = "getPublicExistingSwapDetailsAndStatus";
464
+ try {
465
+ const result =
466
+ await SwapUtils.getExistingSwapsDetailsWithFiatAmounts(
467
+ this._swapProvider,
468
+ swapIds
469
+ );
470
+ if (!result?.result) {
471
+ if (
472
+ result.reason ===
473
+ SwapProvider.COMMON_ERRORS.REQUESTS_LIMIT_EXCEEDED
474
+ ) {
475
+ SwapUtils.safeHandleRequestsLimitExceeding();
476
+ return {
477
+ result: false,
478
+ reason: this.PUBLIC_SWAPS_COMMON_ERRORS
479
+ .REQUESTS_LIMIT_EXCEEDED,
480
+ };
481
+ }
482
+ throw new Error("Unknown reason: " + result?.reason);
483
+ }
484
+
485
+ return result;
486
+ } catch (e) {
487
+ improveAndRethrow(e, loggerSource);
488
+ }
489
+ }
490
+
491
+ /**
492
+ * Retrieves the whole available swaps history by ids saved locally.
493
+ *
494
+ * @return {Promise<{
495
+ * result: true,
496
+ * swaps: ExistingSwapWithFiatData[]
497
+ * }|{
498
+ * result: false,
499
+ * reason: string
500
+ * }>}
501
+ */
502
+ static async getPublicSwapsHistory() {
503
+ try {
504
+ const swapIds = this._getPublicSwapIdsSavedLocally();
505
+ if (swapIds.length) {
506
+ return await this.getPublicExistingSwapDetailsAndStatus(
507
+ swapIds
508
+ );
509
+ }
510
+ return { result: true, swaps: [] };
511
+ } catch (e) {
512
+ improveAndRethrow(e, "getPublicSwapsHistory");
513
+ }
514
+ }
515
+
516
+ /**
517
+ * @param swapId {string}
518
+ * @private
519
+ */
520
+ static _savePublicSwapIdLocally(swapId) {
521
+ if (typeof window !== "undefined") {
522
+ try {
523
+ const saved = localStorage.getItem("publicSwapIds");
524
+ const ids =
525
+ typeof saved === "string" && saved.length > 0
526
+ ? saved.split(",")
527
+ : [];
528
+ ids.push(swapId);
529
+ localStorage.setItem("publicSwapIds", ids.join(","));
530
+ } catch (e) {
531
+ improveAndRethrow(e, "_savePublicSwapIdLocally");
532
+ }
533
+ }
534
+ }
535
+
536
+ /**
537
+ * @private
538
+ * @return {string[]}
539
+ */
540
+ static _getPublicSwapIdsSavedLocally() {
541
+ if (typeof window !== "undefined") {
542
+ try {
543
+ const saved = localStorage.getItem("publicSwapIds");
544
+ return typeof saved === "string" && saved.length > 0
545
+ ? saved.split(",")
546
+ : [];
547
+ } catch (e) {
548
+ improveAndRethrow(e, "_getPublicSwapIdsSavedLocally");
549
+ }
550
+ }
551
+ }
552
+
553
+ /**
554
+ * @param coinOrTicker {Coin|string}
555
+ * @return {string} icon URL (ready to use)
556
+ */
557
+ static getAssetIconUrl(coinOrTicker) {
558
+ return this._swapProvider.getIconUrl(coinOrTicker);
559
+ }
560
+
561
+ /**
562
+ * @param ticker {string}
563
+ * @return {Coin|null}
564
+ */
565
+ static getCoinByTickerIfPresent(ticker) {
566
+ return this._swapProvider.getCoinByTickerIfPresent(ticker);
567
+ }
568
+
569
+ /**
570
+ * TODO: [feature, moderate] add other fiat currencies support. task_id=5490e21b8b9c4f89a2247b28db3c9e0a
571
+ * @param asset {Coin}
572
+ * @return {Promise<string|null>}
573
+ */
574
+ static async getAssetToUsdtRate(asset) {
575
+ try {
576
+ const result = await this._swapProvider.getCoinToUSDTRate(asset);
577
+ return result?.rate ?? null;
578
+ } catch (e) {
579
+ improveAndRethrow(e, "getAssetToUsdtRate");
580
+ }
581
+ }
582
+
583
+ /**
584
+ * @param asset {Coin}
585
+ * @param address {string}
586
+ * @return {boolean}
587
+ */
588
+ static isAddressValidForAsset(asset, address) {
589
+ try {
590
+ return this._swapProvider.isAddressValidForAsset(asset, address);
591
+ } catch (e) {
592
+ improveAndRethrow(e, "isAddressValidForAsset");
593
+ }
594
+ }
595
+
596
+ // TODO: [dev] Remove if we don't need this inside the public swap steps
597
+ // /**
598
+ // * TODO: [feature, moderate] add other fiat currencies support. task_id=5490e21b8b9c4f89a2247b28db3c9e0a
599
+ // * @param asset {Coin}
600
+ // * @param amount {string}
601
+ // * @return {Promise<string|null>}
602
+ // */
603
+ // static async convertSingleCoinAmountToUsdtOrNull(asset, amount) {
604
+ // try {
605
+ // const result = await this._swapProvider.getCoinToUSDTRate(asset);
606
+ // if (result?.rate != null) {
607
+ // const decimals = FiatCurrenciesService.getCurrencyDecimalCountByCode("USD");
608
+ // return BigNumber(amount).div(result?.rate).toFixed(decimals);
609
+ // }
610
+ // return null;
611
+ // } catch (e) {
612
+ // improveAndRethrow(e, "convertSingleCoinAmountToUsdtOrNull");
613
+ // }
614
+ // }
615
+ //
616
+ // /**
617
+ // * TODO: [feature, moderate] add other fiat currencies support. task_id=5490e21b8b9c4f89a2247b28db3c9e0a
618
+ // * @param asset {Coin}
619
+ // * @param amount {string}
620
+ // * @return {Promise<string|null>}
621
+ // */
622
+ // static async convertSingleUsdtAmountToCoinOrNull(asset, amount) {
623
+ // try {
624
+ // const result = await this._swapProvider.getCoinToUSDTRate(asset);
625
+ // if (result?.rate != null) {
626
+ // return BigNumber(amount).times(result?.rate).toFixed(asset.digits);
627
+ // }
628
+ // return null;
629
+ // } catch (e) {
630
+ // improveAndRethrow(e, "convertSingleUsdtAmountToCoinOrNull");
631
+ // }
632
+ // }
633
+ }