Adding PriceModule class to help calculate token prices for a set of Whirlpools (#84)
Provide a shared module to provide the source of truth of token prices based off of Whirlpool account data. Add additional liquidity criteria to filter out tokens with outlier prices Co-authored-by: Otto Cheung <otto@orca.so>
This commit is contained in:
parent
c3a02ee3ed
commit
840b0ebbfb
|
@ -9,6 +9,7 @@ export * from "./types/public";
|
|||
export * from "./types/public/anchor-types";
|
||||
export * from "./utils/public";
|
||||
export * from "./whirlpool-client";
|
||||
export * from "./prices";
|
||||
|
||||
// Global rules for Decimals
|
||||
// - 40 digits of precision for the largest number
|
||||
|
|
|
@ -0,0 +1,198 @@
|
|||
import { AddressUtil, DecimalUtil, Percentage } from "@orca-so/common-sdk";
|
||||
import { Address, translateAddress } from "@project-serum/anchor";
|
||||
import { u64 } from "@solana/spl-token";
|
||||
import { PublicKey } from "@solana/web3.js";
|
||||
import Decimal from "decimal.js";
|
||||
import {
|
||||
DecimalsMap,
|
||||
defaultGetPricesConfig,
|
||||
GetPricesConfig,
|
||||
GetPricesThresholdConfig,
|
||||
PoolMap,
|
||||
PriceMap,
|
||||
TickArrayMap
|
||||
} from ".";
|
||||
import { swapQuoteWithParams } from "../quotes/public/swap-quote";
|
||||
import { TickArray, WhirlpoolData } from "../types/public";
|
||||
import { PoolUtil, PriceMath, SwapUtils } from "../utils/public";
|
||||
import { PDAUtil } from "../utils/public/pda-utils";
|
||||
|
||||
function checkLiquidity(
|
||||
pool: WhirlpoolData,
|
||||
tickArrays: TickArray[],
|
||||
aToB: boolean,
|
||||
thresholdConfig: GetPricesThresholdConfig,
|
||||
decimalsMap: DecimalsMap
|
||||
): boolean {
|
||||
const { amountOut, priceImpactThreshold } = thresholdConfig;
|
||||
|
||||
let estimatedAmountIn;
|
||||
|
||||
try {
|
||||
({ estimatedAmountIn } = swapQuoteWithParams(
|
||||
{
|
||||
whirlpoolData: pool,
|
||||
aToB,
|
||||
amountSpecifiedIsInput: false,
|
||||
tokenAmount: amountOut,
|
||||
otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(false),
|
||||
sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToB),
|
||||
tickArrays,
|
||||
},
|
||||
Percentage.fromDecimal(new Decimal(0))
|
||||
));
|
||||
} catch (e) {
|
||||
// If a quote could not be generated, assume there is insufficient liquidity
|
||||
return false;
|
||||
}
|
||||
|
||||
// Calculate the maximum amount in that is allowed against the desired output
|
||||
let price, inputDecimals, outputDecimals;
|
||||
if (aToB) {
|
||||
price = getPrice(pool, decimalsMap);
|
||||
inputDecimals = decimalsMap[pool.tokenMintA.toBase58()];
|
||||
outputDecimals = decimalsMap[pool.tokenMintB.toBase58()];
|
||||
} else {
|
||||
price = getPrice(pool, decimalsMap).pow(-1);
|
||||
inputDecimals = decimalsMap[pool.tokenMintB.toBase58()];
|
||||
outputDecimals = decimalsMap[pool.tokenMintA.toBase58()];
|
||||
}
|
||||
|
||||
const amountOutDecimals = DecimalUtil.fromU64(amountOut, outputDecimals);
|
||||
|
||||
const estimatedAmountInDecimals = DecimalUtil.fromU64(estimatedAmountIn, inputDecimals);
|
||||
|
||||
const maxAmountInDecimals = amountOutDecimals
|
||||
.div(price)
|
||||
.mul(priceImpactThreshold)
|
||||
.toDecimalPlaces(inputDecimals);
|
||||
|
||||
return estimatedAmountInDecimals.lte(maxAmountInDecimals);
|
||||
}
|
||||
|
||||
type PoolObject = { pool: WhirlpoolData; address: PublicKey };
|
||||
function getMostLiquidPool(
|
||||
mintA: Address,
|
||||
mintB: Address,
|
||||
poolMap: PoolMap,
|
||||
config = defaultGetPricesConfig
|
||||
): PoolObject | null {
|
||||
const { tickSpacings, programId, whirlpoolsConfig } = config;
|
||||
const pools = tickSpacings
|
||||
.map((tickSpacing) => {
|
||||
const pda = PDAUtil.getWhirlpool(
|
||||
programId,
|
||||
whirlpoolsConfig,
|
||||
AddressUtil.toPubKey(mintA),
|
||||
AddressUtil.toPubKey(mintB),
|
||||
tickSpacing
|
||||
);
|
||||
|
||||
return { address: pda.publicKey, pool: poolMap[pda.publicKey.toBase58()] };
|
||||
})
|
||||
.filter(({ pool }) => pool != null);
|
||||
|
||||
if (pools.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return pools.slice(1).reduce<PoolObject>((acc, { address, pool }) => {
|
||||
if (pool.liquidity.lt(acc.pool.liquidity)) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
return { pool, address };
|
||||
}, pools[0]);
|
||||
}
|
||||
|
||||
export function calculatePricesForQuoteToken(
|
||||
mints: Address[],
|
||||
quoteTokenMint: PublicKey,
|
||||
poolMap: PoolMap,
|
||||
tickArrayMap: TickArrayMap,
|
||||
decimalsMap: DecimalsMap,
|
||||
config: GetPricesConfig,
|
||||
thresholdConfig: GetPricesThresholdConfig
|
||||
): PriceMap {
|
||||
return Object.fromEntries(
|
||||
mints.map((mintAddr) => {
|
||||
const mint = AddressUtil.toPubKey(mintAddr);
|
||||
if (mint.equals(quoteTokenMint)) {
|
||||
return [mint.toBase58(), new Decimal(1)];
|
||||
}
|
||||
|
||||
const [mintA, mintB] = PoolUtil.orderMints(mint, quoteTokenMint);
|
||||
|
||||
// The quote token is the output token.
|
||||
// Therefore, if the quote token is mintB, then we are swapping from mintA to mintB.
|
||||
const aToB = translateAddress(mintB).equals(quoteTokenMint);
|
||||
|
||||
const poolCandidate = getMostLiquidPool(mintA, mintB, poolMap, config);
|
||||
if (poolCandidate == null) {
|
||||
return [mint.toBase58(), null];
|
||||
}
|
||||
|
||||
const { pool, address } = poolCandidate;
|
||||
|
||||
const tickArrays = getTickArrays(pool, address, aToB, tickArrayMap, config);
|
||||
|
||||
const isPoolLiquid = checkLiquidity(pool, tickArrays, aToB, thresholdConfig, decimalsMap);
|
||||
|
||||
if (!isPoolLiquid) {
|
||||
return [mint.toBase58(), null];
|
||||
}
|
||||
|
||||
const price = getPrice(pool, decimalsMap);
|
||||
const quotePrice = aToB ? price : price.pow(-1);
|
||||
return [mint.toBase58(), quotePrice];
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function getTickArrays(
|
||||
pool: WhirlpoolData,
|
||||
address: PublicKey,
|
||||
aToB: boolean,
|
||||
tickArrayMap: TickArrayMap,
|
||||
config = defaultGetPricesConfig
|
||||
): TickArray[] {
|
||||
const { programId } = config;
|
||||
const tickArrayPublicKeys = SwapUtils.getTickArrayPublicKeys(
|
||||
pool.tickCurrentIndex,
|
||||
pool.tickSpacing,
|
||||
aToB,
|
||||
programId,
|
||||
address
|
||||
);
|
||||
|
||||
return tickArrayPublicKeys.map((tickArrayPublicKey) => {
|
||||
return { address: tickArrayPublicKey, data: tickArrayMap[tickArrayPublicKey.toBase58()] };
|
||||
});
|
||||
}
|
||||
|
||||
function getPrice(pool: WhirlpoolData, decimalsMap: DecimalsMap) {
|
||||
const tokenAAddress = pool.tokenMintA.toBase58();
|
||||
const tokenBAddress = pool.tokenMintB.toBase58();
|
||||
if (!(tokenAAddress in decimalsMap) || !(tokenBAddress in decimalsMap)) {
|
||||
throw new Error("Missing token decimals");
|
||||
}
|
||||
|
||||
return PriceMath.sqrtPriceX64ToPrice(
|
||||
pool.sqrtPrice,
|
||||
decimalsMap[tokenAAddress],
|
||||
decimalsMap[tokenBAddress]
|
||||
);
|
||||
}
|
||||
|
||||
export function isSubset(listA: string[], listB: string[]): boolean {
|
||||
return listA.every((itemA) => listB.includes(itemA));
|
||||
}
|
||||
|
||||
export function convertAmount(
|
||||
amount: u64,
|
||||
price: Decimal,
|
||||
amountDecimal: number,
|
||||
resultDecimal: number
|
||||
): u64 {
|
||||
return DecimalUtil.toU64(DecimalUtil.fromU64(amount, amountDecimal).div(price), resultDecimal);
|
||||
}
|
|
@ -0,0 +1,115 @@
|
|||
import { u64 } from "@solana/spl-token";
|
||||
import { PublicKey } from "@solana/web3.js";
|
||||
import Decimal from "decimal.js";
|
||||
import {
|
||||
ORCA_SUPPORTED_TICK_SPACINGS,
|
||||
ORCA_WHIRLPOOLS_CONFIG,
|
||||
ORCA_WHIRLPOOL_PROGRAM_ID,
|
||||
TickArrayData,
|
||||
WhirlpoolData
|
||||
} from "../types/public";
|
||||
import { TOKEN_MINTS } from "../utils/constants";
|
||||
|
||||
export * from "./price-module";
|
||||
|
||||
/**
|
||||
* A config object for the {@link PriceModule} functions.
|
||||
*
|
||||
* @category PriceModule
|
||||
* @param quoteTokens The group of quote tokens that you want to search Whirlpools for.
|
||||
* The first token must be the token that is being priced against the other tokens.
|
||||
* The subsequent tokens are alternative tokens that can be used to price the first token.
|
||||
* @param tickSpacings The group of tick spacings that you want to search Whirlpools for.
|
||||
* @param programId The public key of the Whirlpool Program account that you want to search Whirlpools for.
|
||||
* @param whirlpoolsConfig The public key of the {@link WhirlpoolsConfig} account that you want to search Whirlpools for.
|
||||
*/
|
||||
export type GetPricesConfig = {
|
||||
quoteTokens: PublicKey[];
|
||||
tickSpacings: number[];
|
||||
programId: PublicKey;
|
||||
whirlpoolsConfig: PublicKey;
|
||||
};
|
||||
|
||||
/**
|
||||
* A config object for the {@link PriceModule} functions to define thresholds for price calculations.
|
||||
* Whirlpools that do not fit the criteria set by the parameters below will be excluded in the price calculation.
|
||||
*
|
||||
* @category PriceModule
|
||||
* @param amountOut The token amount in terms of the first quote token amount to evaluate a Whirlpool's liquidity against.
|
||||
* @param priceImpactThreshold Using amountOut to perform a swap quote on a pool, this value is the maximum price impact
|
||||
* that a Whirlpool can have to be included in the price calculation.
|
||||
*/
|
||||
export type GetPricesThresholdConfig = {
|
||||
amountOut: u64;
|
||||
priceImpactThreshold: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* A set of fetched accounts that are used for price calculations in {@link PriceModule} functions.
|
||||
*
|
||||
* @category PriceModule
|
||||
* @param poolMap A map of {@link WhirlpoolData} accounts that are used for price calculations.
|
||||
* @param tickArrayMap A map of {@link TickArrayData} accounts that are used for price calculations.
|
||||
* @param decimalsMap A map of token decimals that are used for price calculations.
|
||||
*/
|
||||
export type PriceCalculationData = {
|
||||
poolMap: PoolMap;
|
||||
tickArrayMap: TickArrayMap;
|
||||
decimalsMap: DecimalsMap;
|
||||
};
|
||||
|
||||
/**
|
||||
* A map of whirlpool addresses against {@link WhirlpoolData} accounts
|
||||
* @category PriceModule
|
||||
*/
|
||||
export type PoolMap = Record<string, WhirlpoolData>;
|
||||
|
||||
/**
|
||||
* A map of tick-array addresses against {@link TickArrayData} accounts
|
||||
* @category PriceModule
|
||||
*/
|
||||
export type TickArrayMap = Record<string, TickArrayData>;
|
||||
|
||||
/**
|
||||
* A map of token mint addresses against price values. If a price is not available, the value will be null.
|
||||
* @category PriceModule
|
||||
*/
|
||||
export type PriceMap = Record<string, Decimal | null>;
|
||||
|
||||
/**
|
||||
* A map of token mint addresses against token decimals.
|
||||
* @category PriceModule
|
||||
*/
|
||||
export type DecimalsMap = Record<string, number>;
|
||||
|
||||
/**
|
||||
* The default quote tokens used for Orca's mainnet deployment.
|
||||
* Supply your own if you are using a different deployment.
|
||||
* @category PriceModule
|
||||
*/
|
||||
export const defaultQuoteTokens: PublicKey[] = [
|
||||
TOKEN_MINTS["USDC"],
|
||||
TOKEN_MINTS["SOL"],
|
||||
TOKEN_MINTS["mSOL"],
|
||||
TOKEN_MINTS["stSOL"],
|
||||
].map((mint) => new PublicKey(mint));
|
||||
|
||||
/**
|
||||
* The default {@link GetPricesConfig} config for Orca's mainnet deployment.
|
||||
* @category PriceModule
|
||||
*/
|
||||
export const defaultGetPricesConfig: GetPricesConfig = {
|
||||
quoteTokens: defaultQuoteTokens,
|
||||
tickSpacings: ORCA_SUPPORTED_TICK_SPACINGS,
|
||||
programId: ORCA_WHIRLPOOL_PROGRAM_ID,
|
||||
whirlpoolsConfig: ORCA_WHIRLPOOLS_CONFIG,
|
||||
};
|
||||
|
||||
/**
|
||||
* The default {@link GetPricesThresholdConfig} config for Orca's mainnet deployment.
|
||||
* @category PriceModule
|
||||
*/
|
||||
export const defaultGetPricesThresholdConfig: GetPricesThresholdConfig = {
|
||||
amountOut: new u64(1_000_000_000),
|
||||
priceImpactThreshold: 1.05,
|
||||
};
|
|
@ -0,0 +1,344 @@
|
|||
import { AddressUtil } from "@orca-so/common-sdk";
|
||||
import { Address } from "@project-serum/anchor";
|
||||
import { PublicKey } from "@solana/web3.js";
|
||||
import {
|
||||
DecimalsMap,
|
||||
defaultGetPricesConfig,
|
||||
defaultGetPricesThresholdConfig,
|
||||
PoolMap,
|
||||
PriceCalculationData,
|
||||
PriceMap,
|
||||
TickArrayMap
|
||||
} from ".";
|
||||
import { WhirlpoolContext } from "../context";
|
||||
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 ctx {@link WhirlpoolContext}
|
||||
* @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 refresh Whether to refresh the cache.
|
||||
* @param availableData - Data that is already available to avoid redundant fetches.
|
||||
* @returns A map of token addresses to prices.
|
||||
*/
|
||||
static async fetchTokenPricesByMints(
|
||||
ctx: WhirlpoolContext,
|
||||
mints: Address[],
|
||||
config = defaultGetPricesConfig,
|
||||
thresholdConfig = defaultGetPricesThresholdConfig,
|
||||
refresh = true,
|
||||
availableData: Partial<PriceCalculationData> = {}
|
||||
): Promise<PriceMap> {
|
||||
const poolMap = availableData?.poolMap
|
||||
? availableData?.poolMap
|
||||
: await PriceModuleUtils.fetchPoolDataFromMints(ctx, mints, config, refresh);
|
||||
const tickArrayMap = availableData?.tickArrayMap
|
||||
? availableData.tickArrayMap
|
||||
: await PriceModuleUtils.fetchTickArraysForPools(ctx, poolMap, config, refresh);
|
||||
const decimalsMap = availableData?.decimalsMap
|
||||
? availableData.decimalsMap
|
||||
: await PriceModuleUtils.fetchDecimalsForMints(ctx, mints, false);
|
||||
|
||||
return PriceModule.calculateTokenPrices(
|
||||
mints,
|
||||
{
|
||||
poolMap,
|
||||
tickArrayMap,
|
||||
decimalsMap,
|
||||
},
|
||||
config,
|
||||
thresholdConfig
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches and calculates the token prices from a set of pools.
|
||||
*
|
||||
* @param ctx {@link WhirlpoolContext}
|
||||
* @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 refresh Whether to refresh the cache.
|
||||
* @returns A map of token addresses to prices
|
||||
*/
|
||||
static async fetchTokenPricesByPools(
|
||||
ctx: WhirlpoolContext,
|
||||
pools: Address[],
|
||||
config = defaultGetPricesConfig,
|
||||
thresholdConfig = defaultGetPricesThresholdConfig,
|
||||
refresh = true
|
||||
): Promise<PriceMap> {
|
||||
const poolDatas = await ctx.fetcher.listPools(pools, refresh);
|
||||
const [filteredPoolDatas, filteredPoolAddresses] = filterNullObjects(poolDatas, pools);
|
||||
const poolMap = convertListToMap(
|
||||
filteredPoolDatas,
|
||||
AddressUtil.toStrings(filteredPoolAddresses)
|
||||
);
|
||||
|
||||
const tickArrayMap = await PriceModuleUtils.fetchTickArraysForPools(
|
||||
ctx,
|
||||
poolMap,
|
||||
config,
|
||||
refresh
|
||||
);
|
||||
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(ctx, mints, false);
|
||||
|
||||
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 ctx {@link WhirlpoolContext}
|
||||
* @param mints The mints to fetch pool data for.
|
||||
* @param config The configuration for the price calculation.
|
||||
* @param refresh Whether to refresh the cache.
|
||||
* @returns A {@link PoolMap} of pool addresses to pool data.
|
||||
*/
|
||||
static async fetchPoolDataFromMints(
|
||||
ctx: WhirlpoolContext,
|
||||
mints: Address[],
|
||||
config = defaultGetPricesConfig,
|
||||
refresh = true
|
||||
): 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 = await ctx.fetcher.listPools(poolAddresses, refresh);
|
||||
|
||||
const [filteredPoolDatas, filteredPoolAddresses] = filterNullObjects(poolDatas, poolAddresses);
|
||||
return convertListToMap(filteredPoolDatas, filteredPoolAddresses);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch tick-array data for the given pools
|
||||
*
|
||||
* @param ctx {@link WhirlpoolData}
|
||||
* @param pools The pools to fetch tick-array data for.
|
||||
* @param config The configuration for the price calculation.
|
||||
* @param refresh Whether to refresh the cache.
|
||||
* @returns A {@link TickArrayMap} of tick-array addresses to tick-array data.
|
||||
*/
|
||||
static async fetchTickArraysForPools(
|
||||
ctx: WhirlpoolContext,
|
||||
pools: PoolMap,
|
||||
config = defaultGetPricesConfig,
|
||||
refresh = true
|
||||
): Promise<TickArrayMap> {
|
||||
const { programId } = config;
|
||||
const tickArrayAddresses = Object.entries(pools)
|
||||
.map(([poolAddress, pool]): PublicKey[] => {
|
||||
const aToBTickArrayPublicKeys = SwapUtils.getTickArrayPublicKeys(
|
||||
pool.tickCurrentIndex,
|
||||
pool.tickSpacing,
|
||||
true,
|
||||
programId,
|
||||
new PublicKey(poolAddress)
|
||||
);
|
||||
|
||||
const bToATickArrayPublicKeys = SwapUtils.getTickArrayPublicKeys(
|
||||
pool.tickCurrentIndex,
|
||||
pool.tickSpacing,
|
||||
false,
|
||||
programId,
|
||||
new PublicKey(poolAddress)
|
||||
);
|
||||
|
||||
// Fetch tick arrays in both directions
|
||||
if (aToBTickArrayPublicKeys[0].equals(bToATickArrayPublicKeys[0])) {
|
||||
return aToBTickArrayPublicKeys.concat(bToATickArrayPublicKeys.slice(1));
|
||||
} else {
|
||||
return aToBTickArrayPublicKeys.concat(bToATickArrayPublicKeys);
|
||||
}
|
||||
})
|
||||
.flat()
|
||||
.map((tickArray): string => tickArray.toBase58());
|
||||
|
||||
const tickArrays = await ctx.fetcher.listTickArrays(tickArrayAddresses, refresh);
|
||||
|
||||
const [filteredTickArrays, filteredTickArrayAddresses] = filterNullObjects(
|
||||
tickArrays,
|
||||
tickArrayAddresses
|
||||
);
|
||||
return convertListToMap(filteredTickArrays, filteredTickArrayAddresses);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the decimals to token mapping for the given mints.
|
||||
* @param ctx {@link WhirlpoolContext}
|
||||
* @param mints The mints to fetch decimals for.
|
||||
* @param refresh Whether to refresh the cache.
|
||||
* @returns A {@link DecimalsMap} of mint addresses to decimals.
|
||||
*/
|
||||
static async fetchDecimalsForMints(
|
||||
ctx: WhirlpoolContext,
|
||||
mints: Address[],
|
||||
refresh = true
|
||||
): Promise<DecimalsMap> {
|
||||
const mintInfos = await ctx.fetcher.listMintInfos(mints, refresh);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
|
@ -15,6 +15,12 @@ export const ORCA_WHIRLPOOL_PROGRAM_ID = new PublicKey(
|
|||
*/
|
||||
export const ORCA_WHIRLPOOLS_CONFIG = new PublicKey("2LecshUwdy9xi7meFgHtFJQNSKk4KdTrcpvaB56dP2NQ");
|
||||
|
||||
/**
|
||||
* Orca's supported tick spacings.
|
||||
* @category Constants
|
||||
*/
|
||||
export const ORCA_SUPPORTED_TICK_SPACINGS = [1, 8, 64, 128];
|
||||
|
||||
/**
|
||||
* The number of rewards supported by this whirlpool.
|
||||
* @category Constants
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
export const TOKEN_MINTS = {
|
||||
USDC: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
|
||||
SOL: "So11111111111111111111111111111111111111112",
|
||||
USDT: "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB",
|
||||
USDH: "USDH1SM1ojwWUga67PGrgFWUHibbjqMvuMaDkRJTgkX",
|
||||
mSOL: "mSoLzYCxHdYgdzU16g5QSh3i5K3z3KZK7ytfqcJm7So",
|
||||
stSOL: "7dHbWXmci3dT8UFYWYZweBLXgycu7Y3iL6trKn1Y7ARj",
|
||||
};
|
|
@ -4,6 +4,7 @@ import { u64 } from "@solana/spl-token";
|
|||
import { PublicKey } from "@solana/web3.js";
|
||||
import Decimal from "decimal.js";
|
||||
import { WhirlpoolData, WhirlpoolRewardInfoData } from "../../types/public";
|
||||
import { TOKEN_MINTS } from "../constants";
|
||||
import { PriceMath } from "./price-math";
|
||||
import { TokenType } from "./types";
|
||||
|
||||
|
@ -11,7 +12,7 @@ import { TokenType } from "./types";
|
|||
* @category Whirlpool Utils
|
||||
*/
|
||||
export class PoolUtil {
|
||||
private constructor() {}
|
||||
private constructor() { }
|
||||
|
||||
public static isRewardInitialized(rewardInfo: WhirlpoolRewardInfoData): boolean {
|
||||
return (
|
||||
|
@ -205,12 +206,12 @@ export function toTokenAmount(a: number, b: number): TokenAmounts {
|
|||
// The number that the mint maps to determines the priority that it will be used as the quote
|
||||
// currency.
|
||||
const QUOTE_TOKENS: { [mint: string]: number } = {
|
||||
Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB: 100, // USDT
|
||||
EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v: 90, // USDC
|
||||
USDH1SM1ojwWUga67PGrgFWUHibbjqMvuMaDkRJTgkX: 80, // USDH
|
||||
So11111111111111111111111111111111111111112: 70, // SOL
|
||||
mSoLzYCxHdYgdzU16g5QSh3i5K3z3KZK7ytfqcJm7So: 60, // mSOL
|
||||
"7dHbWXmci3dT8UFYWYZweBLXgycu7Y3iL6trKn1Y7ARj": 50, // stSOL
|
||||
[TOKEN_MINTS["USDT"]]: 100,
|
||||
[TOKEN_MINTS["USDC"]]: 90, // USDC
|
||||
[TOKEN_MINTS["USDH"]]: 80, // USDH
|
||||
[TOKEN_MINTS["SOL"]]: 70, // SOL
|
||||
[TOKEN_MINTS["mSOL"]]: 60, // mSOL
|
||||
[TOKEN_MINTS["stSOL"]]: 50, // stSOL
|
||||
};
|
||||
|
||||
const DEFAULT_QUOTE_PRIORITY = 0;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { TransactionBuilder } from "@orca-so/common-sdk";;
|
||||
import { TransactionBuilder } from "@orca-so/common-sdk";
|
||||
import { WhirlpoolContext } from "..";
|
||||
|
||||
export function convertListToMap<T>(fetchedData: T[], addresses: string[]) {
|
||||
export function convertListToMap<T>(fetchedData: T[], addresses: string[]): Record<string, T> {
|
||||
const result: Record<string, T> = {};
|
||||
fetchedData.forEach((data, index) => {
|
||||
if (data) {
|
||||
|
@ -12,13 +12,31 @@ export function convertListToMap<T>(fetchedData: T[], addresses: string[]) {
|
|||
return result;
|
||||
}
|
||||
|
||||
// Filter out null objects in the first array and remove the corresponding objects in the second array
|
||||
export function filterNullObjects<T, K>(
|
||||
firstArray: Array<T | null>,
|
||||
secondArray: Array<K>
|
||||
): [Array<T>, Array<K>] {
|
||||
const filteredFirstArray: Array<T> = [];
|
||||
const filteredSecondArray: Array<K> = [];
|
||||
|
||||
firstArray.forEach((item, idx) => {
|
||||
if (item !== null) {
|
||||
filteredFirstArray.push(item);
|
||||
filteredSecondArray.push(secondArray[idx]);
|
||||
}
|
||||
});
|
||||
|
||||
return [filteredFirstArray, filteredSecondArray];
|
||||
}
|
||||
|
||||
export async function checkMergedTransactionSizeIsValid(
|
||||
ctx: WhirlpoolContext,
|
||||
builders: TransactionBuilder[],
|
||||
latestBlockhash: Readonly<{
|
||||
blockhash: string;
|
||||
lastValidBlockHeight: number;
|
||||
}>,
|
||||
}>
|
||||
): Promise<boolean> {
|
||||
const merged = new TransactionBuilder(ctx.connection, ctx.wallet);
|
||||
builders.forEach((builder) => merged.addInstruction(builder.compressIx(true)));
|
||||
|
@ -29,4 +47,4 @@ export async function checkMergedTransactionSizeIsValid(
|
|||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,269 @@
|
|||
import { MathUtil } from "@orca-so/common-sdk";
|
||||
import * as anchor from "@project-serum/anchor";
|
||||
import { u64 } from "@solana/spl-token";
|
||||
import { PublicKey } from "@solana/web3.js";
|
||||
import * as assert from "assert";
|
||||
import Decimal from "decimal.js";
|
||||
import {
|
||||
GetPricesConfig, GetPricesThresholdConfig, PriceModule,
|
||||
PriceModuleUtils, WhirlpoolContext
|
||||
} from "../../src";
|
||||
import { TickSpacing } from "../utils";
|
||||
import {
|
||||
buildTestAquariums,
|
||||
FundedPositionParams,
|
||||
getDefaultAquarium,
|
||||
initTestPoolWithLiquidity
|
||||
} from "../utils/init-utils";
|
||||
|
||||
// TODO: Move these tests to use mock data instead of relying on solana localnet. It's very slow.
|
||||
describe.only("get_pool_prices", () => {
|
||||
const provider = anchor.AnchorProvider.env();
|
||||
const program = anchor.workspace.Whirlpool;
|
||||
const context = WhirlpoolContext.fromWorkspace(provider, program);
|
||||
|
||||
async function fetchMaps(context: WhirlpoolContext, mints: PublicKey[], config: GetPricesConfig) {
|
||||
const poolMap = await PriceModuleUtils.fetchPoolDataFromMints(context, mints, config);
|
||||
const tickArrayMap = await PriceModuleUtils.fetchTickArraysForPools(context, poolMap);
|
||||
const decimalsMap = await PriceModuleUtils.fetchDecimalsForMints(context, mints);
|
||||
|
||||
return { poolMap, tickArrayMap, decimalsMap };
|
||||
}
|
||||
|
||||
async function fetchAndCalculate(
|
||||
context: WhirlpoolContext,
|
||||
mints: PublicKey[],
|
||||
config: GetPricesConfig,
|
||||
thresholdConfig: GetPricesThresholdConfig
|
||||
) {
|
||||
const { poolMap, tickArrayMap, decimalsMap } = await fetchMaps(context, mints, config);
|
||||
|
||||
const priceMap = PriceModule.calculateTokenPrices(
|
||||
mints,
|
||||
{
|
||||
poolMap,
|
||||
tickArrayMap,
|
||||
decimalsMap,
|
||||
},
|
||||
config,
|
||||
thresholdConfig
|
||||
);
|
||||
|
||||
return {
|
||||
poolMap,
|
||||
tickArrayMap,
|
||||
decimalsMap,
|
||||
priceMap,
|
||||
};
|
||||
}
|
||||
|
||||
function getDefaultThresholdConfig(): GetPricesThresholdConfig {
|
||||
return {
|
||||
amountOut: new u64(1_000_000),
|
||||
priceImpactThreshold: 1.05,
|
||||
};
|
||||
}
|
||||
|
||||
it("successfully calculates the price for one token with a single pool", async () => {
|
||||
const { poolInitInfo, configInitInfo } = await initTestPoolWithLiquidity(context);
|
||||
|
||||
const config: GetPricesConfig = {
|
||||
quoteTokens: [poolInitInfo.tokenMintB],
|
||||
tickSpacings: [TickSpacing.Standard],
|
||||
programId: program.programId,
|
||||
whirlpoolsConfig: configInitInfo.whirlpoolsConfigKeypair.publicKey,
|
||||
};
|
||||
|
||||
const thresholdConfig = getDefaultThresholdConfig();
|
||||
|
||||
const mints = [poolInitInfo.tokenMintA, poolInitInfo.tokenMintB];
|
||||
|
||||
const { poolMap, tickArrayMap, priceMap } = await fetchAndCalculate(
|
||||
context,
|
||||
mints,
|
||||
config,
|
||||
thresholdConfig
|
||||
);
|
||||
|
||||
assert.equal(Object.keys(poolMap).length, 1);
|
||||
assert.equal(Object.keys(tickArrayMap).length, 3);
|
||||
|
||||
assert.equal(Object.keys(priceMap).length, 2);
|
||||
});
|
||||
|
||||
it("successfully calculates the price for two tokens against a third quote token", async () => {
|
||||
const aqConfig = getDefaultAquarium();
|
||||
// Add a third token and account and a second pool
|
||||
aqConfig.initMintParams.push({});
|
||||
aqConfig.initTokenAccParams.push({ mintIndex: 2 });
|
||||
aqConfig.initPoolParams.push({ mintIndices: [1, 2], tickSpacing: TickSpacing.Standard });
|
||||
|
||||
// Add tick arrays and positions
|
||||
const aToB = false;
|
||||
aqConfig.initTickArrayRangeParams.push({
|
||||
poolIndex: 0,
|
||||
startTickIndex: 22528,
|
||||
arrayCount: 3,
|
||||
aToB,
|
||||
});
|
||||
aqConfig.initTickArrayRangeParams.push({
|
||||
poolIndex: 1,
|
||||
startTickIndex: 22528,
|
||||
arrayCount: 3,
|
||||
aToB,
|
||||
});
|
||||
const fundParams: FundedPositionParams[] = [
|
||||
{
|
||||
liquidityAmount: new anchor.BN(10_000_000),
|
||||
tickLowerIndex: 29440,
|
||||
tickUpperIndex: 33536,
|
||||
},
|
||||
];
|
||||
aqConfig.initPositionParams.push({ poolIndex: 0, fundParams });
|
||||
aqConfig.initPositionParams.push({ poolIndex: 1, fundParams });
|
||||
|
||||
const aquarium = (await buildTestAquariums(context, [aqConfig]))[0];
|
||||
const { mintKeys, configParams } = aquarium;
|
||||
|
||||
const config: GetPricesConfig = {
|
||||
quoteTokens: [mintKeys[1]],
|
||||
tickSpacings: [TickSpacing.Standard],
|
||||
programId: program.programId,
|
||||
whirlpoolsConfig: configParams.configInitInfo.whirlpoolsConfigKeypair.publicKey,
|
||||
};
|
||||
|
||||
const thresholdConfig = getDefaultThresholdConfig();
|
||||
|
||||
const mints = [mintKeys[0], mintKeys[1], mintKeys[2]];
|
||||
|
||||
const { poolMap, tickArrayMap, priceMap } = await fetchAndCalculate(
|
||||
context,
|
||||
mints,
|
||||
config,
|
||||
thresholdConfig
|
||||
);
|
||||
|
||||
assert.equal(Object.keys(poolMap).length, 2);
|
||||
assert.equal(Object.keys(tickArrayMap).length, 6);
|
||||
assert.equal(Object.keys(priceMap).length, 3);
|
||||
});
|
||||
|
||||
it("successfully calculates the price for one token with multiple pools against a quote token", async () => {
|
||||
const aqConfig = getDefaultAquarium();
|
||||
// Add a third token and account and a second pool
|
||||
aqConfig.initPoolParams.push({
|
||||
mintIndices: [0, 1],
|
||||
tickSpacing: TickSpacing.SixtyFour,
|
||||
initSqrtPrice: MathUtil.toX64(new Decimal(5.2)),
|
||||
});
|
||||
|
||||
// Add tick arrays and positions
|
||||
const aToB = false;
|
||||
|
||||
aqConfig.initTickArrayRangeParams.push({
|
||||
poolIndex: 0,
|
||||
startTickIndex: 22528,
|
||||
arrayCount: 3,
|
||||
aToB,
|
||||
});
|
||||
aqConfig.initTickArrayRangeParams.push({
|
||||
poolIndex: 1,
|
||||
startTickIndex: 22528,
|
||||
arrayCount: 3,
|
||||
aToB,
|
||||
});
|
||||
const fundParams0: FundedPositionParams[] = [
|
||||
{
|
||||
liquidityAmount: new anchor.BN(10_000_000),
|
||||
tickLowerIndex: 29440,
|
||||
tickUpperIndex: 33536,
|
||||
},
|
||||
];
|
||||
const fundParams1: FundedPositionParams[] = [
|
||||
{
|
||||
liquidityAmount: new anchor.BN(50_000_000),
|
||||
tickLowerIndex: 29440,
|
||||
tickUpperIndex: 33536,
|
||||
},
|
||||
];
|
||||
aqConfig.initPositionParams.push({ poolIndex: 0, fundParams: fundParams0 });
|
||||
aqConfig.initPositionParams.push({ poolIndex: 1, fundParams: fundParams1 });
|
||||
|
||||
const aquarium = (await buildTestAquariums(context, [aqConfig]))[0];
|
||||
const { mintKeys, configParams } = aquarium;
|
||||
|
||||
const config: GetPricesConfig = {
|
||||
quoteTokens: [mintKeys[1]],
|
||||
tickSpacings: [TickSpacing.Standard, TickSpacing.SixtyFour],
|
||||
programId: program.programId,
|
||||
whirlpoolsConfig: configParams.configInitInfo.whirlpoolsConfigKeypair.publicKey,
|
||||
};
|
||||
|
||||
const thresholdConfig = getDefaultThresholdConfig();
|
||||
|
||||
const mints = [mintKeys[0], mintKeys[1]];
|
||||
|
||||
const { poolMap, priceMap } = await fetchAndCalculate(context, mints, config, thresholdConfig);
|
||||
|
||||
assert.equal(Object.keys(poolMap).length, 2);
|
||||
assert.equal(Object.keys(priceMap).length, 2);
|
||||
});
|
||||
|
||||
it("successfully calculates the price for one token which requires an indirect pool to calculate price", async () => {
|
||||
const aqConfig = getDefaultAquarium();
|
||||
// Add a third token and account and a second pool
|
||||
aqConfig.initMintParams.push({});
|
||||
aqConfig.initTokenAccParams.push({ mintIndex: 2 });
|
||||
aqConfig.initPoolParams.push({ mintIndices: [1, 2], tickSpacing: TickSpacing.Standard });
|
||||
|
||||
// Add tick arrays and positions
|
||||
const aToB = false;
|
||||
aqConfig.initTickArrayRangeParams.push({
|
||||
poolIndex: 0,
|
||||
startTickIndex: 22528,
|
||||
arrayCount: 3,
|
||||
aToB,
|
||||
});
|
||||
aqConfig.initTickArrayRangeParams.push({
|
||||
poolIndex: 1,
|
||||
startTickIndex: 22528,
|
||||
arrayCount: 3,
|
||||
aToB,
|
||||
});
|
||||
const fundParams: FundedPositionParams[] = [
|
||||
{
|
||||
liquidityAmount: new anchor.BN(10_000_000_000),
|
||||
tickLowerIndex: 29440,
|
||||
tickUpperIndex: 33536,
|
||||
},
|
||||
];
|
||||
aqConfig.initPositionParams.push({ poolIndex: 0, fundParams });
|
||||
aqConfig.initPositionParams.push({ poolIndex: 1, fundParams });
|
||||
|
||||
const aquarium = (await buildTestAquariums(context, [aqConfig]))[0];
|
||||
const { mintKeys, configParams } = aquarium;
|
||||
|
||||
const config: GetPricesConfig = {
|
||||
quoteTokens: [mintKeys[2], mintKeys[1]],
|
||||
tickSpacings: [TickSpacing.Standard],
|
||||
programId: program.programId,
|
||||
whirlpoolsConfig: configParams.configInitInfo.whirlpoolsConfigKeypair.publicKey,
|
||||
};
|
||||
|
||||
const thresholdConfig = getDefaultThresholdConfig();
|
||||
|
||||
const mints = [mintKeys[0], mintKeys[1], mintKeys[2]];
|
||||
|
||||
const { poolMap, tickArrayMap, priceMap } = await fetchAndCalculate(
|
||||
context,
|
||||
mints,
|
||||
config,
|
||||
thresholdConfig
|
||||
);
|
||||
|
||||
assert.equal(Object.keys(poolMap).length, 2);
|
||||
assert.equal(Object.keys(tickArrayMap).length, 6);
|
||||
|
||||
assert.equal(Object.keys(priceMap).length, 3);
|
||||
});
|
||||
});
|
|
@ -89,7 +89,7 @@ describe("position-impl", () => {
|
|||
|
||||
// [Action] Withdraw half of the liquidity away from the position and verify
|
||||
const withdrawHalf = postIncreaseData.liquidity.div(new anchor.BN(2));
|
||||
const decrease_quote = await decreaseLiquidityQuoteByLiquidity(
|
||||
const decrease_quote = decreaseLiquidityQuoteByLiquidity(
|
||||
withdrawHalf,
|
||||
Percentage.fromFraction(0, 100),
|
||||
position,
|
||||
|
|
|
@ -30,8 +30,7 @@ describe("whirlpool-client-impl", () => {
|
|||
TickSpacing.Standard,
|
||||
3000,
|
||||
PriceMath.priceToSqrtPriceX64(new Decimal(100), 6, 6),
|
||||
funderKeypair.publicKey,
|
||||
false
|
||||
funderKeypair.publicKey
|
||||
)
|
||||
).poolInitInfo;
|
||||
});
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
|
||||
|
||||
import { AddressUtil, MathUtil, PDA } from "@orca-so/common-sdk";
|
||||
import * as anchor from "@project-serum/anchor";
|
||||
import { NATIVE_MINT, u64 } from "@solana/spl-token";
|
||||
|
@ -54,8 +52,8 @@ interface InitTestFeeTierParams {
|
|||
interface InitTestPoolParams {
|
||||
mintIndices: [number, number];
|
||||
tickSpacing: number;
|
||||
feeTierIndex?: number;
|
||||
initSqrtPrice?: anchor.BN;
|
||||
feeTierIndex?: number;
|
||||
initSqrtPrice?: anchor.BN;
|
||||
}
|
||||
|
||||
interface InitTestMintParams {
|
||||
|
@ -101,9 +99,9 @@ export interface TestAquarium {
|
|||
configParams: TestConfigParams;
|
||||
feeTierParams: InitFeeTierParams[];
|
||||
mintKeys: PublicKey[];
|
||||
tokenAccounts: { mint: PublicKey, account: PublicKey }[];
|
||||
tokenAccounts: { mint: PublicKey; account: PublicKey }[];
|
||||
pools: InitPoolParams[];
|
||||
tickArrays: { params: InitTestTickArrayRangeParams, pdas: PDA[] }[];
|
||||
tickArrays: { params: InitTestTickArrayRangeParams; pdas: PDA[] }[];
|
||||
}
|
||||
|
||||
const DEFAULT_FEE_RATE = 3000;
|
||||
|
@ -113,7 +111,9 @@ const DEFAULT_SQRT_PRICE = MathUtil.toX64(new Decimal(5));
|
|||
const DEFAULT_INIT_FEE_TIER = [{ tickSpacing: TickSpacing.Standard }];
|
||||
const DEFAULT_INIT_MINT = [{}, {}];
|
||||
const DEFAULT_INIT_TOKEN = [{ mintIndex: 0 }, { mintIndex: 1 }];
|
||||
const DEFAULT_INIT_POOL: InitTestPoolParams[] = [{ mintIndices: [0, 1], tickSpacing: TickSpacing.Standard }];
|
||||
const DEFAULT_INIT_POOL: InitTestPoolParams[] = [
|
||||
{ mintIndices: [0, 1], tickSpacing: TickSpacing.Standard },
|
||||
];
|
||||
const DEFAULT_INIT_TICK_ARR: InitTestTickArrayRangeParams[] = [];
|
||||
const DEFAULT_INIT_POSITION: InitTestPositionParams[] = [];
|
||||
|
||||
|
@ -130,7 +130,7 @@ export function getDefaultAquarium(): InitAquariumParams {
|
|||
|
||||
export async function buildTestAquariums(
|
||||
ctx: WhirlpoolContext,
|
||||
initParams: InitAquariumParams[],
|
||||
initParams: InitAquariumParams[]
|
||||
): Promise<TestAquarium[]> {
|
||||
const aquariums = [];
|
||||
// Airdrop SOL into provider wallet;
|
||||
|
@ -142,7 +142,10 @@ export async function buildTestAquariums(
|
|||
configParams = generateDefaultConfigParams(ctx);
|
||||
}
|
||||
// Could batch
|
||||
await toTx(ctx, WhirlpoolIx.initializeConfigIx(ctx.program, configParams.configInitInfo)).buildAndExecute();
|
||||
await toTx(
|
||||
ctx,
|
||||
WhirlpoolIx.initializeConfigIx(ctx.program, configParams.configInitInfo)
|
||||
).buildAndExecute();
|
||||
|
||||
const {
|
||||
initFeeTierParams,
|
||||
|
@ -156,91 +159,107 @@ export async function buildTestAquariums(
|
|||
const feeTierParams: InitFeeTierParams[] = [];
|
||||
for (const initFeeTierParam of initFeeTierParams) {
|
||||
const { tickSpacing } = initFeeTierParam;
|
||||
const feeRate = initFeeTierParam.feeRate !== undefined
|
||||
? initFeeTierParam.feeRate
|
||||
: DEFAULT_FEE_RATE;
|
||||
const feeRate =
|
||||
initFeeTierParam.feeRate !== undefined ? initFeeTierParam.feeRate : DEFAULT_FEE_RATE;
|
||||
const { params } = await initFeeTier(
|
||||
ctx,
|
||||
configParams.configInitInfo,
|
||||
configParams.configKeypairs.feeAuthorityKeypair,
|
||||
tickSpacing,
|
||||
feeRate,
|
||||
feeRate
|
||||
);
|
||||
feeTierParams.push(params);
|
||||
}
|
||||
|
||||
// TODO: Handle native vs sorted mint keys
|
||||
const mintKeys = (await Promise.all(initMintParams.map(({ isNative }) =>
|
||||
isNative ? NATIVE_MINT : createMint(ctx.provider)
|
||||
))).sort(PoolUtil.compareMints);
|
||||
const mintKeys = (
|
||||
await Promise.all(
|
||||
initMintParams.map(({ isNative }) => (isNative ? NATIVE_MINT : createMint(ctx.provider)))
|
||||
)
|
||||
).sort(PoolUtil.compareMints);
|
||||
|
||||
const tokenAccounts = await Promise.all(initTokenAccParams.map(async (initTokenAccParam) => {
|
||||
const { mintIndex, mintAmount = DEFAULT_MINT_AMOUNT } = initTokenAccParam;
|
||||
const mintKey = mintKeys[mintIndex];
|
||||
const account = await createAndMintToAssociatedTokenAccount(
|
||||
ctx.provider,
|
||||
mintKey,
|
||||
mintAmount,
|
||||
);
|
||||
return { mint: mintKey, account };
|
||||
}));
|
||||
const tokenAccounts = await Promise.all(
|
||||
initTokenAccParams.map(async (initTokenAccParam) => {
|
||||
const { mintIndex, mintAmount = DEFAULT_MINT_AMOUNT } = initTokenAccParam;
|
||||
const mintKey = mintKeys[mintIndex];
|
||||
const account = await createAndMintToAssociatedTokenAccount(
|
||||
ctx.provider,
|
||||
mintKey,
|
||||
mintAmount
|
||||
);
|
||||
return { mint: mintKey, account };
|
||||
})
|
||||
);
|
||||
|
||||
const pools = await Promise.all(initPoolParams.map(async initPoolParam => {
|
||||
const {
|
||||
tickSpacing,
|
||||
mintIndices,
|
||||
initSqrtPrice = DEFAULT_SQRT_PRICE,
|
||||
feeTierIndex = 0,
|
||||
} = initPoolParam;
|
||||
const [mintOne, mintTwo] = mintIndices.map(idx => mintKeys[idx]);
|
||||
const [tokenMintA, tokenMintB] = PoolUtil.orderMints(mintOne, mintTwo).map(AddressUtil.toPubKey);
|
||||
const pools = await Promise.all(
|
||||
initPoolParams.map(async (initPoolParam) => {
|
||||
const {
|
||||
tickSpacing,
|
||||
mintIndices,
|
||||
initSqrtPrice = DEFAULT_SQRT_PRICE,
|
||||
feeTierIndex = 0,
|
||||
} = initPoolParam;
|
||||
const [mintOne, mintTwo] = mintIndices.map((idx) => mintKeys[idx]);
|
||||
const [tokenMintA, tokenMintB] = PoolUtil.orderMints(mintOne, mintTwo).map(
|
||||
AddressUtil.toPubKey
|
||||
);
|
||||
|
||||
const configKey = configParams!.configInitInfo.whirlpoolsConfigKeypair.publicKey;
|
||||
const whirlpoolPda = PDAUtil.getWhirlpool(
|
||||
ctx.program.programId,
|
||||
configKey,
|
||||
tokenMintA,
|
||||
tokenMintB,
|
||||
tickSpacing,
|
||||
);
|
||||
|
||||
const poolParam = {
|
||||
initSqrtPrice,
|
||||
whirlpoolsConfig: configKey,
|
||||
tokenMintA,
|
||||
tokenMintB,
|
||||
whirlpoolPda,
|
||||
tokenVaultAKeypair: Keypair.generate(),
|
||||
tokenVaultBKeypair: Keypair.generate(),
|
||||
feeTierKey: feeTierParams[feeTierIndex].feeTierPda.publicKey,
|
||||
tickSpacing,
|
||||
// TODO: funder
|
||||
funder: ctx.wallet.publicKey,
|
||||
};
|
||||
const configKey = configParams!.configInitInfo.whirlpoolsConfigKeypair.publicKey;
|
||||
const whirlpoolPda = PDAUtil.getWhirlpool(
|
||||
ctx.program.programId,
|
||||
configKey,
|
||||
tokenMintA,
|
||||
tokenMintB,
|
||||
tickSpacing
|
||||
);
|
||||
|
||||
const tx = toTx(ctx, WhirlpoolIx.initializePoolIx(ctx.program, poolParam));
|
||||
await tx.buildAndExecute();
|
||||
return poolParam;
|
||||
}));
|
||||
const poolParam = {
|
||||
initSqrtPrice,
|
||||
whirlpoolsConfig: configKey,
|
||||
tokenMintA,
|
||||
tokenMintB,
|
||||
whirlpoolPda,
|
||||
tokenVaultAKeypair: Keypair.generate(),
|
||||
tokenVaultBKeypair: Keypair.generate(),
|
||||
feeTierKey: feeTierParams[feeTierIndex].feeTierPda.publicKey,
|
||||
tickSpacing,
|
||||
// TODO: funder
|
||||
funder: ctx.wallet.publicKey,
|
||||
};
|
||||
|
||||
const tickArrays = await Promise.all(initTickArrayRangeParams.map(async initTickArrayRangeParam => {
|
||||
const { poolIndex, startTickIndex, arrayCount, aToB } = initTickArrayRangeParam;
|
||||
const pool = pools[poolIndex];
|
||||
const pdas = await initTickArrayRange(ctx, pool.whirlpoolPda.publicKey, startTickIndex, arrayCount, pool.tickSpacing, aToB);
|
||||
return {
|
||||
params: initTickArrayRangeParam,
|
||||
pdas,
|
||||
};
|
||||
}));
|
||||
const tx = toTx(ctx, WhirlpoolIx.initializePoolIx(ctx.program, poolParam));
|
||||
await tx.buildAndExecute();
|
||||
return poolParam;
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
await Promise.all(initPositionParams.map(async initPositionParam => {
|
||||
const { poolIndex, fundParams } = initPositionParam;
|
||||
const pool = pools[poolIndex];
|
||||
const tokenAccKeys = getTokenAccsForPools([pool], tokenAccounts);
|
||||
await fundPositions(ctx, pool, tokenAccKeys[0], tokenAccKeys[1], fundParams);
|
||||
}));
|
||||
const tickArrays = await Promise.all(
|
||||
initTickArrayRangeParams.map(async (initTickArrayRangeParam) => {
|
||||
const { poolIndex, startTickIndex, arrayCount, aToB } = initTickArrayRangeParam;
|
||||
const pool = pools[poolIndex];
|
||||
const pdas = await initTickArrayRange(
|
||||
ctx,
|
||||
pool.whirlpoolPda.publicKey,
|
||||
startTickIndex,
|
||||
arrayCount,
|
||||
pool.tickSpacing,
|
||||
aToB
|
||||
);
|
||||
return {
|
||||
params: initTickArrayRangeParam,
|
||||
pdas,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
initPositionParams.map(async (initPositionParam) => {
|
||||
const { poolIndex, fundParams } = initPositionParam;
|
||||
const pool = pools[poolIndex];
|
||||
const tokenAccKeys = getTokenAccsForPools([pool], tokenAccounts);
|
||||
await fundPositions(ctx, pool, tokenAccKeys[0], tokenAccKeys[1], fundParams);
|
||||
})
|
||||
);
|
||||
|
||||
aquariums.push({
|
||||
configParams,
|
||||
|
@ -254,15 +273,16 @@ export async function buildTestAquariums(
|
|||
return aquariums;
|
||||
}
|
||||
|
||||
export function getTokenAccsForPools(pools: InitPoolParams[], tokenAccounts: { mint: PublicKey, account: PublicKey }[]) {
|
||||
export function getTokenAccsForPools(
|
||||
pools: InitPoolParams[],
|
||||
tokenAccounts: { mint: PublicKey; account: PublicKey }[]
|
||||
) {
|
||||
const mints = [];
|
||||
for (const pool of pools) {
|
||||
mints.push(pool.tokenMintA);
|
||||
mints.push(pool.tokenMintB);
|
||||
}
|
||||
return mints.map(mint =>
|
||||
tokenAccounts.find(acc => acc.mint === mint)!.account
|
||||
);
|
||||
return mints.map((mint) => tokenAccounts.find((acc) => acc.mint === mint)!.account);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -278,7 +298,7 @@ export async function buildTestPoolParams(
|
|||
defaultFeeRate = 3000,
|
||||
initSqrtPrice = DEFAULT_SQRT_PRICE,
|
||||
funder?: PublicKey,
|
||||
tokenAIsNative = false
|
||||
reuseTokenA?: PublicKey
|
||||
) {
|
||||
const { configInitInfo, configKeypairs } = generateDefaultConfigParams(ctx);
|
||||
await toTx(ctx, WhirlpoolIx.initializeConfigIx(ctx.program, configInitInfo)).buildAndExecute();
|
||||
|
@ -297,7 +317,7 @@ export async function buildTestPoolParams(
|
|||
tickSpacing,
|
||||
initSqrtPrice,
|
||||
funder,
|
||||
tokenAIsNative
|
||||
reuseTokenA
|
||||
);
|
||||
return {
|
||||
configInitInfo,
|
||||
|
@ -318,7 +338,7 @@ export async function initTestPool(
|
|||
tickSpacing: number,
|
||||
initSqrtPrice = DEFAULT_SQRT_PRICE,
|
||||
funder?: Keypair,
|
||||
tokenAIsNative = false
|
||||
reuseTokenA?: PublicKey
|
||||
) {
|
||||
const poolParams = await buildTestPoolParams(
|
||||
ctx,
|
||||
|
@ -326,16 +346,16 @@ export async function initTestPool(
|
|||
3000,
|
||||
initSqrtPrice,
|
||||
funder?.publicKey,
|
||||
tokenAIsNative
|
||||
reuseTokenA
|
||||
);
|
||||
|
||||
return await initTestPoolFromParams(ctx, poolParams, funder);
|
||||
return initTestPoolFromParams(ctx, poolParams, funder);
|
||||
}
|
||||
|
||||
export async function initTestPoolFromParams(
|
||||
ctx: WhirlpoolContext,
|
||||
poolParams: TestPoolParams,
|
||||
funder?: Keypair,
|
||||
funder?: Keypair
|
||||
) {
|
||||
const { configInitInfo, poolInitInfo, configKeypairs, feeTierParams } = poolParams;
|
||||
const tx = toTx(ctx, WhirlpoolIx.initializePoolIx(ctx.program, poolInitInfo));
|
||||
|
@ -349,7 +369,7 @@ export async function initTestPoolFromParams(
|
|||
configKeypairs,
|
||||
poolInitInfo,
|
||||
feeTierParams,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export async function initFeeTier(
|
||||
|
@ -533,16 +553,16 @@ export async function initTestPoolWithTokens(
|
|||
tickSpacing: number,
|
||||
initSqrtPrice = DEFAULT_SQRT_PRICE,
|
||||
mintAmount = new anchor.BN("15000000000"),
|
||||
tokenAIsNative = false
|
||||
reuseTokenA?: PublicKey
|
||||
) {
|
||||
const provider = ctx.provider;
|
||||
|
||||
const { poolInitInfo, configInitInfo, configKeypairs } = await initTestPool(
|
||||
const { poolInitInfo, configInitInfo, configKeypairs, feeTierParams } = await initTestPool(
|
||||
ctx,
|
||||
tickSpacing,
|
||||
initSqrtPrice,
|
||||
undefined,
|
||||
tokenAIsNative
|
||||
reuseTokenA
|
||||
);
|
||||
|
||||
const { tokenMintA, tokenMintB, whirlpoolPda } = poolInitInfo;
|
||||
|
@ -574,6 +594,7 @@ export async function initTestPoolWithTokens(
|
|||
poolInitInfo,
|
||||
configInitInfo,
|
||||
configKeypairs,
|
||||
feeTierParams,
|
||||
whirlpoolPda,
|
||||
tokenAccountA,
|
||||
tokenAccountB,
|
||||
|
@ -804,15 +825,27 @@ export async function fundPositions(
|
|||
);
|
||||
}
|
||||
|
||||
export async function initTestPoolWithLiquidity(ctx: WhirlpoolContext) {
|
||||
export async function initTestPoolWithLiquidity(
|
||||
ctx: WhirlpoolContext,
|
||||
initSqrtPrice = DEFAULT_SQRT_PRICE,
|
||||
mintAmount = new anchor.BN("15000000000"),
|
||||
reuseTokenA?: PublicKey
|
||||
) {
|
||||
const {
|
||||
poolInitInfo,
|
||||
configInitInfo,
|
||||
configKeypairs,
|
||||
feeTierParams,
|
||||
whirlpoolPda,
|
||||
tokenAccountA,
|
||||
tokenAccountB,
|
||||
} = await initTestPoolWithTokens(ctx, TickSpacing.Standard);
|
||||
} = await initTestPoolWithTokens(
|
||||
ctx,
|
||||
TickSpacing.Standard,
|
||||
initSqrtPrice,
|
||||
mintAmount,
|
||||
reuseTokenA
|
||||
);
|
||||
|
||||
const tickArrays = await initTickArrayRange(
|
||||
ctx,
|
||||
|
@ -847,5 +880,6 @@ export async function initTestPoolWithLiquidity(ctx: WhirlpoolContext) {
|
|||
tokenAccountA,
|
||||
tokenAccountB,
|
||||
tickArrays,
|
||||
feeTierParams,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Percentage } from "@orca-so/common-sdk";
|
||||
import * as anchor from "@project-serum/anchor";
|
||||
import { u64 } from "@solana/spl-token";
|
||||
import { NATIVE_MINT, u64 } from "@solana/spl-token";
|
||||
import { PublicKey } from "@solana/web3.js";
|
||||
import { TickSpacing } from ".";
|
||||
import { TICK_ARRAY_SIZE, Whirlpool, WhirlpoolClient, WhirlpoolContext } from "../../src";
|
||||
|
@ -39,7 +39,7 @@ export async function setupSwapTest(setup: SwapTestPoolParams, tokenAIsNative =
|
|||
setup.tickSpacing,
|
||||
setup.initSqrtPrice,
|
||||
setup.tokenMintAmount,
|
||||
tokenAIsNative
|
||||
NATIVE_MINT
|
||||
);
|
||||
|
||||
const whirlpool = await setup.client.getPool(whirlpoolPda.publicKey, true);
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
import { AddressUtil, MathUtil, PDA, Percentage } from "@orca-so/common-sdk";
|
||||
import { AnchorProvider } from "@project-serum/anchor";
|
||||
import { ASSOCIATED_TOKEN_PROGRAM_ID, NATIVE_MINT, Token, TOKEN_PROGRAM_ID } from "@solana/spl-token";
|
||||
import {
|
||||
ASSOCIATED_TOKEN_PROGRAM_ID,
|
||||
NATIVE_MINT,
|
||||
Token,
|
||||
TOKEN_PROGRAM_ID,
|
||||
} from "@solana/spl-token";
|
||||
import { Keypair, PublicKey } from "@solana/web3.js";
|
||||
import Decimal from "decimal.js";
|
||||
import { createAndMintToAssociatedTokenAccount, createMint } from ".";
|
||||
|
@ -14,7 +19,7 @@ import {
|
|||
PDAUtil,
|
||||
PoolUtil,
|
||||
PriceMath,
|
||||
Whirlpool
|
||||
Whirlpool,
|
||||
} from "../../src";
|
||||
import { WhirlpoolContext } from "../../src/context";
|
||||
|
||||
|
@ -26,7 +31,7 @@ export interface TestWhirlpoolsConfigKeypairs {
|
|||
|
||||
export interface TestConfigParams {
|
||||
configInitInfo: InitConfigParams;
|
||||
configKeypairs: TestWhirlpoolsConfigKeypairs;
|
||||
configKeypairs: TestWhirlpoolsConfigKeypairs;
|
||||
}
|
||||
|
||||
export const generateDefaultConfigParams = (
|
||||
|
@ -49,9 +54,9 @@ export const generateDefaultConfigParams = (
|
|||
return { configInitInfo, configKeypairs };
|
||||
};
|
||||
|
||||
export const createInOrderMints = async (context: WhirlpoolContext, tokenAIsNative = false) => {
|
||||
export const createInOrderMints = async (context: WhirlpoolContext, reuseTokenA?: PublicKey) => {
|
||||
const provider = context.provider;
|
||||
const tokenXMintPubKey = tokenAIsNative ? NATIVE_MINT : await createMint(provider);
|
||||
const tokenXMintPubKey = reuseTokenA || (await createMint(provider));
|
||||
const tokenYMintPubKey = await createMint(provider);
|
||||
return PoolUtil.orderMints(tokenXMintPubKey, tokenYMintPubKey).map(AddressUtil.toPubKey);
|
||||
};
|
||||
|
@ -63,9 +68,9 @@ export const generateDefaultInitPoolParams = async (
|
|||
tickSpacing: number,
|
||||
initSqrtPrice = MathUtil.toX64(new Decimal(5)),
|
||||
funder?: PublicKey,
|
||||
tokenAIsNative = false
|
||||
reuseTokenA?: PublicKey
|
||||
): Promise<InitPoolParams> => {
|
||||
const [tokenAMintPubKey, tokenBMintPubKey] = await createInOrderMints(context, tokenAIsNative);
|
||||
const [tokenAMintPubKey, tokenBMintPubKey] = await createInOrderMints(context, reuseTokenA);
|
||||
|
||||
const whirlpoolPda = PDAUtil.getWhirlpool(
|
||||
context.program.programId,
|
||||
|
@ -242,4 +247,4 @@ export async function initPosition(
|
|||
positionMint,
|
||||
positionAddress: PDAUtil.getPosition(ctx.program.programId, positionMint),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue