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:
Yutaro Mori 2023-03-15 20:56:21 +08:00 committed by GitHub
parent c3a02ee3ed
commit 840b0ebbfb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 1116 additions and 118 deletions

View File

@ -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

View File

@ -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);
}

115
sdk/src/prices/index.ts Normal file
View File

@ -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,
};

View File

@ -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);
}
}

View File

@ -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

View File

@ -0,0 +1,8 @@
export const TOKEN_MINTS = {
USDC: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
SOL: "So11111111111111111111111111111111111111112",
USDT: "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB",
USDH: "USDH1SM1ojwWUga67PGrgFWUHibbjqMvuMaDkRJTgkX",
mSOL: "mSoLzYCxHdYgdzU16g5QSh3i5K3z3KZK7ytfqcJm7So",
stSOL: "7dHbWXmci3dT8UFYWYZweBLXgycu7Y3iL6trKn1Y7ARj",
};

View File

@ -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;

View File

@ -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;
}
}
}

View File

@ -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);
});
});

View File

@ -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,

View File

@ -30,8 +30,7 @@ describe("whirlpool-client-impl", () => {
TickSpacing.Standard,
3000,
PriceMath.priceToSqrtPriceX64(new Decimal(100), 6, 6),
funderKeypair.publicKey,
false
funderKeypair.publicKey
)
).poolInitInfo;
});

View File

@ -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,
};
}
}

View File

@ -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);

View File

@ -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),
};
}
}