@dolomite-exchange/zap-sdk 0.3.21 → 0.3.23

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 (35) hide show
  1. package/dist/src/lib/Constants.js +220 -170
  2. package/dist/src/lib/Constants.js.map +1 -1
  3. package/package.json +4 -3
  4. package/src/DolomiteZap.ts +940 -0
  5. package/src/abis/IArbitrumGasInfo.json +198 -0
  6. package/src/abis/IDolomiteMarginExchangeWrapper.json +80 -0
  7. package/src/abis/IERC20.json +185 -0
  8. package/src/abis/IGmxV2DataStore.json +101 -0
  9. package/src/abis/IGmxV2Reader.json +3487 -0
  10. package/src/abis/IIsolationModeFactory.json +902 -0
  11. package/src/abis/types/IArbitrumGasInfo.ts +299 -0
  12. package/src/abis/types/IERC20.ts +269 -0
  13. package/src/abis/types/IGmxV2DataStore.ts +170 -0
  14. package/src/abis/types/IGmxV2Reader.ts +825 -0
  15. package/src/clients/AggregatorClient.ts +22 -0
  16. package/src/clients/DolomiteClient.ts +301 -0
  17. package/src/clients/IsolationModeClient.ts +75 -0
  18. package/src/clients/OdosAggregator.ts +107 -0
  19. package/src/clients/OogaBoogaAggregator.ts +76 -0
  20. package/src/clients/ParaswapAggregator.ts +135 -0
  21. package/src/index.ts +29 -0
  22. package/src/lib/ApiTypes.ts +241 -0
  23. package/src/lib/Constants.ts +1441 -0
  24. package/src/lib/Environment.ts +1 -0
  25. package/src/lib/GraphqlPageable.ts +23 -0
  26. package/src/lib/LocalCache.ts +34 -0
  27. package/src/lib/Logger.ts +59 -0
  28. package/src/lib/MathUtils.ts +13 -0
  29. package/src/lib/Utils.ts +52 -0
  30. package/src/lib/estimators/GmxV2GmEstimator.ts +349 -0
  31. package/src/lib/estimators/PendlePtEstimatorV3.ts +321 -0
  32. package/src/lib/estimators/PendleYtEstimatorV3.ts +77 -0
  33. package/src/lib/estimators/SimpleEstimator.ts +16 -0
  34. package/src/lib/estimators/StandardEstimator.ts +137 -0
  35. package/src/lib/graphql-types.ts +19 -0
