@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.
- package/README.md +377 -377
- package/build/config.js +9 -5
- package/build/config.js.map +1 -1
- package/build/dex/algebra/algebra.js +25 -25
- package/build/dex/balancer-v1/balancer-v1.js +34 -34
- package/build/dex/balancer-v2/balancer-v2.d.ts +1 -1
- package/build/dex/balancer-v2/balancer-v2.js +65 -78
- package/build/dex/balancer-v2/balancer-v2.js.map +1 -1
- package/build/dex/camelot/camelot.js +27 -27
- package/build/dex/kyberdmm/kyberdmm.js +27 -27
- package/build/dex/pancakeswap-v3/pancakeswap-v3.js +25 -25
- package/build/dex/solidly/solidly.js +27 -27
- package/build/dex/solidly-v3/solidly-v3.js +25 -25
- package/build/dex/swaap-v2/config.js +15 -0
- package/build/dex/swaap-v2/config.js.map +1 -1
- package/build/dex/uniswap-v2/uniswap-v2.js +26 -26
- package/build/dex/uniswap-v3/uniswap-v3.js +25 -25
- package/funding.json +5 -5
- package/package.json +1 -1
- package/src/config.ts +500 -496
- package/src/constants.ts +67 -67
- package/src/dex/aave-v2/aave-v2.ts +409 -409
- package/src/dex/aave-v3/aave-v3.ts +320 -320
- package/src/dex/algebra/algebra.ts +1180 -1180
- package/src/dex/angle-staked-stable/angle-staked-stable.ts +347 -347
- package/src/dex/augustus-rfq.ts +180 -180
- package/src/dex/bProtocol/bProtocol.ts +108 -108
- package/src/dex/balancer-v1/balancer-v1.ts +572 -572
- package/src/dex/balancer-v2/balancer-v2-e2e.test.ts +1699 -1699
- package/src/dex/balancer-v2/balancer-v2.ts +1669 -1701
- package/src/dex/bancor/bancor.ts +128 -128
- package/src/dex/camelot/camelot.ts +873 -873
- package/src/dex/compound/compound.ts +107 -107
- package/src/dex/curve-v1/config.ts +1179 -1179
- package/src/dex/curve-v1/curve-v1-e2e.test.ts +507 -507
- package/src/dex/curve-v1/curve-v1.ts +1315 -1315
- package/src/dex/curve-v1-factory/config.ts +1018 -1018
- package/src/dex/curve-v1-factory/curve-v1-factory.ts +1288 -1288
- package/src/dex/curve-v2/curve-v2.ts +440 -440
- package/src/dex/dexalot/dexalot.ts +1100 -1100
- package/src/dex/dodo-v1/dodo-v1.ts +132 -132
- package/src/dex/dodo-v2.ts +266 -266
- package/src/dex/etherfi.ts +239 -239
- package/src/dex/generic-rfq/generic-rfq.ts +431 -431
- package/src/dex/gmx/gmx.ts +321 -321
- package/src/dex/hashflow/hashflow.ts +1135 -1135
- package/src/dex/idex.ts +254 -254
- package/src/dex/index.ts +357 -357
- package/src/dex/jarvis-v6/jarvis-v6.ts +509 -509
- package/src/dex/jarvis.ts +262 -262
- package/src/dex/kyberdmm/kyberdmm.ts +649 -649
- package/src/dex/lido/lido.ts +110 -110
- package/src/dex/maker-psm/maker-psm.ts +592 -592
- package/src/dex/maverick-v1/maverick-v1.ts +537 -537
- package/src/dex/nerve/config.ts +635 -635
- package/src/dex/nerve/nerve-e2e.test.ts +731 -731
- package/src/dex/nerve/nerve-events.test.ts +911 -911
- package/src/dex/nerve/nerve.ts +467 -467
- package/src/dex/onebit/onebit.ts +113 -113
- package/src/dex/pancakeswap-v3/pancakeswap-v3.ts +1150 -1150
- package/src/dex/paraswap-limit-orders/config.ts +60 -60
- package/src/dex/paraswap-limit-orders/paraswap-limit-orders-e2e.test.ts +137 -137
- package/src/dex/paraswap-limit-orders/paraswap-limit-orders-integration.test.ts +328 -328
- package/src/dex/paraswap-limit-orders/paraswap-limit-orders.ts +631 -631
- package/src/dex/polygon-migrator/polygon-migrator.ts +194 -194
- package/src/dex/quick-perps/quick-perps.ts +337 -337
- package/src/dex/quickswap/config.ts +16 -16
- package/src/dex/quickswap/quickswap-e2e.test.ts +384 -384
- package/src/dex/smoothy/smoothy.ts +110 -110
- package/src/dex/solidly/config.ts +225 -225
- package/src/dex/solidly/solidly-e2e.test.ts +1421 -1421
- package/src/dex/solidly/solidly-integration.test.ts +1753 -1753
- package/src/dex/solidly/solidly.ts +687 -687
- package/src/dex/solidly-v3/solidly-v3.ts +968 -968
- package/src/dex/spark/spark.ts +284 -284
- package/src/dex/stable-pool/stable-pool.ts +129 -129
- package/src/dex/swaap-v2/config.ts +15 -0
- package/src/dex/swaap-v2/swaap-v2-e2e.test.ts +233 -17
- package/src/dex/swaap-v2/swaap-v2-integration.test.ts +1 -1
- package/src/dex/swaap-v2/swaap-v2.ts +1056 -1056
- package/src/dex/swell/swell.ts +312 -312
- package/src/dex/synthetix/synthetix.ts +420 -420
- package/src/dex/trader-joe-v2.1/trader-joe-v2.1.ts +180 -180
- package/src/dex/uniswap-v2/config.ts +637 -637
- package/src/dex/uniswap-v2/constants.ts +58 -58
- package/src/dex/uniswap-v2/uniswap-v2-e2e-avalanche.test.ts +1257 -1257
- package/src/dex/uniswap-v2/uniswap-v2-e2e-base.test.ts +42 -42
- package/src/dex/uniswap-v2/uniswap-v2-e2e-bsc.test.ts +1295 -1295
- package/src/dex/uniswap-v2/uniswap-v2-e2e-mainnet.test.ts +1834 -1834
- package/src/dex/uniswap-v2/uniswap-v2-e2e-polygon.test.ts +871 -871
- package/src/dex/uniswap-v2/uniswap-v2.ts +1117 -1117
- package/src/dex/uniswap-v3/uniswap-v3.ts +1501 -1501
- package/src/dex/uniswap-v3.ts +242 -242
- package/src/dex/weth/config.ts +73 -73
- package/src/dex/weth/weth.ts +230 -230
- package/src/dex/wombat/wombat.ts +467 -467
- package/src/dex/woo-fi-v2/woo-fi-v2.ts +529 -529
- package/src/dex/wsteth/wsteth.ts +304 -304
- package/src/dex/zerox/index.ts +328 -328
- package/src/executor/Executor01BytecodeBuilder.ts +474 -474
- package/src/executor/Executor02BytecodeBuilder.ts +1392 -1392
- package/src/executor/ExecutorBytecodeBuilder.ts +390 -390
- package/src/executor/ExecutorDetector.ts +131 -131
- package/src/executor/constants.ts +66 -66
- package/src/executor/executor01-bytecode-builder-e2e.test.ts +1478 -1478
- package/src/executor/utils.ts +62 -62
- package/src/router/simpleswap.ts +366 -366
- package/src/types.ts +342 -342
- package/tests/constants-e2e.ts +1632 -1622
- package/tests/generate-new-limit-order.ts +107 -107
|
@@ -1,1056 +1,1056 @@
|
|
|
1
|
-
import {
|
|
2
|
-
Token,
|
|
3
|
-
Address,
|
|
4
|
-
ExchangePrices,
|
|
5
|
-
PoolPrices,
|
|
6
|
-
AdapterExchangeParam,
|
|
7
|
-
SimpleExchangeParam,
|
|
8
|
-
PoolLiquidity,
|
|
9
|
-
Logger,
|
|
10
|
-
ExchangeTxInfo,
|
|
11
|
-
OptimalSwapExchange,
|
|
12
|
-
PreprocessTransactionOptions,
|
|
13
|
-
DexExchangeParam,
|
|
14
|
-
} from '../../types';
|
|
15
|
-
import {
|
|
16
|
-
SwapSide,
|
|
17
|
-
Network,
|
|
18
|
-
MAX_INT,
|
|
19
|
-
MAX_UINT,
|
|
20
|
-
CACHE_PREFIX,
|
|
21
|
-
} from '../../constants';
|
|
22
|
-
import * as CALLDATA_GAS_COST from '../../calldata-gas-cost';
|
|
23
|
-
import { getDexKeysWithNetwork, isAxiosError } from '../../utils';
|
|
24
|
-
import { IDex } from '../idex';
|
|
25
|
-
import { IDexHelper } from '../../dex-helper/idex-helper';
|
|
26
|
-
import {
|
|
27
|
-
SwaapV2Data,
|
|
28
|
-
SwaapV2PriceLevel,
|
|
29
|
-
SwaapV2PriceLevels,
|
|
30
|
-
SwaapV2APIParameters,
|
|
31
|
-
SwaapV2QuoteError,
|
|
32
|
-
TokensMap,
|
|
33
|
-
SwaapV2NotificationResponse,
|
|
34
|
-
} from './types';
|
|
35
|
-
import { SimpleExchange } from '../simple-exchange';
|
|
36
|
-
import { Adapters, SwaapV2Config } from './config';
|
|
37
|
-
import { RateFetcher } from './rate-fetcher';
|
|
38
|
-
import routerAbi from '../../abi/swaap-v2/vault.json';
|
|
39
|
-
import BigNumber from 'bignumber.js';
|
|
40
|
-
import { BN_0, BN_1, getBigNumberPow } from '../../bignumber-constants';
|
|
41
|
-
import { Interface } from 'ethers/lib/utils';
|
|
42
|
-
import { AsyncOrSync, assert } from 'ts-essentials';
|
|
43
|
-
import {
|
|
44
|
-
SWAAP_RFQ_API_URL,
|
|
45
|
-
SWAAP_RFQ_PRICES_ENDPOINT,
|
|
46
|
-
SWAAP_RFQ_QUOTE_ENDPOINT,
|
|
47
|
-
SWAAP_RFQ_API_PRICES_POLLING_INTERVAL_MS,
|
|
48
|
-
SWAAP_RFQ_PRICES_CACHES_TTL_S,
|
|
49
|
-
STABLE_SWAP_GAS_COST_ESTIMATION,
|
|
50
|
-
VOLATILE_SWAP_GAS_COST_ESTIMATION,
|
|
51
|
-
BATCH_SWAP_SELECTOR,
|
|
52
|
-
CALLER_SLOT,
|
|
53
|
-
SWAAP_403_TTL_S,
|
|
54
|
-
SWAAP_429_TTL_S,
|
|
55
|
-
SWAAP_RFQ_TOKENS_ENDPOINT,
|
|
56
|
-
SWAAP_POOL_RESTRICT_TTL_S,
|
|
57
|
-
SWAAP_RFQ_API_TOKENS_POLLING_INTERVAL_MS,
|
|
58
|
-
SWAAP_RFQ_TOKENS_CACHES_TTL_S,
|
|
59
|
-
SWAAP_PRICES_CACHE_KEY,
|
|
60
|
-
SWAAP_TOKENS_CACHE_KEY,
|
|
61
|
-
SWAAP_ORDER_TYPE_SELL,
|
|
62
|
-
SWAAP_ORDER_TYPE_BUY,
|
|
63
|
-
SWAAP_MIN_SLIPPAGE_FACTOR_THRESHOLD_FOR_RESTRICTION,
|
|
64
|
-
SWAAP_BANNED_CODE,
|
|
65
|
-
SWAAP_NOTIFY_ENDPOINT,
|
|
66
|
-
} from './constants';
|
|
67
|
-
import {
|
|
68
|
-
getPoolIdentifier,
|
|
69
|
-
normalizeTokenAddress,
|
|
70
|
-
getPairName,
|
|
71
|
-
isStablePair,
|
|
72
|
-
} from './utils';
|
|
73
|
-
import { Method } from '../../dex-helper/irequest-wrapper';
|
|
74
|
-
import {
|
|
75
|
-
SlippageCheckError,
|
|
76
|
-
TooStrictSlippageCheckError,
|
|
77
|
-
} from '../generic-rfq/types';
|
|
78
|
-
import { BI_MAX_UINT256 } from '../../bigint-constants';
|
|
79
|
-
import { SpecialDex } from '../../executor/types';
|
|
80
|
-
import { extractReturnAmountPosition } from '../../executor/utils';
|
|
81
|
-
|
|
82
|
-
const BLACKLISTED = 'blacklisted';
|
|
83
|
-
|
|
84
|
-
export class SwaapV2 extends SimpleExchange implements IDex<SwaapV2Data> {
|
|
85
|
-
readonly isStatePollingDex = true;
|
|
86
|
-
readonly hasConstantPriceLargeAmounts = false;
|
|
87
|
-
readonly needWrapNative = false;
|
|
88
|
-
readonly isFeeOnTransferSupported = false;
|
|
89
|
-
private rateFetcher: RateFetcher;
|
|
90
|
-
private swaapV2AuthToken: string;
|
|
91
|
-
private tokensMap: TokensMap = {};
|
|
92
|
-
private runtimeMMsRestrictHashMapKey: string;
|
|
93
|
-
|
|
94
|
-
public static dexKeysWithNetwork: { key: string; networks: Network[] }[] =
|
|
95
|
-
getDexKeysWithNetwork(SwaapV2Config);
|
|
96
|
-
|
|
97
|
-
logger: Logger;
|
|
98
|
-
|
|
99
|
-
constructor(
|
|
100
|
-
readonly network: Network,
|
|
101
|
-
readonly dexKey: string,
|
|
102
|
-
readonly dexHelper: IDexHelper,
|
|
103
|
-
protected adapters = Adapters[network] || {},
|
|
104
|
-
protected routerInterface = new Interface(routerAbi),
|
|
105
|
-
) {
|
|
106
|
-
super(dexHelper, dexKey);
|
|
107
|
-
this.logger = dexHelper.getLogger(dexKey);
|
|
108
|
-
const token = dexHelper.config.data.swaapV2AuthToken;
|
|
109
|
-
assert(
|
|
110
|
-
token !== undefined,
|
|
111
|
-
'SwaapV2 auth token is not specified with env variable',
|
|
112
|
-
);
|
|
113
|
-
|
|
114
|
-
this.swaapV2AuthToken = token;
|
|
115
|
-
|
|
116
|
-
this.rateFetcher = new RateFetcher(
|
|
117
|
-
this.dexHelper,
|
|
118
|
-
this.dexKey,
|
|
119
|
-
this.logger,
|
|
120
|
-
{
|
|
121
|
-
rateConfig: {
|
|
122
|
-
pricesIntervalMs: SWAAP_RFQ_API_PRICES_POLLING_INTERVAL_MS,
|
|
123
|
-
pricesReqParams: this.getPriceLevelsReqParams(),
|
|
124
|
-
pricesCacheTTLSecs: SWAAP_RFQ_PRICES_CACHES_TTL_S,
|
|
125
|
-
pricesCacheKey: SWAAP_PRICES_CACHE_KEY,
|
|
126
|
-
},
|
|
127
|
-
tokensConfig: {
|
|
128
|
-
tokensIntervalMs: SWAAP_RFQ_API_TOKENS_POLLING_INTERVAL_MS,
|
|
129
|
-
tokensReqParams: this.getTokensReqParams(),
|
|
130
|
-
tokensCacheTTLSecs: SWAAP_RFQ_TOKENS_CACHES_TTL_S,
|
|
131
|
-
tokensCacheKey: SWAAP_TOKENS_CACHE_KEY,
|
|
132
|
-
},
|
|
133
|
-
},
|
|
134
|
-
);
|
|
135
|
-
|
|
136
|
-
this.runtimeMMsRestrictHashMapKey =
|
|
137
|
-
`${CACHE_PREFIX}_${this.dexKey}_${this.network}_restricted_mms`.toLowerCase();
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
async initializePricing(blockNumber: number): Promise<void> {
|
|
141
|
-
if (!this.dexHelper.config.isSlave) {
|
|
142
|
-
await this.rateFetcher.start();
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
getAdapters(side: SwapSide): { name: string; index: number }[] | null {
|
|
149
|
-
return this.adapters[side] ? this.adapters[side] : null;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
async getPoolIdentifiers(
|
|
153
|
-
srcToken: Token,
|
|
154
|
-
destToken: Token,
|
|
155
|
-
side: SwapSide,
|
|
156
|
-
blockNumber: number,
|
|
157
|
-
): Promise<string[]> {
|
|
158
|
-
const normalizedSrcToken = this.normalizeToken(srcToken);
|
|
159
|
-
const normalizedDestToken = this.normalizeToken(destToken);
|
|
160
|
-
|
|
161
|
-
if (normalizedSrcToken.address === normalizedDestToken.address) {
|
|
162
|
-
return [];
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
const poolIdentifier = getPoolIdentifier(
|
|
166
|
-
this.dexKey,
|
|
167
|
-
normalizedSrcToken.address,
|
|
168
|
-
normalizedDestToken.address,
|
|
169
|
-
);
|
|
170
|
-
|
|
171
|
-
const levels = await this.getCachedLevels();
|
|
172
|
-
if (levels === null) {
|
|
173
|
-
return [];
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
return Object.keys(levels)
|
|
177
|
-
.map((pair: string) => {
|
|
178
|
-
return getPoolIdentifier(
|
|
179
|
-
this.dexKey,
|
|
180
|
-
levels[pair].base!,
|
|
181
|
-
levels[pair].quote!,
|
|
182
|
-
);
|
|
183
|
-
})
|
|
184
|
-
.filter((pi: string) => pi === poolIdentifier);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
computePricesFromLevelsBids(
|
|
188
|
-
amounts: BigNumber[],
|
|
189
|
-
asksAndBids: SwaapV2PriceLevels,
|
|
190
|
-
srcToken: Token,
|
|
191
|
-
destToken: Token,
|
|
192
|
-
side: SwapSide,
|
|
193
|
-
): bigint[] {
|
|
194
|
-
let levels: SwaapV2PriceLevel[] = [];
|
|
195
|
-
if (side === SwapSide.BUY) {
|
|
196
|
-
if (destToken.address === asksAndBids.base!) {
|
|
197
|
-
levels = asksAndBids.asks;
|
|
198
|
-
} else {
|
|
199
|
-
levels = this.invertPrices(asksAndBids.bids);
|
|
200
|
-
}
|
|
201
|
-
} else {
|
|
202
|
-
if (srcToken.address === asksAndBids.base!) {
|
|
203
|
-
levels = asksAndBids.bids;
|
|
204
|
-
} else {
|
|
205
|
-
levels = this.invertPrices(asksAndBids.asks);
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
const firstLevelRaw = levels[0];
|
|
210
|
-
const firstLevelAmountBN = new BigNumber(firstLevelRaw.level);
|
|
211
|
-
|
|
212
|
-
if (amounts[amounts.length - 1].lt(firstLevelAmountBN)) {
|
|
213
|
-
return [];
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
return this.computeLevelsQuote(amounts, levels, srcToken, destToken, side);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
inversePrice(priceLevel: SwaapV2PriceLevel): SwaapV2PriceLevel {
|
|
220
|
-
return {
|
|
221
|
-
level: priceLevel.level * priceLevel.price,
|
|
222
|
-
price: 1 / priceLevel.price,
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
invertPrices(levels: SwaapV2PriceLevel[]): SwaapV2PriceLevel[] {
|
|
227
|
-
return levels.map(pl => this.inversePrice(pl));
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
computeLevelsQuote(
|
|
231
|
-
amounts: BigNumber[],
|
|
232
|
-
levels: SwaapV2PriceLevel[],
|
|
233
|
-
srcToken: Token,
|
|
234
|
-
destToken: Token,
|
|
235
|
-
side: SwapSide,
|
|
236
|
-
): bigint[] {
|
|
237
|
-
const size = levels.length;
|
|
238
|
-
if (size === 0) {
|
|
239
|
-
return amounts.map(_ => BigInt(0));
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
return amounts.map((amount: BigNumber) => {
|
|
243
|
-
if (amount.isZero()) {
|
|
244
|
-
return BigInt(0);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
let i = 0;
|
|
248
|
-
let output = BN_0;
|
|
249
|
-
let previousLevel = BN_0;
|
|
250
|
-
let remaininig = BigNumber(amount);
|
|
251
|
-
let enoughLiquidity = false;
|
|
252
|
-
while (i < size) {
|
|
253
|
-
const levelDelta = BigNumber(levels[i].level).minus(previousLevel);
|
|
254
|
-
if (levelDelta.lte(remaininig)) {
|
|
255
|
-
output = output.plus(levelDelta.times(levels[i].price));
|
|
256
|
-
remaininig = remaininig.minus(levelDelta);
|
|
257
|
-
previousLevel = BigNumber(levels[i].level);
|
|
258
|
-
i += 1;
|
|
259
|
-
} else {
|
|
260
|
-
output = output.plus(remaininig.times(levels[i].price));
|
|
261
|
-
enoughLiquidity = true;
|
|
262
|
-
break;
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
if (enoughLiquidity) {
|
|
266
|
-
return BigInt(
|
|
267
|
-
output
|
|
268
|
-
.multipliedBy(
|
|
269
|
-
getBigNumberPow(
|
|
270
|
-
side === SwapSide.BUY ? srcToken.decimals : destToken.decimals,
|
|
271
|
-
),
|
|
272
|
-
)
|
|
273
|
-
.toFixed(0),
|
|
274
|
-
);
|
|
275
|
-
}
|
|
276
|
-
return BigInt(0); // not enough liquidity
|
|
277
|
-
});
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
async getCachedTokens(): Promise<TokensMap | null> {
|
|
281
|
-
const cachedTokens = await this.dexHelper.cache.get(
|
|
282
|
-
this.dexKey,
|
|
283
|
-
this.network,
|
|
284
|
-
SWAAP_TOKENS_CACHE_KEY,
|
|
285
|
-
);
|
|
286
|
-
|
|
287
|
-
if (cachedTokens) {
|
|
288
|
-
return JSON.parse(cachedTokens) as TokensMap;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
return null;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
async getCachedLevels(): Promise<Record<string, SwaapV2PriceLevels> | null> {
|
|
295
|
-
const cachedLevels = await this.dexHelper.cache.get(
|
|
296
|
-
this.dexKey,
|
|
297
|
-
this.network,
|
|
298
|
-
SWAAP_PRICES_CACHE_KEY,
|
|
299
|
-
);
|
|
300
|
-
|
|
301
|
-
if (cachedLevels) {
|
|
302
|
-
return JSON.parse(cachedLevels) as Record<string, SwaapV2PriceLevels>;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
return null;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
normalizeToken(token: Token): Token {
|
|
309
|
-
return {
|
|
310
|
-
address: normalizeTokenAddress(token.address),
|
|
311
|
-
decimals: token.decimals,
|
|
312
|
-
};
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
async getPricesVolume(
|
|
316
|
-
srcToken: Token,
|
|
317
|
-
destToken: Token,
|
|
318
|
-
amounts: bigint[],
|
|
319
|
-
side: SwapSide,
|
|
320
|
-
blockNumber: number,
|
|
321
|
-
limitPools?: string[],
|
|
322
|
-
): Promise<null | ExchangePrices<SwaapV2Data>> {
|
|
323
|
-
const normalizedSrcToken = this.normalizeToken(srcToken);
|
|
324
|
-
const normalizedDestToken = this.normalizeToken(destToken);
|
|
325
|
-
|
|
326
|
-
const requestedPoolIdentifier: string = getPoolIdentifier(
|
|
327
|
-
this.dexKey,
|
|
328
|
-
normalizedSrcToken.address,
|
|
329
|
-
normalizedDestToken.address,
|
|
330
|
-
);
|
|
331
|
-
|
|
332
|
-
try {
|
|
333
|
-
if (await this.isRestrictedPool(requestedPoolIdentifier)) {
|
|
334
|
-
return null;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
this.tokensMap = (await this.getCachedTokens()) || {};
|
|
338
|
-
|
|
339
|
-
if (normalizedSrcToken.address === normalizedDestToken.address) {
|
|
340
|
-
return null;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
const pools = limitPools
|
|
344
|
-
? limitPools.filter(
|
|
345
|
-
p =>
|
|
346
|
-
p ===
|
|
347
|
-
getPoolIdentifier(
|
|
348
|
-
this.dexKey,
|
|
349
|
-
normalizedSrcToken.address,
|
|
350
|
-
normalizedDestToken.address,
|
|
351
|
-
) ||
|
|
352
|
-
p ===
|
|
353
|
-
getPoolIdentifier(
|
|
354
|
-
this.dexKey,
|
|
355
|
-
normalizedDestToken.address,
|
|
356
|
-
normalizedSrcToken.address,
|
|
357
|
-
),
|
|
358
|
-
)
|
|
359
|
-
: await this.getPoolIdentifiers(srcToken, destToken, side, blockNumber);
|
|
360
|
-
|
|
361
|
-
if (pools.length === 0) {
|
|
362
|
-
return null;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
const levels = await this.getCachedLevels();
|
|
366
|
-
if (levels === null) {
|
|
367
|
-
return null;
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
const levelEntries: SwaapV2PriceLevels[] = Object.keys(levels)
|
|
371
|
-
.filter(pair => {
|
|
372
|
-
const { base, quote } = levels[pair];
|
|
373
|
-
if (
|
|
374
|
-
pools.includes(getPoolIdentifier(this.dexKey, quote, base)) ||
|
|
375
|
-
pools.includes(getPoolIdentifier(this.dexKey, base, quote))
|
|
376
|
-
) {
|
|
377
|
-
return true;
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
return false;
|
|
381
|
-
})
|
|
382
|
-
.map(pair => {
|
|
383
|
-
return levels[pair];
|
|
384
|
-
});
|
|
385
|
-
|
|
386
|
-
const unitVolume = getBigNumberPow(
|
|
387
|
-
(side === SwapSide.SELL ? normalizedSrcToken : normalizedDestToken)
|
|
388
|
-
.decimals,
|
|
389
|
-
);
|
|
390
|
-
|
|
391
|
-
const amountsFloat = amounts.map(a =>
|
|
392
|
-
new BigNumber(a.toString()).dividedBy(unitVolume),
|
|
393
|
-
);
|
|
394
|
-
|
|
395
|
-
const prices = levelEntries.map((askAndBids: SwaapV2PriceLevels) => {
|
|
396
|
-
const unitPrice: bigint | undefined = this.computePricesFromLevelsBids(
|
|
397
|
-
[unitVolume],
|
|
398
|
-
askAndBids,
|
|
399
|
-
normalizedSrcToken,
|
|
400
|
-
normalizedDestToken,
|
|
401
|
-
side,
|
|
402
|
-
)[0];
|
|
403
|
-
|
|
404
|
-
const prices = this.computePricesFromLevelsBids(
|
|
405
|
-
amountsFloat,
|
|
406
|
-
askAndBids,
|
|
407
|
-
normalizedSrcToken,
|
|
408
|
-
normalizedDestToken,
|
|
409
|
-
side,
|
|
410
|
-
);
|
|
411
|
-
|
|
412
|
-
if (!prices.length) {
|
|
413
|
-
return null;
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
const gasCost = isStablePair(
|
|
417
|
-
this.network,
|
|
418
|
-
normalizedSrcToken.address,
|
|
419
|
-
normalizedDestToken.address,
|
|
420
|
-
)
|
|
421
|
-
? STABLE_SWAP_GAS_COST_ESTIMATION
|
|
422
|
-
: VOLATILE_SWAP_GAS_COST_ESTIMATION;
|
|
423
|
-
|
|
424
|
-
return {
|
|
425
|
-
gasCost: gasCost,
|
|
426
|
-
exchange: this.dexKey,
|
|
427
|
-
data: {},
|
|
428
|
-
prices,
|
|
429
|
-
unit: unitPrice === undefined ? 0n : unitPrice,
|
|
430
|
-
poolIdentifier: requestedPoolIdentifier,
|
|
431
|
-
} as PoolPrices<SwaapV2Data>;
|
|
432
|
-
});
|
|
433
|
-
|
|
434
|
-
return prices.filter((p): p is PoolPrices<SwaapV2Data> => !!p);
|
|
435
|
-
} catch (e) {
|
|
436
|
-
this.logger.error(
|
|
437
|
-
`Error_getPrices ${srcToken}, ${destToken}, ${side}:`,
|
|
438
|
-
e,
|
|
439
|
-
);
|
|
440
|
-
return null;
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
getBaseToken(poolIdentifier: string): string {
|
|
445
|
-
return poolIdentifier.split('_')[1];
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
getDexParam(
|
|
449
|
-
srcToken: string,
|
|
450
|
-
destToken: string,
|
|
451
|
-
srcAmount: string,
|
|
452
|
-
destAmount: string,
|
|
453
|
-
recipient: string,
|
|
454
|
-
data: SwaapV2Data,
|
|
455
|
-
side: SwapSide,
|
|
456
|
-
): AsyncOrSync<DexExchangeParam> {
|
|
457
|
-
const { router, callData } = data;
|
|
458
|
-
const isBatchSwap = callData.slice(0, 10) === BATCH_SWAP_SELECTOR;
|
|
459
|
-
|
|
460
|
-
// at the moment of writing, batch swap is not supported by SwappV2 API
|
|
461
|
-
assert(isBatchSwap !== true, 'Batch swap is not supported');
|
|
462
|
-
|
|
463
|
-
assert(
|
|
464
|
-
router !== undefined,
|
|
465
|
-
`${this.dexKey}-${this.network}: router undefined`,
|
|
466
|
-
);
|
|
467
|
-
|
|
468
|
-
assert(
|
|
469
|
-
callData !== undefined,
|
|
470
|
-
`${this.dexKey}-${this.network}: callData undefined`,
|
|
471
|
-
);
|
|
472
|
-
|
|
473
|
-
return {
|
|
474
|
-
needWrapNative: this.needWrapNative,
|
|
475
|
-
specialDexSupportsInsertFromAmount: true,
|
|
476
|
-
dexFuncHasRecipient: true,
|
|
477
|
-
exchangeData: callData,
|
|
478
|
-
specialDexFlag: SpecialDex.SWAP_ON_SWAAP_V2_SINGLE,
|
|
479
|
-
targetExchange: router,
|
|
480
|
-
returnAmountPos:
|
|
481
|
-
side === SwapSide.SELL
|
|
482
|
-
? extractReturnAmountPosition(
|
|
483
|
-
this.routerInterface,
|
|
484
|
-
'swap',
|
|
485
|
-
'amountCalculated',
|
|
486
|
-
)
|
|
487
|
-
: undefined,
|
|
488
|
-
};
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
async preProcessTransaction(
|
|
492
|
-
optimalSwapExchange: OptimalSwapExchange<SwaapV2Data>,
|
|
493
|
-
srcToken: Token,
|
|
494
|
-
destToken: Token,
|
|
495
|
-
side: SwapSide,
|
|
496
|
-
options: PreprocessTransactionOptions,
|
|
497
|
-
): Promise<[OptimalSwapExchange<SwaapV2Data>, ExchangeTxInfo]> {
|
|
498
|
-
if (await this.isBlacklisted(options.txOrigin)) {
|
|
499
|
-
this.logger.warn(
|
|
500
|
-
`${this.dexKey}-${this.network}: blacklisted TX Origin address '${options.txOrigin}' trying to build a transaction. Bailing...`,
|
|
501
|
-
);
|
|
502
|
-
throw new Error(
|
|
503
|
-
`${this.dexKey}-${
|
|
504
|
-
this.network
|
|
505
|
-
}: user=${options.txOrigin.toLowerCase()} is blacklisted`,
|
|
506
|
-
);
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
const isSell = side === SwapSide.SELL;
|
|
510
|
-
|
|
511
|
-
const normalizedSrcToken = this.normalizeToken(srcToken);
|
|
512
|
-
const normalizedDestToken = this.normalizeToken(destToken);
|
|
513
|
-
|
|
514
|
-
const tolerance = (
|
|
515
|
-
options.slippageFactor > BN_1
|
|
516
|
-
? options.slippageFactor.minus(BN_1)
|
|
517
|
-
: BN_1.minus(options.slippageFactor)
|
|
518
|
-
).toNumber();
|
|
519
|
-
|
|
520
|
-
try {
|
|
521
|
-
const quote = await this.rateFetcher.getQuote(
|
|
522
|
-
this.network,
|
|
523
|
-
normalizedSrcToken,
|
|
524
|
-
normalizedDestToken,
|
|
525
|
-
isSell ? optimalSwapExchange.srcAmount : optimalSwapExchange.destAmount,
|
|
526
|
-
isSell ? SWAAP_ORDER_TYPE_SELL : SWAAP_ORDER_TYPE_BUY,
|
|
527
|
-
options.txOrigin,
|
|
528
|
-
options.executionContractAddress,
|
|
529
|
-
options.recipient,
|
|
530
|
-
tolerance,
|
|
531
|
-
this.getQuoteReqParams(),
|
|
532
|
-
);
|
|
533
|
-
|
|
534
|
-
if (!quote.success) {
|
|
535
|
-
const message = `${this.dexKey}-${
|
|
536
|
-
this.network
|
|
537
|
-
}: Failed to fetch RFQ for ${getPairName(
|
|
538
|
-
normalizedSrcToken.address,
|
|
539
|
-
normalizedDestToken.address,
|
|
540
|
-
)}: ${JSON.stringify(quote)}`;
|
|
541
|
-
this.logger.warn(message);
|
|
542
|
-
throw new SwaapV2QuoteError(message);
|
|
543
|
-
} else if (!quote.calldata) {
|
|
544
|
-
const message = `${this.dexKey}-${
|
|
545
|
-
this.network
|
|
546
|
-
}: Failed to fetch RFQ for ${getPairName(
|
|
547
|
-
normalizedSrcToken.address,
|
|
548
|
-
normalizedDestToken.address,
|
|
549
|
-
)}. Missing quote data`;
|
|
550
|
-
this.logger.warn(message);
|
|
551
|
-
throw new SwaapV2QuoteError(message);
|
|
552
|
-
} else if (!quote.router) {
|
|
553
|
-
const message = `${this.dexKey}-${
|
|
554
|
-
this.network
|
|
555
|
-
}: Failed to fetch RFQ for ${getPairName(
|
|
556
|
-
normalizedSrcToken.address,
|
|
557
|
-
normalizedDestToken.address,
|
|
558
|
-
)}. Missing router address`;
|
|
559
|
-
this.logger.warn(message);
|
|
560
|
-
throw new SwaapV2QuoteError(message);
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
const srcAmount = BigInt(optimalSwapExchange.srcAmount);
|
|
564
|
-
const destAmount = BigInt(optimalSwapExchange.destAmount);
|
|
565
|
-
const quoteTokenAmount = BigInt(quote.amount);
|
|
566
|
-
const slippageFactor = options.slippageFactor;
|
|
567
|
-
|
|
568
|
-
let isFailOnSlippage = false;
|
|
569
|
-
let slippageErrorMessage = '';
|
|
570
|
-
|
|
571
|
-
if (side === SwapSide.SELL) {
|
|
572
|
-
if (
|
|
573
|
-
quoteTokenAmount <
|
|
574
|
-
BigInt(
|
|
575
|
-
new BigNumber(destAmount.toString())
|
|
576
|
-
.times(slippageFactor)
|
|
577
|
-
.toFixed(0),
|
|
578
|
-
)
|
|
579
|
-
) {
|
|
580
|
-
isFailOnSlippage = true;
|
|
581
|
-
const message = `${this.dexKey}-${this.network}: too much slippage on quote ${side} quoteTokenAmount ${quoteTokenAmount} / destAmount ${destAmount} < ${slippageFactor}`;
|
|
582
|
-
slippageErrorMessage = message;
|
|
583
|
-
this.logger.warn(message);
|
|
584
|
-
}
|
|
585
|
-
} else {
|
|
586
|
-
if (
|
|
587
|
-
quoteTokenAmount >
|
|
588
|
-
BigInt(slippageFactor.times(srcAmount.toString()).toFixed(0))
|
|
589
|
-
) {
|
|
590
|
-
isFailOnSlippage = true;
|
|
591
|
-
const message = `${this.dexKey}-${
|
|
592
|
-
this.network
|
|
593
|
-
}: too much slippage on quote ${side} baseTokenAmount ${srcAmount} / srcAmount ${srcAmount} > ${slippageFactor.toFixed()}`;
|
|
594
|
-
slippageErrorMessage = message;
|
|
595
|
-
this.logger.warn(message);
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
let isTooStrictSlippage = false;
|
|
600
|
-
if (
|
|
601
|
-
isFailOnSlippage &&
|
|
602
|
-
side === SwapSide.SELL &&
|
|
603
|
-
new BigNumber(1)
|
|
604
|
-
.minus(slippageFactor)
|
|
605
|
-
.lt(SWAAP_MIN_SLIPPAGE_FACTOR_THRESHOLD_FOR_RESTRICTION)
|
|
606
|
-
) {
|
|
607
|
-
isTooStrictSlippage = true;
|
|
608
|
-
} else if (
|
|
609
|
-
isFailOnSlippage &&
|
|
610
|
-
side === SwapSide.BUY &&
|
|
611
|
-
slippageFactor
|
|
612
|
-
.minus(1)
|
|
613
|
-
.lt(SWAAP_MIN_SLIPPAGE_FACTOR_THRESHOLD_FOR_RESTRICTION)
|
|
614
|
-
) {
|
|
615
|
-
isTooStrictSlippage = true;
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
if (isFailOnSlippage && isTooStrictSlippage) {
|
|
619
|
-
throw new TooStrictSlippageCheckError(slippageErrorMessage);
|
|
620
|
-
} else if (isFailOnSlippage && !isTooStrictSlippage) {
|
|
621
|
-
throw new SlippageCheckError(slippageErrorMessage);
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
const expiryAsBigInt = BigInt(quote.expiration);
|
|
625
|
-
const minDeadline = expiryAsBigInt > 0 ? expiryAsBigInt : BI_MAX_UINT256;
|
|
626
|
-
|
|
627
|
-
return [
|
|
628
|
-
{
|
|
629
|
-
...optimalSwapExchange,
|
|
630
|
-
data: {
|
|
631
|
-
router: quote.router,
|
|
632
|
-
callData: quote.calldata,
|
|
633
|
-
},
|
|
634
|
-
},
|
|
635
|
-
{ deadline: minDeadline },
|
|
636
|
-
];
|
|
637
|
-
} catch (e) {
|
|
638
|
-
if (isAxiosError(e) && e.response?.status === 403) {
|
|
639
|
-
await this.setBlacklist(options.txOrigin, SWAAP_403_TTL_S);
|
|
640
|
-
this.logger.warn(
|
|
641
|
-
`${this.dexKey}-${this.network}: Encountered blacklisted user=${options.txOrigin}. Adding to local blacklist cache`,
|
|
642
|
-
);
|
|
643
|
-
} else if (isAxiosError(e) && e.response?.status === 429) {
|
|
644
|
-
await this.setBlacklist(options.txOrigin, SWAAP_429_TTL_S);
|
|
645
|
-
this.logger.warn(
|
|
646
|
-
`${this.dexKey}-${this.network}: Encountered restricted user=${options.txOrigin}. Adding to local blacklist cache`,
|
|
647
|
-
);
|
|
648
|
-
} else {
|
|
649
|
-
if (e instanceof TooStrictSlippageCheckError) {
|
|
650
|
-
this.logger.warn(
|
|
651
|
-
`${this.dexKey}-${this.network}: failed to build transaction on side ${side} with too strict slippage. Skipping restriction`,
|
|
652
|
-
);
|
|
653
|
-
} else {
|
|
654
|
-
this.logger.warn(
|
|
655
|
-
`${this.dexKey}-${this.network}: protocol is restricted`,
|
|
656
|
-
);
|
|
657
|
-
const poolIdentifier = getPoolIdentifier(
|
|
658
|
-
this.dexKey,
|
|
659
|
-
normalizedSrcToken.address,
|
|
660
|
-
normalizedDestToken.address,
|
|
661
|
-
);
|
|
662
|
-
var message = 'Unknown error';
|
|
663
|
-
if (e instanceof Error) {
|
|
664
|
-
message = `${e.name}: ${e.message}`;
|
|
665
|
-
}
|
|
666
|
-
await this.restrictPool(message, poolIdentifier);
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
throw e;
|
|
671
|
-
}
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
async restrictPool(message: string, poolIdentifier: string): Promise<void> {
|
|
675
|
-
this.logger.warn(
|
|
676
|
-
`${this.dexKey}-${this.network}: ${poolIdentifier} was restricted for ${SWAAP_POOL_RESTRICT_TTL_S} sec. due to fails`,
|
|
677
|
-
);
|
|
678
|
-
|
|
679
|
-
// We use timestamp for creation date to later discern if it already expired or not
|
|
680
|
-
await this.dexHelper.cache.hset(
|
|
681
|
-
this.runtimeMMsRestrictHashMapKey,
|
|
682
|
-
poolIdentifier,
|
|
683
|
-
Date.now().toString(),
|
|
684
|
-
);
|
|
685
|
-
|
|
686
|
-
this.rateFetcher
|
|
687
|
-
.notify(SWAAP_BANNED_CODE, message, this.getNotifyReqParams())
|
|
688
|
-
.then((answer: SwaapV2NotificationResponse) => {
|
|
689
|
-
if (answer.success) {
|
|
690
|
-
this.logger.info(`Successfully notified Swaap API`);
|
|
691
|
-
} else {
|
|
692
|
-
this.logger.error(`Swaap API was not successfully notified`);
|
|
693
|
-
}
|
|
694
|
-
})
|
|
695
|
-
.catch(e => {
|
|
696
|
-
// Must never happen
|
|
697
|
-
this.logger.error(`Swaap API notification failed: ${e}`);
|
|
698
|
-
});
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
async isRestrictedPool(poolIdentifier: string): Promise<boolean> {
|
|
702
|
-
const expirationThreshold = Date.now() - SWAAP_POOL_RESTRICT_TTL_S * 1000;
|
|
703
|
-
const createdAt = await this.dexHelper.cache.hget(
|
|
704
|
-
this.runtimeMMsRestrictHashMapKey,
|
|
705
|
-
poolIdentifier,
|
|
706
|
-
);
|
|
707
|
-
const wasNotRestricted = createdAt === null;
|
|
708
|
-
if (wasNotRestricted) {
|
|
709
|
-
return false;
|
|
710
|
-
}
|
|
711
|
-
const restrictionExpired = +createdAt < expirationThreshold;
|
|
712
|
-
return !restrictionExpired;
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
async isBlacklisted(txOrigin: Address): Promise<boolean> {
|
|
716
|
-
const result = await this.dexHelper.cache.get(
|
|
717
|
-
this.dexKey,
|
|
718
|
-
this.network,
|
|
719
|
-
this.getBlackListKey(txOrigin),
|
|
720
|
-
);
|
|
721
|
-
return result === BLACKLISTED;
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
getBlackListKey(address: Address) {
|
|
725
|
-
return `blacklist_${address}`.toLowerCase();
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
async setBlacklist(
|
|
729
|
-
txOrigin: Address,
|
|
730
|
-
ttl: number = SWAAP_403_TTL_S,
|
|
731
|
-
): Promise<boolean> {
|
|
732
|
-
await this.dexHelper.cache.setex(
|
|
733
|
-
this.dexKey,
|
|
734
|
-
this.network,
|
|
735
|
-
this.getBlackListKey(txOrigin),
|
|
736
|
-
ttl,
|
|
737
|
-
BLACKLISTED,
|
|
738
|
-
);
|
|
739
|
-
return true;
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
// Returns estimated gas cost of calldata for this DEX in multiSwap
|
|
743
|
-
getCalldataGasCost(poolPrices: PoolPrices<SwaapV2Data>): number | number[] {
|
|
744
|
-
return (
|
|
745
|
-
CALLDATA_GAS_COST.DEX_OVERHEAD +
|
|
746
|
-
CALLDATA_GAS_COST.LENGTH_LARGE +
|
|
747
|
-
// ParentStruct header
|
|
748
|
-
CALLDATA_GAS_COST.OFFSET_SMALL +
|
|
749
|
-
// ParentStruct -> swaps[] header
|
|
750
|
-
CALLDATA_GAS_COST.OFFSET_LARGE +
|
|
751
|
-
// ParentStruct -> assets[] header
|
|
752
|
-
CALLDATA_GAS_COST.OFFSET_LARGE +
|
|
753
|
-
// ParentStruct -> funds
|
|
754
|
-
CALLDATA_GAS_COST.ADDRESS +
|
|
755
|
-
CALLDATA_GAS_COST.BOOL +
|
|
756
|
-
CALLDATA_GAS_COST.ADDRESS +
|
|
757
|
-
CALLDATA_GAS_COST.BOOL +
|
|
758
|
-
// ParentStruct -> limits[] header
|
|
759
|
-
CALLDATA_GAS_COST.OFFSET_LARGE +
|
|
760
|
-
// ParentStruct -> deadline
|
|
761
|
-
CALLDATA_GAS_COST.TIMESTAMP +
|
|
762
|
-
// ParentStruct -> swaps[]
|
|
763
|
-
CALLDATA_GAS_COST.LENGTH_SMALL +
|
|
764
|
-
// ParentStruct -> swaps[0] header
|
|
765
|
-
CALLDATA_GAS_COST.OFFSET_SMALL +
|
|
766
|
-
// ParentStruct -> swaps[0] -> poolId
|
|
767
|
-
CALLDATA_GAS_COST.FULL_WORD +
|
|
768
|
-
// ParentStruct -> swaps[0] -> assetInIndex
|
|
769
|
-
CALLDATA_GAS_COST.INDEX +
|
|
770
|
-
// ParentStruct -> swaps[0] -> assetOutIndex
|
|
771
|
-
CALLDATA_GAS_COST.INDEX +
|
|
772
|
-
// ParentStruct -> swaps[0] -> amount
|
|
773
|
-
CALLDATA_GAS_COST.AMOUNT +
|
|
774
|
-
// ParentStruct -> swaps[0] -> userData header
|
|
775
|
-
CALLDATA_GAS_COST.OFFSET_SMALL +
|
|
776
|
-
// ParentStruct -> swaps[0] -> userData
|
|
777
|
-
CALLDATA_GAS_COST.ZERO +
|
|
778
|
-
// ParentStruct -> assets[]
|
|
779
|
-
CALLDATA_GAS_COST.LENGTH_SMALL +
|
|
780
|
-
// ParentStruct -> assets[0:2]
|
|
781
|
-
CALLDATA_GAS_COST.ADDRESS * 2 +
|
|
782
|
-
// ParentStruct -> limits[]
|
|
783
|
-
CALLDATA_GAS_COST.LENGTH_SMALL +
|
|
784
|
-
// ParentStruct -> limits[0:2]
|
|
785
|
-
CALLDATA_GAS_COST.FULL_WORD * 2 +
|
|
786
|
-
// Bool -> IsBatchSwap
|
|
787
|
-
CALLDATA_GAS_COST.BOOL
|
|
788
|
-
);
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
getAdapterParam(
|
|
792
|
-
srcToken: string,
|
|
793
|
-
destToken: string,
|
|
794
|
-
srcAmount: string,
|
|
795
|
-
destAmount: string,
|
|
796
|
-
data: SwaapV2Data,
|
|
797
|
-
side: SwapSide,
|
|
798
|
-
): AdapterExchangeParam {
|
|
799
|
-
const { router, callData } = data;
|
|
800
|
-
|
|
801
|
-
assert(
|
|
802
|
-
router !== undefined,
|
|
803
|
-
`${this.dexKey}-${this.network}: router undefined`,
|
|
804
|
-
);
|
|
805
|
-
|
|
806
|
-
assert(
|
|
807
|
-
callData !== undefined,
|
|
808
|
-
`${this.dexKey}-${this.network}: callData undefined`,
|
|
809
|
-
);
|
|
810
|
-
|
|
811
|
-
let payload: string;
|
|
812
|
-
const truncatedCalldata = '0x' + callData.slice(10); // droping the function selector
|
|
813
|
-
const isBatchSwap = callData.slice(0, 10) === BATCH_SWAP_SELECTOR;
|
|
814
|
-
|
|
815
|
-
if (isBatchSwap) {
|
|
816
|
-
const batchswapAbi = routerAbi.filter(abi => abi.name === 'batchSwap');
|
|
817
|
-
|
|
818
|
-
const callDataStruct = this.abiCoder.decodeParameters(
|
|
819
|
-
batchswapAbi[0].inputs!,
|
|
820
|
-
truncatedCalldata,
|
|
821
|
-
);
|
|
822
|
-
|
|
823
|
-
const callerSlots = Array(callDataStruct.swaps.length).fill(160);
|
|
824
|
-
|
|
825
|
-
payload = this.abiCoder.encodeParameters(
|
|
826
|
-
[
|
|
827
|
-
'bool',
|
|
828
|
-
'uint256[]',
|
|
829
|
-
{
|
|
830
|
-
ParentStruct: {
|
|
831
|
-
'swaps[]': {
|
|
832
|
-
poolId: 'bytes32',
|
|
833
|
-
assetInIndex: 'uint256',
|
|
834
|
-
assetOutIndex: 'uint256',
|
|
835
|
-
amount: 'uint256',
|
|
836
|
-
userData: 'bytes',
|
|
837
|
-
},
|
|
838
|
-
assets: 'address[]',
|
|
839
|
-
funds: {
|
|
840
|
-
sender: 'address',
|
|
841
|
-
fromInternalBalance: 'bool',
|
|
842
|
-
recipient: 'address',
|
|
843
|
-
toInternalBalance: 'bool',
|
|
844
|
-
},
|
|
845
|
-
limits: 'int256[]',
|
|
846
|
-
},
|
|
847
|
-
},
|
|
848
|
-
],
|
|
849
|
-
[
|
|
850
|
-
isBatchSwap,
|
|
851
|
-
callerSlots,
|
|
852
|
-
{
|
|
853
|
-
swaps: callDataStruct.swaps,
|
|
854
|
-
assets: callDataStruct.assets,
|
|
855
|
-
funds: callDataStruct.funds,
|
|
856
|
-
limits: callDataStruct.limits.map((_: any) => MAX_INT),
|
|
857
|
-
},
|
|
858
|
-
],
|
|
859
|
-
);
|
|
860
|
-
} else {
|
|
861
|
-
const swapAbi = routerAbi.filter(abi => abi.name === 'swap');
|
|
862
|
-
|
|
863
|
-
const callDataStruct = this.abiCoder.decodeParameters(
|
|
864
|
-
swapAbi[0].inputs!,
|
|
865
|
-
truncatedCalldata,
|
|
866
|
-
);
|
|
867
|
-
|
|
868
|
-
const callerSlot = CALLER_SLOT;
|
|
869
|
-
|
|
870
|
-
payload = this.abiCoder.encodeParameters(
|
|
871
|
-
[
|
|
872
|
-
'bool',
|
|
873
|
-
'uint256',
|
|
874
|
-
{
|
|
875
|
-
ParentStruct: {
|
|
876
|
-
swap: {
|
|
877
|
-
poolId: 'bytes32',
|
|
878
|
-
kind: 'uint8',
|
|
879
|
-
assetIn: 'address',
|
|
880
|
-
assetOut: 'address',
|
|
881
|
-
amount: 'uint256',
|
|
882
|
-
userData: 'bytes',
|
|
883
|
-
},
|
|
884
|
-
funds: {
|
|
885
|
-
sender: 'address',
|
|
886
|
-
fromInternalBalance: 'bool',
|
|
887
|
-
recipient: 'address',
|
|
888
|
-
toInternalBalance: 'bool',
|
|
889
|
-
},
|
|
890
|
-
limit: 'uint256',
|
|
891
|
-
},
|
|
892
|
-
},
|
|
893
|
-
],
|
|
894
|
-
[
|
|
895
|
-
isBatchSwap,
|
|
896
|
-
callerSlot,
|
|
897
|
-
{
|
|
898
|
-
swap: {
|
|
899
|
-
poolId: callDataStruct.singleSwap.poolId,
|
|
900
|
-
kind: callDataStruct.singleSwap.kind,
|
|
901
|
-
assetIn: callDataStruct.singleSwap.assetIn,
|
|
902
|
-
assetOut: callDataStruct.singleSwap.assetOut,
|
|
903
|
-
amount: callDataStruct.singleSwap.amount,
|
|
904
|
-
userData: callDataStruct.singleSwap.userData,
|
|
905
|
-
},
|
|
906
|
-
funds: {
|
|
907
|
-
sender: callDataStruct.funds.sender,
|
|
908
|
-
fromInternalBalance: callDataStruct.funds.fromInternalBalance,
|
|
909
|
-
recipient: callDataStruct.funds.recipient,
|
|
910
|
-
toInternalBalance: callDataStruct.funds.toInternalBalance,
|
|
911
|
-
},
|
|
912
|
-
limit: side === SwapSide.BUY ? MAX_UINT : '0',
|
|
913
|
-
},
|
|
914
|
-
],
|
|
915
|
-
);
|
|
916
|
-
}
|
|
917
|
-
|
|
918
|
-
return {
|
|
919
|
-
targetExchange: router,
|
|
920
|
-
payload: payload,
|
|
921
|
-
networkFee: '0',
|
|
922
|
-
};
|
|
923
|
-
}
|
|
924
|
-
|
|
925
|
-
async getSimpleParam(
|
|
926
|
-
srcToken: string,
|
|
927
|
-
destToken: string,
|
|
928
|
-
srcAmount: string,
|
|
929
|
-
destAmount: string,
|
|
930
|
-
data: SwaapV2Data,
|
|
931
|
-
side: SwapSide,
|
|
932
|
-
): Promise<SimpleExchangeParam> {
|
|
933
|
-
const { router, callData } = data;
|
|
934
|
-
|
|
935
|
-
assert(
|
|
936
|
-
router !== undefined,
|
|
937
|
-
`${this.dexKey}-${this.network}: router undefined`,
|
|
938
|
-
);
|
|
939
|
-
|
|
940
|
-
assert(
|
|
941
|
-
callData !== undefined,
|
|
942
|
-
`${this.dexKey}-${this.network}: callData undefined`,
|
|
943
|
-
);
|
|
944
|
-
|
|
945
|
-
return this.buildSimpleParamWithoutWETHConversion(
|
|
946
|
-
srcToken,
|
|
947
|
-
srcAmount,
|
|
948
|
-
destToken,
|
|
949
|
-
destAmount,
|
|
950
|
-
callData,
|
|
951
|
-
router,
|
|
952
|
-
);
|
|
953
|
-
}
|
|
954
|
-
|
|
955
|
-
computeMaxLiquidity = (
|
|
956
|
-
levels: SwaapV2PriceLevels,
|
|
957
|
-
tokenAddress: string,
|
|
958
|
-
): number => {
|
|
959
|
-
if (tokenAddress === levels.base!) {
|
|
960
|
-
return (
|
|
961
|
-
(levels.asks[levels.asks.length - 1]?.level ?? 0) *
|
|
962
|
-
levels.asks[0]?.price
|
|
963
|
-
);
|
|
964
|
-
}
|
|
965
|
-
return (
|
|
966
|
-
(levels.asks[levels.bids.length - 1]?.level ?? 0) * levels.asks[0]?.price
|
|
967
|
-
);
|
|
968
|
-
};
|
|
969
|
-
|
|
970
|
-
async getTopPoolsForToken(
|
|
971
|
-
tokenAddress: Address,
|
|
972
|
-
limit: number,
|
|
973
|
-
): Promise<PoolLiquidity[]> {
|
|
974
|
-
this.tokensMap = (await this.getCachedTokens()) || {};
|
|
975
|
-
const normalizedTokenAddress = normalizeTokenAddress(tokenAddress);
|
|
976
|
-
|
|
977
|
-
const pLevels = await this.getCachedLevels();
|
|
978
|
-
|
|
979
|
-
if (pLevels === null) {
|
|
980
|
-
return [];
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
return Object.keys(pLevels)
|
|
984
|
-
.filter(async (pair: string) => {
|
|
985
|
-
const { base, quote } = pLevels[pair];
|
|
986
|
-
|
|
987
|
-
const levelDoesNotIncludeToken =
|
|
988
|
-
normalizedTokenAddress !== base && normalizedTokenAddress !== quote;
|
|
989
|
-
if (levelDoesNotIncludeToken) {
|
|
990
|
-
return false;
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
const poolIdentifier: string = getPoolIdentifier(
|
|
994
|
-
this.dexKey,
|
|
995
|
-
base,
|
|
996
|
-
quote,
|
|
997
|
-
);
|
|
998
|
-
const isRestrictedPool = await this.isRestrictedPool(poolIdentifier);
|
|
999
|
-
return !isRestrictedPool;
|
|
1000
|
-
})
|
|
1001
|
-
.map((pair: string) => {
|
|
1002
|
-
return {
|
|
1003
|
-
exchange: this.dexKey,
|
|
1004
|
-
connectorTokens: [
|
|
1005
|
-
this.getTokenFromAddress(
|
|
1006
|
-
normalizedTokenAddress === pLevels[pair].base
|
|
1007
|
-
? pLevels[pair].quote
|
|
1008
|
-
: pLevels[pair].base,
|
|
1009
|
-
),
|
|
1010
|
-
],
|
|
1011
|
-
liquidityUSD: this.computeMaxLiquidity(pLevels[pair], tokenAddress),
|
|
1012
|
-
} as PoolLiquidity;
|
|
1013
|
-
})
|
|
1014
|
-
.sort((pl: PoolLiquidity) => {
|
|
1015
|
-
return -pl.liquidityUSD;
|
|
1016
|
-
})
|
|
1017
|
-
.slice(0, limit);
|
|
1018
|
-
}
|
|
1019
|
-
|
|
1020
|
-
getTokenFromAddress(address: Address): Token {
|
|
1021
|
-
return this.tokensMap[normalizeTokenAddress(address)];
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
releaseResources(): void {
|
|
1025
|
-
if (this.rateFetcher) {
|
|
1026
|
-
this.rateFetcher.stop();
|
|
1027
|
-
}
|
|
1028
|
-
}
|
|
1029
|
-
|
|
1030
|
-
getPriceLevelsReqParams(): SwaapV2APIParameters {
|
|
1031
|
-
return this.getAPIReqParams(SWAAP_RFQ_PRICES_ENDPOINT, 'GET');
|
|
1032
|
-
}
|
|
1033
|
-
|
|
1034
|
-
getQuoteReqParams(): SwaapV2APIParameters {
|
|
1035
|
-
return this.getAPIReqParams(SWAAP_RFQ_QUOTE_ENDPOINT, 'POST');
|
|
1036
|
-
}
|
|
1037
|
-
|
|
1038
|
-
getNotifyReqParams(): SwaapV2APIParameters {
|
|
1039
|
-
return this.getAPIReqParams(SWAAP_NOTIFY_ENDPOINT, 'POST');
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
|
-
getTokensReqParams(): SwaapV2APIParameters {
|
|
1043
|
-
return this.getAPIReqParams(SWAAP_RFQ_TOKENS_ENDPOINT, 'GET');
|
|
1044
|
-
}
|
|
1045
|
-
|
|
1046
|
-
getAPIReqParams(endpoint: string, method: Method): SwaapV2APIParameters {
|
|
1047
|
-
return {
|
|
1048
|
-
url: `${SWAAP_RFQ_API_URL}/${endpoint}`,
|
|
1049
|
-
params: {
|
|
1050
|
-
networkId: this.network,
|
|
1051
|
-
},
|
|
1052
|
-
headers: { 'x-api-key': this.swaapV2AuthToken },
|
|
1053
|
-
method: method,
|
|
1054
|
-
};
|
|
1055
|
-
}
|
|
1056
|
-
}
|
|
1
|
+
import {
|
|
2
|
+
Token,
|
|
3
|
+
Address,
|
|
4
|
+
ExchangePrices,
|
|
5
|
+
PoolPrices,
|
|
6
|
+
AdapterExchangeParam,
|
|
7
|
+
SimpleExchangeParam,
|
|
8
|
+
PoolLiquidity,
|
|
9
|
+
Logger,
|
|
10
|
+
ExchangeTxInfo,
|
|
11
|
+
OptimalSwapExchange,
|
|
12
|
+
PreprocessTransactionOptions,
|
|
13
|
+
DexExchangeParam,
|
|
14
|
+
} from '../../types';
|
|
15
|
+
import {
|
|
16
|
+
SwapSide,
|
|
17
|
+
Network,
|
|
18
|
+
MAX_INT,
|
|
19
|
+
MAX_UINT,
|
|
20
|
+
CACHE_PREFIX,
|
|
21
|
+
} from '../../constants';
|
|
22
|
+
import * as CALLDATA_GAS_COST from '../../calldata-gas-cost';
|
|
23
|
+
import { getDexKeysWithNetwork, isAxiosError } from '../../utils';
|
|
24
|
+
import { IDex } from '../idex';
|
|
25
|
+
import { IDexHelper } from '../../dex-helper/idex-helper';
|
|
26
|
+
import {
|
|
27
|
+
SwaapV2Data,
|
|
28
|
+
SwaapV2PriceLevel,
|
|
29
|
+
SwaapV2PriceLevels,
|
|
30
|
+
SwaapV2APIParameters,
|
|
31
|
+
SwaapV2QuoteError,
|
|
32
|
+
TokensMap,
|
|
33
|
+
SwaapV2NotificationResponse,
|
|
34
|
+
} from './types';
|
|
35
|
+
import { SimpleExchange } from '../simple-exchange';
|
|
36
|
+
import { Adapters, SwaapV2Config } from './config';
|
|
37
|
+
import { RateFetcher } from './rate-fetcher';
|
|
38
|
+
import routerAbi from '../../abi/swaap-v2/vault.json';
|
|
39
|
+
import BigNumber from 'bignumber.js';
|
|
40
|
+
import { BN_0, BN_1, getBigNumberPow } from '../../bignumber-constants';
|
|
41
|
+
import { Interface } from 'ethers/lib/utils';
|
|
42
|
+
import { AsyncOrSync, assert } from 'ts-essentials';
|
|
43
|
+
import {
|
|
44
|
+
SWAAP_RFQ_API_URL,
|
|
45
|
+
SWAAP_RFQ_PRICES_ENDPOINT,
|
|
46
|
+
SWAAP_RFQ_QUOTE_ENDPOINT,
|
|
47
|
+
SWAAP_RFQ_API_PRICES_POLLING_INTERVAL_MS,
|
|
48
|
+
SWAAP_RFQ_PRICES_CACHES_TTL_S,
|
|
49
|
+
STABLE_SWAP_GAS_COST_ESTIMATION,
|
|
50
|
+
VOLATILE_SWAP_GAS_COST_ESTIMATION,
|
|
51
|
+
BATCH_SWAP_SELECTOR,
|
|
52
|
+
CALLER_SLOT,
|
|
53
|
+
SWAAP_403_TTL_S,
|
|
54
|
+
SWAAP_429_TTL_S,
|
|
55
|
+
SWAAP_RFQ_TOKENS_ENDPOINT,
|
|
56
|
+
SWAAP_POOL_RESTRICT_TTL_S,
|
|
57
|
+
SWAAP_RFQ_API_TOKENS_POLLING_INTERVAL_MS,
|
|
58
|
+
SWAAP_RFQ_TOKENS_CACHES_TTL_S,
|
|
59
|
+
SWAAP_PRICES_CACHE_KEY,
|
|
60
|
+
SWAAP_TOKENS_CACHE_KEY,
|
|
61
|
+
SWAAP_ORDER_TYPE_SELL,
|
|
62
|
+
SWAAP_ORDER_TYPE_BUY,
|
|
63
|
+
SWAAP_MIN_SLIPPAGE_FACTOR_THRESHOLD_FOR_RESTRICTION,
|
|
64
|
+
SWAAP_BANNED_CODE,
|
|
65
|
+
SWAAP_NOTIFY_ENDPOINT,
|
|
66
|
+
} from './constants';
|
|
67
|
+
import {
|
|
68
|
+
getPoolIdentifier,
|
|
69
|
+
normalizeTokenAddress,
|
|
70
|
+
getPairName,
|
|
71
|
+
isStablePair,
|
|
72
|
+
} from './utils';
|
|
73
|
+
import { Method } from '../../dex-helper/irequest-wrapper';
|
|
74
|
+
import {
|
|
75
|
+
SlippageCheckError,
|
|
76
|
+
TooStrictSlippageCheckError,
|
|
77
|
+
} from '../generic-rfq/types';
|
|
78
|
+
import { BI_MAX_UINT256 } from '../../bigint-constants';
|
|
79
|
+
import { SpecialDex } from '../../executor/types';
|
|
80
|
+
import { extractReturnAmountPosition } from '../../executor/utils';
|
|
81
|
+
|
|
82
|
+
const BLACKLISTED = 'blacklisted';
|
|
83
|
+
|
|
84
|
+
export class SwaapV2 extends SimpleExchange implements IDex<SwaapV2Data> {
|
|
85
|
+
readonly isStatePollingDex = true;
|
|
86
|
+
readonly hasConstantPriceLargeAmounts = false;
|
|
87
|
+
readonly needWrapNative = false;
|
|
88
|
+
readonly isFeeOnTransferSupported = false;
|
|
89
|
+
private rateFetcher: RateFetcher;
|
|
90
|
+
private swaapV2AuthToken: string;
|
|
91
|
+
private tokensMap: TokensMap = {};
|
|
92
|
+
private runtimeMMsRestrictHashMapKey: string;
|
|
93
|
+
|
|
94
|
+
public static dexKeysWithNetwork: { key: string; networks: Network[] }[] =
|
|
95
|
+
getDexKeysWithNetwork(SwaapV2Config);
|
|
96
|
+
|
|
97
|
+
logger: Logger;
|
|
98
|
+
|
|
99
|
+
constructor(
|
|
100
|
+
readonly network: Network,
|
|
101
|
+
readonly dexKey: string,
|
|
102
|
+
readonly dexHelper: IDexHelper,
|
|
103
|
+
protected adapters = Adapters[network] || {},
|
|
104
|
+
protected routerInterface = new Interface(routerAbi),
|
|
105
|
+
) {
|
|
106
|
+
super(dexHelper, dexKey);
|
|
107
|
+
this.logger = dexHelper.getLogger(dexKey);
|
|
108
|
+
const token = dexHelper.config.data.swaapV2AuthToken;
|
|
109
|
+
assert(
|
|
110
|
+
token !== undefined,
|
|
111
|
+
'SwaapV2 auth token is not specified with env variable',
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
this.swaapV2AuthToken = token;
|
|
115
|
+
|
|
116
|
+
this.rateFetcher = new RateFetcher(
|
|
117
|
+
this.dexHelper,
|
|
118
|
+
this.dexKey,
|
|
119
|
+
this.logger,
|
|
120
|
+
{
|
|
121
|
+
rateConfig: {
|
|
122
|
+
pricesIntervalMs: SWAAP_RFQ_API_PRICES_POLLING_INTERVAL_MS,
|
|
123
|
+
pricesReqParams: this.getPriceLevelsReqParams(),
|
|
124
|
+
pricesCacheTTLSecs: SWAAP_RFQ_PRICES_CACHES_TTL_S,
|
|
125
|
+
pricesCacheKey: SWAAP_PRICES_CACHE_KEY,
|
|
126
|
+
},
|
|
127
|
+
tokensConfig: {
|
|
128
|
+
tokensIntervalMs: SWAAP_RFQ_API_TOKENS_POLLING_INTERVAL_MS,
|
|
129
|
+
tokensReqParams: this.getTokensReqParams(),
|
|
130
|
+
tokensCacheTTLSecs: SWAAP_RFQ_TOKENS_CACHES_TTL_S,
|
|
131
|
+
tokensCacheKey: SWAAP_TOKENS_CACHE_KEY,
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
this.runtimeMMsRestrictHashMapKey =
|
|
137
|
+
`${CACHE_PREFIX}_${this.dexKey}_${this.network}_restricted_mms`.toLowerCase();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async initializePricing(blockNumber: number): Promise<void> {
|
|
141
|
+
if (!this.dexHelper.config.isSlave) {
|
|
142
|
+
await this.rateFetcher.start();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
getAdapters(side: SwapSide): { name: string; index: number }[] | null {
|
|
149
|
+
return this.adapters[side] ? this.adapters[side] : null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async getPoolIdentifiers(
|
|
153
|
+
srcToken: Token,
|
|
154
|
+
destToken: Token,
|
|
155
|
+
side: SwapSide,
|
|
156
|
+
blockNumber: number,
|
|
157
|
+
): Promise<string[]> {
|
|
158
|
+
const normalizedSrcToken = this.normalizeToken(srcToken);
|
|
159
|
+
const normalizedDestToken = this.normalizeToken(destToken);
|
|
160
|
+
|
|
161
|
+
if (normalizedSrcToken.address === normalizedDestToken.address) {
|
|
162
|
+
return [];
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const poolIdentifier = getPoolIdentifier(
|
|
166
|
+
this.dexKey,
|
|
167
|
+
normalizedSrcToken.address,
|
|
168
|
+
normalizedDestToken.address,
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
const levels = await this.getCachedLevels();
|
|
172
|
+
if (levels === null) {
|
|
173
|
+
return [];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return Object.keys(levels)
|
|
177
|
+
.map((pair: string) => {
|
|
178
|
+
return getPoolIdentifier(
|
|
179
|
+
this.dexKey,
|
|
180
|
+
levels[pair].base!,
|
|
181
|
+
levels[pair].quote!,
|
|
182
|
+
);
|
|
183
|
+
})
|
|
184
|
+
.filter((pi: string) => pi === poolIdentifier);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
computePricesFromLevelsBids(
|
|
188
|
+
amounts: BigNumber[],
|
|
189
|
+
asksAndBids: SwaapV2PriceLevels,
|
|
190
|
+
srcToken: Token,
|
|
191
|
+
destToken: Token,
|
|
192
|
+
side: SwapSide,
|
|
193
|
+
): bigint[] {
|
|
194
|
+
let levels: SwaapV2PriceLevel[] = [];
|
|
195
|
+
if (side === SwapSide.BUY) {
|
|
196
|
+
if (destToken.address === asksAndBids.base!) {
|
|
197
|
+
levels = asksAndBids.asks;
|
|
198
|
+
} else {
|
|
199
|
+
levels = this.invertPrices(asksAndBids.bids);
|
|
200
|
+
}
|
|
201
|
+
} else {
|
|
202
|
+
if (srcToken.address === asksAndBids.base!) {
|
|
203
|
+
levels = asksAndBids.bids;
|
|
204
|
+
} else {
|
|
205
|
+
levels = this.invertPrices(asksAndBids.asks);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const firstLevelRaw = levels[0];
|
|
210
|
+
const firstLevelAmountBN = new BigNumber(firstLevelRaw.level);
|
|
211
|
+
|
|
212
|
+
if (amounts[amounts.length - 1].lt(firstLevelAmountBN)) {
|
|
213
|
+
return [];
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return this.computeLevelsQuote(amounts, levels, srcToken, destToken, side);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
inversePrice(priceLevel: SwaapV2PriceLevel): SwaapV2PriceLevel {
|
|
220
|
+
return {
|
|
221
|
+
level: priceLevel.level * priceLevel.price,
|
|
222
|
+
price: 1 / priceLevel.price,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
invertPrices(levels: SwaapV2PriceLevel[]): SwaapV2PriceLevel[] {
|
|
227
|
+
return levels.map(pl => this.inversePrice(pl));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
computeLevelsQuote(
|
|
231
|
+
amounts: BigNumber[],
|
|
232
|
+
levels: SwaapV2PriceLevel[],
|
|
233
|
+
srcToken: Token,
|
|
234
|
+
destToken: Token,
|
|
235
|
+
side: SwapSide,
|
|
236
|
+
): bigint[] {
|
|
237
|
+
const size = levels.length;
|
|
238
|
+
if (size === 0) {
|
|
239
|
+
return amounts.map(_ => BigInt(0));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return amounts.map((amount: BigNumber) => {
|
|
243
|
+
if (amount.isZero()) {
|
|
244
|
+
return BigInt(0);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
let i = 0;
|
|
248
|
+
let output = BN_0;
|
|
249
|
+
let previousLevel = BN_0;
|
|
250
|
+
let remaininig = BigNumber(amount);
|
|
251
|
+
let enoughLiquidity = false;
|
|
252
|
+
while (i < size) {
|
|
253
|
+
const levelDelta = BigNumber(levels[i].level).minus(previousLevel);
|
|
254
|
+
if (levelDelta.lte(remaininig)) {
|
|
255
|
+
output = output.plus(levelDelta.times(levels[i].price));
|
|
256
|
+
remaininig = remaininig.minus(levelDelta);
|
|
257
|
+
previousLevel = BigNumber(levels[i].level);
|
|
258
|
+
i += 1;
|
|
259
|
+
} else {
|
|
260
|
+
output = output.plus(remaininig.times(levels[i].price));
|
|
261
|
+
enoughLiquidity = true;
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (enoughLiquidity) {
|
|
266
|
+
return BigInt(
|
|
267
|
+
output
|
|
268
|
+
.multipliedBy(
|
|
269
|
+
getBigNumberPow(
|
|
270
|
+
side === SwapSide.BUY ? srcToken.decimals : destToken.decimals,
|
|
271
|
+
),
|
|
272
|
+
)
|
|
273
|
+
.toFixed(0),
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
return BigInt(0); // not enough liquidity
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async getCachedTokens(): Promise<TokensMap | null> {
|
|
281
|
+
const cachedTokens = await this.dexHelper.cache.get(
|
|
282
|
+
this.dexKey,
|
|
283
|
+
this.network,
|
|
284
|
+
SWAAP_TOKENS_CACHE_KEY,
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
if (cachedTokens) {
|
|
288
|
+
return JSON.parse(cachedTokens) as TokensMap;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async getCachedLevels(): Promise<Record<string, SwaapV2PriceLevels> | null> {
|
|
295
|
+
const cachedLevels = await this.dexHelper.cache.get(
|
|
296
|
+
this.dexKey,
|
|
297
|
+
this.network,
|
|
298
|
+
SWAAP_PRICES_CACHE_KEY,
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
if (cachedLevels) {
|
|
302
|
+
return JSON.parse(cachedLevels) as Record<string, SwaapV2PriceLevels>;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
normalizeToken(token: Token): Token {
|
|
309
|
+
return {
|
|
310
|
+
address: normalizeTokenAddress(token.address),
|
|
311
|
+
decimals: token.decimals,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
async getPricesVolume(
|
|
316
|
+
srcToken: Token,
|
|
317
|
+
destToken: Token,
|
|
318
|
+
amounts: bigint[],
|
|
319
|
+
side: SwapSide,
|
|
320
|
+
blockNumber: number,
|
|
321
|
+
limitPools?: string[],
|
|
322
|
+
): Promise<null | ExchangePrices<SwaapV2Data>> {
|
|
323
|
+
const normalizedSrcToken = this.normalizeToken(srcToken);
|
|
324
|
+
const normalizedDestToken = this.normalizeToken(destToken);
|
|
325
|
+
|
|
326
|
+
const requestedPoolIdentifier: string = getPoolIdentifier(
|
|
327
|
+
this.dexKey,
|
|
328
|
+
normalizedSrcToken.address,
|
|
329
|
+
normalizedDestToken.address,
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
try {
|
|
333
|
+
if (await this.isRestrictedPool(requestedPoolIdentifier)) {
|
|
334
|
+
return null;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
this.tokensMap = (await this.getCachedTokens()) || {};
|
|
338
|
+
|
|
339
|
+
if (normalizedSrcToken.address === normalizedDestToken.address) {
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const pools = limitPools
|
|
344
|
+
? limitPools.filter(
|
|
345
|
+
p =>
|
|
346
|
+
p ===
|
|
347
|
+
getPoolIdentifier(
|
|
348
|
+
this.dexKey,
|
|
349
|
+
normalizedSrcToken.address,
|
|
350
|
+
normalizedDestToken.address,
|
|
351
|
+
) ||
|
|
352
|
+
p ===
|
|
353
|
+
getPoolIdentifier(
|
|
354
|
+
this.dexKey,
|
|
355
|
+
normalizedDestToken.address,
|
|
356
|
+
normalizedSrcToken.address,
|
|
357
|
+
),
|
|
358
|
+
)
|
|
359
|
+
: await this.getPoolIdentifiers(srcToken, destToken, side, blockNumber);
|
|
360
|
+
|
|
361
|
+
if (pools.length === 0) {
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const levels = await this.getCachedLevels();
|
|
366
|
+
if (levels === null) {
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const levelEntries: SwaapV2PriceLevels[] = Object.keys(levels)
|
|
371
|
+
.filter(pair => {
|
|
372
|
+
const { base, quote } = levels[pair];
|
|
373
|
+
if (
|
|
374
|
+
pools.includes(getPoolIdentifier(this.dexKey, quote, base)) ||
|
|
375
|
+
pools.includes(getPoolIdentifier(this.dexKey, base, quote))
|
|
376
|
+
) {
|
|
377
|
+
return true;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return false;
|
|
381
|
+
})
|
|
382
|
+
.map(pair => {
|
|
383
|
+
return levels[pair];
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
const unitVolume = getBigNumberPow(
|
|
387
|
+
(side === SwapSide.SELL ? normalizedSrcToken : normalizedDestToken)
|
|
388
|
+
.decimals,
|
|
389
|
+
);
|
|
390
|
+
|
|
391
|
+
const amountsFloat = amounts.map(a =>
|
|
392
|
+
new BigNumber(a.toString()).dividedBy(unitVolume),
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
const prices = levelEntries.map((askAndBids: SwaapV2PriceLevels) => {
|
|
396
|
+
const unitPrice: bigint | undefined = this.computePricesFromLevelsBids(
|
|
397
|
+
[unitVolume],
|
|
398
|
+
askAndBids,
|
|
399
|
+
normalizedSrcToken,
|
|
400
|
+
normalizedDestToken,
|
|
401
|
+
side,
|
|
402
|
+
)[0];
|
|
403
|
+
|
|
404
|
+
const prices = this.computePricesFromLevelsBids(
|
|
405
|
+
amountsFloat,
|
|
406
|
+
askAndBids,
|
|
407
|
+
normalizedSrcToken,
|
|
408
|
+
normalizedDestToken,
|
|
409
|
+
side,
|
|
410
|
+
);
|
|
411
|
+
|
|
412
|
+
if (!prices.length) {
|
|
413
|
+
return null;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const gasCost = isStablePair(
|
|
417
|
+
this.network,
|
|
418
|
+
normalizedSrcToken.address,
|
|
419
|
+
normalizedDestToken.address,
|
|
420
|
+
)
|
|
421
|
+
? STABLE_SWAP_GAS_COST_ESTIMATION
|
|
422
|
+
: VOLATILE_SWAP_GAS_COST_ESTIMATION;
|
|
423
|
+
|
|
424
|
+
return {
|
|
425
|
+
gasCost: gasCost,
|
|
426
|
+
exchange: this.dexKey,
|
|
427
|
+
data: {},
|
|
428
|
+
prices,
|
|
429
|
+
unit: unitPrice === undefined ? 0n : unitPrice,
|
|
430
|
+
poolIdentifier: requestedPoolIdentifier,
|
|
431
|
+
} as PoolPrices<SwaapV2Data>;
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
return prices.filter((p): p is PoolPrices<SwaapV2Data> => !!p);
|
|
435
|
+
} catch (e) {
|
|
436
|
+
this.logger.error(
|
|
437
|
+
`Error_getPrices ${srcToken}, ${destToken}, ${side}:`,
|
|
438
|
+
e,
|
|
439
|
+
);
|
|
440
|
+
return null;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
getBaseToken(poolIdentifier: string): string {
|
|
445
|
+
return poolIdentifier.split('_')[1];
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
getDexParam(
|
|
449
|
+
srcToken: string,
|
|
450
|
+
destToken: string,
|
|
451
|
+
srcAmount: string,
|
|
452
|
+
destAmount: string,
|
|
453
|
+
recipient: string,
|
|
454
|
+
data: SwaapV2Data,
|
|
455
|
+
side: SwapSide,
|
|
456
|
+
): AsyncOrSync<DexExchangeParam> {
|
|
457
|
+
const { router, callData } = data;
|
|
458
|
+
const isBatchSwap = callData.slice(0, 10) === BATCH_SWAP_SELECTOR;
|
|
459
|
+
|
|
460
|
+
// at the moment of writing, batch swap is not supported by SwappV2 API
|
|
461
|
+
assert(isBatchSwap !== true, 'Batch swap is not supported');
|
|
462
|
+
|
|
463
|
+
assert(
|
|
464
|
+
router !== undefined,
|
|
465
|
+
`${this.dexKey}-${this.network}: router undefined`,
|
|
466
|
+
);
|
|
467
|
+
|
|
468
|
+
assert(
|
|
469
|
+
callData !== undefined,
|
|
470
|
+
`${this.dexKey}-${this.network}: callData undefined`,
|
|
471
|
+
);
|
|
472
|
+
|
|
473
|
+
return {
|
|
474
|
+
needWrapNative: this.needWrapNative,
|
|
475
|
+
specialDexSupportsInsertFromAmount: true,
|
|
476
|
+
dexFuncHasRecipient: true,
|
|
477
|
+
exchangeData: callData,
|
|
478
|
+
specialDexFlag: SpecialDex.SWAP_ON_SWAAP_V2_SINGLE,
|
|
479
|
+
targetExchange: router,
|
|
480
|
+
returnAmountPos:
|
|
481
|
+
side === SwapSide.SELL
|
|
482
|
+
? extractReturnAmountPosition(
|
|
483
|
+
this.routerInterface,
|
|
484
|
+
'swap',
|
|
485
|
+
'amountCalculated',
|
|
486
|
+
)
|
|
487
|
+
: undefined,
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
async preProcessTransaction(
|
|
492
|
+
optimalSwapExchange: OptimalSwapExchange<SwaapV2Data>,
|
|
493
|
+
srcToken: Token,
|
|
494
|
+
destToken: Token,
|
|
495
|
+
side: SwapSide,
|
|
496
|
+
options: PreprocessTransactionOptions,
|
|
497
|
+
): Promise<[OptimalSwapExchange<SwaapV2Data>, ExchangeTxInfo]> {
|
|
498
|
+
if (await this.isBlacklisted(options.txOrigin)) {
|
|
499
|
+
this.logger.warn(
|
|
500
|
+
`${this.dexKey}-${this.network}: blacklisted TX Origin address '${options.txOrigin}' trying to build a transaction. Bailing...`,
|
|
501
|
+
);
|
|
502
|
+
throw new Error(
|
|
503
|
+
`${this.dexKey}-${
|
|
504
|
+
this.network
|
|
505
|
+
}: user=${options.txOrigin.toLowerCase()} is blacklisted`,
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const isSell = side === SwapSide.SELL;
|
|
510
|
+
|
|
511
|
+
const normalizedSrcToken = this.normalizeToken(srcToken);
|
|
512
|
+
const normalizedDestToken = this.normalizeToken(destToken);
|
|
513
|
+
|
|
514
|
+
const tolerance = (
|
|
515
|
+
options.slippageFactor > BN_1
|
|
516
|
+
? options.slippageFactor.minus(BN_1)
|
|
517
|
+
: BN_1.minus(options.slippageFactor)
|
|
518
|
+
).toNumber();
|
|
519
|
+
|
|
520
|
+
try {
|
|
521
|
+
const quote = await this.rateFetcher.getQuote(
|
|
522
|
+
this.network,
|
|
523
|
+
normalizedSrcToken,
|
|
524
|
+
normalizedDestToken,
|
|
525
|
+
isSell ? optimalSwapExchange.srcAmount : optimalSwapExchange.destAmount,
|
|
526
|
+
isSell ? SWAAP_ORDER_TYPE_SELL : SWAAP_ORDER_TYPE_BUY,
|
|
527
|
+
options.txOrigin,
|
|
528
|
+
options.executionContractAddress,
|
|
529
|
+
options.recipient,
|
|
530
|
+
tolerance,
|
|
531
|
+
this.getQuoteReqParams(),
|
|
532
|
+
);
|
|
533
|
+
|
|
534
|
+
if (!quote.success) {
|
|
535
|
+
const message = `${this.dexKey}-${
|
|
536
|
+
this.network
|
|
537
|
+
}: Failed to fetch RFQ for ${getPairName(
|
|
538
|
+
normalizedSrcToken.address,
|
|
539
|
+
normalizedDestToken.address,
|
|
540
|
+
)}: ${JSON.stringify(quote)}`;
|
|
541
|
+
this.logger.warn(message);
|
|
542
|
+
throw new SwaapV2QuoteError(message);
|
|
543
|
+
} else if (!quote.calldata) {
|
|
544
|
+
const message = `${this.dexKey}-${
|
|
545
|
+
this.network
|
|
546
|
+
}: Failed to fetch RFQ for ${getPairName(
|
|
547
|
+
normalizedSrcToken.address,
|
|
548
|
+
normalizedDestToken.address,
|
|
549
|
+
)}. Missing quote data`;
|
|
550
|
+
this.logger.warn(message);
|
|
551
|
+
throw new SwaapV2QuoteError(message);
|
|
552
|
+
} else if (!quote.router) {
|
|
553
|
+
const message = `${this.dexKey}-${
|
|
554
|
+
this.network
|
|
555
|
+
}: Failed to fetch RFQ for ${getPairName(
|
|
556
|
+
normalizedSrcToken.address,
|
|
557
|
+
normalizedDestToken.address,
|
|
558
|
+
)}. Missing router address`;
|
|
559
|
+
this.logger.warn(message);
|
|
560
|
+
throw new SwaapV2QuoteError(message);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const srcAmount = BigInt(optimalSwapExchange.srcAmount);
|
|
564
|
+
const destAmount = BigInt(optimalSwapExchange.destAmount);
|
|
565
|
+
const quoteTokenAmount = BigInt(quote.amount);
|
|
566
|
+
const slippageFactor = options.slippageFactor;
|
|
567
|
+
|
|
568
|
+
let isFailOnSlippage = false;
|
|
569
|
+
let slippageErrorMessage = '';
|
|
570
|
+
|
|
571
|
+
if (side === SwapSide.SELL) {
|
|
572
|
+
if (
|
|
573
|
+
quoteTokenAmount <
|
|
574
|
+
BigInt(
|
|
575
|
+
new BigNumber(destAmount.toString())
|
|
576
|
+
.times(slippageFactor)
|
|
577
|
+
.toFixed(0),
|
|
578
|
+
)
|
|
579
|
+
) {
|
|
580
|
+
isFailOnSlippage = true;
|
|
581
|
+
const message = `${this.dexKey}-${this.network}: too much slippage on quote ${side} quoteTokenAmount ${quoteTokenAmount} / destAmount ${destAmount} < ${slippageFactor}`;
|
|
582
|
+
slippageErrorMessage = message;
|
|
583
|
+
this.logger.warn(message);
|
|
584
|
+
}
|
|
585
|
+
} else {
|
|
586
|
+
if (
|
|
587
|
+
quoteTokenAmount >
|
|
588
|
+
BigInt(slippageFactor.times(srcAmount.toString()).toFixed(0))
|
|
589
|
+
) {
|
|
590
|
+
isFailOnSlippage = true;
|
|
591
|
+
const message = `${this.dexKey}-${
|
|
592
|
+
this.network
|
|
593
|
+
}: too much slippage on quote ${side} baseTokenAmount ${srcAmount} / srcAmount ${srcAmount} > ${slippageFactor.toFixed()}`;
|
|
594
|
+
slippageErrorMessage = message;
|
|
595
|
+
this.logger.warn(message);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
let isTooStrictSlippage = false;
|
|
600
|
+
if (
|
|
601
|
+
isFailOnSlippage &&
|
|
602
|
+
side === SwapSide.SELL &&
|
|
603
|
+
new BigNumber(1)
|
|
604
|
+
.minus(slippageFactor)
|
|
605
|
+
.lt(SWAAP_MIN_SLIPPAGE_FACTOR_THRESHOLD_FOR_RESTRICTION)
|
|
606
|
+
) {
|
|
607
|
+
isTooStrictSlippage = true;
|
|
608
|
+
} else if (
|
|
609
|
+
isFailOnSlippage &&
|
|
610
|
+
side === SwapSide.BUY &&
|
|
611
|
+
slippageFactor
|
|
612
|
+
.minus(1)
|
|
613
|
+
.lt(SWAAP_MIN_SLIPPAGE_FACTOR_THRESHOLD_FOR_RESTRICTION)
|
|
614
|
+
) {
|
|
615
|
+
isTooStrictSlippage = true;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
if (isFailOnSlippage && isTooStrictSlippage) {
|
|
619
|
+
throw new TooStrictSlippageCheckError(slippageErrorMessage);
|
|
620
|
+
} else if (isFailOnSlippage && !isTooStrictSlippage) {
|
|
621
|
+
throw new SlippageCheckError(slippageErrorMessage);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
const expiryAsBigInt = BigInt(quote.expiration);
|
|
625
|
+
const minDeadline = expiryAsBigInt > 0 ? expiryAsBigInt : BI_MAX_UINT256;
|
|
626
|
+
|
|
627
|
+
return [
|
|
628
|
+
{
|
|
629
|
+
...optimalSwapExchange,
|
|
630
|
+
data: {
|
|
631
|
+
router: quote.router,
|
|
632
|
+
callData: quote.calldata,
|
|
633
|
+
},
|
|
634
|
+
},
|
|
635
|
+
{ deadline: minDeadline },
|
|
636
|
+
];
|
|
637
|
+
} catch (e) {
|
|
638
|
+
if (isAxiosError(e) && e.response?.status === 403) {
|
|
639
|
+
await this.setBlacklist(options.txOrigin, SWAAP_403_TTL_S);
|
|
640
|
+
this.logger.warn(
|
|
641
|
+
`${this.dexKey}-${this.network}: Encountered blacklisted user=${options.txOrigin}. Adding to local blacklist cache`,
|
|
642
|
+
);
|
|
643
|
+
} else if (isAxiosError(e) && e.response?.status === 429) {
|
|
644
|
+
await this.setBlacklist(options.txOrigin, SWAAP_429_TTL_S);
|
|
645
|
+
this.logger.warn(
|
|
646
|
+
`${this.dexKey}-${this.network}: Encountered restricted user=${options.txOrigin}. Adding to local blacklist cache`,
|
|
647
|
+
);
|
|
648
|
+
} else {
|
|
649
|
+
if (e instanceof TooStrictSlippageCheckError) {
|
|
650
|
+
this.logger.warn(
|
|
651
|
+
`${this.dexKey}-${this.network}: failed to build transaction on side ${side} with too strict slippage. Skipping restriction`,
|
|
652
|
+
);
|
|
653
|
+
} else {
|
|
654
|
+
this.logger.warn(
|
|
655
|
+
`${this.dexKey}-${this.network}: protocol is restricted`,
|
|
656
|
+
);
|
|
657
|
+
const poolIdentifier = getPoolIdentifier(
|
|
658
|
+
this.dexKey,
|
|
659
|
+
normalizedSrcToken.address,
|
|
660
|
+
normalizedDestToken.address,
|
|
661
|
+
);
|
|
662
|
+
var message = 'Unknown error';
|
|
663
|
+
if (e instanceof Error) {
|
|
664
|
+
message = `${e.name}: ${e.message}`;
|
|
665
|
+
}
|
|
666
|
+
await this.restrictPool(message, poolIdentifier);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
throw e;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
async restrictPool(message: string, poolIdentifier: string): Promise<void> {
|
|
675
|
+
this.logger.warn(
|
|
676
|
+
`${this.dexKey}-${this.network}: ${poolIdentifier} was restricted for ${SWAAP_POOL_RESTRICT_TTL_S} sec. due to fails`,
|
|
677
|
+
);
|
|
678
|
+
|
|
679
|
+
// We use timestamp for creation date to later discern if it already expired or not
|
|
680
|
+
await this.dexHelper.cache.hset(
|
|
681
|
+
this.runtimeMMsRestrictHashMapKey,
|
|
682
|
+
poolIdentifier,
|
|
683
|
+
Date.now().toString(),
|
|
684
|
+
);
|
|
685
|
+
|
|
686
|
+
this.rateFetcher
|
|
687
|
+
.notify(SWAAP_BANNED_CODE, message, this.getNotifyReqParams())
|
|
688
|
+
.then((answer: SwaapV2NotificationResponse) => {
|
|
689
|
+
if (answer.success) {
|
|
690
|
+
this.logger.info(`Successfully notified Swaap API`);
|
|
691
|
+
} else {
|
|
692
|
+
this.logger.error(`Swaap API was not successfully notified`);
|
|
693
|
+
}
|
|
694
|
+
})
|
|
695
|
+
.catch(e => {
|
|
696
|
+
// Must never happen
|
|
697
|
+
this.logger.error(`Swaap API notification failed: ${e}`);
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
async isRestrictedPool(poolIdentifier: string): Promise<boolean> {
|
|
702
|
+
const expirationThreshold = Date.now() - SWAAP_POOL_RESTRICT_TTL_S * 1000;
|
|
703
|
+
const createdAt = await this.dexHelper.cache.hget(
|
|
704
|
+
this.runtimeMMsRestrictHashMapKey,
|
|
705
|
+
poolIdentifier,
|
|
706
|
+
);
|
|
707
|
+
const wasNotRestricted = createdAt === null;
|
|
708
|
+
if (wasNotRestricted) {
|
|
709
|
+
return false;
|
|
710
|
+
}
|
|
711
|
+
const restrictionExpired = +createdAt < expirationThreshold;
|
|
712
|
+
return !restrictionExpired;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
async isBlacklisted(txOrigin: Address): Promise<boolean> {
|
|
716
|
+
const result = await this.dexHelper.cache.get(
|
|
717
|
+
this.dexKey,
|
|
718
|
+
this.network,
|
|
719
|
+
this.getBlackListKey(txOrigin),
|
|
720
|
+
);
|
|
721
|
+
return result === BLACKLISTED;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
getBlackListKey(address: Address) {
|
|
725
|
+
return `blacklist_${address}`.toLowerCase();
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
async setBlacklist(
|
|
729
|
+
txOrigin: Address,
|
|
730
|
+
ttl: number = SWAAP_403_TTL_S,
|
|
731
|
+
): Promise<boolean> {
|
|
732
|
+
await this.dexHelper.cache.setex(
|
|
733
|
+
this.dexKey,
|
|
734
|
+
this.network,
|
|
735
|
+
this.getBlackListKey(txOrigin),
|
|
736
|
+
ttl,
|
|
737
|
+
BLACKLISTED,
|
|
738
|
+
);
|
|
739
|
+
return true;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// Returns estimated gas cost of calldata for this DEX in multiSwap
|
|
743
|
+
getCalldataGasCost(poolPrices: PoolPrices<SwaapV2Data>): number | number[] {
|
|
744
|
+
return (
|
|
745
|
+
CALLDATA_GAS_COST.DEX_OVERHEAD +
|
|
746
|
+
CALLDATA_GAS_COST.LENGTH_LARGE +
|
|
747
|
+
// ParentStruct header
|
|
748
|
+
CALLDATA_GAS_COST.OFFSET_SMALL +
|
|
749
|
+
// ParentStruct -> swaps[] header
|
|
750
|
+
CALLDATA_GAS_COST.OFFSET_LARGE +
|
|
751
|
+
// ParentStruct -> assets[] header
|
|
752
|
+
CALLDATA_GAS_COST.OFFSET_LARGE +
|
|
753
|
+
// ParentStruct -> funds
|
|
754
|
+
CALLDATA_GAS_COST.ADDRESS +
|
|
755
|
+
CALLDATA_GAS_COST.BOOL +
|
|
756
|
+
CALLDATA_GAS_COST.ADDRESS +
|
|
757
|
+
CALLDATA_GAS_COST.BOOL +
|
|
758
|
+
// ParentStruct -> limits[] header
|
|
759
|
+
CALLDATA_GAS_COST.OFFSET_LARGE +
|
|
760
|
+
// ParentStruct -> deadline
|
|
761
|
+
CALLDATA_GAS_COST.TIMESTAMP +
|
|
762
|
+
// ParentStruct -> swaps[]
|
|
763
|
+
CALLDATA_GAS_COST.LENGTH_SMALL +
|
|
764
|
+
// ParentStruct -> swaps[0] header
|
|
765
|
+
CALLDATA_GAS_COST.OFFSET_SMALL +
|
|
766
|
+
// ParentStruct -> swaps[0] -> poolId
|
|
767
|
+
CALLDATA_GAS_COST.FULL_WORD +
|
|
768
|
+
// ParentStruct -> swaps[0] -> assetInIndex
|
|
769
|
+
CALLDATA_GAS_COST.INDEX +
|
|
770
|
+
// ParentStruct -> swaps[0] -> assetOutIndex
|
|
771
|
+
CALLDATA_GAS_COST.INDEX +
|
|
772
|
+
// ParentStruct -> swaps[0] -> amount
|
|
773
|
+
CALLDATA_GAS_COST.AMOUNT +
|
|
774
|
+
// ParentStruct -> swaps[0] -> userData header
|
|
775
|
+
CALLDATA_GAS_COST.OFFSET_SMALL +
|
|
776
|
+
// ParentStruct -> swaps[0] -> userData
|
|
777
|
+
CALLDATA_GAS_COST.ZERO +
|
|
778
|
+
// ParentStruct -> assets[]
|
|
779
|
+
CALLDATA_GAS_COST.LENGTH_SMALL +
|
|
780
|
+
// ParentStruct -> assets[0:2]
|
|
781
|
+
CALLDATA_GAS_COST.ADDRESS * 2 +
|
|
782
|
+
// ParentStruct -> limits[]
|
|
783
|
+
CALLDATA_GAS_COST.LENGTH_SMALL +
|
|
784
|
+
// ParentStruct -> limits[0:2]
|
|
785
|
+
CALLDATA_GAS_COST.FULL_WORD * 2 +
|
|
786
|
+
// Bool -> IsBatchSwap
|
|
787
|
+
CALLDATA_GAS_COST.BOOL
|
|
788
|
+
);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
getAdapterParam(
|
|
792
|
+
srcToken: string,
|
|
793
|
+
destToken: string,
|
|
794
|
+
srcAmount: string,
|
|
795
|
+
destAmount: string,
|
|
796
|
+
data: SwaapV2Data,
|
|
797
|
+
side: SwapSide,
|
|
798
|
+
): AdapterExchangeParam {
|
|
799
|
+
const { router, callData } = data;
|
|
800
|
+
|
|
801
|
+
assert(
|
|
802
|
+
router !== undefined,
|
|
803
|
+
`${this.dexKey}-${this.network}: router undefined`,
|
|
804
|
+
);
|
|
805
|
+
|
|
806
|
+
assert(
|
|
807
|
+
callData !== undefined,
|
|
808
|
+
`${this.dexKey}-${this.network}: callData undefined`,
|
|
809
|
+
);
|
|
810
|
+
|
|
811
|
+
let payload: string;
|
|
812
|
+
const truncatedCalldata = '0x' + callData.slice(10); // droping the function selector
|
|
813
|
+
const isBatchSwap = callData.slice(0, 10) === BATCH_SWAP_SELECTOR;
|
|
814
|
+
|
|
815
|
+
if (isBatchSwap) {
|
|
816
|
+
const batchswapAbi = routerAbi.filter(abi => abi.name === 'batchSwap');
|
|
817
|
+
|
|
818
|
+
const callDataStruct = this.abiCoder.decodeParameters(
|
|
819
|
+
batchswapAbi[0].inputs!,
|
|
820
|
+
truncatedCalldata,
|
|
821
|
+
);
|
|
822
|
+
|
|
823
|
+
const callerSlots = Array(callDataStruct.swaps.length).fill(160);
|
|
824
|
+
|
|
825
|
+
payload = this.abiCoder.encodeParameters(
|
|
826
|
+
[
|
|
827
|
+
'bool',
|
|
828
|
+
'uint256[]',
|
|
829
|
+
{
|
|
830
|
+
ParentStruct: {
|
|
831
|
+
'swaps[]': {
|
|
832
|
+
poolId: 'bytes32',
|
|
833
|
+
assetInIndex: 'uint256',
|
|
834
|
+
assetOutIndex: 'uint256',
|
|
835
|
+
amount: 'uint256',
|
|
836
|
+
userData: 'bytes',
|
|
837
|
+
},
|
|
838
|
+
assets: 'address[]',
|
|
839
|
+
funds: {
|
|
840
|
+
sender: 'address',
|
|
841
|
+
fromInternalBalance: 'bool',
|
|
842
|
+
recipient: 'address',
|
|
843
|
+
toInternalBalance: 'bool',
|
|
844
|
+
},
|
|
845
|
+
limits: 'int256[]',
|
|
846
|
+
},
|
|
847
|
+
},
|
|
848
|
+
],
|
|
849
|
+
[
|
|
850
|
+
isBatchSwap,
|
|
851
|
+
callerSlots,
|
|
852
|
+
{
|
|
853
|
+
swaps: callDataStruct.swaps,
|
|
854
|
+
assets: callDataStruct.assets,
|
|
855
|
+
funds: callDataStruct.funds,
|
|
856
|
+
limits: callDataStruct.limits.map((_: any) => MAX_INT),
|
|
857
|
+
},
|
|
858
|
+
],
|
|
859
|
+
);
|
|
860
|
+
} else {
|
|
861
|
+
const swapAbi = routerAbi.filter(abi => abi.name === 'swap');
|
|
862
|
+
|
|
863
|
+
const callDataStruct = this.abiCoder.decodeParameters(
|
|
864
|
+
swapAbi[0].inputs!,
|
|
865
|
+
truncatedCalldata,
|
|
866
|
+
);
|
|
867
|
+
|
|
868
|
+
const callerSlot = CALLER_SLOT;
|
|
869
|
+
|
|
870
|
+
payload = this.abiCoder.encodeParameters(
|
|
871
|
+
[
|
|
872
|
+
'bool',
|
|
873
|
+
'uint256',
|
|
874
|
+
{
|
|
875
|
+
ParentStruct: {
|
|
876
|
+
swap: {
|
|
877
|
+
poolId: 'bytes32',
|
|
878
|
+
kind: 'uint8',
|
|
879
|
+
assetIn: 'address',
|
|
880
|
+
assetOut: 'address',
|
|
881
|
+
amount: 'uint256',
|
|
882
|
+
userData: 'bytes',
|
|
883
|
+
},
|
|
884
|
+
funds: {
|
|
885
|
+
sender: 'address',
|
|
886
|
+
fromInternalBalance: 'bool',
|
|
887
|
+
recipient: 'address',
|
|
888
|
+
toInternalBalance: 'bool',
|
|
889
|
+
},
|
|
890
|
+
limit: 'uint256',
|
|
891
|
+
},
|
|
892
|
+
},
|
|
893
|
+
],
|
|
894
|
+
[
|
|
895
|
+
isBatchSwap,
|
|
896
|
+
callerSlot,
|
|
897
|
+
{
|
|
898
|
+
swap: {
|
|
899
|
+
poolId: callDataStruct.singleSwap.poolId,
|
|
900
|
+
kind: callDataStruct.singleSwap.kind,
|
|
901
|
+
assetIn: callDataStruct.singleSwap.assetIn,
|
|
902
|
+
assetOut: callDataStruct.singleSwap.assetOut,
|
|
903
|
+
amount: callDataStruct.singleSwap.amount,
|
|
904
|
+
userData: callDataStruct.singleSwap.userData,
|
|
905
|
+
},
|
|
906
|
+
funds: {
|
|
907
|
+
sender: callDataStruct.funds.sender,
|
|
908
|
+
fromInternalBalance: callDataStruct.funds.fromInternalBalance,
|
|
909
|
+
recipient: callDataStruct.funds.recipient,
|
|
910
|
+
toInternalBalance: callDataStruct.funds.toInternalBalance,
|
|
911
|
+
},
|
|
912
|
+
limit: side === SwapSide.BUY ? MAX_UINT : '0',
|
|
913
|
+
},
|
|
914
|
+
],
|
|
915
|
+
);
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
return {
|
|
919
|
+
targetExchange: router,
|
|
920
|
+
payload: payload,
|
|
921
|
+
networkFee: '0',
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
async getSimpleParam(
|
|
926
|
+
srcToken: string,
|
|
927
|
+
destToken: string,
|
|
928
|
+
srcAmount: string,
|
|
929
|
+
destAmount: string,
|
|
930
|
+
data: SwaapV2Data,
|
|
931
|
+
side: SwapSide,
|
|
932
|
+
): Promise<SimpleExchangeParam> {
|
|
933
|
+
const { router, callData } = data;
|
|
934
|
+
|
|
935
|
+
assert(
|
|
936
|
+
router !== undefined,
|
|
937
|
+
`${this.dexKey}-${this.network}: router undefined`,
|
|
938
|
+
);
|
|
939
|
+
|
|
940
|
+
assert(
|
|
941
|
+
callData !== undefined,
|
|
942
|
+
`${this.dexKey}-${this.network}: callData undefined`,
|
|
943
|
+
);
|
|
944
|
+
|
|
945
|
+
return this.buildSimpleParamWithoutWETHConversion(
|
|
946
|
+
srcToken,
|
|
947
|
+
srcAmount,
|
|
948
|
+
destToken,
|
|
949
|
+
destAmount,
|
|
950
|
+
callData,
|
|
951
|
+
router,
|
|
952
|
+
);
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
computeMaxLiquidity = (
|
|
956
|
+
levels: SwaapV2PriceLevels,
|
|
957
|
+
tokenAddress: string,
|
|
958
|
+
): number => {
|
|
959
|
+
if (tokenAddress === levels.base!) {
|
|
960
|
+
return (
|
|
961
|
+
(levels.asks[levels.asks.length - 1]?.level ?? 0) *
|
|
962
|
+
levels.asks[0]?.price
|
|
963
|
+
);
|
|
964
|
+
}
|
|
965
|
+
return (
|
|
966
|
+
(levels.asks[levels.bids.length - 1]?.level ?? 0) * levels.asks[0]?.price
|
|
967
|
+
);
|
|
968
|
+
};
|
|
969
|
+
|
|
970
|
+
async getTopPoolsForToken(
|
|
971
|
+
tokenAddress: Address,
|
|
972
|
+
limit: number,
|
|
973
|
+
): Promise<PoolLiquidity[]> {
|
|
974
|
+
this.tokensMap = (await this.getCachedTokens()) || {};
|
|
975
|
+
const normalizedTokenAddress = normalizeTokenAddress(tokenAddress);
|
|
976
|
+
|
|
977
|
+
const pLevels = await this.getCachedLevels();
|
|
978
|
+
|
|
979
|
+
if (pLevels === null) {
|
|
980
|
+
return [];
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
return Object.keys(pLevels)
|
|
984
|
+
.filter(async (pair: string) => {
|
|
985
|
+
const { base, quote } = pLevels[pair];
|
|
986
|
+
|
|
987
|
+
const levelDoesNotIncludeToken =
|
|
988
|
+
normalizedTokenAddress !== base && normalizedTokenAddress !== quote;
|
|
989
|
+
if (levelDoesNotIncludeToken) {
|
|
990
|
+
return false;
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
const poolIdentifier: string = getPoolIdentifier(
|
|
994
|
+
this.dexKey,
|
|
995
|
+
base,
|
|
996
|
+
quote,
|
|
997
|
+
);
|
|
998
|
+
const isRestrictedPool = await this.isRestrictedPool(poolIdentifier);
|
|
999
|
+
return !isRestrictedPool;
|
|
1000
|
+
})
|
|
1001
|
+
.map((pair: string) => {
|
|
1002
|
+
return {
|
|
1003
|
+
exchange: this.dexKey,
|
|
1004
|
+
connectorTokens: [
|
|
1005
|
+
this.getTokenFromAddress(
|
|
1006
|
+
normalizedTokenAddress === pLevels[pair].base
|
|
1007
|
+
? pLevels[pair].quote
|
|
1008
|
+
: pLevels[pair].base,
|
|
1009
|
+
),
|
|
1010
|
+
],
|
|
1011
|
+
liquidityUSD: this.computeMaxLiquidity(pLevels[pair], tokenAddress),
|
|
1012
|
+
} as PoolLiquidity;
|
|
1013
|
+
})
|
|
1014
|
+
.sort((pl: PoolLiquidity) => {
|
|
1015
|
+
return -pl.liquidityUSD;
|
|
1016
|
+
})
|
|
1017
|
+
.slice(0, limit);
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
getTokenFromAddress(address: Address): Token {
|
|
1021
|
+
return this.tokensMap[normalizeTokenAddress(address)];
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
releaseResources(): void {
|
|
1025
|
+
if (this.rateFetcher) {
|
|
1026
|
+
this.rateFetcher.stop();
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
getPriceLevelsReqParams(): SwaapV2APIParameters {
|
|
1031
|
+
return this.getAPIReqParams(SWAAP_RFQ_PRICES_ENDPOINT, 'GET');
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
getQuoteReqParams(): SwaapV2APIParameters {
|
|
1035
|
+
return this.getAPIReqParams(SWAAP_RFQ_QUOTE_ENDPOINT, 'POST');
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
getNotifyReqParams(): SwaapV2APIParameters {
|
|
1039
|
+
return this.getAPIReqParams(SWAAP_NOTIFY_ENDPOINT, 'POST');
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
getTokensReqParams(): SwaapV2APIParameters {
|
|
1043
|
+
return this.getAPIReqParams(SWAAP_RFQ_TOKENS_ENDPOINT, 'GET');
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
getAPIReqParams(endpoint: string, method: Method): SwaapV2APIParameters {
|
|
1047
|
+
return {
|
|
1048
|
+
url: `${SWAAP_RFQ_API_URL}/${endpoint}`,
|
|
1049
|
+
params: {
|
|
1050
|
+
networkId: this.network,
|
|
1051
|
+
},
|
|
1052
|
+
headers: { 'x-api-key': this.swaapV2AuthToken },
|
|
1053
|
+
method: method,
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
}
|