@paraswap/dex-lib 3.4.2-balancer-v2-single-swap.0 → 3.4.2

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.
Files changed (110) hide show
  1. package/README.md +377 -377
  2. package/build/config.js +9 -5
  3. package/build/config.js.map +1 -1
  4. package/build/dex/algebra/algebra.js +25 -25
  5. package/build/dex/balancer-v1/balancer-v1.js +34 -34
  6. package/build/dex/balancer-v2/balancer-v2.d.ts +1 -1
  7. package/build/dex/balancer-v2/balancer-v2.js +65 -78
  8. package/build/dex/balancer-v2/balancer-v2.js.map +1 -1
  9. package/build/dex/camelot/camelot.js +27 -27
  10. package/build/dex/kyberdmm/kyberdmm.js +27 -27
  11. package/build/dex/pancakeswap-v3/pancakeswap-v3.js +25 -25
  12. package/build/dex/solidly/solidly.js +27 -27
  13. package/build/dex/solidly-v3/solidly-v3.js +25 -25
  14. package/build/dex/swaap-v2/config.js +15 -0
  15. package/build/dex/swaap-v2/config.js.map +1 -1
  16. package/build/dex/uniswap-v2/uniswap-v2.js +26 -26
  17. package/build/dex/uniswap-v3/uniswap-v3.js +25 -25
  18. package/funding.json +5 -5
  19. package/package.json +1 -1
  20. package/src/config.ts +500 -496
  21. package/src/constants.ts +67 -67
  22. package/src/dex/aave-v2/aave-v2.ts +409 -409
  23. package/src/dex/aave-v3/aave-v3.ts +320 -320
  24. package/src/dex/algebra/algebra.ts +1180 -1180
  25. package/src/dex/angle-staked-stable/angle-staked-stable.ts +347 -347
  26. package/src/dex/augustus-rfq.ts +180 -180
  27. package/src/dex/bProtocol/bProtocol.ts +108 -108
  28. package/src/dex/balancer-v1/balancer-v1.ts +572 -572
  29. package/src/dex/balancer-v2/balancer-v2-e2e.test.ts +1699 -1699
  30. package/src/dex/balancer-v2/balancer-v2.ts +1669 -1701
  31. package/src/dex/bancor/bancor.ts +128 -128
  32. package/src/dex/camelot/camelot.ts +873 -873
  33. package/src/dex/compound/compound.ts +107 -107
  34. package/src/dex/curve-v1/config.ts +1179 -1179
  35. package/src/dex/curve-v1/curve-v1-e2e.test.ts +507 -507
  36. package/src/dex/curve-v1/curve-v1.ts +1315 -1315
  37. package/src/dex/curve-v1-factory/config.ts +1018 -1018
  38. package/src/dex/curve-v1-factory/curve-v1-factory.ts +1288 -1288
  39. package/src/dex/curve-v2/curve-v2.ts +440 -440
  40. package/src/dex/dexalot/dexalot.ts +1100 -1100
  41. package/src/dex/dodo-v1/dodo-v1.ts +132 -132
  42. package/src/dex/dodo-v2.ts +266 -266
  43. package/src/dex/etherfi.ts +239 -239
  44. package/src/dex/generic-rfq/generic-rfq.ts +431 -431
  45. package/src/dex/gmx/gmx.ts +321 -321
  46. package/src/dex/hashflow/hashflow.ts +1135 -1135
  47. package/src/dex/idex.ts +254 -254
  48. package/src/dex/index.ts +357 -357
  49. package/src/dex/jarvis-v6/jarvis-v6.ts +509 -509
  50. package/src/dex/jarvis.ts +262 -262
  51. package/src/dex/kyberdmm/kyberdmm.ts +649 -649
  52. package/src/dex/lido/lido.ts +110 -110
  53. package/src/dex/maker-psm/maker-psm.ts +592 -592
  54. package/src/dex/maverick-v1/maverick-v1.ts +537 -537
  55. package/src/dex/nerve/config.ts +635 -635
  56. package/src/dex/nerve/nerve-e2e.test.ts +731 -731
  57. package/src/dex/nerve/nerve-events.test.ts +911 -911
  58. package/src/dex/nerve/nerve.ts +467 -467
  59. package/src/dex/onebit/onebit.ts +113 -113
  60. package/src/dex/pancakeswap-v3/pancakeswap-v3.ts +1150 -1150
  61. package/src/dex/paraswap-limit-orders/config.ts +60 -60
  62. package/src/dex/paraswap-limit-orders/paraswap-limit-orders-e2e.test.ts +137 -137
  63. package/src/dex/paraswap-limit-orders/paraswap-limit-orders-integration.test.ts +328 -328
  64. package/src/dex/paraswap-limit-orders/paraswap-limit-orders.ts +631 -631
  65. package/src/dex/polygon-migrator/polygon-migrator.ts +194 -194
  66. package/src/dex/quick-perps/quick-perps.ts +337 -337
  67. package/src/dex/quickswap/config.ts +16 -16
  68. package/src/dex/quickswap/quickswap-e2e.test.ts +384 -384
  69. package/src/dex/smoothy/smoothy.ts +110 -110
  70. package/src/dex/solidly/config.ts +225 -225
  71. package/src/dex/solidly/solidly-e2e.test.ts +1421 -1421
  72. package/src/dex/solidly/solidly-integration.test.ts +1753 -1753
  73. package/src/dex/solidly/solidly.ts +687 -687
  74. package/src/dex/solidly-v3/solidly-v3.ts +968 -968
  75. package/src/dex/spark/spark.ts +284 -284
  76. package/src/dex/stable-pool/stable-pool.ts +129 -129
  77. package/src/dex/swaap-v2/config.ts +15 -0
  78. package/src/dex/swaap-v2/swaap-v2-e2e.test.ts +233 -17
  79. package/src/dex/swaap-v2/swaap-v2-integration.test.ts +1 -1
  80. package/src/dex/swaap-v2/swaap-v2.ts +1056 -1056
  81. package/src/dex/swell/swell.ts +312 -312
  82. package/src/dex/synthetix/synthetix.ts +420 -420
  83. package/src/dex/trader-joe-v2.1/trader-joe-v2.1.ts +180 -180
  84. package/src/dex/uniswap-v2/config.ts +637 -637
  85. package/src/dex/uniswap-v2/constants.ts +58 -58
  86. package/src/dex/uniswap-v2/uniswap-v2-e2e-avalanche.test.ts +1257 -1257
  87. package/src/dex/uniswap-v2/uniswap-v2-e2e-base.test.ts +42 -42
  88. package/src/dex/uniswap-v2/uniswap-v2-e2e-bsc.test.ts +1295 -1295
  89. package/src/dex/uniswap-v2/uniswap-v2-e2e-mainnet.test.ts +1834 -1834
  90. package/src/dex/uniswap-v2/uniswap-v2-e2e-polygon.test.ts +871 -871
  91. package/src/dex/uniswap-v2/uniswap-v2.ts +1117 -1117
  92. package/src/dex/uniswap-v3/uniswap-v3.ts +1501 -1501
  93. package/src/dex/uniswap-v3.ts +242 -242
  94. package/src/dex/weth/config.ts +73 -73
  95. package/src/dex/weth/weth.ts +230 -230
  96. package/src/dex/wombat/wombat.ts +467 -467
  97. package/src/dex/woo-fi-v2/woo-fi-v2.ts +529 -529
  98. package/src/dex/wsteth/wsteth.ts +304 -304
  99. package/src/dex/zerox/index.ts +328 -328
  100. package/src/executor/Executor01BytecodeBuilder.ts +474 -474
  101. package/src/executor/Executor02BytecodeBuilder.ts +1392 -1392
  102. package/src/executor/ExecutorBytecodeBuilder.ts +390 -390
  103. package/src/executor/ExecutorDetector.ts +131 -131
  104. package/src/executor/constants.ts +66 -66
  105. package/src/executor/executor01-bytecode-builder-e2e.test.ts +1478 -1478
  106. package/src/executor/utils.ts +62 -62
  107. package/src/router/simpleswap.ts +366 -366
  108. package/src/types.ts +342 -342
  109. package/tests/constants-e2e.ts +1632 -1622
  110. package/tests/generate-new-limit-order.ts +107 -107
