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