@@ -0,0 +1,940 @@
1
+ import BigNumber from 'bignumber.js';
2
+ import { ethers } from 'ethers';
3
+ import AggregatorClient from './clients/AggregatorClient';
4
+ import DolomiteClient from './clients/DolomiteClient';
5
+ import OdosAggregator from './clients/OdosAggregator';
6
+ import OogaBoogaAggregator from './clients/OogaBoogaAggregator';
7
+ import ParaswapAggregator from './clients/ParaswapAggregator';
8
+ import {
9
+ Address,
10
+ ApiAsyncAction,
11
+ ApiAsyncActionType,
12
+ ApiAsyncTradeType,
13
+ ApiMarket,
14
+ ApiMarketHelper,
15
+ ApiOraclePrice,
16
+ ApiToken,
17
+ ApiWrapperHelper,
18
+ ApiWrapperInfo,
19
+ BlockTag,
20
+ EstimateOutputResult,
21
+ GenericTraderParam,
22
+ GenericTraderType,
23
+ Integer,
24
+ MarketId,
25
+ MinimalApiToken,
26
+ Network,
27
+ ReferralOutput,
28
+ ZapConfig,
29
+ ZapOutputParam,
30
+ } from './lib/ApiTypes';
31
+ import {
32
+ ADDRESS_ZERO,
33
+ ApiMarketConverter,
34
+ BYTES_EMPTY,
35
+ getGmxV2IsolationModeAsset,
36
+ getPendlePtMarketForIsolationModeToken,
37
+ INTEGERS,
38
+ INVALID_NAME,
39
+ ISOLATION_MODE_CONVERSION_MARKET_ID_MAP,
40
+ LIQUIDITY_TOKEN_CONVERSION_MARKET_ID_MAP,
41
+ } from './lib/Constants';
42
+ import { LocalCache } from './lib/LocalCache';
43
+ import Logger from './lib/Logger';
44
+ import { toChecksumOpt, zapOutputParamIsInvalid } from './lib/Utils';
45
+
46
+ const ONE_HOUR = 60 * 60;
47
+
48
+ const THIRTY_BASIS_POINTS = 0.003;
49
+
50
+ const marketsKey = 'MARKETS';
51
+ const marketHelpersKey = 'MARKET_HELPERS';
52
+
53
+ const INVALID_ESTIMATION = {
54
+ amountOut: INTEGERS.NEGATIVE_ONE,
55
+ tradeData: BYTES_EMPTY,
56
+ extraData: undefined,
57
+ };
58
+
59
+ export interface DolomiteZapConfig {
60
+ /**
61
+ * The network on which this instance of the Dolomite Zap is running.
62
+ */
63
+ network: Network;
64
+ /**
65
+ * The URL of the subgraph to use for fetching market data.
66
+ */
67
+ subgraphUrl: string;
68
+ /**
69
+ * The web3 provider to use for fetching on-chain data.
70
+ */
71
+ web3Provider: ethers.providers.Provider;
72
+ /**
73
+ * The number of seconds to cache market data for. Defaults to 1 hour (3600s).
74
+ */
75
+ cacheSeconds?: number;
76
+ /**
77
+ * True if these zaps are for processing liquidations or false for ordinary zaps.
78
+ */
79
+ defaultIsLiquidation?: boolean;
80
+ /**
81
+ * The default slippage tolerance to use when estimating output. Defaults to 0.3% (0.003).
82
+ */
83
+ defaultSlippageTolerance?: number;
84
+ /**
85
+ * The default block tag to use when fetching on-chain data. Defaults to 'latest'.
86
+ */
87
+ defaultBlockTag?: BlockTag;
88
+ /**
89
+ * The referral information to use for the various aggregators. Defaults to undefined.
90
+ */
91
+ referralInfo?: ReferralOutput;
92
+ /**
93
+ * True if the Dolomite proxy server should be used for aggregators that support it. The proxy server is used to make
94
+ * the API requests consistent and prevent browser plugins from blocking requests. Defaults to true.
95
+ */
96
+ useProxyServer?: boolean;
97
+ /**
98
+ * The multiplier to apply to any gas prices being used for estimating execution fees for intent-driven calls (like
99
+ * GMX V2).
100
+ */
101
+ gasMultiplier?: BigNumber;
102
+ }
103
+
104
+ export class DolomiteZap {
105
+ public readonly network: Network;
106
+ public readonly validAggregators: AggregatorClient[];
107
+ private readonly _defaultIsLiquidation: boolean;
108
+ private client: DolomiteClient;
109
+ private marketsCache: LocalCache<Record<string, ApiMarket>>;
110
+ private marketHelpersCache: LocalCache<Record<string, ApiMarketHelper>>;
111
+ private readonly _web3Provider: ethers.providers.Provider;
112
+
113
+ public constructor(
114
+ {
115
+ network,
116
+ subgraphUrl,
117
+ web3Provider,
118
+ cacheSeconds = ONE_HOUR,
119
+ defaultIsLiquidation = false,
120
+ defaultSlippageTolerance = THIRTY_BASIS_POINTS,
121
+ defaultBlockTag = 'latest',
122
+ referralInfo = {
123
+ odosReferralCode: undefined,
124
+ oogaBoogaApiKey: undefined,
125
+ referralAddress: undefined,
126
+ },
127
+ useProxyServer = true,
128
+ gasMultiplier = new BigNumber('1'),
129
+ }: DolomiteZapConfig,
130
+ ) {
131
+ this.network = network;
132
+ this._subgraphUrl = subgraphUrl;
133
+ this._web3Provider = web3Provider;
134
+ this._defaultIsLiquidation = defaultIsLiquidation;
135
+ this._defaultSlippageTolerance = defaultSlippageTolerance;
136
+ this._defaultBlockTag = defaultBlockTag;
137
+
138
+ this.client = new DolomiteClient(network, subgraphUrl, web3Provider, gasMultiplier);
139
+ this.marketsCache = new LocalCache<Record<string, ApiMarket>>(cacheSeconds);
140
+ this.marketHelpersCache = new LocalCache<Record<string, ApiMarketHelper>>(cacheSeconds);
141
+
142
+ this.validAggregators = this.getAllAggregators(network, referralInfo, useProxyServer)
143
+ .filter(aggregator => aggregator.isValidForNetwork());
144
+ }
145
+
146
+ private _subgraphUrl: string;
147
+
148
+ public get subgraphUrl(): string {
149
+ return this._subgraphUrl;
150
+ }
151
+
152
+ public set subgraphUrl(newSubgraphUrl: string) {
153
+ this._subgraphUrl = newSubgraphUrl;
154
+ this.client.subgraphUrl = newSubgraphUrl;
155
+ }
156
+
157
+ public get web3Provider(): ethers.providers.Provider {
158
+ return this._web3Provider;
159
+ }
160
+
161
+ private _defaultSlippageTolerance: number;
162
+
163
+ public get defaultSlippageTolerance(): number {
164
+ return this._defaultSlippageTolerance;
165
+ }
166
+
167
+ private _defaultBlockTag: BlockTag;
168
+
169
+ public get defaultBlockTag(): BlockTag {
170
+ return this._defaultBlockTag;
171
+ }
172
+
173
+ public get defaultIsLiquidation(): boolean {
174
+ return this._defaultIsLiquidation;
175
+ }
176
+
177
+ private static copyForWrapper(
178
+ effectiveOutputMarketIds: BigNumber[],
179
+ marketIdsPaths: Integer[][],
180
+ amountsPaths: Integer[][],
181
+ traderParamsArrays: GenericTraderParam[][],
182
+ executionFees: BigNumber[],
183
+ ): [MarketId[][], BigNumber[][], GenericTraderParam[][], BigNumber[]] {
184
+ return effectiveOutputMarketIds.reduce(
185
+ (acc, outputMarketId) => {
186
+ marketIdsPaths.forEach((path, i) => {
187
+ if (!outputMarketId.eq(path[path.length - 1])) {
188
+ acc[0].push([...path, outputMarketId]);
189
+ } else {
190
+ acc[0].push([...path]);
191
+ }
192
+ acc[1].push([...amountsPaths[i]]);
193
+ acc[2].push([...traderParamsArrays[i]]);
194
+ acc[3].push(...executionFees);
195
+ });
196
+ return acc;
197
+ },
198
+ [
199
+ [] as MarketId[][],
200
+ [] as BigNumber[][],
201
+ [] as GenericTraderParam[][],
202
+ [] as BigNumber[],
203
+ ],
204
+ );
205
+ }
206
+
207
+ public async forceRefreshCache(): Promise<void> {
208
+ await this.getMarketIdToMarketMap(true);
209
+ }
210
+
211
+ public getIsAsyncAssetByMarketId(marketId: MarketId): boolean {
212
+ return !!this.getIsolationModeConverterByMarketId(marketId)?.isAsync;
213
+ }
214
+
215
+ public getAsyncAssetOutputMarketsByMarketId(marketId: MarketId): MarketId[] | undefined {
216
+ const converter = this.getIsolationModeConverterByMarketId(marketId);
217
+ if (!converter) {
218
+ return undefined;
219
+ }
220
+
221
+ const gmMarket = getGmxV2IsolationModeAsset(this.network, converter.tokenAddress);
222
+ if (!gmMarket) {
223
+ return undefined;
224
+ }
225
+
226
+ const longMarketId = gmMarket.longTokenId;
227
+ if (longMarketId) {
228
+ return [longMarketId, gmMarket.shortTokenId];
229
+ }
230
+
231
+ return [gmMarket.shortTokenId];
232
+ }
233
+
234
+ public getPendleMarketByIsolationModeAddress(isolationModeAddress: Address): Address | undefined {
235
+ return getPendlePtMarketForIsolationModeToken(this.network, isolationModeAddress);
236
+ }
237
+
238
+ public setDefaultSlippageTolerance(slippageTolerance: number): void {
239
+ this._defaultSlippageTolerance = slippageTolerance;
240
+ }
241
+
242
+ public setDefaultBlockTag(blockTag: BlockTag): void {
243
+ this._defaultBlockTag = blockTag;
244
+ }
245
+
246
+ public setMarketsToAdd(marketsToAdd: ApiMarket[]): void {
247
+ this.client.setMarketsToAdd(marketsToAdd);
248
+ }
249
+
250
+ public getIsolationModeConverterByMarketId(marketId: MarketId): ApiMarketConverter | undefined {
251
+ return ISOLATION_MODE_CONVERSION_MARKET_ID_MAP[this.network][marketId.toFixed()];
252
+ }
253
+
254
+ public getLiquidityTokenConverterByMarketId(marketId: MarketId): ApiMarketConverter | undefined {
255
+ return LIQUIDITY_TOKEN_CONVERSION_MARKET_ID_MAP[this.network][marketId.toFixed()];
256
+ }
257
+
258
+ /**
259
+ *
260
+ * @param tokenIn The input token for the zap
261
+ * @param amountIn The input amount for the zap
262
+ * @param tokenOut The output token for the zap
263
+ * @param amountOutMin The minimum amount out required for the swap to be considered valid
264
+ * @param txOrigin The address that will execute the transaction
265
+ * @param config The additional config for zapping
266
+ * @return {Promise<ZapOutputParam[]>} A list of outputs that can be used to execute the trade. The outputs are
267
+ * sorted by execution, with the best ones being first.
268
+ */
269
+ public async getSwapExactTokensForTokensParams(
270
+ tokenIn: ApiToken | MinimalApiToken,
271
+ amountIn: Integer,
272
+ tokenOut: ApiToken | MinimalApiToken,
273
+ amountOutMin: Integer,
274
+ txOrigin: Address,
275
+ config?: Partial<ZapConfig>,
276
+ ): Promise<ZapOutputParam[]> {
277
+ const actualConfig: ZapConfig = {
278
+ isLiquidation: config?.isLiquidation ?? this.defaultIsLiquidation,
279
+ isVaporizable: config?.isVaporizable ?? false,
280
+ slippageTolerance: config?.slippageTolerance ?? this.defaultSlippageTolerance,
281
+ blockTag: config?.blockTag ?? this._defaultBlockTag,
282
+ filterOutZapsWithInsufficientOutput: config?.filterOutZapsWithInsufficientOutput ?? true,
283
+ subAccountNumber: config?.subAccountNumber,
284
+ disallowAggregator: config?.disallowAggregator ?? false,
285
+ gasPriceInWei: config?.gasPriceInWei,
286
+ };
287
+ const marketsMap = await this.getMarketIdToMarketMap(false);
288
+ const marketHelpersMap = await this.getMarketHelpersMap(marketsMap);
289
+ const inputMarket = marketsMap[tokenIn.marketId.toFixed()];
290
+ const outputMarket = marketsMap[tokenOut.marketId.toFixed()];
291
+
292
+ if (!inputMarket) {
293
+ return Promise.reject(new Error(`Invalid tokenIn: ${tokenIn.symbol} / ${tokenIn.marketId}`));
294
+ } else if (!outputMarket) {
295
+ return Promise.reject(new Error(`Invalid tokenOut: ${tokenOut.symbol} / ${tokenOut.marketId}`));
296
+ } else if (amountIn.lte(INTEGERS.ZERO)) {
297
+ return Promise.reject(new Error('Invalid amountIn. Must be greater than 0'));
298
+ } else if (amountOutMin.lte(INTEGERS.ZERO)) {
299
+ return Promise.reject(new Error('Invalid amountOutMin. Must be greater than 0'));
300
+ } else if (!toChecksumOpt(txOrigin)) {
301
+ return Promise.reject(new Error('Invalid address for txOrigin'));
302
+ } else if (actualConfig.slippageTolerance < 0 || actualConfig.slippageTolerance > 0.1) {
303
+ return Promise.reject(new Error('Invalid slippageTolerance. Must be between 0 and 0.1 (10%)'));
304
+ }
305
+
306
+ let marketIdsPaths: MarketId[][] = [];
307
+ let amountsPaths: BigNumber[][] = [];
308
+ let traderParamsArrays: GenericTraderParam[][] = [];
309
+ let executionFees: BigNumber[] = [];
310
+
311
+ const {
312
+ effectiveInputMarketIds,
313
+ } = await this.calculateUnwrapperAmountsIfNecessary(
314
+ inputMarket,
315
+ amountIn,
316
+ outputMarket,
317
+ marketIdsPaths,
318
+ amountsPaths,
319
+ traderParamsArrays,
320
+ executionFees,
321
+ actualConfig,
322
+ marketsMap,
323
+ marketHelpersMap,
324
+ );
325
+
326
+ const wrapperUsage = this.calculateWrapperUsages(
327
+ outputMarket,
328
+ [outputMarket.marketId],
329
+ marketIdsPaths,
330
+ amountsPaths,
331
+ traderParamsArrays,
332
+ executionFees,
333
+ marketsMap,
334
+ marketHelpersMap,
335
+ );
336
+ const { effectiveOutputMarketIds, wrapperInfos, wrapperHelpers, isIsolationModeWrappers } = wrapperUsage;
337
+ marketIdsPaths = wrapperUsage.marketIdsPaths;
338
+ amountsPaths = wrapperUsage.amountsPaths;
339
+ traderParamsArrays = wrapperUsage.traderParamsArrays;
340
+ executionFees = wrapperUsage.executionFees;
341
+ const { outputMarkets } = wrapperUsage;
342
+
343
+ if (
344
+ effectiveInputMarketIds.length !== marketIdsPaths.length
345
+ && effectiveOutputMarketIds.length !== marketIdsPaths.length
346
+ ) {
347
+ // eslint-disable-next-line max-len
348
+ return Promise.reject(new Error(`Developer error: marketIds length does not match <${effectiveInputMarketIds.length}, ${marketIdsPaths.length}>`));
349
+ } else if (marketIdsPaths.length !== amountsPaths.length) {
350
+ // eslint-disable-next-line max-len
351
+ return Promise.reject(new Error(`Developer error: amountsPaths length does not match <${amountsPaths.length}, ${marketIdsPaths.length}>`));
352
+ }
353
+
354
+ for (let i = 0; i < effectiveInputMarketIds.length; i += 1) {
355
+ const effectiveInputMarketId = effectiveInputMarketIds[i];
356
+ for (let j = 0; j < effectiveOutputMarketIds.length; j += 1) {
357
+ const effectiveOutputMarketId = effectiveOutputMarketIds[j];
358
+ if (!effectiveInputMarketId.eq(effectiveOutputMarketId)) {
359
+ const c = i * effectiveOutputMarketIds.length + j;
360
+ const effectiveInputMarket = marketsMap[effectiveInputMarketId.toFixed()];
361
+ const effectiveOutputMarket = marketsMap[effectiveOutputMarketId.toFixed()];
362
+ const aggregatorOutputOrUndefinedList = await Promise.all(
363
+ this.validAggregators.map(async aggregator => {
364
+ if (actualConfig.disallowAggregator) {
365
+ return undefined;
366
+ }
367
+
368
+ try {
369
+ return await aggregator.getSwapExactTokensForTokensData(
370
+ effectiveInputMarket,
371
+ amountsPaths[c][amountsPaths[c].length - 1],
372
+ effectiveOutputMarket,
373
+ INTEGERS.ONE,
374
+ txOrigin,
375
+ actualConfig,
376
+ );
377
+ } catch (e) {
378
+ return undefined;
379
+ }
380
+ }),
381
+ );
382
+
383
+ // eslint-disable-next-line no-loop-func
384
+ aggregatorOutputOrUndefinedList.forEach((aggregatorOutput, aggregatorIndex) => {
385
+ if (aggregatorIndex === 0) {
386
+ amountsPaths[c] = amountsPaths[c].concat(
387
+ aggregatorOutput?.expectedAmountOut ?? INVALID_ESTIMATION.amountOut,
388
+ );
389
+ traderParamsArrays[c] = traderParamsArrays[c].concat({
390
+ traderType: GenericTraderType.ExternalLiquidity,
391
+ makerAccountIndex: 0,
392
+ trader: aggregatorOutput?.traderAddress ?? ADDRESS_ZERO,
393
+ tradeData: aggregatorOutput?.tradeData ?? BYTES_EMPTY,
394
+ readableName: aggregatorOutput?.readableName ?? INVALID_NAME,
395
+ });
396
+ } else {
397
+ marketIdsPaths.push([...marketIdsPaths[c]]);
398
+ amountsPaths.push(
399
+ amountsPaths[c].slice(0, -1)
400
+ .concat(aggregatorOutput?.expectedAmountOut ?? INVALID_ESTIMATION.amountOut),
401
+ );
402
+ traderParamsArrays.push(
403
+ traderParamsArrays[c].slice(0, -1).concat({
404
+ traderType: GenericTraderType.ExternalLiquidity,
405
+ makerAccountIndex: 0,
406
+ trader: aggregatorOutput?.traderAddress ?? ADDRESS_ZERO,
407
+ tradeData: aggregatorOutput?.tradeData ?? BYTES_EMPTY,
408
+ readableName: aggregatorOutput?.readableName ?? INVALID_NAME,
409
+ }),
410
+ );
411
+ executionFees.push(executionFees[c]);
412
+ }
413
+ });
414
+ }
415
+ }
416
+ }
417
+
418
+ await this.calculateWrapperAmounts(
419
+ wrapperInfos,
420
+ wrapperHelpers,
421
+ isIsolationModeWrappers,
422
+ outputMarkets,
423
+ marketIdsPaths,
424
+ amountsPaths,
425
+ traderParamsArrays,
426
+ executionFees,
427
+ actualConfig,
428
+ );
429
+
430
+ const tokensPaths = marketIdsPaths.map<ApiToken[]>(marketIdsPath => {
431
+ return marketIdsPath.map(marketId => ({
432
+ marketId,
433
+ symbol: marketsMap[marketId.toFixed()].symbol,
434
+ name: marketsMap[marketId.toFixed()].name,
435
+ tokenAddress: marketsMap[marketId.toFixed()].tokenAddress,
436
+ decimals: marketsMap[marketId.toFixed()].decimals,
437
+ }));
438
+ });
439
+
440
+ // Unify the min amount out to be the same for UX's sake
441
+ const expectedAmountOut = amountsPaths.reduce((max, amountsPath) => {
442
+ const current = amountsPath[amountsPath.length - 1];
443
+ if (current.gt(max)) {
444
+ return current;
445
+ }
446
+
447
+ return max;
448
+ }, INTEGERS.ZERO);
449
+
450
+ const minAmountOut = expectedAmountOut
451
+ .multipliedBy(1 - actualConfig.slippageTolerance)
452
+ .integerValue(BigNumber.ROUND_DOWN);
453
+
454
+ const result = marketIdsPaths.map<ZapOutputParam>((_, i) => {
455
+ const expectedAmountOutBeforeOverwrite = amountsPaths[i][amountsPaths[i].length - 1];
456
+ if (!amountsPaths[i].some(amount => amount.eq(INVALID_ESTIMATION.amountOut))) {
457
+ amountsPaths[i][amountsPaths[i].length - 1] = minAmountOut;
458
+ }
459
+ return {
460
+ marketIdsPath: marketIdsPaths[i],
461
+ tokensPath: tokensPaths[i],
462
+ expectedAmountOut: expectedAmountOutBeforeOverwrite,
463
+ amountWeisPath: amountsPaths[i],
464
+ traderParams: traderParamsArrays[i],
465
+ makerAccounts: [],
466
+ originalAmountOutMin: amountOutMin,
467
+ executionFee: executionFees[i].gt(INTEGERS.ZERO) ? executionFees[i] : undefined,
468
+ };
469
+ });
470
+
471
+ const zaps = result
472
+ .filter(p => !zapOutputParamIsInvalid(p))
473
+ .sort((a, b) => (a.expectedAmountOut.gt(b.expectedAmountOut) ? -1 : 1));
474
+
475
+ if (actualConfig.filterOutZapsWithInsufficientOutput) {
476
+ return zaps.filter(zap => zap.expectedAmountOut.gte(amountOutMin));
477
+ } else {
478
+ return zaps;
479
+ }
480
+ }
481
+
482
+ /**
483
+ *
484
+ * @param tokenIn The input token for the zap. Must be an async market.
485
+ * @param amountIn The input amount for the zap. This should be held amount of collateral seized for liquidation
486
+ * @param tokenOut The output token for the zap. Must not be an async market.
487
+ * @param amountOutMin The minimum amount out required for the swap to be considered valid
488
+ * @param txOrigin The address that will execute the transaction
489
+ * @param marketIdToActionsMap A mapping from output market to a list of async deposits/withdrawals that output a
490
+ * valid output token from `tokenIn`
491
+ * @param marketIdToOracleMap A mapping from market ID to the corresponding market's oracle price
492
+ * @param config The additional config for zapping
493
+ * @return {Promise<ZapOutputParam[]>} A list of outputs that can be used to execute the trade. The outputs are
494
+ * sorted by execution, with the best ones being first.
495
+ */
496
+ public async getSwapExactAsyncTokensForTokensParamsForLiquidation(
497
+ tokenIn: ApiToken | MinimalApiToken,
498
+ amountIn: Integer,
499
+ tokenOut: ApiToken | MinimalApiToken,
500
+ amountOutMin: Integer,
501
+ txOrigin: Address,
502
+ marketIdToActionsMap: Record<string, ApiAsyncAction[]>,
503
+ marketIdToOracleMap: Record<string, ApiOraclePrice>,
504
+ config?: Partial<ZapConfig>,
505
+ ): Promise<ZapOutputParam[]> {
506
+ if (typeof config?.isLiquidation === 'undefined' || !config.isLiquidation) {
507
+ return Promise.reject(new Error('Config must include `isLiquidation=true`'));
508
+ }
509
+
510
+ const marketsMap = await this.getMarketIdToMarketMap(false);
511
+ const allActions = Object.values(marketIdToActionsMap);
512
+ if (!this.getIsAsyncAssetByMarketId(tokenIn.marketId)) {
513
+ return Promise.reject(new Error('tokenIn must be an async asset!'));
514
+ } else if (this.getIsAsyncAssetByMarketId(tokenOut.marketId)) {
515
+ return Promise.reject(new Error('tokenOut must not be an async asset!'));
516
+ } else if (allActions.length === 0 || allActions.every(a => a.length === 0)) {
517
+ return Promise.reject(new Error('marketIdToActionsMap must not be empty'));
518
+ }
519
+
520
+ const outputWeiFromActionsWithMarket = Object.keys(marketIdToActionsMap).reduce(
521
+ (acc, outputMarketId) => {
522
+ const actions = marketIdToActionsMap[outputMarketId];
523
+ const oraclePriceUsd = marketIdToOracleMap[outputMarketId]?.oraclePrice;
524
+ if (!oraclePriceUsd) {
525
+ throw new Error(`Oracle price for ${outputMarketId} could not be found!`);
526
+ }
527
+
528
+ const outputValue = actions.reduce(
529
+ (sum, action) => {
530
+ if (sum.inputValue.gt(INTEGERS.ZERO)) {
531
+ if (action.inputToken.marketId.eq(tokenIn.marketId)) {
532
+ const usedInputAmount = sum.inputValue.lt(action.inputAmount)
533
+ ? sum.inputValue
534
+ : action.inputAmount;
535
+ const usedOutputAmount = sum.inputValue.lt(action.inputAmount)
536
+ ? action.outputAmount.times(sum.inputValue).dividedToIntegerBy(action.inputAmount)
537
+ : action.outputAmount;
538
+
539
+ sum.inputValue = sum.inputValue.minus(usedInputAmount);
540
+ sum.outputValue = sum.outputValue.plus(usedOutputAmount);
541
+ sum.outputValueUsd = sum.outputValueUsd.plus(usedOutputAmount.times(oraclePriceUsd));
542
+ } else if (action.outputToken.marketId.eq(tokenIn.marketId)) {
543
+ const usedInputAmount = sum.inputValue.lt(action.outputAmount)
544
+ ? sum.inputValue
545
+ : action.outputAmount;
546
+ const usedOutputAmount = sum.inputValue.lt(action.outputAmount)
547
+ ? action.inputAmount.times(sum.inputValue).dividedToIntegerBy(action.outputAmount)
548
+ : action.inputAmount;
549
+
550
+ sum.inputValue = sum.inputValue.minus(usedInputAmount);
551
+ sum.outputValue = sum.outputValue.plus(usedOutputAmount);
552
+ sum.outputValueUsd = sum.outputValueUsd.plus(usedOutputAmount.times(oraclePriceUsd));
553
+ }
554
+ }
555
+ return sum;
556
+ },
557
+ {
558
+ inputValue: amountIn,
559
+ outputValue: INTEGERS.ZERO,
560
+ outputValueUsd: INTEGERS.ZERO,
561
+ outputMarket: marketsMap[outputMarketId],
562
+ },
563
+ );
564
+
565
+ if (acc.outputValueUsd.gt(outputValue.outputValueUsd)) {
566
+ return acc;
567
+ } else {
568
+ return outputValue;
569
+ }
570
+ },
571
+ {
572
+ outputValue: INTEGERS.ZERO,
573
+ outputValueUsd: INTEGERS.ZERO,
574
+ outputMarket: marketsMap[Object.keys(marketIdToActionsMap)[0]],
575
+ },
576
+ );
577
+
578
+ const outputToken: MinimalApiToken = {
579
+ marketId: new BigNumber(outputWeiFromActionsWithMarket.outputMarket.marketId.toFixed()),
580
+ symbol: outputWeiFromActionsWithMarket.outputMarket.symbol,
581
+ };
582
+
583
+ const actions = marketIdToActionsMap[outputToken.marketId.toFixed()];
584
+
585
+ let outputs: ZapOutputParam[];
586
+ if (outputToken.marketId.eq(tokenOut.marketId)) {
587
+ const expectedAmountOutWithSlippage = outputWeiFromActionsWithMarket.outputValue
588
+ .multipliedBy(1 - (config.slippageTolerance ?? this.defaultSlippageTolerance))
589
+ .integerValue(BigNumber.ROUND_DOWN);
590
+ outputs = [
591
+ {
592
+ marketIdsPath: [tokenIn.marketId, tokenOut.marketId],
593
+ tokensPath: [marketsMap[tokenIn.marketId.toFixed()], marketsMap[tokenOut.marketId.toFixed()]],
594
+ amountWeisPath: [amountIn, expectedAmountOutWithSlippage],
595
+ traderParams: [this.getAsyncUnwrapperTraderParam(tokenIn, actions, config)],
596
+ makerAccounts: [],
597
+ expectedAmountOut: outputWeiFromActionsWithMarket.outputValue,
598
+ originalAmountOutMin: amountOutMin,
599
+ },
600
+ ];
601
+ } else {
602
+ outputs = await this.getSwapExactTokensForTokensParams(
603
+ outputToken,
604
+ outputWeiFromActionsWithMarket.outputValue,
605
+ tokenOut,
606
+ amountOutMin,
607
+ txOrigin,
608
+ config,
609
+ );
610
+ outputs.forEach(output => {
611
+ output.marketIdsPath = [
612
+ new BigNumber(tokenIn.marketId.toFixed()),
613
+ ...output.marketIdsPath,
614
+ ];
615
+ output.amountWeisPath = [
616
+ amountIn,
617
+ ...output.amountWeisPath,
618
+ ];
619
+
620
+ output.traderParams = [
621
+ this.getAsyncUnwrapperTraderParam(tokenIn, actions, config),
622
+ ...output.traderParams,
623
+ ];
624
+ });
625
+ }
626
+
627
+ return outputs;
628
+ }
629
+
630
+ protected getAllAggregators(
631
+ network: Network,
632
+ referralInfo: ReferralOutput,
633
+ useProxyServer: boolean,
634
+ ): AggregatorClient[] {
635
+ const odosAggregator = new OdosAggregator(network, referralInfo.odosReferralCode, useProxyServer);
636
+ const oogaBoogaAggregator = new OogaBoogaAggregator(network, referralInfo.oogaBoogaApiKey);
637
+ const paraswapAggregator = new ParaswapAggregator(network, referralInfo.referralAddress, useProxyServer);
638
+ return [odosAggregator, oogaBoogaAggregator, paraswapAggregator];
639
+ }
640
+
641
+ protected async getMarketIdToMarketMap(forceRefresh: boolean): Promise<Record<string, ApiMarket>> {
642
+ if (!forceRefresh) {
643
+ const cachedMarkets = this.marketsCache.get(marketsKey);
644
+ if (cachedMarkets) {
645
+ return cachedMarkets;
646
+ }
647
+ }
648
+
649
+ const marketsMap = await this.client.getDolomiteMarketsMap();
650
+ this.marketsCache.set(marketsKey, marketsMap);
651
+ return marketsMap;
652
+ }
653
+
654
+ private async calculateUnwrapperAmountsIfNecessary(
655
+ inputMarket: ApiMarket,
656
+ amountIn: Integer,
657
+ outputMarket: ApiMarket,
658
+ marketIdsPaths: Integer[][],
659
+ amountsPaths: Integer[][],
660
+ traderParamsArrays: GenericTraderParam[][],
661
+ executionFees: Integer[],
662
+ actualConfig: ZapConfig,
663
+ marketsMap: Record<string, ApiMarket>,
664
+ marketHelpersMap: Record<string, ApiMarketHelper>,
665
+ ): Promise<{ effectiveInputMarketIds: Integer[] }> {
666
+ let effectiveInputMarketIds: Integer[];
667
+ const inputHelper = marketHelpersMap[inputMarket.marketId.toFixed()];
668
+ const isIsolationModeUnwrapper = inputMarket.isolationModeUnwrapperInfo;
669
+ const unwrapperInfo = inputMarket.isolationModeUnwrapperInfo ?? inputMarket.liquidityTokenUnwrapperInfo;
670
+ const unwrapperHelper = inputHelper.isolationModeUnwrapperHelper ?? inputHelper.liquidityTokenUnwrapperHelper;
671
+ if (unwrapperInfo && unwrapperHelper) {
672
+ effectiveInputMarketIds = unwrapperInfo.outputMarketIds;
673
+
674
+ const estimateResults = await Promise.all(
675
+ effectiveInputMarketIds.map((inputMarketId, i) => {
676
+ if (!marketIdsPaths[i] || marketIdsPaths[i].length === 0) {
677
+ marketIdsPaths[i] = [inputMarket.marketId];
678
+ amountsPaths[i] = [amountIn];
679
+ traderParamsArrays[i] = [];
680
+ executionFees[i] = INTEGERS.ZERO;
681
+ }
682
+
683
+ if (actualConfig.disallowAggregator && !inputMarketId.eq(outputMarket.marketId)) {
684
+ // If the aggregator is disabled, and we cannot connect the input market to the output, don't bother
685
+ return Promise.resolve(INVALID_ESTIMATION);
686
+ }
687
+
688
+ return unwrapperHelper.estimateOutputFunction(
689
+ amountIn,
690
+ inputMarketId,
691
+ actualConfig,
692
+ ).catch(e => {
693
+ Logger.error({
694
+ message: `Caught error while estimating unwrapping: ${e.message}`,
695
+ error: e,
696
+ });
697
+ return INVALID_ESTIMATION;
698
+ });
699
+ }),
700
+ );
701
+
702
+ estimateResults.forEach(({ amountOut, tradeData, extraData }, i) => {
703
+ marketIdsPaths[i] = marketIdsPaths[i].concat(effectiveInputMarketIds[i]);
704
+ amountsPaths[i] = amountsPaths[i].concat(amountOut);
705
+ traderParamsArrays[i] = traderParamsArrays[i].concat({
706
+ traderType: isIsolationModeUnwrapper
707
+ ? GenericTraderType.IsolationModeUnwrapper
708
+ : GenericTraderType.ExternalLiquidity,
709
+ makerAccountIndex: 0,
710
+ trader: actualConfig.isLiquidation
711
+ ? (unwrapperInfo.unwrapperForLiquidationAddress ?? unwrapperInfo.unwrapperAddress)
712
+ : unwrapperInfo.unwrapperAddress,
713
+ tradeData,
714
+ readableName: unwrapperInfo.readableName,
715
+ });
716
+ executionFees[i] = executionFees[i].plus(extraData?.executionFee ?? INTEGERS.ZERO);
717
+ });
718
+ } else {
719
+ effectiveInputMarketIds = [inputMarket.marketId];
720
+ marketIdsPaths[0] = [inputMarket.marketId];
721
+ amountsPaths[0] = [amountIn];
722
+ traderParamsArrays[0] = [];
723
+ executionFees[0] = INTEGERS.ZERO;
724
+ }
725
+
726
+ if (effectiveInputMarketIds.length === 1) {
727
+ const liquidityMarketId = effectiveInputMarketIds[0];
728
+ const converter = this.getLiquidityTokenConverterByMarketId(liquidityMarketId);
729
+ if (converter) {
730
+ return this.calculateUnwrapperAmountsIfNecessary(
731
+ marketsMap[liquidityMarketId.toFixed()],
732
+ amountsPaths[0][amountsPaths[0].length - 1],
733
+ outputMarket,
734
+ marketIdsPaths,
735
+ amountsPaths,
736
+ traderParamsArrays,
737
+ executionFees,
738
+ actualConfig,
739
+ marketsMap,
740
+ marketHelpersMap,
741
+ );
742
+ }
743
+ }
744
+
745
+ return { effectiveInputMarketIds };
746
+ }
747
+
748
+ private calculateWrapperUsages(
749
+ outputMarket: ApiMarket,
750
+ effectiveOutputMarketIds: Integer[], // sGLP first --> USDC.e second
751
+ marketIdsPaths: Integer[][],
752
+ amountsPaths: Integer[][],
753
+ traderParamsArrays: GenericTraderParam[][],
754
+ executionFees: Integer[],
755
+ marketsMap: Record<string, ApiMarket>,
756
+ marketHelpers: Record<string, ApiMarketHelper>,
757
+ ): CalculatedWrapperUsage {
758
+ const outputHelper = marketHelpers[outputMarket.marketId.toFixed()];
759
+ const isIsolationModeWrapper = !!outputMarket.isolationModeWrapperInfo;
760
+ const wrapperInfo = outputMarket.isolationModeWrapperInfo ?? outputMarket.liquidityTokenWrapperInfo;
761
+ const wrapperHelper = outputHelper.isolationModeWrapperHelper ?? outputHelper.liquidityTokenWrapperHelper;
762
+
763
+ const isIsolationModeWrappers = wrapperInfo ? [!!outputMarket.isolationModeWrapperInfo] : [];
764
+ const wrapperInfos = wrapperInfo ? [wrapperInfo] : [];
765
+ const wrapperHelpers = wrapperHelper ? [wrapperHelper] : [];
766
+
767
+ if (wrapperInfo) {
768
+ // We can't get the amount yet until we know if we need to use an aggregator in the middle
769
+ effectiveOutputMarketIds = wrapperInfo.inputMarketIds;
770
+
771
+ if (effectiveOutputMarketIds.length === 1) {
772
+ const converter = this.getLiquidityTokenConverterByMarketId(effectiveOutputMarketIds[0]);
773
+ if (converter) {
774
+ const innerUsage = this.calculateWrapperUsages(
775
+ marketsMap[effectiveOutputMarketIds[0].toFixed()],
776
+ effectiveOutputMarketIds, // USDC.e (2) first
777
+ marketIdsPaths,
778
+ amountsPaths,
779
+ traderParamsArrays,
780
+ executionFees,
781
+ marketsMap,
782
+ marketHelpers,
783
+ );
784
+ return {
785
+ effectiveOutputMarketIds: innerUsage.effectiveOutputMarketIds,
786
+ marketIdsPaths: innerUsage.marketIdsPaths,
787
+ amountsPaths: innerUsage.amountsPaths,
788
+ traderParamsArrays: innerUsage.traderParamsArrays,
789
+ executionFees: innerUsage.executionFees,
790
+ isIsolationModeWrappers: [...innerUsage.isIsolationModeWrappers, isIsolationModeWrapper],
791
+ wrapperInfos: [...innerUsage.wrapperInfos, ...(wrapperInfo ? [wrapperInfo] : [])],
792
+ wrapperHelpers: [...innerUsage.wrapperHelpers, ...(wrapperHelper ? [wrapperHelper] : [])],
793
+ outputMarkets: [...innerUsage.outputMarkets, outputMarket],
794
+ };
795
+ }
796
+ }
797
+
798
+ [marketIdsPaths, amountsPaths, traderParamsArrays, executionFees] = DolomiteZap.copyForWrapper(
799
+ effectiveOutputMarketIds,
800
+ marketIdsPaths,
801
+ amountsPaths,
802
+ traderParamsArrays,
803
+ executionFees,
804
+ );
805
+ }
806
+
807
+ return {
808
+ effectiveOutputMarketIds,
809
+ marketIdsPaths,
810
+ amountsPaths,
811
+ traderParamsArrays,
812
+ executionFees,
813
+ isIsolationModeWrappers,
814
+ wrapperInfos,
815
+ wrapperHelpers,
816
+ outputMarkets: [outputMarket],
817
+ };
818
+ }
819
+
820
+ private async calculateWrapperAmounts(
821
+ wrapperInfos: ApiWrapperInfo[],
822
+ wrapperHelpers: ApiWrapperHelper[],
823
+ isIsolationModeWrappers: boolean[],
824
+ outputMarkets: ApiMarket[],
825
+ marketIdsPaths: Integer[][],
826
+ amountsPaths: Integer[][],
827
+ traderParamsArrays: GenericTraderParam[][],
828
+ executionFees: Integer[],
829
+ actualConfig: ZapConfig,
830
+ ): Promise<void> {
831
+ if (
832
+ wrapperInfos.length > 0
833
+ && wrapperInfos.length === wrapperHelpers.length
834
+ && outputMarkets.length === wrapperHelpers.length
835
+ ) {
836
+ for (let i = 0; i < wrapperInfos.length; i += 1) {
837
+ // Append the amounts and trader params for the wrapper
838
+ const wrapperInfo = wrapperInfos[i];
839
+ const wrapperHelper = wrapperHelpers[i];
840
+ for (let j = 0; j < marketIdsPaths.length; j += 1) {
841
+ const marketIdsPath = marketIdsPaths[j];
842
+ const amountsPath = amountsPaths[j];
843
+ let outputEstimate: EstimateOutputResult;
844
+ if (amountsPath.some(a => a.eq(INVALID_ESTIMATION.amountOut))) {
845
+ outputEstimate = INVALID_ESTIMATION;
846
+ } else {
847
+ outputEstimate = await wrapperHelper.estimateOutputFunction(
848
+ amountsPath[amountsPath.length - 1],
849
+ marketIdsPath[marketIdsPath.length - 1],
850
+ actualConfig,
851
+ ).catch(e => {
852
+ Logger.error({
853
+ message: `Caught error while estimating wrapping: ${e.message}`,
854
+ error: e,
855
+ });
856
+ return INVALID_ESTIMATION;
857
+ });
858
+ }
859
+
860
+ marketIdsPath.push(outputMarkets[i].marketId);
861
+ amountsPath.push(outputEstimate.amountOut);
862
+ traderParamsArrays[j].push({
863
+ traderType: isIsolationModeWrappers[i]
864
+ ? GenericTraderType.IsolationModeWrapper
865
+ : GenericTraderType.ExternalLiquidity,
866
+ makerAccountIndex: 0,
867
+ trader: wrapperInfo.wrapperAddress,
868
+ tradeData: outputEstimate.tradeData,
869
+ readableName: outputEstimate.amountOut.eq(INVALID_ESTIMATION.amountOut)
870
+ ? INVALID_NAME
871
+ : wrapperInfo.readableName,
872
+ });
873
+ executionFees[j] = executionFees[j].plus(outputEstimate.extraData?.executionFee ?? INTEGERS.ZERO);
874
+ }
875
+ }
876
+ } else {
877
+ if (outputMarkets.length !== 1) {
878
+ throw new Error('Invalid output markets length!');
879
+ }
880
+
881
+ marketIdsPaths.forEach(marketIdsPath => {
882
+ if (!marketIdsPath[marketIdsPath.length - 1].eq(outputMarkets[0].marketId)) {
883
+ marketIdsPath.push(outputMarkets[0].marketId);
884
+ }
885
+ });
886
+ }
887
+ }
888
+
889
+ private getAsyncUnwrapperTraderParam(
890
+ asyncToken: MinimalApiToken,
891
+ actions: ApiAsyncAction[],
892
+ config: Partial<ZapConfig>,
893
+ ): GenericTraderParam {
894
+ const converter = this.getIsolationModeConverterByMarketId(asyncToken.marketId)!;
895
+ return {
896
+ traderType: GenericTraderType.IsolationModeUnwrapper,
897
+ tradeData: ethers.utils.defaultAbiCoder.encode(
898
+ ['uint8[]', 'bytes32[]', 'bool'],
899
+ [
900
+ actions.map(a => (a.actionType === ApiAsyncActionType.WITHDRAWAL
901
+ ? ApiAsyncTradeType.FromWithdrawal
902
+ : ApiAsyncTradeType.FromDeposit)),
903
+ actions.map(a => a.key),
904
+ !config.isVaporizable,
905
+ ],
906
+ ),
907
+ readableName: converter.unwrapperReadableName,
908
+ trader: converter.unwrapper,
909
+ makerAccountIndex: 0,
910
+ };
911
+ }
912
+
913
+ private async getMarketHelpersMap(
914
+ marketsMap: Record<string, ApiMarket>,
915
+ ): Promise<Record<string, ApiMarketHelper>> {
916
+ const cachedMarkets = this.marketHelpersCache.get(marketHelpersKey);
917
+ if (cachedMarkets) {
918
+ return cachedMarkets;
919
+ }
920
+
921
+ const marketHelpersMap = await this.client.getDolomiteMarketHelpers(marketsMap);
922
+ this.marketHelpersCache.set(marketHelpersKey, marketHelpersMap);
923
+ return marketHelpersMap;
924
+ }
925
+ }
926
+
927
+ /**
928
+ * Used internally to make data passing cleaner
929
+ */
930
+ interface CalculatedWrapperUsage {
931
+ effectiveOutputMarketIds: Integer[];
932
+ marketIdsPaths: Integer[][];
933
+ amountsPaths: Integer[][];
934
+ traderParamsArrays: GenericTraderParam[][];
935
+ executionFees: Integer[];
936
+ isIsolationModeWrappers: boolean[];
937
+ wrapperInfos: ApiWrapperInfo[];
938
+ wrapperHelpers: ApiWrapperHelper[];
939
+ outputMarkets: ApiMarket[];
940
+ }