/
price-module.ts
353 lines (322 loc) · 12.9 KB
/
price-module.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
import { Address } from "@coral-xyz/anchor";
import { AddressUtil } from "@orca-so/common-sdk";
import { PublicKey } from "@solana/web3.js";
import {
DecimalsMap,
PoolMap,
PriceCalculationData,
PriceMap,
TickArrayMap,
defaultGetPricesConfig,
defaultGetPricesThresholdConfig,
} from ".";
import {
IGNORE_CACHE,
PREFER_CACHE,
WhirlpoolAccountFetchOptions,
WhirlpoolAccountFetcherInterface,
} from "../network/public/fetcher";
import { PDAUtil, PoolUtil, SwapUtils } from "../utils/public";
import { convertListToMap, filterNullObjects } from "../utils/txn-utils";
import { calculatePricesForQuoteToken, convertAmount, isSubset } from "./calculate-pool-prices";
/**
* PriceModule is a static class that provides functions for fetching and calculating
* token prices for a set of pools or mints.
*
* @category PriceModule
*/
export class PriceModule {
/**
* Fetches and calculates the prices for a set of tokens.
* This method will derive the pools that need to be queried from the mints and is not performant.
*
* @param fetcher {@link WhirlpoolAccountFetcherInterface}
* @param mints The mints to fetch prices for.
* @param config The configuration for the price calculation.
* @param thresholdConfig - The threshold configuration for the price calculation.
* @param opts an {@link WhirlpoolAccountFetchOptions} object to define fetch and cache options when accessing on-chain accounts
* @param availableData - Data that is already available to avoid redundant fetches.
* @returns A map of token addresses to prices.
*/
static async fetchTokenPricesByMints(
fetcher: WhirlpoolAccountFetcherInterface,
mints: Address[],
config = defaultGetPricesConfig,
thresholdConfig = defaultGetPricesThresholdConfig,
opts = IGNORE_CACHE,
availableData: Partial<PriceCalculationData> = {}
): Promise<PriceMap> {
const poolMap = availableData?.poolMap
? availableData?.poolMap
: await PriceModuleUtils.fetchPoolDataFromMints(fetcher, mints, config, opts);
const tickArrayMap = availableData?.tickArrayMap
? availableData.tickArrayMap
: await PriceModuleUtils.fetchTickArraysForPools(fetcher, poolMap, config, opts);
const decimalsMap = availableData?.decimalsMap
? availableData.decimalsMap
: await PriceModuleUtils.fetchDecimalsForMints(fetcher, mints, PREFER_CACHE);
return PriceModule.calculateTokenPrices(
mints,
{
poolMap,
tickArrayMap,
decimalsMap,
},
config,
thresholdConfig
);
}
/**
* Fetches and calculates the token prices from a set of pools.
*
* @param fetcher {@link WhirlpoolAccountFetcherInterface}
* @param pools The pools to fetch prices for.
* @param config The configuration for the price calculation.
* @param thresholdConfig The threshold configuration for the price calculation.
* @param opts an {@link WhirlpoolAccountFetchOptions} object to define fetch and cache options when accessing on-chain accounts
* @returns A map of token addresses to prices
*/
static async fetchTokenPricesByPools(
fetcher: WhirlpoolAccountFetcherInterface,
pools: Address[],
config = defaultGetPricesConfig,
thresholdConfig = defaultGetPricesThresholdConfig,
opts: WhirlpoolAccountFetchOptions = IGNORE_CACHE
): Promise<PriceMap> {
const poolDatas = Array.from((await fetcher.getPools(pools, opts)).values());
const [filteredPoolDatas, filteredPoolAddresses] = filterNullObjects(poolDatas, pools);
const poolMap = convertListToMap(
filteredPoolDatas,
AddressUtil.toStrings(filteredPoolAddresses)
);
const tickArrayMap = await PriceModuleUtils.fetchTickArraysForPools(
fetcher,
poolMap,
config,
opts
);
const mints = Array.from(
Object.values(poolMap).reduce((acc, pool) => {
acc.add(pool.tokenMintA.toBase58());
acc.add(pool.tokenMintB.toBase58());
return acc;
}, new Set<string>())
);
const decimalsMap = await PriceModuleUtils.fetchDecimalsForMints(fetcher, mints, PREFER_CACHE);
return PriceModule.calculateTokenPrices(
mints,
{
poolMap,
tickArrayMap,
decimalsMap,
},
config,
thresholdConfig
);
}
/**
* Calculate the price of each token in the mints array.
*
* Each token will be priced against the first quote token in the config.quoteTokens array
* with sufficient liquidity. If a token does not have sufficient liquidity against the
* first quote token, then it will be priced against the next quote token in the array.
* If a token does not have sufficient liquidity against any quote token,
* then the price will be set to null.
*
* @category PriceModule
* @param mints The mints to calculate prices for.
* @param priceCalcData The data required to calculate prices.
* @param config The configuration for the price calculation.
* @param thresholdConfig The threshold configuration for the price calculation.
* @returns A map of token addresses to prices.
*/
static calculateTokenPrices(
mints: Address[],
priceCalcData: PriceCalculationData,
config = defaultGetPricesConfig,
thresholdConfig = defaultGetPricesThresholdConfig
): PriceMap {
const { poolMap, decimalsMap, tickArrayMap } = priceCalcData;
const mintStrings = AddressUtil.toStrings(mints);
// Ensure that quote tokens are in the mints array
if (
!isSubset(
config.quoteTokens.map((mint) => AddressUtil.toString(mint)),
mintStrings.map((mint) => mint)
)
) {
throw new Error("Quote tokens must be in mints array");
}
const results: PriceMap = Object.fromEntries(mintStrings.map((mint) => [mint, null]));
const remainingQuoteTokens = config.quoteTokens.slice();
let remainingMints = mints.slice();
while (remainingQuoteTokens.length > 0 && remainingMints.length > 0) {
// Get prices for mints using the next token in remainingQuoteTokens as the quote token
const quoteToken = remainingQuoteTokens.shift();
if (!quoteToken) {
throw new Error("Unreachable: remainingQuoteTokens is an empty array");
}
// Convert the threshold amount out from the first quote token to the current quote token
let amountOutThresholdAgainstFirstQuoteToken;
// If the quote token is the first quote token, then the amount out is the threshold amount
if (quoteToken.equals(config.quoteTokens[0])) {
amountOutThresholdAgainstFirstQuoteToken = thresholdConfig.amountOut;
} else {
const quoteTokenStr = quoteToken.toBase58();
const quoteTokenPrice = results[quoteTokenStr];
if (!quoteTokenPrice) {
throw new Error(
`Quote token - ${quoteTokenStr} must have a price against the first quote token`
);
}
amountOutThresholdAgainstFirstQuoteToken = convertAmount(
thresholdConfig.amountOut,
quoteTokenPrice,
decimalsMap[config.quoteTokens[0].toBase58()],
decimalsMap[quoteTokenStr]
);
}
const prices = calculatePricesForQuoteToken(
remainingMints,
quoteToken,
poolMap,
tickArrayMap,
decimalsMap,
config,
{
amountOut: amountOutThresholdAgainstFirstQuoteToken,
priceImpactThreshold: thresholdConfig.priceImpactThreshold,
}
);
const quoteTokenPrice = results[quoteToken.toBase58()] || prices[quoteToken.toBase58()];
// Populate the results map with the calculated prices.
// Ensure that the price is quoted against the first quote token and not the current quote token.
remainingMints.forEach((mintAddr) => {
const mint = AddressUtil.toString(mintAddr);
const mintPrice = prices[mint];
if (mintPrice != null && quoteTokenPrice != null) {
results[mint] = mintPrice.mul(quoteTokenPrice);
}
});
// Filter out any mints that do not have a price
remainingMints = remainingMints.filter((mint) => results[AddressUtil.toString(mint)] == null);
}
return results;
}
}
/**
* A list of utility functions for the price module.
* @category PriceModule
*/
export class PriceModuleUtils {
/**
* Fetch pool data for the given mints by deriving the PDA from all combinations of mints & tick-arrays.
* Note that this method can be slow.
*
* @param fetcher {@link WhirlpoolAccountFetcherInterface}
* @param mints The mints to fetch pool data for.
* @param config The configuration for the price calculation.
* @param opts an {@link WhirlpoolAccountFetchOptions} object to define fetch and cache options when accessing on-chain accounts
* @returns A {@link PoolMap} of pool addresses to pool data.
*/
static async fetchPoolDataFromMints(
fetcher: WhirlpoolAccountFetcherInterface,
mints: Address[],
config = defaultGetPricesConfig,
opts = IGNORE_CACHE
): Promise<PoolMap> {
const { quoteTokens, tickSpacings, programId, whirlpoolsConfig } = config;
const poolAddresses: string[] = mints
.map((mint): string[] =>
tickSpacings
.map((tickSpacing): string[] => {
return quoteTokens.map((quoteToken): string => {
const [mintA, mintB] = PoolUtil.orderMints(mint, quoteToken);
return PDAUtil.getWhirlpool(
programId,
whirlpoolsConfig,
AddressUtil.toPubKey(mintA),
AddressUtil.toPubKey(mintB),
tickSpacing
).publicKey.toBase58();
});
})
.flat()
)
.flat();
const poolDatas = Array.from((await fetcher.getPools(poolAddresses, opts)).values());
const [filteredPoolDatas, filteredPoolAddresses] = filterNullObjects(poolDatas, poolAddresses);
return convertListToMap(filteredPoolDatas, filteredPoolAddresses);
}
/**
* Fetch tick-array data for the given pools
*
* @param fetcher {@link WhirlpoolAccountFetcherInterface}
* @param pools The pools to fetch tick-array data for.
* @param config The configuration for the price calculation.
* @param opts an {@link WhirlpoolAccountFetchOptions} object to define fetch and cache options when accessing on-chain accounts
* @returns A {@link TickArrayMap} of tick-array addresses to tick-array data.
*/
static async fetchTickArraysForPools(
fetcher: WhirlpoolAccountFetcherInterface,
pools: PoolMap,
config = defaultGetPricesConfig,
opts: WhirlpoolAccountFetchOptions = IGNORE_CACHE
): Promise<TickArrayMap> {
const { programId } = config;
const getQuoteTokenOrder = (mint: PublicKey) => {
const index = config.quoteTokens.findIndex((quoteToken) => quoteToken.equals(mint));
return index === -1 ? config.quoteTokens.length : index;
};
// select tick arrays based on the direction of swapQuote
// TickArray is a large account, which affects decoding time.
// Fetching can be performed in parallel, but it is preferable to fetch the minimum number of accounts necessary.
const tickArrayAddressSet = new Set<string>();
Object.entries(pools).forEach(([address, pool]) => {
const orderA = getQuoteTokenOrder(pool.tokenMintA);
const orderB = getQuoteTokenOrder(pool.tokenMintB);
if (orderA === orderB) {
// neither tokenMintA nor tokenMintB is a quote token
return;
}
const aToB = orderA > orderB;
const tickArrayPubkeys = SwapUtils.getTickArrayPublicKeys(
pool.tickCurrentIndex,
pool.tickSpacing,
aToB,
programId,
new PublicKey(address)
);
tickArrayPubkeys.forEach((p) => tickArrayAddressSet.add(p.toBase58()));
});
const tickArrayAddresses = Array.from(tickArrayAddressSet);
const tickArrays = await fetcher.getTickArrays(tickArrayAddresses, opts);
const [filteredTickArrays, filteredTickArrayAddresses] = filterNullObjects(
tickArrays,
tickArrayAddresses
);
return convertListToMap(filteredTickArrays, filteredTickArrayAddresses);
}
/**
* Fetch the decimals to token mapping for the given mints.
* @param fetcher {@link WhirlpoolAccountFetcherInterface}
* @param mints The mints to fetch decimals for.
* @param opts an {@link WhirlpoolAccountFetchOptions} object to define fetch and cache options when accessing on-chain accounts
* @returns A {@link DecimalsMap} of mint addresses to decimals.
*/
static async fetchDecimalsForMints(
fetcher: WhirlpoolAccountFetcherInterface,
mints: Address[],
opts = IGNORE_CACHE
): Promise<DecimalsMap> {
const mintInfos = Array.from((await fetcher.getMintInfos(mints, opts)).values());
return mintInfos.reduce((acc, mintInfo, index) => {
const mint = AddressUtil.toString(mints[index]);
if (!mintInfo) {
throw new Error(`Mint account does not exist: ${mint}`);
}
acc[mint] = mintInfo.decimals;
return acc;
}, {} as DecimalsMap);
}
}