@paraswap/dex-lib 3.9.1 → 3.9.2-bebop.0

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,840 @@
1
+ import { assert, AsyncOrSync } from 'ts-essentials';
2
+ import {
3
+ Token,
4
+ Address,
5
+ ExchangePrices,
6
+ PoolPrices,
7
+ AdapterExchangeParam,
8
+ PoolLiquidity,
9
+ Logger,
10
+ OptimalSwapExchange,
11
+ PreprocessTransactionOptions,
12
+ ExchangeTxInfo,
13
+ NumberAsString,
14
+ DexExchangeParam,
15
+ } from '../../types';
16
+ import { SwapSide, Network } from '../../constants';
17
+ import * as CALLDATA_GAS_COST from '../../calldata-gas-cost';
18
+ import { getDexKeysWithNetwork, Utils } from '../../utils';
19
+ import { IDex } from '../../dex/idex';
20
+ import { IDexHelper } from '../../dex-helper/idex-helper';
21
+ import {
22
+ BebopData,
23
+ BebopLevel,
24
+ BebopPair,
25
+ BebopPricingResponse,
26
+ RestrictData,
27
+ RoutingInstruction,
28
+ SlippageError,
29
+ TokenDataMap,
30
+ } from './types';
31
+ import settlementABI from '../../abi/bebop/BebopSettlement.abi.json';
32
+ import { SimpleExchange } from '../simple-exchange';
33
+ import { BebopConfig } from './config';
34
+ import { Interface } from 'ethers/lib/utils';
35
+ import { RateFetcher } from './rate-fetcher';
36
+ import {
37
+ BEBOP_API_URL,
38
+ BEBOP_AUTH_NAME,
39
+ BEBOP_ERRORS_CACHE_KEY,
40
+ BEBOP_GAS_COST,
41
+ BEBOP_INIT_TIMEOUT_MS,
42
+ BEBOP_PRICES_CACHE_TTL,
43
+ BEBOP_QUOTE_TIMEOUT_MS,
44
+ BEBOP_RESTRICTED_CACHE_KEY,
45
+ BEBOP_RESTRICT_CHECK_INTERVAL_MS,
46
+ BEBOP_RESTRICT_COUNT_THRESHOLD,
47
+ BEBOP_RESTRICT_TTL_S,
48
+ BEBOP_TOKENS_CACHE_TTL,
49
+ BEBOP_TOKENS_POLLING_INTERVAL_MS,
50
+ BEBOP_WS_API_URL,
51
+ } from './constants';
52
+ import BigNumber from 'bignumber.js';
53
+ import { getBigNumberPow } from '../../bignumber-constants';
54
+ import { utils } from 'ethers';
55
+ import qs from 'qs';
56
+
57
+ export class Bebop extends SimpleExchange implements IDex<BebopData> {
58
+ readonly hasConstantPriceLargeAmounts = false;
59
+ readonly needWrapNative = false;
60
+
61
+ readonly isFeeOnTransferSupported = false;
62
+ readonly isStatePollingDex = true;
63
+
64
+ public static dexKeysWithNetwork: { key: string; networks: Network[] }[] =
65
+ getDexKeysWithNetwork(BebopConfig);
66
+
67
+ private rateFetcher: RateFetcher;
68
+ private tokensMap: TokenDataMap = {};
69
+
70
+ private pricesCacheKey: string;
71
+ private tokensCacheKey: string;
72
+ private tokensAddrCacheKey: string;
73
+
74
+ private bebopAuthToken: string;
75
+
76
+ logger: Logger;
77
+
78
+ constructor(
79
+ readonly network: Network,
80
+ readonly dexKey: string,
81
+ readonly dexHelper: IDexHelper,
82
+ readonly settlementAddress: string = BebopConfig['Bebop'][network]
83
+ .settlementAddress,
84
+ protected settlementInterface = new Interface(settlementABI),
85
+ ) {
86
+ super(dexHelper, dexKey);
87
+ this.logger = dexHelper.getLogger(dexKey);
88
+ this.tokensCacheKey = `tokens`;
89
+ this.pricesCacheKey = `prices`;
90
+ this.tokensAddrCacheKey = `tokens_addr`;
91
+ const token = this.dexHelper.config.data.bebopAuthToken;
92
+ if (!token) {
93
+ throw new Error('Bebop auth token is not set');
94
+ }
95
+ this.bebopAuthToken = token;
96
+
97
+ this.rateFetcher = new RateFetcher(
98
+ this.dexHelper,
99
+ this.dexKey,
100
+ this.network,
101
+ this.logger,
102
+ {
103
+ rateConfig: {
104
+ tokensIntervalMs: BEBOP_TOKENS_POLLING_INTERVAL_MS,
105
+ pricesCacheKey: this.pricesCacheKey,
106
+ pricesCacheTTLSecs: BEBOP_PRICES_CACHE_TTL,
107
+ tokensCacheKey: this.tokensCacheKey,
108
+ tokensAddrCacheKey: this.tokensAddrCacheKey,
109
+ tokensCacheTTLSecs: BEBOP_TOKENS_CACHE_TTL,
110
+ tokensReqParams: {
111
+ url:
112
+ BEBOP_API_URL +
113
+ `/pmm/${BebopConfig['Bebop'][network].chainName}/v3/token-info`,
114
+ },
115
+ pricesReqParams: {
116
+ url:
117
+ BEBOP_WS_API_URL +
118
+ `/pmm/${BebopConfig['Bebop'][network].chainName}/v3/pricing`,
119
+ headers: {
120
+ name: BEBOP_AUTH_NAME,
121
+ authorization: this.bebopAuthToken,
122
+ },
123
+ },
124
+ },
125
+ },
126
+ );
127
+ }
128
+
129
+ async initializePricing(blockNumber: number) {
130
+ if (!this.dexHelper.config.isSlave) {
131
+ this.rateFetcher.start();
132
+ await sleep(BEBOP_INIT_TIMEOUT_MS);
133
+ }
134
+
135
+ await this.setTokensMap();
136
+ }
137
+
138
+ // Returns the list of contract adapters (name and index)
139
+ // for a buy/sell. Return null if there are no adapters.
140
+ getAdapters(side: SwapSide): { name: string; index: number }[] | null {
141
+ return null;
142
+ }
143
+
144
+ getPoolIdentifier(base: string, quote: string): string {
145
+ const identifier = `${
146
+ this.dexKey
147
+ }_${base.toLowerCase()}_${quote.toLowerCase()}`;
148
+ return identifier;
149
+ }
150
+
151
+ invertLevels(levels: BebopLevel[]): BebopLevel[] {
152
+ return levels.map(([price, size]) => [1 / price, size * price]);
153
+ }
154
+
155
+ invertBook(book: BebopPair): BebopPair {
156
+ return {
157
+ bids: this.invertLevels(book.asks),
158
+ asks: this.invertLevels(book.bids),
159
+ last_update_ts: book.last_update_ts,
160
+ };
161
+ }
162
+
163
+ // considers only swapside.sell
164
+ async calculateInstructions(
165
+ srcToken: Token,
166
+ destToken: Token,
167
+ side: SwapSide,
168
+ ): Promise<RoutingInstruction[]> {
169
+ const prices = await this.getCachedPrices();
170
+ if (!prices) {
171
+ throw new Error('No prices available');
172
+ }
173
+ const directBook =
174
+ prices[
175
+ `${srcToken.address.toLowerCase()}/${destToken.address.toLowerCase()}`
176
+ ];
177
+ if (directBook) {
178
+ return [
179
+ {
180
+ pair: `${srcToken.address.toLowerCase()}/${destToken.address.toLowerCase()}`,
181
+ side,
182
+ book: directBook,
183
+ targetQuote: side == SwapSide.BUY,
184
+ },
185
+ ];
186
+ }
187
+
188
+ const inverseBook =
189
+ prices[
190
+ `${destToken.address.toLowerCase()}/${srcToken.address.toLowerCase()}`
191
+ ];
192
+ if (inverseBook) {
193
+ const invertedBook = this.invertBook(inverseBook);
194
+ return [
195
+ {
196
+ pair: `${srcToken.address.toLowerCase()}/${destToken.address.toLowerCase()}`,
197
+ side,
198
+ book: invertedBook,
199
+ targetQuote: side == SwapSide.BUY,
200
+ },
201
+ ];
202
+ }
203
+
204
+ for (const middleToken of BebopConfig['Bebop'][this.network].middleTokens) {
205
+ const baseMiddle =
206
+ prices[
207
+ `${srcToken.address.toLowerCase()}/${middleToken.toLowerCase()}`
208
+ ];
209
+ const quoteMiddle =
210
+ prices[
211
+ `${destToken.address.toLowerCase()}/${middleToken.toLowerCase()}`
212
+ ];
213
+ if (baseMiddle && quoteMiddle) {
214
+ if (side == SwapSide.SELL) {
215
+ return [
216
+ {
217
+ pair: `${srcToken.address.toLowerCase()}/${middleToken.toLowerCase()}`,
218
+ side: side,
219
+ book: baseMiddle,
220
+ targetQuote: false,
221
+ },
222
+ {
223
+ pair: `${middleToken.toLowerCase()}/${destToken.address.toLowerCase()}`,
224
+ side: side,
225
+ book: this.invertBook(quoteMiddle),
226
+ targetQuote: false,
227
+ },
228
+ ];
229
+ } else {
230
+ return [
231
+ {
232
+ pair: `${middleToken.toLowerCase()}/${destToken.address.toLowerCase()}`,
233
+ side: side,
234
+ book: this.invertBook(quoteMiddle),
235
+ targetQuote: true,
236
+ },
237
+ {
238
+ pair: `${srcToken.address.toLowerCase()}/${middleToken.toLowerCase()}`,
239
+ side: side,
240
+ book: baseMiddle,
241
+ targetQuote: true,
242
+ },
243
+ ];
244
+ }
245
+ }
246
+ }
247
+
248
+ return [];
249
+ }
250
+
251
+ // Returns list of pool identifiers that can be used
252
+ // for a given swap. poolIdentifiers must be unique
253
+ // across DEXes. It is recommended to use
254
+ // ${dexKey}_${poolAddress} as a poolIdentifier
255
+ async getPoolIdentifiers(
256
+ srcToken: Token,
257
+ destToken: Token,
258
+ side: SwapSide,
259
+ blockNumber: number,
260
+ ): Promise<string[]> {
261
+ if (
262
+ (await this.calculateInstructions(srcToken, destToken, side)).length > 0
263
+ ) {
264
+ const identifier = this.getPoolIdentifier(
265
+ srcToken.address,
266
+ destToken.address,
267
+ );
268
+ return [identifier];
269
+ }
270
+
271
+ return [];
272
+ }
273
+
274
+ runInstruction(
275
+ instruction: RoutingInstruction,
276
+ amount: BigNumber,
277
+ ): BigNumber {
278
+ let accumulated = BigNumber(0);
279
+ let output = BigNumber(0);
280
+ let filled = false;
281
+ for (const level of instruction.book.bids) {
282
+ const [price, size] = level;
283
+ const amountToAccumulate = instruction.targetQuote
284
+ ? BigNumber(size).times(price)
285
+ : BigNumber(size);
286
+ const afterAccumulated = accumulated.plus(amountToAccumulate);
287
+ if (afterAccumulated.lt(amount)) {
288
+ accumulated = accumulated.plus(amountToAccumulate);
289
+ const amountToAddToOutput = instruction.targetQuote
290
+ ? BigNumber(size)
291
+ : BigNumber(size).times(price);
292
+ output = output.plus(amountToAddToOutput);
293
+ if (accumulated.eq(amount)) {
294
+ filled = true;
295
+ break;
296
+ }
297
+ } else {
298
+ const remaining = amount.minus(accumulated);
299
+ output = output.plus(
300
+ instruction.targetQuote
301
+ ? remaining.div(price)
302
+ : remaining.times(price),
303
+ );
304
+ filled = true;
305
+ break;
306
+ }
307
+ }
308
+ if (filled) {
309
+ return output;
310
+ } else {
311
+ return BigNumber(0);
312
+ }
313
+ }
314
+
315
+ calculateOutput(
316
+ instructions: RoutingInstruction[],
317
+ srcToken: Token,
318
+ destToken: Token,
319
+ amounts: bigint[],
320
+ side: SwapSide,
321
+ ): bigint[] {
322
+ const outputs = [];
323
+ const inputDecimals =
324
+ side == SwapSide.SELL ? srcToken.decimals : destToken.decimals;
325
+ const outputDecimals =
326
+ side == SwapSide.SELL ? destToken.decimals : srcToken.decimals;
327
+
328
+ for (const amount of amounts) {
329
+ if (amount == 0n) {
330
+ outputs.push(0n);
331
+ continue;
332
+ }
333
+ const amountDecimals = BigNumber(amount.toString()).div(
334
+ getBigNumberPow(inputDecimals),
335
+ );
336
+ let output: BigNumber = BigNumber(0);
337
+ for (const instruction of instructions) {
338
+ output = this.runInstruction(
339
+ instruction,
340
+ output.gt(0) ? output : amountDecimals,
341
+ );
342
+ if (output.eq(0)) {
343
+ break;
344
+ }
345
+ }
346
+ if (output.gt(0)) {
347
+ outputs.push(
348
+ BigInt(output.times(getBigNumberPow(outputDecimals)).toFixed(0)),
349
+ );
350
+ } else {
351
+ outputs.push(0n);
352
+ }
353
+ }
354
+ return outputs;
355
+ }
356
+
357
+ // Returns pool prices for amounts.
358
+ // If limitPools is defined only pools in limitPools
359
+ // should be used. If limitPools is undefined then
360
+ // any pools can be used.
361
+ async getPricesVolume(
362
+ srcToken: Token,
363
+ destToken: Token,
364
+ amounts: bigint[],
365
+ side: SwapSide,
366
+ blockNumber: number,
367
+ limitPools?: string[],
368
+ ): Promise<null | ExchangePrices<BebopData>> {
369
+ const isRestricted = await this.isRestricted();
370
+ if (isRestricted) {
371
+ return null;
372
+ }
373
+
374
+ try {
375
+ let pools = limitPools
376
+ ? limitPools.filter(
377
+ p =>
378
+ p === this.getPoolIdentifier(srcToken.address, destToken.address),
379
+ )
380
+ : await this.getPoolIdentifiers(srcToken, destToken, side, blockNumber);
381
+
382
+ if (pools.length === 0) {
383
+ return null;
384
+ }
385
+
386
+ const instructions = await this.calculateInstructions(
387
+ srcToken,
388
+ destToken,
389
+ side,
390
+ );
391
+
392
+ if (!instructions) {
393
+ return null;
394
+ }
395
+
396
+ const outputs = this.calculateOutput(
397
+ instructions,
398
+ srcToken,
399
+ destToken,
400
+ amounts,
401
+ side,
402
+ );
403
+
404
+ // Up to 3bps deviation is expected in the output compared to the on-chain result
405
+ // On SwapSide.Sell, outputs compared to quoting are coming out: -0.1 bips -> USDC, -1 bips Alt -> Alt.
406
+ // On SwapSide.Buy, outputs compared to quoteing are coming out: 0.1 bips -> USDC, 1-3 bips Alt -> Alt.
407
+
408
+ const outDecimals = SwapSide.SELL
409
+ ? destToken.decimals
410
+ : srcToken.decimals;
411
+
412
+ return [
413
+ {
414
+ prices: outputs,
415
+ unit: BigInt(outDecimals),
416
+ data: {},
417
+ poolIdentifier: pools[0],
418
+ exchange: this.dexKey,
419
+ gasCost: BEBOP_GAS_COST,
420
+ poolAddresses: [this.settlementAddress],
421
+ },
422
+ ];
423
+ } catch (e: unknown) {
424
+ this.logger.error(
425
+ `Error_getPricesVolume ${srcToken.address || srcToken.symbol}, ${
426
+ destToken.address || destToken.symbol
427
+ }, ${side}:`,
428
+ e,
429
+ );
430
+ return null;
431
+ }
432
+ }
433
+
434
+ // Returns estimated gas cost of calldata for this DEX in multiSwap
435
+ getCalldataGasCost(poolPrices: PoolPrices<BebopData>): number | number[] {
436
+ // This relies heavily on exact quote. Can we note use Bebop Data to find this? either via pools or the data itself?
437
+ // This assumes that a single maker was used to fill this trade
438
+ // "order":{
439
+ return (
440
+ CALLDATA_GAS_COST.DEX_OVERHEAD +
441
+ // "expiry":"1725541751"
442
+ CALLDATA_GAS_COST.TIMESTAMP +
443
+ // "taker_address":"0x9008d19f58aabd9ed0d60971565aa8510560ab41"
444
+ CALLDATA_GAS_COST.ADDRESS +
445
+ // "maker_address":"0x807cf9a772d5a3f9cefbc1192e939d62f0d9bd38"
446
+ CALLDATA_GAS_COST.ADDRESS +
447
+ // "maker_nonce":"1725541661402362944"
448
+ CALLDATA_GAS_COST.UUID +
449
+ // "taker_token":"0x3429d03c6f7521aec737a0bbf2e5ddcef2c3ae31"
450
+ CALLDATA_GAS_COST.ADDRESS +
451
+ // "maker_token":"0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"
452
+ CALLDATA_GAS_COST.ADDRESS +
453
+ // "taker_amount":"3789033775358849793876"
454
+ CALLDATA_GAS_COST.AMOUNT +
455
+ // "maker_amount":"199726023273987970"
456
+ CALLDATA_GAS_COST.AMOUNT +
457
+ // "receiver":"0x9008d19f58aabd9ed0d60971565aa8510560ab41"
458
+ CALLDATA_GAS_COST.ADDRESS +
459
+ // "packed_commands":"0"
460
+ CALLDATA_GAS_COST.wordNonZeroBytes(4) +
461
+ // "flags":"103712057072216206793253728668500811363805133225332165859664901734817333248000"
462
+ CALLDATA_GAS_COST.FULL_WORD +
463
+ // }
464
+ // "makerSignature":{
465
+ // "signatureBytes":"0x34e1fbda1d48296949f0a003d4194646ed750a4ccd408b52979f60c2e7332207190a91600a40396b25fd645422dde85bc0ff53f26b66202a985ac8b9da87b47e1c"
466
+ CALLDATA_GAS_COST.FULL_WORD +
467
+ // "flags":"1"
468
+ CALLDATA_GAS_COST.LENGTH_SMALL +
469
+ // }
470
+ // "filledTakerAmount":"0"
471
+ CALLDATA_GAS_COST.AMOUNT
472
+ );
473
+ }
474
+
475
+ // Encode params required by the exchange adapter
476
+ // V5: Used for multiSwap, buy & megaSwap
477
+ // V6: Not used, can be left blank
478
+ // Hint: abiCoder.encodeParameter() could be useful
479
+ getAdapterParam(
480
+ srcToken: string,
481
+ destToken: string,
482
+ srcAmount: string,
483
+ destAmount: string,
484
+ data: BebopData,
485
+ side: SwapSide,
486
+ ): AdapterExchangeParam {
487
+ const { tx } = data;
488
+
489
+ if (!tx) {
490
+ throw new Error('No tx in data');
491
+ }
492
+
493
+ // Encode here the payload for adapter
494
+ const payload = tx.data;
495
+
496
+ return {
497
+ targetExchange: this.settlementAddress,
498
+ payload,
499
+ networkFee: '0',
500
+ };
501
+ }
502
+
503
+ async updatePoolState(): Promise<void> {
504
+ await this.setTokensMap();
505
+ }
506
+
507
+ async setTokensMap() {
508
+ const tokens = await this.getCachedTokens();
509
+
510
+ if (tokens) {
511
+ this.tokensMap = tokens;
512
+ }
513
+ }
514
+
515
+ getMaxLiquidity(levels: BebopLevel[]) {
516
+ return levels.reduce((acc, [price, size]) => {
517
+ return acc + price * size;
518
+ }, 0);
519
+ }
520
+
521
+ // Returns list of top pools based on liquidity. Max
522
+ // limit number pools should be returned.
523
+ async getTopPoolsForToken(
524
+ tokenAddress: Address,
525
+ limit: number,
526
+ ): Promise<PoolLiquidity[]> {
527
+ const prices = await this.getCachedPrices();
528
+
529
+ if (!prices) {
530
+ return [];
531
+ }
532
+
533
+ const pools: PoolLiquidity[] = [];
534
+
535
+ for (const [pair, pairData] of Object.entries(prices)) {
536
+ let liquidityUSD = 0;
537
+ let token;
538
+ const [base, quote] = pair.split('/');
539
+
540
+ const isBase = base.toLowerCase() == tokenAddress.toLowerCase();
541
+ const isQuote = quote.toLowerCase() == tokenAddress.toLowerCase();
542
+
543
+ if (isBase) {
544
+ const liquidityInQuote = this.getMaxLiquidity(pairData.bids);
545
+ token = {
546
+ address: quote,
547
+ decimals: this.tokensMap[quote.toLowerCase()].decimals,
548
+ };
549
+ const quoteTokenUsd = await this.dexHelper.getTokenUSDPrice(
550
+ token,
551
+ BigInt(Math.round(liquidityInQuote)),
552
+ );
553
+ liquidityUSD = liquidityInQuote * quoteTokenUsd;
554
+ } else if (isQuote) {
555
+ const liquidityInBase = this.getMaxLiquidity(pairData.asks);
556
+ token = {
557
+ address: base,
558
+ decimals: this.tokensMap[base.toLowerCase()].decimals,
559
+ };
560
+ const baseTokenUsd = await this.dexHelper.getTokenUSDPrice(
561
+ token,
562
+ BigInt(Math.round(liquidityInBase)),
563
+ );
564
+ liquidityUSD = liquidityInBase * baseTokenUsd;
565
+ }
566
+
567
+ if (liquidityUSD) {
568
+ assert(token, 'Token not found');
569
+ const address = token.address.toLowerCase();
570
+
571
+ if (pools.length === 0) {
572
+ pools.push({
573
+ exchange: this.dexKey,
574
+ address: this.settlementAddress,
575
+ connectorTokens: [
576
+ {
577
+ address: address,
578
+ decimals: this.tokensMap[address].decimals,
579
+ symbol: this.tokensMap[address].ticker,
580
+ },
581
+ ],
582
+ liquidityUSD,
583
+ });
584
+ } else {
585
+ pools[0].liquidityUSD += liquidityUSD;
586
+ pools[0].connectorTokens.push({
587
+ address: address,
588
+ decimals: this.tokensMap[address].decimals,
589
+ symbol: this.tokensMap[address].ticker,
590
+ });
591
+ }
592
+ }
593
+ }
594
+
595
+ return pools
596
+ .sort((a, b) => b.liquidityUSD - a.liquidityUSD)
597
+ .slice(0, limit);
598
+ }
599
+
600
+ getDexParam(
601
+ srcToken: Address,
602
+ destToken: Address,
603
+ srcAmount: NumberAsString,
604
+ destAmount: NumberAsString,
605
+ recipient: Address,
606
+ data: BebopData,
607
+ side: SwapSide,
608
+ ): DexExchangeParam {
609
+ const { tx } = data;
610
+
611
+ assert(tx !== undefined, `${this.dexKey}-${this.network}: tx undefined`);
612
+
613
+ return {
614
+ exchangeData: tx.data,
615
+ needWrapNative: this.needWrapNative,
616
+ dexFuncHasRecipient: true,
617
+ targetExchange: this.settlementAddress,
618
+ returnAmountPos: undefined,
619
+ };
620
+ }
621
+
622
+ async preProcessTransaction(
623
+ optimalSwapExchange: OptimalSwapExchange<BebopData>,
624
+ srcToken: Token,
625
+ destToken: Token,
626
+ side: SwapSide,
627
+ options: PreprocessTransactionOptions,
628
+ ): Promise<[OptimalSwapExchange<BebopData>, ExchangeTxInfo]> {
629
+ const isSell = side === SwapSide.SELL;
630
+ const isBuy = side === SwapSide.BUY;
631
+
632
+ const params = {
633
+ sell_tokens: utils.getAddress(srcToken.address),
634
+ buy_tokens: utils.getAddress(destToken.address),
635
+ sell_amounts: isSell ? optimalSwapExchange.srcAmount : undefined,
636
+ buy_amounts: isBuy ? optimalSwapExchange.destAmount : undefined,
637
+ taker_address: utils.getAddress(options.executionContractAddress),
638
+ receiver_address: utils.getAddress(options.recipient),
639
+ gasless: false,
640
+ skip_validation: true,
641
+ source: BEBOP_AUTH_NAME,
642
+ };
643
+
644
+ try {
645
+ const response: BebopData = await this.dexHelper.httpRequest.get(
646
+ `${BEBOP_API_URL}/pmm/${
647
+ BebopConfig['Bebop'][this.network].chainName
648
+ }/v3/quote?${qs.stringify(params)}`,
649
+ BEBOP_QUOTE_TIMEOUT_MS,
650
+ {
651
+ 'source-auth': this.bebopAuthToken,
652
+ },
653
+ );
654
+
655
+ if (!response) {
656
+ throw new Error('Failed to get quote');
657
+ }
658
+
659
+ if (
660
+ !response.tx ||
661
+ !response.buyTokens ||
662
+ !response.sellTokens ||
663
+ !response.expiry
664
+ ) {
665
+ throw new Error('Failed to get quote. No tx info');
666
+ }
667
+
668
+ if (side == SwapSide.SELL) {
669
+ const requiredAmount = BigInt(optimalSwapExchange.destAmount);
670
+ const quoteAmount = BigInt(
671
+ response.buyTokens[utils.getAddress(destToken.address)].amount,
672
+ );
673
+ const requiredAmountWithSlippage = new BigNumber(
674
+ requiredAmount.toString(),
675
+ )
676
+ .times(options.slippageFactor)
677
+ .toFixed(0);
678
+ if (quoteAmount < BigInt(requiredAmountWithSlippage)) {
679
+ throw new SlippageError(
680
+ `Slipped, factor: ${quoteAmount.toString()} < ${requiredAmountWithSlippage}`,
681
+ );
682
+ }
683
+ } else {
684
+ const requiredAmount = BigInt(optimalSwapExchange.srcAmount);
685
+ const quoteAmount = BigInt(
686
+ response.sellTokens[utils.getAddress(srcToken.address)].amount,
687
+ );
688
+ const requiredAmountWithSlippage = new BigNumber(
689
+ requiredAmount.toString(),
690
+ )
691
+ .times(options.slippageFactor)
692
+ .toFixed(0);
693
+ if (quoteAmount > BigInt(requiredAmountWithSlippage)) {
694
+ throw new SlippageError(
695
+ `Slipped, factor: ${
696
+ options.slippageFactor
697
+ } ${quoteAmount.toString()} > ${requiredAmountWithSlippage}`,
698
+ );
699
+ }
700
+ }
701
+ return [
702
+ {
703
+ ...optimalSwapExchange,
704
+ data: {
705
+ ...response,
706
+ },
707
+ },
708
+ { deadline: BigInt(response.expiry) },
709
+ ];
710
+ } catch (e: any) {
711
+ const message = `${this.dexKey}-${this.network}: ${e}`;
712
+ this.logger.error(message);
713
+ if (!e?.isSlippageError) {
714
+ this.restrict();
715
+ }
716
+ throw new Error(message);
717
+ }
718
+ }
719
+
720
+ async restrict() {
721
+ const errorsDataRaw = await this.dexHelper.cache.get(
722
+ this.dexKey,
723
+ this.network,
724
+ BEBOP_ERRORS_CACHE_KEY,
725
+ );
726
+
727
+ const errorsData: RestrictData = Utils.Parse(errorsDataRaw);
728
+ const ERRORS_TTL_S = Math.floor(BEBOP_RESTRICT_CHECK_INTERVAL_MS / 1000);
729
+
730
+ if (
731
+ !errorsData ||
732
+ errorsData?.addedDatetimeMs + BEBOP_RESTRICT_CHECK_INTERVAL_MS <
733
+ Date.now()
734
+ ) {
735
+ this.logger.warn(
736
+ `${this.dexKey}-${this.network}: First encounter of error OR error ocurred outside of threshold, setting up counter`,
737
+ );
738
+ const data: RestrictData = {
739
+ count: 1,
740
+ addedDatetimeMs: Date.now(),
741
+ };
742
+ await this.dexHelper.cache.setex(
743
+ this.dexKey,
744
+ this.network,
745
+ BEBOP_ERRORS_CACHE_KEY,
746
+ ERRORS_TTL_S,
747
+ Utils.Serialize(data),
748
+ );
749
+ return;
750
+ } else {
751
+ if (errorsData.count + 1 >= BEBOP_RESTRICT_COUNT_THRESHOLD) {
752
+ this.logger.warn(
753
+ `${this.dexKey}-${this.network}: Restricting due to error count=${
754
+ errorsData.count + 1
755
+ } within ${BEBOP_RESTRICT_CHECK_INTERVAL_MS / 1000 / 60} minutes`,
756
+ );
757
+ await this.dexHelper.cache.setex(
758
+ this.dexKey,
759
+ this.network,
760
+ BEBOP_RESTRICTED_CACHE_KEY,
761
+ BEBOP_RESTRICT_TTL_S,
762
+ 'true',
763
+ );
764
+ } else {
765
+ this.logger.warn(
766
+ `${this.dexKey}-${this.network}: Error count increased`,
767
+ );
768
+ const data: RestrictData = {
769
+ count: errorsData.count + 1,
770
+ addedDatetimeMs: errorsData.addedDatetimeMs,
771
+ };
772
+ await this.dexHelper.cache.setex(
773
+ this.dexKey,
774
+ this.network,
775
+ BEBOP_ERRORS_CACHE_KEY,
776
+ ERRORS_TTL_S,
777
+ Utils.Serialize(data),
778
+ );
779
+ }
780
+ }
781
+ }
782
+
783
+ async isRestricted(): Promise<boolean> {
784
+ const result = await this.dexHelper.cache.get(
785
+ this.dexKey,
786
+ this.network,
787
+ BEBOP_RESTRICTED_CACHE_KEY,
788
+ );
789
+
790
+ return result === 'true';
791
+ }
792
+
793
+ async getCachedPrices(): Promise<BebopPricingResponse | null> {
794
+ const cachedPrices = await this.dexHelper.cache.get(
795
+ this.dexKey,
796
+ this.network,
797
+ this.pricesCacheKey,
798
+ );
799
+
800
+ if (cachedPrices) {
801
+ return JSON.parse(cachedPrices) as BebopPricingResponse;
802
+ }
803
+
804
+ return null;
805
+ }
806
+
807
+ async getCachedTokens(): Promise<TokenDataMap | null> {
808
+ const cachedTokens = await this.dexHelper.cache.get(
809
+ this.dexKey,
810
+ this.network,
811
+ this.tokensAddrCacheKey,
812
+ );
813
+
814
+ if (cachedTokens) {
815
+ return JSON.parse(cachedTokens) as TokenDataMap;
816
+ }
817
+
818
+ return null;
819
+ }
820
+
821
+ getTokenFromAddress(address: Address): Token {
822
+ const bebopToken = this.tokensMap[address.toLowerCase()];
823
+ return {
824
+ address,
825
+ decimals: bebopToken.decimals,
826
+ symbol: bebopToken.ticker,
827
+ };
828
+ }
829
+
830
+ releaseResources(): AsyncOrSync<void> {
831
+ if (!this.dexHelper.config.isSlave) {
832
+ this.rateFetcher.stop();
833
+ }
834
+ }
835
+ }
836
+
837
+ const sleep = (time: number) =>
838
+ new Promise(resolve => {
839
+ setTimeout(resolve, time);
840
+ });