@@ -1,1135 +1,1135 @@
1
- import { ChainId, ZERO_ADDRESS } from '@hashflow/sdk';
2
- import { Chain, ChainType, HashflowApi } from '@hashflow/taker-js';
3
- import {
4
- MarketMakersResponse,
5
- PriceLevelsResponse,
6
- RfqResponse,
7
- } from '@hashflow/taker-js/dist/types/rest';
8
- import BigNumber from 'bignumber.js';
9
- import { Interface } from 'ethers/lib/utils';
10
- import { assert } from 'ts-essentials';
11
- import routerAbi from '../../abi/hashflow/HashflowRouter.abi.json';
12
- import { BI_MAX_UINT256 } from '../../bigint-constants';
13
- import { BN_0, BN_1, getBigNumberPow } from '../../bignumber-constants';
14
- import * as CALLDATA_GAS_COST from '../../calldata-gas-cost';
15
- import {
16
- CACHE_PREFIX,
17
- ETHER_ADDRESS,
18
- Network,
19
- SwapSide,
20
- } from '../../constants';
21
- import { IDexHelper } from '../../dex-helper/idex-helper';
22
- import { IDex } from '../../dex/idex';
23
- import {
24
- AdapterExchangeParam,
25
- Address,
26
- DexExchangeParam,
27
- ExchangePrices,
28
- ExchangeTxInfo,
29
- Logger,
30
- NumberAsString,
31
- OptimalSwapExchange,
32
- PoolLiquidity,
33
- PoolPrices,
34
- PreprocessTransactionOptions,
35
- SimpleExchangeParam,
36
- Token,
37
- } from '../../types';
38
- import { getDexKeysWithNetwork, Utils } from '../../utils';
39
- import { TooStrictSlippageCheckError } from '../generic-rfq/types';
40
- import { SimpleExchange } from '../simple-exchange';
41
- import { Adapters, HashflowConfig } from './config';
42
- import {
43
- CONSECUTIVE_ERROR_THRESHOLD,
44
- CONSECUTIVE_ERROR_TIMESPAN_MS,
45
- ERROR_CODE_TO_RESTRICT_TTL,
46
- HASHFLOW_API_CLIENT_NAME,
47
- HASHFLOW_API_MARKET_MAKERS_POLLING_INTERVAL_MS,
48
- HASHFLOW_API_PRICES_POLLING_INTERVAL_MS,
49
- HASHFLOW_API_URL,
50
- HASHFLOW_BLACKLIST_TTL_S,
51
- HASHFLOW_GAS_COST,
52
- HASHFLOW_MARKET_MAKERS_CACHES_TTL_S,
53
- HASHFLOW_MIN_SLIPPAGE_FACTOR_THRESHOLD_FOR_RESTRICTION,
54
- HASHFLOW_MM_RESTRICT_TTL_S,
55
- HASHFLOW_PRICES_CACHES_TTL_S,
56
- UNKNOWN_ERROR_CODE,
57
- } from './constants';
58
- import { RateFetcher } from './rate-fetcher';
59
- import {
60
- CacheErrorCodesData,
61
- ErrorCode,
62
- HashflowData,
63
- PriceLevel,
64
- RfqError,
65
- SlippageCheckError,
66
- } from './types';
67
-
68
- export class Hashflow extends SimpleExchange implements IDex<HashflowData> {
69
- readonly isStatePollingDex = true;
70
- readonly hasConstantPriceLargeAmounts = false;
71
- readonly needWrapNative = false;
72
- readonly needsSequentialPreprocessing = true;
73
- readonly isFeeOnTransferSupported = false;
74
- private api: HashflowApi;
75
- private rateFetcher: RateFetcher;
76
-
77
- private hashFlowAuthToken: string;
78
- private disabledMMs: Set<string>;
79
- private runtimeMMsRestrictHashMapKey: string;
80
- private runtimeMMsRestrictHashMapErrorCodesKey: string;
81
-
82
- private pricesCacheKey: string;
83
- private marketMakersCacheKey: string;
84
-
85
- public static dexKeysWithNetwork: { key: string; networks: Network[] }[] =
86
- getDexKeysWithNetwork(HashflowConfig);
87
-
88
- logger: Logger;
89
-
90
- constructor(
91
- readonly network: Network,
92
- readonly dexKey: string,
93
- readonly dexHelper: IDexHelper,
94
- protected adapters = Adapters[network] || {},
95
- readonly routerAddress: string = HashflowConfig['Hashflow'][network]
96
- .routerAddress,
97
- protected routerInterface = new Interface(routerAbi),
98
- ) {
99
- super(dexHelper, dexKey);
100
- this.logger = dexHelper.getLogger(dexKey);
101
- const token = dexHelper.config.data.hashFlowAuthToken;
102
- assert(
103
- token !== undefined,
104
- 'Hashflow auth token is not specified with env variable',
105
- );
106
-
107
- this.hashFlowAuthToken = token;
108
- this.api = new HashflowApi(
109
- 'taker',
110
- HASHFLOW_API_CLIENT_NAME,
111
- this.hashFlowAuthToken,
112
- );
113
-
114
- this.pricesCacheKey = `${CACHE_PREFIX}_${this.dexHelper.config.data.network}_${this.dexKey}_prices`;
115
- this.marketMakersCacheKey = `${CACHE_PREFIX}_${this.dexHelper.config.data.network}_${this.dexKey}_mms`;
116
-
117
- this.disabledMMs = new Set(dexHelper.config.data.hashFlowDisabledMMs);
118
- this.runtimeMMsRestrictHashMapKey =
119
- `${CACHE_PREFIX}_${this.dexKey}_${this.network}_restricted_mms`.toLowerCase();
120
- this.runtimeMMsRestrictHashMapErrorCodesKey =
121
- `${CACHE_PREFIX}_${this.dexKey}_${this.network}_restricted_mms_error_codes`.toLowerCase();
122
-
123
- this.rateFetcher = new RateFetcher(
124
- this.dexHelper,
125
- this.dexKey,
126
- this.network,
127
- this.logger,
128
- {
129
- rateConfig: {
130
- pricesIntervalMs: HASHFLOW_API_PRICES_POLLING_INTERVAL_MS,
131
- markerMakersIntervalMs:
132
- HASHFLOW_API_MARKET_MAKERS_POLLING_INTERVAL_MS,
133
- marketMakersReqParams: {
134
- url: `${HASHFLOW_API_URL}/taker/v3/market-makers`,
135
- params: {
136
- baseChainId: this.network,
137
- source: HASHFLOW_API_CLIENT_NAME,
138
- baseChainType: 'evm',
139
- },
140
- headers: { Authorization: this.hashFlowAuthToken },
141
- },
142
- pricesReqParams: {
143
- url: `${HASHFLOW_API_URL}/taker/v3/price-levels`,
144
- params: {
145
- baseChainId: this.network,
146
- source: HASHFLOW_API_CLIENT_NAME,
147
- marketMakers: [],
148
- baseChainType: 'evm',
149
- },
150
- headers: { Authorization: this.hashFlowAuthToken },
151
- },
152
- getCachedMarketMakers: this.getCachedMarketMakers.bind(this),
153
- filterMarketMakers: this.getFilteredMarketMakers.bind(this),
154
- pricesCacheKey: this.pricesCacheKey,
155
- pricesCacheTTLSecs: HASHFLOW_PRICES_CACHES_TTL_S,
156
- marketMakersCacheKey: this.marketMakersCacheKey,
157
- marketMakersCacheTTLSecs: HASHFLOW_MARKET_MAKERS_CACHES_TTL_S,
158
- },
159
- },
160
- );
161
- }
162
-
163
- async initializePricing(blockNumber: number): Promise<void> {
164
- if (!this.dexHelper.config.isSlave) {
165
- this.rateFetcher.start();
166
- }
167
-
168
- return;
169
- }
170
-
171
- getAdapters(side: SwapSide): { name: string; index: number }[] | null {
172
- return this.adapters[side] ? this.adapters[side] : null;
173
- }
174
-
175
- getPairName = (srcAddress: Address, destAddress: Address) =>
176
- `${srcAddress}_${destAddress}`.toLowerCase();
177
-
178
- getIdentifierPrefix(srcAddress: Address, destAddress: Address) {
179
- return `${this.dexKey}_${this.getPairName(
180
- srcAddress,
181
- destAddress,
182
- )}`.toLowerCase();
183
- }
184
-
185
- getPoolIdentifier(srcAddress: Address, destAddress: Address, mm: string) {
186
- return `${this.getIdentifierPrefix(
187
- srcAddress,
188
- destAddress,
189
- )}_${mm}`.toLowerCase();
190
- }
191
-
192
- async getPoolIdentifiers(
193
- srcToken: Token,
194
- destToken: Token,
195
- side: SwapSide,
196
- blockNumber: number,
197
- ): Promise<string[]> {
198
- const normalizedSrcToken = this.normalizeToken(srcToken);
199
- const normalizedDestToken = this.normalizeToken(destToken);
200
-
201
- if (normalizedSrcToken.address === normalizedDestToken.address) {
202
- return [];
203
- }
204
-
205
- const levels = (await this.getCachedLevels()) || {};
206
- const makers = Object.keys(levels);
207
-
208
- return makers
209
- .filter(m => {
210
- const pairs = levels[m]?.map(entry => entry.pair) ?? [];
211
- return pairs.some(
212
- p =>
213
- normalizedSrcToken.address === p.baseToken.toLowerCase() &&
214
- normalizedDestToken.address === p.quoteToken.toLowerCase(),
215
- );
216
- })
217
- .map(m =>
218
- this.getPoolIdentifier(
219
- normalizedSrcToken.address,
220
- normalizedDestToken.address,
221
- m,
222
- ),
223
- );
224
- }
225
-
226
- private async getFilteredMarketMakers(makers: string[]): Promise<string[]> {
227
- const cachedRestrictionUnparsed = await this.dexHelper.cache.hgetAll(
228
- this.runtimeMMsRestrictHashMapKey,
229
- );
230
-
231
- const runtimeRestrictedMMs = this.parseCacheRestrictionAndExpiryIfNeeded(
232
- cachedRestrictionUnparsed,
233
- );
234
-
235
- return makers.filter(
236
- mm => !(this.disabledMMs.has(mm) || runtimeRestrictedMMs.has(mm)),
237
- );
238
- }
239
-
240
- parseCacheRestrictionAndExpiryIfNeeded(
241
- cachedValues: Record<string, string>,
242
- ): Set<string> {
243
- const restrictedMMs = new Set<string>();
244
- const toDelete: string[] = [];
245
- const expirationThreshold = Date.now() - HASHFLOW_MM_RESTRICT_TTL_S * 1000;
246
-
247
- // For log message
248
- let stringifiedRestrictedMMs = '';
249
-
250
- Object.entries(cachedValues).forEach(([mm, createdAt]) => {
251
- if (+createdAt < expirationThreshold) {
252
- toDelete.push(mm);
253
- } else {
254
- restrictedMMs.add(mm);
255
- stringifiedRestrictedMMs += `${mm}, `;
256
- }
257
- });
258
-
259
- if (restrictedMMs.size > 0) {
260
- this.logger.debug(
261
- `${this.dexKey}-${
262
- this.network
263
- }: pricing is skipped for ${stringifiedRestrictedMMs.slice(
264
- 0,
265
- -2,
266
- )} due to restriction`,
267
- );
268
- }
269
-
270
- if (toDelete.length > 0) {
271
- this.logger.debug(
272
- `${this.dexKey}-${this.network}: Deleting expired keys: `,
273
- toDelete.join(`,`),
274
- );
275
-
276
- // No need to await since we don't care about when it executes
277
- // And we don't want to stop pricing request because of this
278
- this.dexHelper.cache
279
- .hdel(this.runtimeMMsRestrictHashMapKey, toDelete)
280
- .catch(e => {
281
- this.logger.error(
282
- `${this.dexKey}-${this.network}: Failed to delete expired keys: `,
283
- e,
284
- );
285
- });
286
- }
287
-
288
- return restrictedMMs;
289
- }
290
-
291
- computePricesFromLevels(
292
- amounts: BigNumber[],
293
- levels: PriceLevel[],
294
- srcToken: Token,
295
- destToken: Token,
296
- side: SwapSide,
297
- ): bigint[] {
298
- assert(levels.length > 0, 'Levels should not be empty');
299
-
300
- const outputs = new Array<BigNumber>(amounts.length).fill(BN_0);
301
- // FIXME: There is still case when last amount is fillable, but in between
302
- // we may have splits that are less than min. amount. I assume that case is very
303
- // and not addressing in current fix. If someone will look into that case, just be aware
304
- // that it is not addressed
305
-
306
- for (const [i, amount] of amounts.entries()) {
307
- if (amount.isZero()) {
308
- outputs[i] = BN_0;
309
- } else {
310
- const output =
311
- side === SwapSide.SELL
312
- ? this.computeLevelsQuote(levels, amount, undefined)
313
- : this.computeLevelsQuote(levels, undefined, amount);
314
-
315
- if (output === undefined) {
316
- // If current amount was unfillable, then bigger amounts are unfillable as well
317
- break;
318
- } else {
319
- outputs[i] = output;
320
- }
321
- }
322
- }
323
-
324
- const decimals =
325
- side === SwapSide.SELL ? destToken.decimals : srcToken.decimals;
326
-
327
- return outputs.map(o =>
328
- BigInt(o.multipliedBy(getBigNumberPow(decimals)).toFixed(0)),
329
- );
330
- }
331
-
332
- toPriceLevelsBN = (
333
- priceLevels: PriceLevel[],
334
- ): { level: BigNumber; price: BigNumber }[] =>
335
- priceLevels.map(l => ({
336
- level: new BigNumber(l.q),
337
- price: new BigNumber(l.p),
338
- }));
339
-
340
- computeLevelsQuote(
341
- priceLevels: PriceLevel[],
342
- reqBaseAmount?: BigNumber,
343
- reqQuoteAmount?: BigNumber,
344
- ): BigNumber | undefined {
345
- if (reqBaseAmount && reqQuoteAmount) {
346
- return undefined;
347
- }
348
-
349
- const levels = this.toPriceLevelsBN(priceLevels);
350
- if (!levels.length) {
351
- return undefined;
352
- }
353
-
354
- const quote = {
355
- baseAmount: levels[0]!.level,
356
- quoteAmount: levels[0]!.level.multipliedBy(levels[0]!.price),
357
- };
358
- if (
359
- (reqBaseAmount && reqBaseAmount.lt(quote.baseAmount)) ||
360
- (reqQuoteAmount && reqQuoteAmount.lt(quote.quoteAmount))
361
- ) {
362
- return undefined;
363
- }
364
-
365
- for (let i = 1; i < levels.length; i++) {
366
- const nextLevel = levels[i]!;
367
- const nextLevelDepth = nextLevel.level.minus(levels[i - 1]!.level);
368
- const nextLevelQuote = quote.quoteAmount.plus(
369
- nextLevelDepth.multipliedBy(nextLevel.price),
370
- );
371
- if (reqBaseAmount && reqBaseAmount.lte(nextLevel.level)) {
372
- const baseDifference = reqBaseAmount.minus(quote.baseAmount);
373
- const quoteAmount = quote.quoteAmount.plus(
374
- baseDifference.multipliedBy(nextLevel.price),
375
- );
376
- return quoteAmount;
377
- } else if (reqQuoteAmount && reqQuoteAmount.lte(nextLevelQuote)) {
378
- const quoteDifference = reqQuoteAmount.minus(quote.quoteAmount);
379
- const baseAmount = quote.baseAmount.plus(
380
- quoteDifference.dividedBy(nextLevel.price),
381
- );
382
- return baseAmount;
383
- }
384
-
385
- quote.baseAmount = nextLevel.level;
386
- quote.quoteAmount = nextLevelQuote;
387
- }
388
-
389
- return undefined;
390
- }
391
-
392
- async getCachedMarketMakers(): Promise<
393
- MarketMakersResponse['marketMakers'] | null
394
- > {
395
- const cachedMarketMakers = await this.dexHelper.cache.rawget(
396
- this.marketMakersCacheKey,
397
- );
398
-
399
- if (cachedMarketMakers) {
400
- return JSON.parse(
401
- cachedMarketMakers,
402
- ) as MarketMakersResponse['marketMakers'];
403
- }
404
-
405
- return null;
406
- }
407
-
408
- async getCachedLevels(): Promise<PriceLevelsResponse['levels'] | null> {
409
- const cachedLevels = await this.dexHelper.cache.rawget(this.pricesCacheKey);
410
-
411
- if (cachedLevels) {
412
- return JSON.parse(cachedLevels) as PriceLevelsResponse['levels'];
413
- }
414
-
415
- return null;
416
- }
417
-
418
- // Hashflow protocol for native token expects 0x00000... instead of 0xeeeee...
419
- normalizeToken(token: Token): Token {
420
- return {
421
- address:
422
- token.address.toLowerCase() === ETHER_ADDRESS
423
- ? ZERO_ADDRESS
424
- : token.address.toLowerCase(),
425
- decimals: token.decimals,
426
- };
427
- }
428
-
429
- async getPricesVolume(
430
- srcToken: Token,
431
- destToken: Token,
432
- amounts: bigint[],
433
- side: SwapSide,
434
- blockNumber: number,
435
- limitPools?: string[],
436
- ): Promise<null | ExchangePrices<HashflowData>> {
437
- try {
438
- const normalizedSrcToken = this.normalizeToken(srcToken);
439
- const normalizedDestToken = this.normalizeToken(destToken);
440
-
441
- if (normalizedSrcToken.address === normalizedDestToken.address) {
442
- return null;
443
- }
444
-
445
- const prefix = this.getIdentifierPrefix(
446
- normalizedSrcToken.address,
447
- normalizedDestToken.address,
448
- );
449
-
450
- const pools =
451
- limitPools ??
452
- (await this.getPoolIdentifiers(srcToken, destToken, side, blockNumber));
453
-
454
- const marketMakersToUse = pools.map(p => p.split(`${prefix}_`).pop());
455
-
456
- const levelsMap = (await this.getCachedLevels()) || {};
457
-
458
- Object.keys(levelsMap).forEach(mmKey => {
459
- if (!marketMakersToUse.includes(mmKey)) {
460
- delete levelsMap[mmKey];
461
- }
462
- });
463
-
464
- const levelEntries: {
465
- mm: string;
466
- levels: PriceLevel[];
467
- }[] = Object.keys(levelsMap)
468
- .map(mm => {
469
- const entry = levelsMap[mm]?.find(
470
- e =>
471
- `${e.pair.baseToken}_${e.pair.quoteToken}` ===
472
- this.getPairName(
473
- normalizedSrcToken.address,
474
- normalizedDestToken.address,
475
- ),
476
- );
477
- if (entry === undefined) {
478
- return undefined;
479
- } else {
480
- return { mm, levels: entry.levels };
481
- }
482
- })
483
- .filter(o => o !== undefined)
484
- .map(o => o!);
485
-
486
- const prices = levelEntries.map(lEntry => {
487
- const { mm, levels } = lEntry;
488
-
489
- if (levels.length === 0) {
490
- return null;
491
- }
492
-
493
- const divider = getBigNumberPow(
494
- side === SwapSide.SELL
495
- ? normalizedSrcToken.decimals
496
- : normalizedDestToken.decimals,
497
- );
498
-
499
- const amountsRaw = amounts.map(a =>
500
- new BigNumber(a.toString()).dividedBy(divider),
501
- );
502
- const firstLevelRaw = levels[0];
503
- const firstLevelAmountBN = new BigNumber(firstLevelRaw.q);
504
-
505
- if (amountsRaw[amountsRaw.length - 1].lt(firstLevelAmountBN)) {
506
- return null;
507
- }
508
-
509
- if (firstLevelAmountBN.gt(0)) {
510
- // Add zero level for price computation
511
- levels.unshift({ q: '0', p: firstLevelRaw.p });
512
- }
513
-
514
- const unitPrice = this.computePricesFromLevels(
515
- [BN_1],
516
- levels,
517
- normalizedSrcToken,
518
- normalizedDestToken,
519
- side,
520
- )[0];
521
-
522
- const prices = this.computePricesFromLevels(
523
- amountsRaw,
524
- levels,
525
- normalizedSrcToken,
526
- normalizedDestToken,
527
- side,
528
- );
529
-
530
- return {
531
- gasCost: HASHFLOW_GAS_COST,
532
- exchange: this.dexKey,
533
- data: { mm },
534
- prices,
535
- unit: unitPrice,
536
- poolIdentifier: this.getPoolIdentifier(
537
- normalizedSrcToken.address,
538
- normalizedDestToken.address,
539
- mm,
540
- ),
541
- poolAddresses: [this.routerAddress],
542
- } as PoolPrices<HashflowData>;
543
- });
544
-
545
- return prices.filter((p): p is PoolPrices<HashflowData> => !!p);
546
- } catch (e: unknown) {
547
- this.logger.error(
548
- `Error_getPricesVolume ${srcToken.symbol || srcToken.address}, ${
549
- destToken.symbol || destToken.address
550
- }, ${side}:`,
551
- e,
552
- );
553
- return null;
554
- }
555
- }
556
-
557
- async preProcessTransaction(
558
- optimalSwapExchange: OptimalSwapExchange<HashflowData>,
559
- srcToken: Token,
560
- destToken: Token,
561
- side: SwapSide,
562
- options: PreprocessTransactionOptions,
563
- ): Promise<[OptimalSwapExchange<HashflowData>, ExchangeTxInfo]> {
564
- if (await this.isBlacklisted(options.txOrigin)) {
565
- this.logger.warn(
566
- `${this.dexKey}-${this.network}: blacklisted TX Origin address '${options.txOrigin}' trying to build a transaction. Bailing...`,
567
- );
568
- throw new Error(
569
- `${this.dexKey}-${
570
- this.network
571
- }: user=${options.txOrigin.toLowerCase()} is blacklisted`,
572
- );
573
- }
574
- const mm = optimalSwapExchange.data?.mm;
575
- assert(
576
- mm !== undefined,
577
- `${this.dexKey}-${this.network}: MM was not provided in data`,
578
- );
579
- const chainId = this.network as ChainId;
580
- let chainType: ChainType = 'evm';
581
- const chain: Chain = { chainType, chainId };
582
- const normalizedSrcToken = this.normalizeToken(srcToken);
583
- const normalizedDestToken = this.normalizeToken(destToken);
584
-
585
- let rfq: RfqResponse;
586
- try {
587
- rfq = await this.api.requestQuote({
588
- // sender is not passed, so for now ignore executionContractAddress
589
- baseChain: chain,
590
- baseToken: normalizedSrcToken.address,
591
- quoteToken: normalizedDestToken.address,
592
- ...(side === SwapSide.SELL
593
- ? {
594
- baseTokenAmount: optimalSwapExchange.srcAmount,
595
- }
596
- : { quoteTokenAmount: optimalSwapExchange.destAmount }),
597
- // receiver address
598
- wallet: options.recipient.toLowerCase(),
599
- effectiveTrader: options.txOrigin.toLowerCase(),
600
- marketMakers: [mm],
601
- });
602
-
603
- if (rfq.status !== 'success') {
604
- const message = `${this.dexKey}-${
605
- this.network
606
- }: Failed to fetch RFQ for ${this.getPairName(
607
- normalizedSrcToken.address,
608
- normalizedDestToken.address,
609
- )}: ${JSON.stringify(rfq)}`;
610
- this.logger.warn(message);
611
- throw new RfqError(message, `${rfq?.error?.code}` as ErrorCode);
612
- } else if (!rfq.quotes[0].quoteData) {
613
- const message = `${this.dexKey}-${
614
- this.network
615
- }: Failed to fetch RFQ for ${this.getPairName(
616
- normalizedSrcToken.address,
617
- normalizedDestToken.address,
618
- )}. Missing quote data`;
619
- this.logger.warn(message);
620
- throw new RfqError(message, 'MISSING_QUOTE_DATA');
621
- } else if (!rfq.quotes[0].signature) {
622
- const message = `${this.dexKey}-${
623
- this.network
624
- }: Failed to fetch RFQ for ${this.getPairName(
625
- normalizedSrcToken.address,
626
- normalizedDestToken.address,
627
- )}. Missing signature`;
628
- this.logger.warn(message);
629
- throw new RfqError(message, 'MISSING_SIGNATURE_DATA');
630
- }
631
-
632
- assert(
633
- rfq.quotes[0].quoteData.baseToken === normalizedSrcToken.address,
634
- `QuoteData baseToken=${rfq.quotes[0].quoteData.baseToken} is different from srcToken=${normalizedSrcToken.address}`,
635
- );
636
- assert(
637
- rfq.quotes[0].quoteData.quoteToken === normalizedDestToken.address,
638
- `QuoteData baseToken=${rfq.quotes[0].quoteData.quoteToken} is different from srcToken=${normalizedDestToken.address}`,
639
- );
640
-
641
- const expiryAsBigInt = BigInt(rfq.quotes[0].quoteData.quoteExpiry);
642
- const minDeadline = expiryAsBigInt > 0 ? expiryAsBigInt : BI_MAX_UINT256;
643
-
644
- const baseTokenAmount = BigInt(rfq.quotes[0].quoteData.baseTokenAmount);
645
- const quoteTokenAmount = BigInt(rfq.quotes[0].quoteData.quoteTokenAmount);
646
-
647
- const srcAmount = BigInt(optimalSwapExchange.srcAmount);
648
- const destAmount = BigInt(optimalSwapExchange.destAmount);
649
-
650
- const slippageFactor = options.slippageFactor;
651
-
652
- let isFailOnSlippage = false;
653
- let slippageErrorMessage = '';
654
-
655
- if (side === SwapSide.SELL) {
656
- if (
657
- quoteTokenAmount <
658
- BigInt(
659
- new BigNumber(destAmount.toString())
660
- .times(slippageFactor)
661
- .toFixed(0),
662
- )
663
- ) {
664
- isFailOnSlippage = true;
665
- const message = `${this.dexKey}-${this.network}: too much slippage on quote ${side} quoteTokenAmount ${quoteTokenAmount} / destAmount ${destAmount} < ${slippageFactor}`;
666
- slippageErrorMessage = message;
667
- this.logger.warn(message);
668
- }
669
- } else {
670
- if (quoteTokenAmount < destAmount) {
671
- isFailOnSlippage = true;
672
- // Won't receive enough assets
673
- const message = `${this.dexKey}-${this.network}: too much slippage on quote ${side} quoteTokenAmount ${quoteTokenAmount} < destAmount ${destAmount}`;
674
- slippageErrorMessage = message;
675
- this.logger.warn(message);
676
- } else {
677
- if (
678
- baseTokenAmount >
679
- BigInt(slippageFactor.times(srcAmount.toString()).toFixed(0))
680
- ) {
681
- isFailOnSlippage = true;
682
- const message = `${this.dexKey}-${
683
- this.network
684
- }: too much slippage on quote ${side} baseTokenAmount ${baseTokenAmount} / srcAmount ${srcAmount} > ${slippageFactor.toFixed()}`;
685
- slippageErrorMessage = message;
686
- this.logger.warn(message);
687
- }
688
- }
689
- }
690
-
691
- let isTooStrictSlippage = false;
692
- if (
693
- isFailOnSlippage &&
694
- side === SwapSide.SELL &&
695
- new BigNumber(1)
696
- .minus(slippageFactor)
697
- .lt(HASHFLOW_MIN_SLIPPAGE_FACTOR_THRESHOLD_FOR_RESTRICTION)
698
- ) {
699
- isTooStrictSlippage = true;
700
- } else if (
701
- isFailOnSlippage &&
702
- side === SwapSide.BUY &&
703
- slippageFactor
704
- .minus(1)
705
- .lt(HASHFLOW_MIN_SLIPPAGE_FACTOR_THRESHOLD_FOR_RESTRICTION)
706
- ) {
707
- isTooStrictSlippage = true;
708
- }
709
-
710
- if (isFailOnSlippage && isTooStrictSlippage) {
711
- throw new TooStrictSlippageCheckError(slippageErrorMessage);
712
- } else if (isFailOnSlippage && !isTooStrictSlippage) {
713
- throw new SlippageCheckError(slippageErrorMessage);
714
- }
715
-
716
- return [
717
- {
718
- ...optimalSwapExchange,
719
- data: {
720
- mm,
721
- quoteData: rfq.quotes[0].quoteData,
722
- signature: rfq.quotes[0].signature,
723
- },
724
- },
725
- { deadline: minDeadline },
726
- ];
727
- } catch (e) {
728
- if (
729
- e instanceof Error &&
730
- e.message?.toLowerCase().includes('user is restricted')
731
- ) {
732
- this.logger.warn(
733
- `${this.dexKey}-${this.network}: Encountered restricted user=${options.txOrigin}. Adding to local blacklist cache`,
734
- );
735
- await this.setBlacklist(options.txOrigin);
736
- } else {
737
- if (e instanceof TooStrictSlippageCheckError) {
738
- this.logger.warn(
739
- `${this.dexKey}-${this.network}: Market Maker ${mm} failed to build transaction on side ${side} with too strict slippage. Skipping restriction`,
740
- );
741
- } else {
742
- this.logger.warn(
743
- `${this.dexKey}-${this.network} MM unknown preprocess transaction error: ${e}`,
744
- );
745
- const code =
746
- e instanceof RfqError || e instanceof SlippageCheckError
747
- ? e?.code || UNKNOWN_ERROR_CODE
748
- : UNKNOWN_ERROR_CODE;
749
- await this.restrictMM(mm, code).catch(err =>
750
- this.logger.warn(`Failed to restrict MM ${mm}: ${err}`),
751
- );
752
- }
753
- }
754
-
755
- throw e;
756
- }
757
- }
758
-
759
- async restrictMM(mm: string, errorCode: ErrorCode): Promise<void> {
760
- const errorCodesRaw = await this.dexHelper.cache.hget(
761
- this.runtimeMMsRestrictHashMapErrorCodesKey,
762
- mm,
763
- );
764
-
765
- const errorCodes: CacheErrorCodesData = Utils.Parse(errorCodesRaw) || {};
766
-
767
- const error = errorCodes?.[errorCode];
768
-
769
- if (
770
- !error ||
771
- error.addedDatetimeMS + CONSECUTIVE_ERROR_TIMESPAN_MS < Date.now()
772
- ) {
773
- this.logger.warn(
774
- `${this.dexKey}-${this.network}: First encounter of error code=${errorCode} for ${mm} OR error ocurred outside of threshold, setting up counter`,
775
- );
776
- const data: CacheErrorCodesData = {
777
- ...errorCodes,
778
- [errorCode]: {
779
- count: 1,
780
- addedDatetimeMS: Date.now(),
781
- },
782
- };
783
- await this.dexHelper.cache.hset(
784
- this.runtimeMMsRestrictHashMapErrorCodesKey,
785
- mm,
786
- Utils.Serialize(data),
787
- );
788
- return;
789
- } else {
790
- const restrictTTLMs =
791
- ERROR_CODE_TO_RESTRICT_TTL[errorCode] ||
792
- ERROR_CODE_TO_RESTRICT_TTL[UNKNOWN_ERROR_CODE];
793
-
794
- if (error.count + 1 > CONSECUTIVE_ERROR_THRESHOLD) {
795
- this.logger.warn(
796
- `${this.dexKey}-${this.network}: ${mm} was restricted for ${
797
- restrictTTLMs / 1000
798
- } sec. due to ${errorCode} happening ${
799
- error.count + 1
800
- } times within last ${Math.floor(
801
- CONSECUTIVE_ERROR_TIMESPAN_MS / 1000 / 60,
802
- )} minutes`,
803
- );
804
-
805
- // date added is checked against HASHFLOW_MM_RESTRICT_TTL_S, as a hack we set a date so that it's checked against specific ttls based on error type to not change parsing logic for mms
806
- //
807
- // Example1: ttl for a particular error is 20 minutes (< default 60 minutes)
808
- // Meaning we need to set `addedDatetimeMS` for restrict hash as Date.now() - 40 minutes (diff between default restrict ttl=1h and particular error restrict ttl = 20 minutes). So the `addedDatetimeMS` is in the past, and after 20 minutes mm will not be considered as restricted
809
- //
810
- // Example 2: ttl for a particular error is 80 minutes (> default 60 minutes)
811
- // Meaning we need to set `addedDatetimeMS` for restrict hash as Date.now() + 20 minutes (in the future), then after an hour (default) it'll still be 20 minutes left for restriction to be cleared
812
-
813
- const defaultRestrictTTLMS = Math.floor(
814
- HASHFLOW_MM_RESTRICT_TTL_S * 1000,
815
- );
816
- const dateModifierMS =
817
- defaultRestrictTTLMS > restrictTTLMs
818
- ? -(defaultRestrictTTLMS - restrictTTLMs)
819
- : restrictTTLMs - defaultRestrictTTLMS;
820
-
821
- const date = (Date.now() + dateModifierMS).toString();
822
-
823
- // We use timestamp for creation date to later discern if it already expired or not
824
- await this.dexHelper.cache.hset(
825
- this.runtimeMMsRestrictHashMapKey,
826
- mm,
827
- date,
828
- );
829
-
830
- // resetting error count
831
- const data: CacheErrorCodesData = {
832
- ...errorCodes,
833
- [errorCode]: {
834
- count: 1,
835
- addedDatetimeMS: Date.now(),
836
- },
837
- };
838
- await this.dexHelper.cache.hset(
839
- this.runtimeMMsRestrictHashMapErrorCodesKey,
840
- mm,
841
- Utils.Serialize(data),
842
- );
843
-
844
- return;
845
- } else {
846
- const newCount = +error.count + 1;
847
- this.logger.warn(
848
- `${this.dexKey}-${this.network}: ${mm} Error with code: ${errorCode} happened ${newCount} times (below or equal to the limit: ${CONSECUTIVE_ERROR_THRESHOLD}), updating counter`,
849
- );
850
- const data: CacheErrorCodesData = {
851
- ...errorCodes,
852
- [errorCode]: {
853
- count: newCount,
854
- addedDatetimeMS: error.addedDatetimeMS, // initial date stays
855
- },
856
- };
857
- await this.dexHelper.cache.hset(
858
- this.runtimeMMsRestrictHashMapErrorCodesKey,
859
- mm,
860
- Utils.Serialize(data),
861
- );
862
- }
863
- }
864
- }
865
-
866
- getCalldataGasCost(poolPrices: PoolPrices<HashflowData>): number | number[] {
867
- // I am not sure if that is correct. If anybody know how to fix it,
868
- // please, go ahead :)
869
- return (
870
- CALLDATA_GAS_COST.DEX_OVERHEAD +
871
- // addresses: pool, quoteToken, externalAccount
872
- CALLDATA_GAS_COST.ADDRESS * 3 +
873
- // uint256: baseTokenAmount, quoteTokenAmount, quoteExpiry, nonce
874
- CALLDATA_GAS_COST.AMOUNT * 4 +
875
- // bytes32 txid;
876
- CALLDATA_GAS_COST.FULL_WORD +
877
- // I don't know how big is it, but from google results, I see 65 bytes for signature
878
- // bytes signature
879
- CALLDATA_GAS_COST.FULL_WORD * 2 +
880
- CALLDATA_GAS_COST.OFFSET_SMALL
881
- );
882
- }
883
-
884
- getTokenFromAddress?(address: Address): Token {
885
- // We don't have predefined set of tokens with decimals
886
- // Anyway we don't use decimals, so it is fine to do this
887
- return { address, decimals: 0 };
888
- }
889
-
890
- getAdapterParam(
891
- srcToken: string,
892
- destToken: string,
893
- srcAmount: string,
894
- destAmount: string,
895
- data: HashflowData,
896
- side: SwapSide,
897
- ): AdapterExchangeParam {
898
- const { quoteData, signature } = data;
899
-
900
- assert(
901
- quoteData !== undefined,
902
- `${this.dexKey}-${this.network}: quoteData undefined`,
903
- );
904
-
905
- const payload = this.routerInterface._abiCoder.encode(
906
- [
907
- 'tuple(address pool, address quoteToken, address externalAccount, uint256 baseTokenAmount, uint256 quoteTokenAmount, uint256 quoteExpiry, uint256 nonce, bytes32 txid, bytes signature)',
908
- ],
909
- [
910
- {
911
- pool: quoteData.pool,
912
- quoteToken: quoteData.quoteToken,
913
- externalAccount: quoteData.externalAccount ?? ZERO_ADDRESS,
914
- baseTokenAmount: quoteData.baseTokenAmount,
915
- quoteTokenAmount: quoteData.quoteTokenAmount,
916
- quoteExpiry: quoteData.quoteExpiry,
917
- nonce: quoteData.nonce ?? 0,
918
- txid: quoteData.txid,
919
- signature,
920
- },
921
- ],
922
- );
923
-
924
- return {
925
- targetExchange: this.routerAddress,
926
- payload,
927
- networkFee: '0',
928
- };
929
- }
930
-
931
- getBlackListKey(address: Address) {
932
- return `blacklist_${address}`.toLowerCase();
933
- }
934
-
935
- async isBlacklisted(txOrigin: Address): Promise<boolean> {
936
- const result = await this.dexHelper.cache.get(
937
- this.dexKey,
938
- this.network,
939
- this.getBlackListKey(txOrigin),
940
- );
941
- return result === 'blacklisted';
942
- }
943
-
944
- async setBlacklist(
945
- txOrigin: Address,
946
- ttl: number = HASHFLOW_BLACKLIST_TTL_S,
947
- ) {
948
- await this.dexHelper.cache.setex(
949
- this.dexKey,
950
- this.network,
951
- this.getBlackListKey(txOrigin),
952
- ttl,
953
- 'blacklisted',
954
- );
955
- return true;
956
- }
957
-
958
- async getSimpleParam(
959
- srcToken: string,
960
- destToken: string,
961
- srcAmount: string,
962
- destAmount: string,
963
- data: HashflowData,
964
- side: SwapSide,
965
- ): Promise<SimpleExchangeParam> {
966
- const { quoteData, signature } = data;
967
-
968
- assert(
969
- quoteData !== undefined,
970
- `${this.dexKey}-${this.network}: quoteData undefined`,
971
- );
972
-
973
- // Encode here the transaction arguments
974
- const swapData = this.routerInterface.encodeFunctionData('tradeRFQT', [
975
- [
976
- quoteData.pool,
977
- quoteData.externalAccount ?? ZERO_ADDRESS,
978
- quoteData.trader,
979
- quoteData.effectiveTrader ?? quoteData.trader,
980
- quoteData.baseToken,
981
- quoteData.quoteToken,
982
- quoteData.baseTokenAmount,
983
- quoteData.baseTokenAmount,
984
- quoteData.quoteTokenAmount,
985
- quoteData.quoteExpiry,
986
- quoteData.nonce ?? 0,
987
- quoteData.txid,
988
- signature,
989
- ],
990
- ]);
991
-
992
- return this.buildSimpleParamWithoutWETHConversion(
993
- srcToken,
994
- quoteData.baseTokenAmount,
995
- destToken,
996
- quoteData.quoteTokenAmount,
997
- swapData,
998
- this.routerAddress,
999
- );
1000
- }
1001
-
1002
- getDexParam(
1003
- srcToken: Address,
1004
- destToken: Address,
1005
- srcAmount: NumberAsString,
1006
- destAmount: NumberAsString,
1007
- recipient: Address,
1008
- data: HashflowData,
1009
- side: SwapSide,
1010
- ): DexExchangeParam {
1011
- const { quoteData, signature } = data;
1012
-
1013
- assert(
1014
- quoteData !== undefined,
1015
- `${this.dexKey}-${this.network}: quoteData undefined`,
1016
- );
1017
-
1018
- // Encode here the transaction arguments
1019
- const exchangeData = this.routerInterface.encodeFunctionData('tradeRFQT', [
1020
- [
1021
- quoteData.pool,
1022
- quoteData.externalAccount ?? ZERO_ADDRESS,
1023
- quoteData.trader,
1024
- quoteData.effectiveTrader ?? quoteData.trader,
1025
- quoteData.baseToken,
1026
- quoteData.quoteToken,
1027
- quoteData.baseTokenAmount,
1028
- quoteData.baseTokenAmount,
1029
- quoteData.quoteTokenAmount,
1030
- quoteData.quoteExpiry,
1031
- quoteData.nonce ?? 0,
1032
- quoteData.txid,
1033
- signature,
1034
- ],
1035
- ]);
1036
-
1037
- return {
1038
- needWrapNative: this.needWrapNative,
1039
- dexFuncHasRecipient: true,
1040
- exchangeData,
1041
- targetExchange: this.routerAddress,
1042
- returnAmountPos: undefined,
1043
- };
1044
- }
1045
-
1046
- extractQuoteToken = (pair: {
1047
- baseToken: string;
1048
- baseTokenName: string;
1049
- quoteToken: string;
1050
- quoteTokenName: string;
1051
- baseTokenDecimals: number;
1052
- quoteTokenDecimals: number;
1053
- }): Token => ({
1054
- address: pair.quoteToken,
1055
- symbol: pair.quoteTokenName,
1056
- decimals: pair.quoteTokenDecimals,
1057
- });
1058
-
1059
- computeMaxLiquidity = (
1060
- levels: PriceLevel[],
1061
- baseTokenPriceUsd: number,
1062
- ): number => {
1063
- const maxLevel = new BigNumber(levels[levels.length - 1]?.q ?? '0');
1064
- return maxLevel.multipliedBy(baseTokenPriceUsd).toNumber();
1065
- };
1066
-
1067
- async getTopPoolsForToken(
1068
- tokenAddress: Address,
1069
- limit: number,
1070
- ): Promise<PoolLiquidity[]> {
1071
- const _tokenAddress = tokenAddress.toLowerCase();
1072
-
1073
- const makers = (await this.getCachedMarketMakers()) || [];
1074
- const filteredMakers = await this.getFilteredMarketMakers(makers);
1075
- const pLevels = (await this.getCachedLevels()) || {};
1076
-
1077
- let baseToken: Token | undefined = undefined;
1078
- // TODO: Improve efficiency of this part. Quite inefficient way to determine
1079
- // Token address and decimals. But since it is not called frequently, not worth
1080
- // optimizing now
1081
- for (const maker of filteredMakers) {
1082
- const baseTokenEntry = pLevels[maker]?.find(
1083
- entry => entry.pair.baseToken.toLowerCase() === _tokenAddress,
1084
- );
1085
- if (baseTokenEntry) {
1086
- baseToken = {
1087
- address: _tokenAddress,
1088
- decimals: baseTokenEntry.pair.baseTokenDecimals,
1089
- };
1090
- break;
1091
- }
1092
- }
1093
-
1094
- if (baseToken === undefined) {
1095
- return [];
1096
- }
1097
-
1098
- const baseTokenPriceUsd = await this.dexHelper.getTokenUSDPrice(
1099
- baseToken,
1100
- BigInt(getBigNumberPow(baseToken.decimals).toFixed(0)),
1101
- );
1102
-
1103
- const pools = makers
1104
- .map(
1105
- m =>
1106
- pLevels[m]
1107
- ?.filter(
1108
- entry => entry.pair.baseToken.toLowerCase() === _tokenAddress,
1109
- )
1110
- .map(
1111
- entry =>
1112
- ({
1113
- exchange: this.dexKey,
1114
- address: this.routerAddress,
1115
- connectorTokens: [this.extractQuoteToken(entry.pair)],
1116
- liquidityUSD: this.computeMaxLiquidity(
1117
- entry.levels,
1118
- baseTokenPriceUsd,
1119
- ),
1120
- } as PoolLiquidity),
1121
- ) ?? [],
1122
- )
1123
- .flatMap(pl => pl);
1124
-
1125
- return pools
1126
- .sort((a, b) => b.liquidityUSD - a.liquidityUSD)
1127
- .slice(0, limit);
1128
- }
1129
-
1130
- releaseResources(): void {
1131
- if (this.rateFetcher) {
1132
- this.rateFetcher.stop();
1133
- }
1134
- }
1135
- }
1
+ import { ChainId, ZERO_ADDRESS } from '@hashflow/sdk';
2
+ import { Chain, ChainType, HashflowApi } from '@hashflow/taker-js';
3
+ import {
4
+ MarketMakersResponse,
5
+ PriceLevelsResponse,
6
+ RfqResponse,
7
+ } from '@hashflow/taker-js/dist/types/rest';
8
+ import BigNumber from 'bignumber.js';
9
+ import { Interface } from 'ethers/lib/utils';
10
+ import { assert } from 'ts-essentials';
11
+ import routerAbi from '../../abi/hashflow/HashflowRouter.abi.json';
12
+ import { BI_MAX_UINT256 } from '../../bigint-constants';
13
+ import { BN_0, BN_1, getBigNumberPow } from '../../bignumber-constants';
14
+ import * as CALLDATA_GAS_COST from '../../calldata-gas-cost';
15
+ import {
16
+ CACHE_PREFIX,
17
+ ETHER_ADDRESS,
18
+ Network,
19
+ SwapSide,
20
+ } from '../../constants';
21
+ import { IDexHelper } from '../../dex-helper/idex-helper';
22
+ import { IDex } from '../../dex/idex';
23
+ import {
24
+ AdapterExchangeParam,
25
+ Address,
26
+ DexExchangeParam,
27
+ ExchangePrices,
28
+ ExchangeTxInfo,
29
+ Logger,
30
+ NumberAsString,
31
+ OptimalSwapExchange,
32
+ PoolLiquidity,
33
+ PoolPrices,
34
+ PreprocessTransactionOptions,
35
+ SimpleExchangeParam,
36
+ Token,
37
+ } from '../../types';
38
+ import { getDexKeysWithNetwork, Utils } from '../../utils';
39
+ import { TooStrictSlippageCheckError } from '../generic-rfq/types';
40
+ import { SimpleExchange } from '../simple-exchange';
41
+ import { Adapters, HashflowConfig } from './config';
42
+ import {
43
+ CONSECUTIVE_ERROR_THRESHOLD,
44
+ CONSECUTIVE_ERROR_TIMESPAN_MS,
45
+ ERROR_CODE_TO_RESTRICT_TTL,
46
+ HASHFLOW_API_CLIENT_NAME,
47
+ HASHFLOW_API_MARKET_MAKERS_POLLING_INTERVAL_MS,
48
+ HASHFLOW_API_PRICES_POLLING_INTERVAL_MS,
49
+ HASHFLOW_API_URL,
50
+ HASHFLOW_BLACKLIST_TTL_S,
51
+ HASHFLOW_GAS_COST,
52
+ HASHFLOW_MARKET_MAKERS_CACHES_TTL_S,
53
+ HASHFLOW_MIN_SLIPPAGE_FACTOR_THRESHOLD_FOR_RESTRICTION,
54
+ HASHFLOW_MM_RESTRICT_TTL_S,
55
+ HASHFLOW_PRICES_CACHES_TTL_S,
56
+ UNKNOWN_ERROR_CODE,
57
+ } from './constants';
58
+ import { RateFetcher } from './rate-fetcher';
59
+ import {
60
+ CacheErrorCodesData,
61
+ ErrorCode,
62
+ HashflowData,
63
+ PriceLevel,
64
+ RfqError,
65
+ SlippageCheckError,
66
+ } from './types';
67
+
68
+ export class Hashflow extends SimpleExchange implements IDex<HashflowData> {
69
+ readonly isStatePollingDex = true;
70
+ readonly hasConstantPriceLargeAmounts = false;
71
+ readonly needWrapNative = false;
72
+ readonly needsSequentialPreprocessing = true;
73
+ readonly isFeeOnTransferSupported = false;
74
+ private api: HashflowApi;
75
+ private rateFetcher: RateFetcher;
76
+
77
+ private hashFlowAuthToken: string;
78
+ private disabledMMs: Set<string>;
79
+ private runtimeMMsRestrictHashMapKey: string;
80
+ private runtimeMMsRestrictHashMapErrorCodesKey: string;
81
+
82
+ private pricesCacheKey: string;
83
+ private marketMakersCacheKey: string;
84
+
85
+ public static dexKeysWithNetwork: { key: string; networks: Network[] }[] =
86
+ getDexKeysWithNetwork(HashflowConfig);
87
+
88
+ logger: Logger;
89
+
90
+ constructor(
91
+ readonly network: Network,
92
+ readonly dexKey: string,
93
+ readonly dexHelper: IDexHelper,
94
+ protected adapters = Adapters[network] || {},
95
+ readonly routerAddress: string = HashflowConfig['Hashflow'][network]
96
+ .routerAddress,
97
+ protected routerInterface = new Interface(routerAbi),
98
+ ) {
99
+ super(dexHelper, dexKey);
100
+ this.logger = dexHelper.getLogger(dexKey);
101
+ const token = dexHelper.config.data.hashFlowAuthToken;
102
+ assert(
103
+ token !== undefined,
104
+ 'Hashflow auth token is not specified with env variable',
105
+ );
106
+
107
+ this.hashFlowAuthToken = token;
108
+ this.api = new HashflowApi(
109
+ 'taker',
110
+ HASHFLOW_API_CLIENT_NAME,
111
+ this.hashFlowAuthToken,
112
+ );
113
+
114
+ this.pricesCacheKey = `${CACHE_PREFIX}_${this.dexHelper.config.data.network}_${this.dexKey}_prices`;
115
+ this.marketMakersCacheKey = `${CACHE_PREFIX}_${this.dexHelper.config.data.network}_${this.dexKey}_mms`;
116
+
117
+ this.disabledMMs = new Set(dexHelper.config.data.hashFlowDisabledMMs);
118
+ this.runtimeMMsRestrictHashMapKey =
119
+ `${CACHE_PREFIX}_${this.dexKey}_${this.network}_restricted_mms`.toLowerCase();
120
+ this.runtimeMMsRestrictHashMapErrorCodesKey =
121
+ `${CACHE_PREFIX}_${this.dexKey}_${this.network}_restricted_mms_error_codes`.toLowerCase();
122
+
123
+ this.rateFetcher = new RateFetcher(
124
+ this.dexHelper,
125
+ this.dexKey,
126
+ this.network,
127
+ this.logger,
128
+ {
129
+ rateConfig: {
130
+ pricesIntervalMs: HASHFLOW_API_PRICES_POLLING_INTERVAL_MS,
131
+ markerMakersIntervalMs:
132
+ HASHFLOW_API_MARKET_MAKERS_POLLING_INTERVAL_MS,
133
+ marketMakersReqParams: {
134
+ url: `${HASHFLOW_API_URL}/taker/v3/market-makers`,
135
+ params: {
136
+ baseChainId: this.network,
137
+ source: HASHFLOW_API_CLIENT_NAME,
138
+ baseChainType: 'evm',
139
+ },
140
+ headers: { Authorization: this.hashFlowAuthToken },
141
+ },
142
+ pricesReqParams: {
143
+ url: `${HASHFLOW_API_URL}/taker/v3/price-levels`,
144
+ params: {
145
+ baseChainId: this.network,
146
+ source: HASHFLOW_API_CLIENT_NAME,
147
+ marketMakers: [],
148
+ baseChainType: 'evm',
149
+ },
150
+ headers: { Authorization: this.hashFlowAuthToken },
151
+ },
152
+ getCachedMarketMakers: this.getCachedMarketMakers.bind(this),
153
+ filterMarketMakers: this.getFilteredMarketMakers.bind(this),
154
+ pricesCacheKey: this.pricesCacheKey,
155
+ pricesCacheTTLSecs: HASHFLOW_PRICES_CACHES_TTL_S,
156
+ marketMakersCacheKey: this.marketMakersCacheKey,
157
+ marketMakersCacheTTLSecs: HASHFLOW_MARKET_MAKERS_CACHES_TTL_S,
158
+ },
159
+ },
160
+ );
161
+ }
162
+
163
+ async initializePricing(blockNumber: number): Promise<void> {
164
+ if (!this.dexHelper.config.isSlave) {
165
+ this.rateFetcher.start();
166
+ }
167
+
168
+ return;
169
+ }
170
+
171
+ getAdapters(side: SwapSide): { name: string; index: number }[] | null {
172
+ return this.adapters[side] ? this.adapters[side] : null;
173
+ }
174
+
175
+ getPairName = (srcAddress: Address, destAddress: Address) =>
176
+ `${srcAddress}_${destAddress}`.toLowerCase();
177
+
178
+ getIdentifierPrefix(srcAddress: Address, destAddress: Address) {
179
+ return `${this.dexKey}_${this.getPairName(
180
+ srcAddress,
181
+ destAddress,
182
+ )}`.toLowerCase();
183
+ }
184
+
185
+ getPoolIdentifier(srcAddress: Address, destAddress: Address, mm: string) {
186
+ return `${this.getIdentifierPrefix(
187
+ srcAddress,
188
+ destAddress,
189
+ )}_${mm}`.toLowerCase();
190
+ }
191
+
192
+ async getPoolIdentifiers(
193
+ srcToken: Token,
194
+ destToken: Token,
195
+ side: SwapSide,
196
+ blockNumber: number,
197
+ ): Promise<string[]> {
198
+ const normalizedSrcToken = this.normalizeToken(srcToken);
199
+ const normalizedDestToken = this.normalizeToken(destToken);
200
+
201
+ if (normalizedSrcToken.address === normalizedDestToken.address) {
202
+ return [];
203
+ }
204
+
205
+ const levels = (await this.getCachedLevels()) || {};
206
+ const makers = Object.keys(levels);
207
+
208
+ return makers
209
+ .filter(m => {
210
+ const pairs = levels[m]?.map(entry => entry.pair) ?? [];
211
+ return pairs.some(
212
+ p =>
213
+ normalizedSrcToken.address === p.baseToken.toLowerCase() &&
214
+ normalizedDestToken.address === p.quoteToken.toLowerCase(),
215
+ );
216
+ })
217
+ .map(m =>
218
+ this.getPoolIdentifier(
219
+ normalizedSrcToken.address,
220
+ normalizedDestToken.address,
221
+ m,
222
+ ),
223
+ );
224
+ }
225
+
226
+ private async getFilteredMarketMakers(makers: string[]): Promise<string[]> {
227
+ const cachedRestrictionUnparsed = await this.dexHelper.cache.hgetAll(
228
+ this.runtimeMMsRestrictHashMapKey,
229
+ );
230
+
231
+ const runtimeRestrictedMMs = this.parseCacheRestrictionAndExpiryIfNeeded(
232
+ cachedRestrictionUnparsed,
233
+ );
234
+
235
+ return makers.filter(
236
+ mm => !(this.disabledMMs.has(mm) || runtimeRestrictedMMs.has(mm)),
237
+ );
238
+ }
239
+
240
+ parseCacheRestrictionAndExpiryIfNeeded(
241
+ cachedValues: Record<string, string>,
242
+ ): Set<string> {
243
+ const restrictedMMs = new Set<string>();
244
+ const toDelete: string[] = [];
245
+ const expirationThreshold = Date.now() - HASHFLOW_MM_RESTRICT_TTL_S * 1000;
246
+
247
+ // For log message
248
+ let stringifiedRestrictedMMs = '';
249
+
250
+ Object.entries(cachedValues).forEach(([mm, createdAt]) => {
251
+ if (+createdAt < expirationThreshold) {
252
+ toDelete.push(mm);
253
+ } else {
254
+ restrictedMMs.add(mm);
255
+ stringifiedRestrictedMMs += `${mm}, `;
256
+ }
257
+ });
258
+
259
+ if (restrictedMMs.size > 0) {
260
+ this.logger.debug(
261
+ `${this.dexKey}-${
262
+ this.network
263
+ }: pricing is skipped for ${stringifiedRestrictedMMs.slice(
264
+ 0,
265
+ -2,
266
+ )} due to restriction`,
267
+ );
268
+ }
269
+
270
+ if (toDelete.length > 0) {
271
+ this.logger.debug(
272
+ `${this.dexKey}-${this.network}: Deleting expired keys: `,
273
+ toDelete.join(`,`),
274
+ );
275
+
276
+ // No need to await since we don't care about when it executes
277
+ // And we don't want to stop pricing request because of this
278
+ this.dexHelper.cache
279
+ .hdel(this.runtimeMMsRestrictHashMapKey, toDelete)
280
+ .catch(e => {
281
+ this.logger.error(
282
+ `${this.dexKey}-${this.network}: Failed to delete expired keys: `,
283
+ e,
284
+ );
285
+ });
286
+ }
287
+
288
+ return restrictedMMs;
289
+ }
290
+
291
+ computePricesFromLevels(
292
+ amounts: BigNumber[],
293
+ levels: PriceLevel[],
294
+ srcToken: Token,
295
+ destToken: Token,
296
+ side: SwapSide,
297
+ ): bigint[] {
298
+ assert(levels.length > 0, 'Levels should not be empty');
299
+
300
+ const outputs = new Array<BigNumber>(amounts.length).fill(BN_0);
301
+ // FIXME: There is still case when last amount is fillable, but in between
302
+ // we may have splits that are less than min. amount. I assume that case is very
303
+ // and not addressing in current fix. If someone will look into that case, just be aware
304
+ // that it is not addressed
305
+
306
+ for (const [i, amount] of amounts.entries()) {
307
+ if (amount.isZero()) {
308
+ outputs[i] = BN_0;
309
+ } else {
310
+ const output =
311
+ side === SwapSide.SELL
312
+ ? this.computeLevelsQuote(levels, amount, undefined)
313
+ : this.computeLevelsQuote(levels, undefined, amount);
314
+
315
+ if (output === undefined) {
316
+ // If current amount was unfillable, then bigger amounts are unfillable as well
317
+ break;
318
+ } else {
319
+ outputs[i] = output;
320
+ }
321
+ }
322
+ }
323
+
324
+ const decimals =
325
+ side === SwapSide.SELL ? destToken.decimals : srcToken.decimals;
326
+
327
+ return outputs.map(o =>
328
+ BigInt(o.multipliedBy(getBigNumberPow(decimals)).toFixed(0)),
329
+ );
330
+ }
331
+
332
+ toPriceLevelsBN = (
333
+ priceLevels: PriceLevel[],
334
+ ): { level: BigNumber; price: BigNumber }[] =>
335
+ priceLevels.map(l => ({
336
+ level: new BigNumber(l.q),
337
+ price: new BigNumber(l.p),
338
+ }));
339
+
340
+ computeLevelsQuote(
341
+ priceLevels: PriceLevel[],
342
+ reqBaseAmount?: BigNumber,
343
+ reqQuoteAmount?: BigNumber,
344
+ ): BigNumber | undefined {
345
+ if (reqBaseAmount && reqQuoteAmount) {
346
+ return undefined;
347
+ }
348
+
349
+ const levels = this.toPriceLevelsBN(priceLevels);
350
+ if (!levels.length) {
351
+ return undefined;
352
+ }
353
+
354
+ const quote = {
355
+ baseAmount: levels[0]!.level,
356
+ quoteAmount: levels[0]!.level.multipliedBy(levels[0]!.price),
357
+ };
358
+ if (
359
+ (reqBaseAmount && reqBaseAmount.lt(quote.baseAmount)) ||
360
+ (reqQuoteAmount && reqQuoteAmount.lt(quote.quoteAmount))
361
+ ) {
362
+ return undefined;
363
+ }
364
+
365
+ for (let i = 1; i < levels.length; i++) {
366
+ const nextLevel = levels[i]!;
367
+ const nextLevelDepth = nextLevel.level.minus(levels[i - 1]!.level);
368
+ const nextLevelQuote = quote.quoteAmount.plus(
369
+ nextLevelDepth.multipliedBy(nextLevel.price),
370
+ );
371
+ if (reqBaseAmount && reqBaseAmount.lte(nextLevel.level)) {
372
+ const baseDifference = reqBaseAmount.minus(quote.baseAmount);
373
+ const quoteAmount = quote.quoteAmount.plus(
374
+ baseDifference.multipliedBy(nextLevel.price),
375
+ );
376
+ return quoteAmount;
377
+ } else if (reqQuoteAmount && reqQuoteAmount.lte(nextLevelQuote)) {
378
+ const quoteDifference = reqQuoteAmount.minus(quote.quoteAmount);
379
+ const baseAmount = quote.baseAmount.plus(
380
+ quoteDifference.dividedBy(nextLevel.price),
381
+ );
382
+ return baseAmount;
383
+ }
384
+
385
+ quote.baseAmount = nextLevel.level;
386
+ quote.quoteAmount = nextLevelQuote;
387
+ }
388
+
389
+ return undefined;
390
+ }
391
+
392
+ async getCachedMarketMakers(): Promise<
393
+ MarketMakersResponse['marketMakers'] | null
394
+ > {
395
+ const cachedMarketMakers = await this.dexHelper.cache.rawget(
396
+ this.marketMakersCacheKey,
397
+ );
398
+
399
+ if (cachedMarketMakers) {
400
+ return JSON.parse(
401
+ cachedMarketMakers,
402
+ ) as MarketMakersResponse['marketMakers'];
403
+ }
404
+
405
+ return null;
406
+ }
407
+
408
+ async getCachedLevels(): Promise<PriceLevelsResponse['levels'] | null> {
409
+ const cachedLevels = await this.dexHelper.cache.rawget(this.pricesCacheKey);
410
+
411
+ if (cachedLevels) {
412
+ return JSON.parse(cachedLevels) as PriceLevelsResponse['levels'];
413
+ }
414
+
415
+ return null;
416
+ }
417
+
418
+ // Hashflow protocol for native token expects 0x00000... instead of 0xeeeee...
419
+ normalizeToken(token: Token): Token {
420
+ return {
421
+ address:
422
+ token.address.toLowerCase() === ETHER_ADDRESS
423
+ ? ZERO_ADDRESS
424
+ : token.address.toLowerCase(),
425
+ decimals: token.decimals,
426
+ };
427
+ }
428
+
429
+ async getPricesVolume(
430
+ srcToken: Token,
431
+ destToken: Token,
432
+ amounts: bigint[],
433
+ side: SwapSide,
434
+ blockNumber: number,
435
+ limitPools?: string[],
436
+ ): Promise<null | ExchangePrices<HashflowData>> {
437
+ try {
438
+ const normalizedSrcToken = this.normalizeToken(srcToken);
439
+ const normalizedDestToken = this.normalizeToken(destToken);
440
+
441
+ if (normalizedSrcToken.address === normalizedDestToken.address) {
442
+ return null;
443
+ }
444
+
445
+ const prefix = this.getIdentifierPrefix(
446
+ normalizedSrcToken.address,
447
+ normalizedDestToken.address,
448
+ );
449
+
450
+ const pools =
451
+ limitPools ??
452
+ (await this.getPoolIdentifiers(srcToken, destToken, side, blockNumber));
453
+
454
+ const marketMakersToUse = pools.map(p => p.split(`${prefix}_`).pop());
455
+
456
+ const levelsMap = (await this.getCachedLevels()) || {};
457
+
458
+ Object.keys(levelsMap).forEach(mmKey => {
459
+ if (!marketMakersToUse.includes(mmKey)) {
460
+ delete levelsMap[mmKey];
461
+ }
462
+ });
463
+
464
+ const levelEntries: {
465
+ mm: string;
466
+ levels: PriceLevel[];
467
+ }[] = Object.keys(levelsMap)
468
+ .map(mm => {
469
+ const entry = levelsMap[mm]?.find(
470
+ e =>
471
+ `${e.pair.baseToken}_${e.pair.quoteToken}` ===
472
+ this.getPairName(
473
+ normalizedSrcToken.address,
474
+ normalizedDestToken.address,
475
+ ),
476
+ );
477
+ if (entry === undefined) {
478
+ return undefined;
479
+ } else {
480
+ return { mm, levels: entry.levels };
481
+ }
482
+ })
483
+ .filter(o => o !== undefined)
484
+ .map(o => o!);
485
+
486
+ const prices = levelEntries.map(lEntry => {
487
+ const { mm, levels } = lEntry;
488
+
489
+ if (levels.length === 0) {
490
+ return null;
491
+ }
492
+
493
+ const divider = getBigNumberPow(
494
+ side === SwapSide.SELL
495
+ ? normalizedSrcToken.decimals
496
+ : normalizedDestToken.decimals,
497
+ );
498
+
499
+ const amountsRaw = amounts.map(a =>
500
+ new BigNumber(a.toString()).dividedBy(divider),
501
+ );
502
+ const firstLevelRaw = levels[0];
503
+ const firstLevelAmountBN = new BigNumber(firstLevelRaw.q);
504
+
505
+ if (amountsRaw[amountsRaw.length - 1].lt(firstLevelAmountBN)) {
506
+ return null;
507
+ }
508
+
509
+ if (firstLevelAmountBN.gt(0)) {
510
+ // Add zero level for price computation
511
+ levels.unshift({ q: '0', p: firstLevelRaw.p });
512
+ }
513
+
514
+ const unitPrice = this.computePricesFromLevels(
515
+ [BN_1],
516
+ levels,
517
+ normalizedSrcToken,
518
+ normalizedDestToken,
519
+ side,
520
+ )[0];
521
+
522
+ const prices = this.computePricesFromLevels(
523
+ amountsRaw,
524
+ levels,
525
+ normalizedSrcToken,
526
+ normalizedDestToken,
527
+ side,
528
+ );
529
+
530
+ return {
531
+ gasCost: HASHFLOW_GAS_COST,
532
+ exchange: this.dexKey,
533
+ data: { mm },
534
+ prices,
535
+ unit: unitPrice,
536
+ poolIdentifier: this.getPoolIdentifier(
537
+ normalizedSrcToken.address,
538
+ normalizedDestToken.address,
539
+ mm,
540
+ ),
541
+ poolAddresses: [this.routerAddress],
542
+ } as PoolPrices<HashflowData>;
543
+ });
544
+
545
+ return prices.filter((p): p is PoolPrices<HashflowData> => !!p);
546
+ } catch (e: unknown) {
547
+ this.logger.error(
548
+ `Error_getPricesVolume ${srcToken.symbol || srcToken.address}, ${
549
+ destToken.symbol || destToken.address
550
+ }, ${side}:`,
551
+ e,
552
+ );
553
+ return null;
554
+ }
555
+ }
556
+
557
+ async preProcessTransaction(
558
+ optimalSwapExchange: OptimalSwapExchange<HashflowData>,
559
+ srcToken: Token,
560
+ destToken: Token,
561
+ side: SwapSide,
562
+ options: PreprocessTransactionOptions,
563
+ ): Promise<[OptimalSwapExchange<HashflowData>, ExchangeTxInfo]> {
564
+ if (await this.isBlacklisted(options.txOrigin)) {
565
+ this.logger.warn(
566
+ `${this.dexKey}-${this.network}: blacklisted TX Origin address '${options.txOrigin}' trying to build a transaction. Bailing...`,
567
+ );
568
+ throw new Error(
569
+ `${this.dexKey}-${
570
+ this.network
571
+ }: user=${options.txOrigin.toLowerCase()} is blacklisted`,
572
+ );
573
+ }
574
+ const mm = optimalSwapExchange.data?.mm;
575
+ assert(
576
+ mm !== undefined,
577
+ `${this.dexKey}-${this.network}: MM was not provided in data`,
578
+ );
579
+ const chainId = this.network as ChainId;
580
+ let chainType: ChainType = 'evm';
581
+ const chain: Chain = { chainType, chainId };
582
+ const normalizedSrcToken = this.normalizeToken(srcToken);
583
+ const normalizedDestToken = this.normalizeToken(destToken);
584
+
585
+ let rfq: RfqResponse;
586
+ try {
587
+ rfq = await this.api.requestQuote({
588
+ // sender is not passed, so for now ignore executionContractAddress
589
+ baseChain: chain,
590
+ baseToken: normalizedSrcToken.address,
591
+ quoteToken: normalizedDestToken.address,
592
+ ...(side === SwapSide.SELL
593
+ ? {
594
+ baseTokenAmount: optimalSwapExchange.srcAmount,
595
+ }
596
+ : { quoteTokenAmount: optimalSwapExchange.destAmount }),
597
+ // receiver address
598
+ wallet: options.recipient.toLowerCase(),
599
+ effectiveTrader: options.txOrigin.toLowerCase(),
600
+ marketMakers: [mm],
601
+ });
602
+
603
+ if (rfq.status !== 'success') {
604
+ const message = `${this.dexKey}-${
605
+ this.network
606
+ }: Failed to fetch RFQ for ${this.getPairName(
607
+ normalizedSrcToken.address,
608
+ normalizedDestToken.address,
609
+ )}: ${JSON.stringify(rfq)}`;
610
+ this.logger.warn(message);
611
+ throw new RfqError(message, `${rfq?.error?.code}` as ErrorCode);
612
+ } else if (!rfq.quotes[0].quoteData) {
613
+ const message = `${this.dexKey}-${
614
+ this.network
615
+ }: Failed to fetch RFQ for ${this.getPairName(
616
+ normalizedSrcToken.address,
617
+ normalizedDestToken.address,
618
+ )}. Missing quote data`;
619
+ this.logger.warn(message);
620
+ throw new RfqError(message, 'MISSING_QUOTE_DATA');
621
+ } else if (!rfq.quotes[0].signature) {
622
+ const message = `${this.dexKey}-${
623
+ this.network
624
+ }: Failed to fetch RFQ for ${this.getPairName(
625
+ normalizedSrcToken.address,
626
+ normalizedDestToken.address,
627
+ )}. Missing signature`;
628
+ this.logger.warn(message);
629
+ throw new RfqError(message, 'MISSING_SIGNATURE_DATA');
630
+ }
631
+
632
+ assert(
633
+ rfq.quotes[0].quoteData.baseToken === normalizedSrcToken.address,
634
+ `QuoteData baseToken=${rfq.quotes[0].quoteData.baseToken} is different from srcToken=${normalizedSrcToken.address}`,
635
+ );
636
+ assert(
637
+ rfq.quotes[0].quoteData.quoteToken === normalizedDestToken.address,
638
+ `QuoteData baseToken=${rfq.quotes[0].quoteData.quoteToken} is different from srcToken=${normalizedDestToken.address}`,
639
+ );
640
+
641
+ const expiryAsBigInt = BigInt(rfq.quotes[0].quoteData.quoteExpiry);
642
+ const minDeadline = expiryAsBigInt > 0 ? expiryAsBigInt : BI_MAX_UINT256;
643
+
644
+ const baseTokenAmount = BigInt(rfq.quotes[0].quoteData.baseTokenAmount);
645
+ const quoteTokenAmount = BigInt(rfq.quotes[0].quoteData.quoteTokenAmount);
646
+
647
+ const srcAmount = BigInt(optimalSwapExchange.srcAmount);
648
+ const destAmount = BigInt(optimalSwapExchange.destAmount);
649
+
650
+ const slippageFactor = options.slippageFactor;
651
+
652
+ let isFailOnSlippage = false;
653
+ let slippageErrorMessage = '';
654
+
655
+ if (side === SwapSide.SELL) {
656
+ if (
657
+ quoteTokenAmount <
658
+ BigInt(
659
+ new BigNumber(destAmount.toString())
660
+ .times(slippageFactor)
661
+ .toFixed(0),
662
+ )
663
+ ) {
664
+ isFailOnSlippage = true;
665
+ const message = `${this.dexKey}-${this.network}: too much slippage on quote ${side} quoteTokenAmount ${quoteTokenAmount} / destAmount ${destAmount} < ${slippageFactor}`;
666
+ slippageErrorMessage = message;
667
+ this.logger.warn(message);
668
+ }
669
+ } else {
670
+ if (quoteTokenAmount < destAmount) {
671
+ isFailOnSlippage = true;
672
+ // Won't receive enough assets
673
+ const message = `${this.dexKey}-${this.network}: too much slippage on quote ${side} quoteTokenAmount ${quoteTokenAmount} < destAmount ${destAmount}`;
674
+ slippageErrorMessage = message;
675
+ this.logger.warn(message);
676
+ } else {
677
+ if (
678
+ baseTokenAmount >
679
+ BigInt(slippageFactor.times(srcAmount.toString()).toFixed(0))
680
+ ) {
681
+ isFailOnSlippage = true;
682
+ const message = `${this.dexKey}-${
683
+ this.network
684
+ }: too much slippage on quote ${side} baseTokenAmount ${baseTokenAmount} / srcAmount ${srcAmount} > ${slippageFactor.toFixed()}`;
685
+ slippageErrorMessage = message;
686
+ this.logger.warn(message);
687
+ }
688
+ }
689
+ }
690
+
691
+ let isTooStrictSlippage = false;
692
+ if (
693
+ isFailOnSlippage &&
694
+ side === SwapSide.SELL &&
695
+ new BigNumber(1)
696
+ .minus(slippageFactor)
697
+ .lt(HASHFLOW_MIN_SLIPPAGE_FACTOR_THRESHOLD_FOR_RESTRICTION)
698
+ ) {
699
+ isTooStrictSlippage = true;
700
+ } else if (
701
+ isFailOnSlippage &&
702
+ side === SwapSide.BUY &&
703
+ slippageFactor
704
+ .minus(1)
705
+ .lt(HASHFLOW_MIN_SLIPPAGE_FACTOR_THRESHOLD_FOR_RESTRICTION)
706
+ ) {
707
+ isTooStrictSlippage = true;
708
+ }
709
+
710
+ if (isFailOnSlippage && isTooStrictSlippage) {
711
+ throw new TooStrictSlippageCheckError(slippageErrorMessage);
712
+ } else if (isFailOnSlippage && !isTooStrictSlippage) {
713
+ throw new SlippageCheckError(slippageErrorMessage);
714
+ }
715
+
716
+ return [
717
+ {
718
+ ...optimalSwapExchange,
719
+ data: {
720
+ mm,
721
+ quoteData: rfq.quotes[0].quoteData,
722
+ signature: rfq.quotes[0].signature,
723
+ },
724
+ },
725
+ { deadline: minDeadline },
726
+ ];
727
+ } catch (e) {
728
+ if (
729
+ e instanceof Error &&
730
+ e.message?.toLowerCase().includes('user is restricted')
731
+ ) {
732
+ this.logger.warn(
733
+ `${this.dexKey}-${this.network}: Encountered restricted user=${options.txOrigin}. Adding to local blacklist cache`,
734
+ );
735
+ await this.setBlacklist(options.txOrigin);
736
+ } else {
737
+ if (e instanceof TooStrictSlippageCheckError) {
738
+ this.logger.warn(
739
+ `${this.dexKey}-${this.network}: Market Maker ${mm} failed to build transaction on side ${side} with too strict slippage. Skipping restriction`,
740
+ );
741
+ } else {
742
+ this.logger.warn(
743
+ `${this.dexKey}-${this.network} MM unknown preprocess transaction error: ${e}`,
744
+ );
745
+ const code =
746
+ e instanceof RfqError || e instanceof SlippageCheckError
747
+ ? e?.code || UNKNOWN_ERROR_CODE
748
+ : UNKNOWN_ERROR_CODE;
749
+ await this.restrictMM(mm, code).catch(err =>
750
+ this.logger.warn(`Failed to restrict MM ${mm}: ${err}`),
751
+ );
752
+ }
753
+ }
754
+
755
+ throw e;
756
+ }
757
+ }
758
+
759
+ async restrictMM(mm: string, errorCode: ErrorCode): Promise<void> {
760
+ const errorCodesRaw = await this.dexHelper.cache.hget(
761
+ this.runtimeMMsRestrictHashMapErrorCodesKey,
762
+ mm,
763
+ );
764
+
765
+ const errorCodes: CacheErrorCodesData = Utils.Parse(errorCodesRaw) || {};
766
+
767
+ const error = errorCodes?.[errorCode];
768
+
769
+ if (
770
+ !error ||
771
+ error.addedDatetimeMS + CONSECUTIVE_ERROR_TIMESPAN_MS < Date.now()
772
+ ) {
773
+ this.logger.warn(
774
+ `${this.dexKey}-${this.network}: First encounter of error code=${errorCode} for ${mm} OR error ocurred outside of threshold, setting up counter`,
775
+ );
776
+ const data: CacheErrorCodesData = {
777
+ ...errorCodes,
778
+ [errorCode]: {
779
+ count: 1,
780
+ addedDatetimeMS: Date.now(),
781
+ },
782
+ };
783
+ await this.dexHelper.cache.hset(
784
+ this.runtimeMMsRestrictHashMapErrorCodesKey,
785
+ mm,
786
+ Utils.Serialize(data),
787
+ );
788
+ return;
789
+ } else {
790
+ const restrictTTLMs =
791
+ ERROR_CODE_TO_RESTRICT_TTL[errorCode] ||
792
+ ERROR_CODE_TO_RESTRICT_TTL[UNKNOWN_ERROR_CODE];
793
+
794
+ if (error.count + 1 > CONSECUTIVE_ERROR_THRESHOLD) {
795
+ this.logger.warn(
796
+ `${this.dexKey}-${this.network}: ${mm} was restricted for ${
797
+ restrictTTLMs / 1000
798
+ } sec. due to ${errorCode} happening ${
799
+ error.count + 1
800
+ } times within last ${Math.floor(
801
+ CONSECUTIVE_ERROR_TIMESPAN_MS / 1000 / 60,
802
+ )} minutes`,
803
+ );
804
+
805
+ // date added is checked against HASHFLOW_MM_RESTRICT_TTL_S, as a hack we set a date so that it's checked against specific ttls based on error type to not change parsing logic for mms
806
+ //
807
+ // Example1: ttl for a particular error is 20 minutes (< default 60 minutes)
808
+ // Meaning we need to set `addedDatetimeMS` for restrict hash as Date.now() - 40 minutes (diff between default restrict ttl=1h and particular error restrict ttl = 20 minutes). So the `addedDatetimeMS` is in the past, and after 20 minutes mm will not be considered as restricted
809
+ //
810
+ // Example 2: ttl for a particular error is 80 minutes (> default 60 minutes)
811
+ // Meaning we need to set `addedDatetimeMS` for restrict hash as Date.now() + 20 minutes (in the future), then after an hour (default) it'll still be 20 minutes left for restriction to be cleared
812
+
813
+ const defaultRestrictTTLMS = Math.floor(
814
+ HASHFLOW_MM_RESTRICT_TTL_S * 1000,
815
+ );
816
+ const dateModifierMS =
817
+ defaultRestrictTTLMS > restrictTTLMs
818
+ ? -(defaultRestrictTTLMS - restrictTTLMs)
819
+ : restrictTTLMs - defaultRestrictTTLMS;
820
+
821
+ const date = (Date.now() + dateModifierMS).toString();
822
+
823
+ // We use timestamp for creation date to later discern if it already expired or not
824
+ await this.dexHelper.cache.hset(
825
+ this.runtimeMMsRestrictHashMapKey,
826
+ mm,
827
+ date,
828
+ );
829
+
830
+ // resetting error count
831
+ const data: CacheErrorCodesData = {
832
+ ...errorCodes,
833
+ [errorCode]: {
834
+ count: 1,
835
+ addedDatetimeMS: Date.now(),
836
+ },
837
+ };
838
+ await this.dexHelper.cache.hset(
839
+ this.runtimeMMsRestrictHashMapErrorCodesKey,
840
+ mm,
841
+ Utils.Serialize(data),
842
+ );
843
+
844
+ return;
845
+ } else {
846
+ const newCount = +error.count + 1;
847
+ this.logger.warn(
848
+ `${this.dexKey}-${this.network}: ${mm} Error with code: ${errorCode} happened ${newCount} times (below or equal to the limit: ${CONSECUTIVE_ERROR_THRESHOLD}), updating counter`,
849
+ );
850
+ const data: CacheErrorCodesData = {
851
+ ...errorCodes,
852
+ [errorCode]: {
853
+ count: newCount,
854
+ addedDatetimeMS: error.addedDatetimeMS, // initial date stays
855
+ },
856
+ };
857
+ await this.dexHelper.cache.hset(
858
+ this.runtimeMMsRestrictHashMapErrorCodesKey,
859
+ mm,
860
+ Utils.Serialize(data),
861
+ );
862
+ }
863
+ }
864
+ }
865
+
866
+ getCalldataGasCost(poolPrices: PoolPrices<HashflowData>): number | number[] {
867
+ // I am not sure if that is correct. If anybody know how to fix it,
868
+ // please, go ahead :)
869
+ return (
870
+ CALLDATA_GAS_COST.DEX_OVERHEAD +
871
+ // addresses: pool, quoteToken, externalAccount
872
+ CALLDATA_GAS_COST.ADDRESS * 3 +
873
+ // uint256: baseTokenAmount, quoteTokenAmount, quoteExpiry, nonce
874
+ CALLDATA_GAS_COST.AMOUNT * 4 +
875
+ // bytes32 txid;
876
+ CALLDATA_GAS_COST.FULL_WORD +
877
+ // I don't know how big is it, but from google results, I see 65 bytes for signature
878
+ // bytes signature
879
+ CALLDATA_GAS_COST.FULL_WORD * 2 +
880
+ CALLDATA_GAS_COST.OFFSET_SMALL
881
+ );
882
+ }
883
+
884
+ getTokenFromAddress?(address: Address): Token {
885
+ // We don't have predefined set of tokens with decimals
886
+ // Anyway we don't use decimals, so it is fine to do this
887
+ return { address, decimals: 0 };
888
+ }
889
+
890
+ getAdapterParam(
891
+ srcToken: string,
892
+ destToken: string,
893
+ srcAmount: string,
894
+ destAmount: string,
895
+ data: HashflowData,
896
+ side: SwapSide,
897
+ ): AdapterExchangeParam {
898
+ const { quoteData, signature } = data;
899
+
900
+ assert(
901
+ quoteData !== undefined,
902
+ `${this.dexKey}-${this.network}: quoteData undefined`,
903
+ );
904
+
905
+ const payload = this.routerInterface._abiCoder.encode(
906
+ [
907
+ 'tuple(address pool, address quoteToken, address externalAccount, uint256 baseTokenAmount, uint256 quoteTokenAmount, uint256 quoteExpiry, uint256 nonce, bytes32 txid, bytes signature)',
908
+ ],
909
+ [
910
+ {
911
+ pool: quoteData.pool,
912
+ quoteToken: quoteData.quoteToken,
913
+ externalAccount: quoteData.externalAccount ?? ZERO_ADDRESS,
914
+ baseTokenAmount: quoteData.baseTokenAmount,
915
+ quoteTokenAmount: quoteData.quoteTokenAmount,
916
+ quoteExpiry: quoteData.quoteExpiry,
917
+ nonce: quoteData.nonce ?? 0,
918
+ txid: quoteData.txid,
919
+ signature,
920
+ },
921
+ ],
922
+ );
923
+
924
+ return {
925
+ targetExchange: this.routerAddress,
926
+ payload,
927
+ networkFee: '0',
928
+ };
929
+ }
930
+
931
+ getBlackListKey(address: Address) {
932
+ return `blacklist_${address}`.toLowerCase();
933
+ }
934
+
935
+ async isBlacklisted(txOrigin: Address): Promise<boolean> {
936
+ const result = await this.dexHelper.cache.get(
937
+ this.dexKey,
938
+ this.network,
939
+ this.getBlackListKey(txOrigin),
940
+ );
941
+ return result === 'blacklisted';
942
+ }
943
+
944
+ async setBlacklist(
945
+ txOrigin: Address,
946
+ ttl: number = HASHFLOW_BLACKLIST_TTL_S,
947
+ ) {
948
+ await this.dexHelper.cache.setex(
949
+ this.dexKey,
950
+ this.network,
951
+ this.getBlackListKey(txOrigin),
952
+ ttl,
953
+ 'blacklisted',
954
+ );
955
+ return true;
956
+ }
957
+
958
+ async getSimpleParam(
959
+ srcToken: string,
960
+ destToken: string,
961
+ srcAmount: string,
962
+ destAmount: string,
963
+ data: HashflowData,
964
+ side: SwapSide,
965
+ ): Promise<SimpleExchangeParam> {
966
+ const { quoteData, signature } = data;
967
+
968
+ assert(
969
+ quoteData !== undefined,
970
+ `${this.dexKey}-${this.network}: quoteData undefined`,
971
+ );
972
+
973
+ // Encode here the transaction arguments
974
+ const swapData = this.routerInterface.encodeFunctionData('tradeRFQT', [
975
+ [
976
+ quoteData.pool,
977
+ quoteData.externalAccount ?? ZERO_ADDRESS,
978
+ quoteData.trader,
979
+ quoteData.effectiveTrader ?? quoteData.trader,
980
+ quoteData.baseToken,
981
+ quoteData.quoteToken,
982
+ quoteData.baseTokenAmount,
983
+ quoteData.baseTokenAmount,
984
+ quoteData.quoteTokenAmount,
985
+ quoteData.quoteExpiry,
986
+ quoteData.nonce ?? 0,
987
+ quoteData.txid,
988
+ signature,
989
+ ],
990
+ ]);
991
+
992
+ return this.buildSimpleParamWithoutWETHConversion(
993
+ srcToken,
994
+ quoteData.baseTokenAmount,
995
+ destToken,
996
+ quoteData.quoteTokenAmount,
997
+ swapData,
998
+ this.routerAddress,
999
+ );
1000
+ }
1001
+
1002
+ getDexParam(
1003
+ srcToken: Address,
1004
+ destToken: Address,
1005
+ srcAmount: NumberAsString,
1006
+ destAmount: NumberAsString,
1007
+ recipient: Address,
1008
+ data: HashflowData,
1009
+ side: SwapSide,
1010
+ ): DexExchangeParam {
1011
+ const { quoteData, signature } = data;
1012
+
1013
+ assert(
1014
+ quoteData !== undefined,
1015
+ `${this.dexKey}-${this.network}: quoteData undefined`,
1016
+ );
1017
+
1018
+ // Encode here the transaction arguments
1019
+ const exchangeData = this.routerInterface.encodeFunctionData('tradeRFQT', [
1020
+ [
1021
+ quoteData.pool,
1022
+ quoteData.externalAccount ?? ZERO_ADDRESS,
1023
+ quoteData.trader,
1024
+ quoteData.effectiveTrader ?? quoteData.trader,
1025
+ quoteData.baseToken,
1026
+ quoteData.quoteToken,
1027
+ quoteData.baseTokenAmount,
1028
+ quoteData.baseTokenAmount,
1029
+ quoteData.quoteTokenAmount,
1030
+ quoteData.quoteExpiry,
1031
+ quoteData.nonce ?? 0,
1032
+ quoteData.txid,
1033
+ signature,
1034
+ ],
1035
+ ]);
1036
+
1037
+ return {
1038
+ needWrapNative: this.needWrapNative,
1039
+ dexFuncHasRecipient: true,
1040
+ exchangeData,
1041
+ targetExchange: this.routerAddress,
1042
+ returnAmountPos: undefined,
1043
+ };
1044
+ }
1045
+
1046
+ extractQuoteToken = (pair: {
1047
+ baseToken: string;
1048
+ baseTokenName: string;
1049
+ quoteToken: string;
1050
+ quoteTokenName: string;
1051
+ baseTokenDecimals: number;
1052
+ quoteTokenDecimals: number;
1053
+ }): Token => ({
1054
+ address: pair.quoteToken,
1055
+ symbol: pair.quoteTokenName,
1056
+ decimals: pair.quoteTokenDecimals,
1057
+ });
1058
+
1059
+ computeMaxLiquidity = (
1060
+ levels: PriceLevel[],
1061
+ baseTokenPriceUsd: number,
1062
+ ): number => {
1063
+ const maxLevel = new BigNumber(levels[levels.length - 1]?.q ?? '0');
1064
+ return maxLevel.multipliedBy(baseTokenPriceUsd).toNumber();
1065
+ };
1066
+
1067
+ async getTopPoolsForToken(
1068
+ tokenAddress: Address,
1069
+ limit: number,
1070
+ ): Promise<PoolLiquidity[]> {
1071
+ const _tokenAddress = tokenAddress.toLowerCase();
1072
+
1073
+ const makers = (await this.getCachedMarketMakers()) || [];
1074
+ const filteredMakers = await this.getFilteredMarketMakers(makers);
1075
+ const pLevels = (await this.getCachedLevels()) || {};
1076
+
1077
+ let baseToken: Token | undefined = undefined;
1078
+ // TODO: Improve efficiency of this part. Quite inefficient way to determine
1079
+ // Token address and decimals. But since it is not called frequently, not worth
1080
+ // optimizing now
1081
+ for (const maker of filteredMakers) {
1082
+ const baseTokenEntry = pLevels[maker]?.find(
1083
+ entry => entry.pair.baseToken.toLowerCase() === _tokenAddress,
1084
+ );
1085
+ if (baseTokenEntry) {
1086
+ baseToken = {
1087
+ address: _tokenAddress,
1088
+ decimals: baseTokenEntry.pair.baseTokenDecimals,
1089
+ };
1090
+ break;
1091
+ }
1092
+ }
1093
+
1094
+ if (baseToken === undefined) {
1095
+ return [];
1096
+ }
1097
+
1098
+ const baseTokenPriceUsd = await this.dexHelper.getTokenUSDPrice(
1099
+ baseToken,
1100
+ BigInt(getBigNumberPow(baseToken.decimals).toFixed(0)),
1101
+ );
1102
+
1103
+ const pools = makers
1104
+ .map(
1105
+ m =>
1106
+ pLevels[m]
1107
+ ?.filter(
1108
+ entry => entry.pair.baseToken.toLowerCase() === _tokenAddress,
1109
+ )
1110
+ .map(
1111
+ entry =>
1112
+ ({
1113
+ exchange: this.dexKey,
1114
+ address: this.routerAddress,
1115
+ connectorTokens: [this.extractQuoteToken(entry.pair)],
1116
+ liquidityUSD: this.computeMaxLiquidity(
1117
+ entry.levels,
1118
+ baseTokenPriceUsd,
1119
+ ),
1120
+ } as PoolLiquidity),
1121
+ ) ?? [],
1122
+ )
1123
+ .flatMap(pl => pl);
1124
+
1125
+ return pools
1126
+ .sort((a, b) => b.liquidityUSD - a.liquidityUSD)
1127
+ .slice(0, limit);
1128
+ }
1129
+
1130
+ releaseResources(): void {
1131
+ if (this.rateFetcher) {
1132
+ this.rateFetcher.stop();
1133
+ }
1134
+ }
1135
+ }