UNPKG

@kamino-finance/kliquidity-sdk

Version:

Typescript SDK for interacting with the Kamino Liquidity (kliquidity) protocol

1,329 lines (1,221 loc) 325 kB
import { getConfigByCluster, HubbleConfig, SolanaCluster } from '@hubbleprotocol/hubble-config'; import { Account, AccountRole, address, Address, Base58EncodedBytes, fetchEncodedAccounts, generateKeyPairSigner, getAddressEncoder, GetProgramAccountsDatasizeFilter, GetProgramAccountsMemcmpFilter, getProgramDerivedAddress, Instruction, isAddress, isSome, none, type Option, some, JsonParsedTokenAccount, Rpc, Slot, SolanaRpcApi, TransactionSigner, } from '@solana/kit'; import bs58 from 'bs58'; import { type GlobalConfig, type TermsSignature, type WhirlpoolStrategy, fetchMaybeWhirlpoolStrategy, fetchAllMaybeWhirlpoolStrategy, fetchMaybeGlobalConfig, fetchMaybeCollateralInfos, fetchMaybeTermsSignature, getWhirlpoolStrategyDecoder, getWhirlpoolStrategySize, WHIRLPOOL_STRATEGY_DISCRIMINATOR, } from './@codegen/kliquidity/accounts'; import { unwrapAccount, unwrapAccounts } from './utils/codamaHelpers'; import Decimal from 'decimal.js'; import { getInitializeTickArrayInstruction } from './@codegen/whirlpools/instructions'; import { type Position as OrcaPosition, type Whirlpool, fetchMaybePosition as fetchMaybeOrcaPosition, fetchAllMaybePosition as fetchAllMaybeOrcaPosition, fetchMaybeWhirlpool, fetchAllMaybeWhirlpool, fetchMaybeTickArray, getWhirlpoolDecoder, getPositionDecoder as getOrcaPositionDecoder, } from './@codegen/whirlpools/accounts'; import { sqrtPriceToPrice as orcaSqrtPriceToPrice, getTickArrayStartTickIndex as orcaGetTickArrayStartTickIndex, priceToTickIndex as orcaPriceToTickIndex, IncreaseLiquidityQuote, sqrtPriceToPrice, tickIndexToPrice as orcaTickIndexToPrice, } from '@orca-so/whirlpools-core'; import { getEmptyShareData, Holdings, KaminoPosition, KaminoPrices, KaminoStrategyWithShareMint, MintToPriceMap, ShareData, ShareDataWithAddress, StrategyBalances, StrategyBalanceWithAddress, StrategyHolder, StrategyPrices, StrategyProgramAddress, StrategyVaultTokens, StrategyWithPendingFees, TokenAmounts, TokenHoldings, TotalStrategyVaultTokens, TreasuryFeeVault, } from './models'; import { Scope } from '@kamino-finance/scope-sdk'; import { OraclePrices } from '@kamino-finance/scope-sdk/dist/@codegen/scope/accounts/OraclePrices'; import { batchFetch, buildStrategyRebalanceParams, collToLamportsDecimal, createAssociatedTokenAccountInstruction, DECIMALS_SOL, DepositAmountsForSwap, Dex, dexToNumber, GenericPoolInfo, GenericPositionRangeInfo, getAssociatedTokenAddress, getAssociatedTokenAddressAndAccount, getMintDecimals, getTickArray, getTokenNameFromCollateralInfo, getUpdateStrategyConfigIx, InitPoolTickIfNeeded, InitStrategyIxs, InputRebalanceFieldInfo, InstructionsWithLookupTables, isSOLMint, isVaultInitialized, keyOrDefault, lamportsToNumberDecimal, LiquidityDistribution, LowerAndUpperTickPubkeys, MaybeTokensBalances, MetadataProgramAddressesOrca, MetadataProgramAddressesRaydium, noopProfiledFunctionExecution, numberToDex, numberToRebalanceType, numberToReferencePriceType, PerformanceFees, PositionRange, PriceReferenceType, ProfiledFunctionExecution, RebalanceFieldInfo, rebalanceFieldsDictToInfo, StrategiesFilters, StrategyTokenScopeFeeds, strategyCreationStatusToBase58, strategyTypeToBase58, stripTwapZeros, SwapperIxBuilder, TokensBalances, VaultParameters, WithdrawAllAndCloseIxns, WithdrawShares, ZERO, getNearestValidTickIndexFromTickIndex as orcaGetNearestValidTickIndexFromTickIndex, getIncreaseLiquidityQuote, defaultSlippagePercentageBPS, } from './utils'; import { encodeUtf8, base64ToBytes } from './utils/bytes'; import { getCheckExpectedVaultsBalancesInstruction, getCloseStrategyInstruction, getCollectFeesAndRewardsInstruction, getDepositInstruction, getExecutiveWithdrawInstruction, getInitializeGlobalConfigInstruction, getInitializeStrategyInstruction, getOpenLiquidityPositionInstruction, getSignTermsInstruction, getSingleTokenDepositWithMinInstruction, getUpdateGlobalConfigInstruction, getUpdateRewardMappingInstruction, getUpdateStrategyConfigInstruction, getWithdrawInstruction, getWithdrawFromTopupInstruction, } from './@codegen/kliquidity/instructions'; import BN from 'bn.js'; import { toBN, fromBN } from './utils/raydiumBridge'; import StrategyWithAddress from './models/StrategyWithAddress'; import { StrategyStatus } from './@codegen/kliquidity/types/strategyStatus'; import { FRONTEND_KAMINO_STRATEGY_URL, METADATA_PROGRAM_ID, U64_MAX, ZERO_BN } from './constants'; import { type CollateralInfo, ExecutiveWithdrawAction, GlobalConfigOption, KaminoRewardInfo, RebalanceType, ReferencePriceType, StrategyConfigOption, } from './@codegen/kliquidity/types'; import { type PersonalPositionState, type PoolState, fetchMaybeAmmConfig, fetchMaybePersonalPositionState, fetchAllMaybePersonalPositionState, fetchMaybePoolState, fetchAllMaybePoolState, getPoolStateDecoder, getPersonalPositionStateDecoder, } from './@codegen/raydium/accounts'; import { OrcaService, RaydiumService, Whirlpool as WhirlpoolAPIResponse, WhirlpoolAprApy } from './services'; import { Pool } from './services/RaydiumPoolsResponse'; // StrategyConfigOption variants are now accessed as StrategyConfigOption.UpdateCollectFeesFee, etc. import { DefaultPerformanceFeeBps } from './constants/DefaultStrategyConfig'; import { CONSENSUS_ID, DEFAULT_PUBLIC_KEY, LUT_OWNER_KEY, MEMO_PROGRAM_ID, STAGING_GLOBAL_CONFIG, STAGING_KAMINO_PROGRAM_ID, } from './constants/pubkeys'; import { AutodriftMethod, DefaultDex, DefaultFeeTierOrca, DefaultMintTokenA, DefaultMintTokenB, DefaultTickSpacing, DriftRebalanceMethod, ExpanderMethod, FullBPS, FullPercentage, ManualRebalanceMethod, PeriodicRebalanceMethod, PricePercentageRebalanceMethod, PricePercentageWithResetRangeRebalanceMethod, RebalanceMethod, TakeProfitMethod, } from './utils/CreationParameters'; import { DOLLAR_BASED, PROPORTION_BASED } from './constants/deposit_method'; import { JupService } from './services/JupService'; import { simulateManualPool, simulatePercentagePool, SimulationPercentagePoolParameters, } from './services/PoolSimulationService'; // RebalanceType variants are now accessed as RebalanceType.Manual, RebalanceType.PricePercentage, etc. import { checkIfAccountExists, createWsolAtaIfMissing, getAtasWithCreateIxnsIfMissing, MAX_ACCOUNTS_PER_TRANSACTION, removeBudgetAndAtaIxns, } from './utils/transactions'; import { deserializeDriftRebalanceFromOnchainParams, deserializeDriftRebalanceWithStateOverride, deserializeExpanderRebalanceWithStateOverride, deserializePeriodicRebalanceFromOnchainParams, deserializePricePercentageRebalanceFromOnchainParams, deserializePricePercentageRebalanceWithStateOverride, deserializePricePercentageWithResetRebalanceFromOnchainParams, deserializePricePercentageWithResetRebalanceWithStateOverride, deserializeTakeProfitRebalanceFromOnchainParams, getDefaultDriftRebalanceFieldInfos, getDefaultExpanderRebalanceFieldInfos, getDefaultManualRebalanceFieldInfos, getDefaultPeriodicRebalanceFieldInfos, getDefaultPricePercentageRebalanceFieldInfos, getDefaultPricePercentageWithResetRebalanceFieldInfos, getDefaultTakeProfitRebalanceFieldsInfos, getDriftRebalanceFieldInfos, getExpanderRebalanceFieldInfos, getManualRebalanceFieldInfos, getPeriodicRebalanceRebalanceFieldInfos, getPositionRangeFromDriftParams, getPositionRangeFromExpanderParams, getPositionRangeFromPercentageRebalanceParams, getPositionRangeFromPeriodicRebalanceParams, getPositionRangeFromPricePercentageWithResetParams, getPricePercentageRebalanceFieldInfos, getPricePercentageWithResetRebalanceFieldInfos, getTakeProfitRebalanceFieldsInfos, readDriftRebalanceParamsFromStrategy, readExpanderRebalanceFieldInfosFromStrategy, readExpanderRebalanceParamsFromStrategy, readPeriodicRebalanceRebalanceParamsFromStrategy, readPeriodicRebalanceRebalanceStateFromStrategy, readPricePercentageRebalanceParamsFromStrategy, readPricePercentageWithResetRebalanceParamsFromStrategy, readRawDriftRebalanceStateFromStrategy, readRawExpanderRebalanceStateFromStrategy, readRawPricePercentageRebalanceStateFromStrategy, readRawPricePercentageWithResetRebalanceStateFromStrategy, readTakeProfitRebalanceParamsFromStrategy, readTakeProfitRebalanceStateFromStrategy, } from './rebalance_methods'; import { PoolPriceReferenceType, TwapPriceReferenceType } from './utils/priceReferenceTypes'; import { extractPricesFromDeserializedState, getRebalanceMethodFromRebalanceFields, getRebalanceTypeFromRebalanceFields, } from './rebalance_methods/utils'; import { RebalanceTypeLabelName } from './rebalance_methods/consts'; import WhirlpoolWithAddress from './models/WhirlpoolWithAddress'; import { PoolSimulationResponse } from './models/PoolSimulationResponseData'; import { deserializeAutodriftRebalanceFromOnchainParams, deserializeAutodriftRebalanceWithStateOverride, getAutodriftRebalanceFieldInfos, getDefaultAutodriftRebalanceFieldInfos, getPositionRangeFromAutodriftParams, readAutodriftRebalanceParamsFromStrategy, readRawAutodriftRebalanceStateFromStrategy, } from './rebalance_methods/autodriftRebalance'; import { getRemoveLiquidityQuote } from './utils/whirlpools'; import { computeMeteoraFee, MeteoraPool, MeteoraService } from './services/MeteoraService'; import { binIdToBinArrayIndex, deriveBinArray, getBinFromBinArray, getBinFromBinArrays, getBinIdFromPriceWithDecimals, getPriceOfBinByBinIdWithDecimals, MeteoraPosition, } from './utils/meteora'; import { type LbPair, type PositionV2, fetchMaybeBinArray, fetchAllMaybeBinArray, fetchMaybeLbPair, fetchAllMaybeLbPair, fetchMaybePositionV2, fetchAllMaybePositionV2, getLbPairDecoder, getPositionV2Decoder, } from './@codegen/meteora/accounts'; import LbPairWithAddress from './models/LbPairWithAddress'; import { getInitializeBinArrayInstruction } from './@codegen/meteora/instructions'; import { getPdaProtocolPositionAddress, i32ToBytes, RaydiumLiquidityMath, RaydiumSqrtPriceMath, RaydiumTickMath, RaydiumTickUtils, } from './utils/raydiumSdkCompat'; import { ASSOCIATED_TOKEN_PROGRAM_ADDRESS, fetchAllMint, getCloseAccountInstruction, Token, TOKEN_2022_PROGRAM_ADDRESS, } from '@solana-program/token-2022'; import { TOKEN_PROGRAM_ADDRESS } from '@solana-program/token'; import { getCreateAccountInstruction, getTransferSolInstruction, SYSTEM_PROGRAM_ADDRESS } from '@solana-program/system'; import { SYSVAR_INSTRUCTIONS_ADDRESS, SYSVAR_RENT_ADDRESS } from '@solana/sysvars'; import { fromLegacyPublicKey } from '@solana/compat'; import { ADDRESS_LOOKUP_TABLE_PROGRAM_ADDRESS, AddressLookupTable, fetchMaybeAddressLookupTable, findAddressLookupTablePda, getCreateLookupTableInstruction, getExtendLookupTableInstruction, } from '@solana-program/address-lookup-table'; import { fetchMultipleLookupTableAccounts } from './utils/lookupTable'; import type { AccountInfoBase, AccountInfoWithJsonData, AccountInfoWithPubkey } from '@solana/rpc-types'; import { toLegacyPublicKey } from './utils/compat'; import { IncreaseLiquidityQuoteParam } from '@orca-so/whirlpools'; import { getTokensPrices } from './services/kSwap'; import { Logger } from './utils/Logger'; const addressEncoder = getAddressEncoder(); export const HUBBLE_SCOPE_FEED_ID = address('3NJYftD5sjVfxSnUdZ1wVML8f3aC6mp1CXCL6L7TnU8C'); export const KAMINO_SCOPE_FEED_ID = address('3t4JZcueEzTbVP6kLxXrL3VpWx45jDer4eqysweBchNH'); export const KSWAP_BASE_API = 'https://api.kamino.finance/kswap'; /** Append extra account metas to a codama instruction without losing type safety */ function appendAccounts<T extends Instruction>(ix: T, extra: { address: Address; role: AccountRole }[]): T { return { ...ix, accounts: [...(ix.accounts || []), ...extra] } as T; } /** Unwrap an Option<T> to T | undefined */ function unwrapOption<T>( opt: { readonly __option: 'Some'; readonly value: T } | { readonly __option: 'None' } ): T | undefined { return opt.__option === 'Some' ? opt.value : undefined; } function yieldToEventLoop(): Promise<void> { return new Promise((resolve) => { if (typeof setImmediate !== 'undefined') { setImmediate(resolve); } else { setTimeout(resolve, 0); } }); } export class Kamino { private readonly _cluster: SolanaCluster; private readonly _rpc: Rpc<SolanaRpcApi>; readonly _config: HubbleConfig; private _globalConfig: Address; private readonly _scope: Scope; private readonly _kliquidityProgramId: Address; private readonly _orcaService: OrcaService; private readonly _raydiumService: RaydiumService; private readonly _meteoraService: MeteoraService; private readonly _kSwapBaseAPI: string = KSWAP_BASE_API; private readonly logger: Logger; /** * Create a new instance of the Kamino SDK class. * @param cluster Name of the Solana cluster * @param rpc Connection to the Solana cluster * @param globalConfig override kamino global config * @param programId override kamino program id * @param whirlpoolProgramId override whirlpool program id * @param raydiumProgramId override raydium program id * @param meteoraProgramId * @param jupBaseAPI */ constructor( cluster: SolanaCluster, rpc: Rpc<SolanaRpcApi>, globalConfig?: Address, programId?: Address, whirlpoolProgramId?: Address, raydiumProgramId?: Address, meteoraProgramId?: Address, jupBaseAPI?: string, // unused, kept for backwards compatibility kSwapBaseAPI?: string, logger?: Logger ) { this._cluster = cluster; this._rpc = rpc; this._config = getConfigByCluster(cluster); if (programId && programId === STAGING_KAMINO_PROGRAM_ID) { this._kliquidityProgramId = programId; this._globalConfig = STAGING_GLOBAL_CONFIG; } else { this._kliquidityProgramId = programId ? programId : fromLegacyPublicKey(this._config.kamino.programId); this._globalConfig = globalConfig ? globalConfig : fromLegacyPublicKey(this._config.kamino.globalConfig); } this.logger = logger ? logger : console; this._scope = new Scope(cluster, rpc); this._orcaService = new OrcaService(rpc, whirlpoolProgramId, this.logger); this._raydiumService = new RaydiumService(rpc, raydiumProgramId, this.logger); this._meteoraService = new MeteoraService(rpc, meteoraProgramId, this.logger); if (kSwapBaseAPI) { this._kSwapBaseAPI = kSwapBaseAPI; } } getConnection = () => this._rpc; getProgramID = () => this._kliquidityProgramId; setGlobalConfig = (globalConfig: Address) => { this._globalConfig = globalConfig; }; getGlobalConfig = () => this._globalConfig; getDepositableTokens = async (): Promise<CollateralInfo[]> => { const collateralInfos = await this.getCollateralInfos(); return collateralInfos.filter((x) => x.mint !== DEFAULT_PUBLIC_KEY); }; getCollateralInfos = async () => { const config = await this.getGlobalConfigState(this._globalConfig); if (!config) { throw Error(`Could not fetch globalConfig with pubkey ${this.getGlobalConfig().toString()}`); } return this.getCollateralInfo(config.tokenInfos); }; /** * Get the prices of all disabled tokens in the specified collateral infos * @param collateralInfos * @returns {Map<Address, Decimal>} - token prices by mint address */ getDisabledTokensPrices = async (collateralInfos?: CollateralInfo[]): Promise<Map<Address, Decimal>> => { const collInfos = collateralInfos ? collateralInfos : await this.getCollateralInfos(); const disabledTokensMints = collInfos.filter((x) => x.disabled && x.mint !== DEFAULT_PUBLIC_KEY).map((x) => x.mint); if (disabledTokensMints.length === 0) { return new Map(); } return getTokensPrices(this._kSwapBaseAPI, disabledTokensMints, this.logger); }; getSupportedDexes = (): Dex[] => ['ORCA', 'RAYDIUM', 'METEORA']; // todo: see if we can read this dynamically getFeeTiersForDex = (dex: Dex): Decimal[] => { if (dex === 'ORCA') { return [new Decimal(0.0001), new Decimal(0.0005), new Decimal(0.003), new Decimal(0.01)]; } else if (dex === 'RAYDIUM') { return [new Decimal(0.0001), new Decimal(0.0005), new Decimal(0.0025), new Decimal(0.01)]; } else if (dex === 'METEORA') { return [new Decimal(0.0001), new Decimal(0.0005), new Decimal(0.0025), new Decimal(0.01)]; } else { throw new Error(`Dex ${dex} is not supported`); } }; getRebalanceMethods = (): RebalanceMethod[] => { return [ ManualRebalanceMethod, PricePercentageRebalanceMethod, PricePercentageWithResetRangeRebalanceMethod, DriftRebalanceMethod, TakeProfitMethod, PeriodicRebalanceMethod, ExpanderMethod, AutodriftMethod, ]; }; getEnabledRebalanceMethods = (): RebalanceMethod[] => { return this.getRebalanceMethods().filter((x) => x.enabled); }; getPriceReferenceTypes = (): PriceReferenceType[] => { return [PoolPriceReferenceType, TwapPriceReferenceType]; }; getDefaultRebalanceMethod = (): RebalanceMethod => PricePercentageRebalanceMethod; getDefaultParametersForNewVault = async (): Promise<VaultParameters> => { const dex = DefaultDex; const tokenMintA = DefaultMintTokenA; const tokenMintB = DefaultMintTokenB; const rebalanceMethod = this.getDefaultRebalanceMethod(); const feeTier = DefaultFeeTierOrca; const tickSpacing = DefaultTickSpacing; const rebalancingParameters = await this.getDefaultRebalanceFields( dex, tokenMintA, tokenMintB, tickSpacing, rebalanceMethod ); const defaultParameters: VaultParameters = { dex, tokenMintA, tokenMintB, feeTier, rebalancingParameters, }; return defaultParameters; }; /** * Retunrs what type of rebalance method the fields represent */ getRebalanceTypeFromRebalanceFields = (rebalanceFields: RebalanceFieldInfo[]): RebalanceType => { return getRebalanceTypeFromRebalanceFields(rebalanceFields); }; /** * Retunrs the rebalance method the fields represent with more details (description, enabled, etc) */ getRebalanceMethodFromRebalanceFields = (rebalanceFields: RebalanceFieldInfo[]): RebalanceMethod => { return getRebalanceMethodFromRebalanceFields(rebalanceFields); }; getReferencePriceTypeForStrategy = async (strategy: Address | StrategyWithAddress): Promise<PriceReferenceType> => { const strategyWithAddress = await this.getStrategyStateIfNotFetched(strategy); return numberToReferencePriceType(strategyWithAddress.strategy.rebalanceRaw.referencePriceType); }; getFieldsForRebalanceMethod = ( rebalanceMethod: RebalanceMethod, dex: Dex, fieldOverrides: RebalanceFieldInfo[], tokenAMint: Address, tokenBMint: Address, tickSpacing: number, poolPrice?: Decimal ): Promise<RebalanceFieldInfo[]> => { switch (rebalanceMethod) { case ManualRebalanceMethod: return this.getFieldsForManualRebalanceMethod(dex, fieldOverrides, tokenAMint, tokenBMint, poolPrice); case PricePercentageRebalanceMethod: return this.getFieldsForPricePercentageMethod(dex, fieldOverrides, tokenAMint, tokenBMint, poolPrice); case PricePercentageWithResetRangeRebalanceMethod: return this.getFieldsForPricePercentageWithResetMethod(dex, fieldOverrides, tokenAMint, tokenBMint, poolPrice); case DriftRebalanceMethod: return this.getFieldsForDriftRebalanceMethod( dex, fieldOverrides, tickSpacing, tokenAMint, tokenBMint, poolPrice ); case TakeProfitMethod: return this.getFieldsForTakeProfitRebalanceMethod(dex, fieldOverrides, tokenAMint, tokenBMint, poolPrice); case PeriodicRebalanceMethod: return this.getFieldsForPeriodicRebalanceMethod(dex, fieldOverrides, tokenAMint, tokenBMint, poolPrice); case ExpanderMethod: return this.getFieldsForExpanderRebalanceMethod(dex, fieldOverrides, tokenAMint, tokenBMint, poolPrice); case AutodriftMethod: return this.getFieldsForAutodriftRebalanceMethod( dex, fieldOverrides, tokenAMint, tokenBMint, tickSpacing, poolPrice ); default: throw new Error(`Rebalance method ${rebalanceMethod} is not supported`); } }; getFieldsForManualRebalanceMethod = async ( dex: Dex, fieldOverrides: RebalanceFieldInfo[], tokenAMint: Address, tokenBMint: Address, poolPrice?: Decimal ): Promise<RebalanceFieldInfo[]> => { const price = poolPrice ? poolPrice : new Decimal(await this.getPriceForPair(dex, tokenAMint, tokenBMint)); const defaultFields = getDefaultManualRebalanceFieldInfos(price); let lowerPrice = defaultFields.find((x) => x.label === 'rangePriceLower')!.value; const lowerPriceInput = fieldOverrides.find((x) => x.label === 'rangePriceLower'); if (lowerPriceInput) { lowerPrice = lowerPriceInput.value; } let upperPrice = defaultFields.find((x) => x.label === 'rangePriceUpper')!.value; const upperPriceInput = fieldOverrides.find((x) => x.label === 'rangePriceUpper'); if (upperPriceInput) { upperPrice = upperPriceInput.value; } return getManualRebalanceFieldInfos(new Decimal(lowerPrice), new Decimal(upperPrice)); }; getFieldsForPricePercentageMethod = async ( dex: Dex, fieldOverrides: RebalanceFieldInfo[], tokenAMint: Address, tokenBMint: Address, poolPrice?: Decimal ): Promise<RebalanceFieldInfo[]> => { const price = poolPrice ? poolPrice : new Decimal(await this.getPriceForPair(dex, tokenAMint, tokenBMint)); const defaultFields = getDefaultPricePercentageRebalanceFieldInfos(price); let lowerPriceDifferenceBPS = defaultFields.find((x) => x.label === 'lowerRangeBps')!.value; const lowerPriceDifferenceBPSInput = fieldOverrides.find((x) => x.label === 'lowerRangeBps'); if (lowerPriceDifferenceBPSInput) { lowerPriceDifferenceBPS = lowerPriceDifferenceBPSInput.value; } let upperPriceDifferenceBPS = defaultFields.find((x) => x.label === 'upperRangeBps')!.value; const upperPriceDifferenceBPSInput = fieldOverrides.find((x) => x.label === 'upperRangeBps'); if (upperPriceDifferenceBPSInput) { upperPriceDifferenceBPS = upperPriceDifferenceBPSInput.value; } return getPricePercentageRebalanceFieldInfos( price, new Decimal(lowerPriceDifferenceBPS), new Decimal(upperPriceDifferenceBPS) ); }; getFieldsForPricePercentageWithResetMethod = async ( dex: Dex, fieldOverrides: RebalanceFieldInfo[], tokenAMint: Address, tokenBMint: Address, poolPrice?: Decimal ): Promise<RebalanceFieldInfo[]> => { const price = poolPrice ? poolPrice : new Decimal(await this.getPriceForPair(dex, tokenAMint, tokenBMint)); const defaultFields = getDefaultPricePercentageWithResetRebalanceFieldInfos(price); let lowerPriceDifferenceBPS = defaultFields.find((x) => x.label === 'lowerRangeBps')!.value; const lowerPriceDifferenceBPSInput = fieldOverrides.find((x) => x.label === 'lowerRangeBps'); if (lowerPriceDifferenceBPSInput) { lowerPriceDifferenceBPS = lowerPriceDifferenceBPSInput.value; } let upperPriceDifferenceBPS = defaultFields.find((x) => x.label === 'upperRangeBps')!.value; const upperPriceDifferenceBPSInput = fieldOverrides.find((x) => x.label === 'upperRangeBps'); if (upperPriceDifferenceBPSInput) { upperPriceDifferenceBPS = upperPriceDifferenceBPSInput.value; } let lowerResetPriceDifferenceBPS = defaultFields.find((x) => x.label === 'resetLowerRangeBps')!.value; const lowerResetPriceDifferenceBPSInput = fieldOverrides.find((x) => x.label === 'resetLowerRangeBps'); if (lowerResetPriceDifferenceBPSInput) { lowerResetPriceDifferenceBPS = lowerResetPriceDifferenceBPSInput.value; } let upperResetPriceDifferenceBPS = defaultFields.find((x) => x.label === 'resetUpperRangeBps')!.value; const upperResetPriceDifferenceBPSInput = fieldOverrides.find((x) => x.label === 'resetUpperRangeBps'); if (upperResetPriceDifferenceBPSInput) { upperResetPriceDifferenceBPS = upperResetPriceDifferenceBPSInput.value; } return getPricePercentageWithResetRebalanceFieldInfos( price, new Decimal(lowerPriceDifferenceBPS), new Decimal(upperPriceDifferenceBPS), new Decimal(lowerResetPriceDifferenceBPS), new Decimal(upperResetPriceDifferenceBPS) ); }; getFieldsForDriftRebalanceMethod = async ( dex: Dex, fieldOverrides: RebalanceFieldInfo[], tickSpacing: number, tokenAMint: Address, tokenBMint: Address, poolPrice?: Decimal ): Promise<RebalanceFieldInfo[]> => { const tokenADecimals = await getMintDecimals(this._rpc, tokenAMint); const tokenBDecimals = await getMintDecimals(this._rpc, tokenBMint); const price = poolPrice ? poolPrice : new Decimal(await this.getPriceForPair(dex, tokenAMint, tokenBMint)); const defaultFields = getDefaultDriftRebalanceFieldInfos(dex, tickSpacing, price, tokenADecimals, tokenBDecimals); let startMidTick = defaultFields.find((x) => x.label === 'startMidTick')!.value; const startMidTickInput = fieldOverrides.find((x) => x.label === 'startMidTick'); if (startMidTickInput) { startMidTick = startMidTickInput.value; } let ticksBelowMid = defaultFields.find((x) => x.label === 'ticksBelowMid')!.value; const ticksBelowMidInput = fieldOverrides.find((x) => x.label === 'ticksBelowMid'); if (ticksBelowMidInput) { ticksBelowMid = ticksBelowMidInput.value; } let ticksAboveMid = defaultFields.find((x) => x.label === 'ticksAboveMid')!.value; const ticksAboveMidInput = fieldOverrides.find((x) => x.label === 'ticksAboveMid'); if (ticksAboveMidInput) { ticksAboveMid = ticksAboveMidInput.value; } let secondsPerTick = defaultFields.find((x) => x.label === 'secondsPerTick')!.value; const secondsPerTickInput = fieldOverrides.find((x) => x.label === 'secondsPerTick'); if (secondsPerTickInput) { secondsPerTick = secondsPerTickInput.value; } let direction = defaultFields.find((x) => x.label === 'direction')!.value; const directionInput = fieldOverrides.find((x) => x.label === 'direction'); if (directionInput) { direction = directionInput.value; } const fieldInfos = getDriftRebalanceFieldInfos( dex, tokenADecimals, tokenBDecimals, tickSpacing, new Decimal(startMidTick), new Decimal(ticksBelowMid), new Decimal(ticksAboveMid), new Decimal(secondsPerTick), new Decimal(direction) ); return fieldInfos; }; getFieldsForTakeProfitRebalanceMethod = async ( dex: Dex, fieldOverrides: RebalanceFieldInfo[], tokenAMint: Address, tokenBMint: Address, poolPrice?: Decimal ): Promise<RebalanceFieldInfo[]> => { const price = poolPrice ? poolPrice : new Decimal(await this.getPriceForPair(dex, tokenAMint, tokenBMint)); const defaultFields = getDefaultTakeProfitRebalanceFieldsInfos(price); let lowerRangePrice = defaultFields.find((x) => x.label === 'rangePriceLower')!.value; const lowerRangePriceInput = fieldOverrides.find((x) => x.label === 'rangePriceLower'); if (lowerRangePriceInput) { lowerRangePrice = lowerRangePriceInput.value; } let upperRangePrice = defaultFields.find((x) => x.label === 'rangePriceUpper')!.value; const upperRangePriceInput = fieldOverrides.find((x) => x.label === 'rangePriceUpper'); if (upperRangePriceInput) { upperRangePrice = upperRangePriceInput.value; } let destinationToken = defaultFields.find((x) => x.label === 'destinationToken')!.value; const destinationTokenInput = fieldOverrides.find((x) => x.label === 'destinationToken'); if (destinationTokenInput) { destinationToken = destinationTokenInput.value; } return getTakeProfitRebalanceFieldsInfos( new Decimal(lowerRangePrice), new Decimal(upperRangePrice), new Decimal(destinationToken), true ); }; getFieldsForPeriodicRebalanceMethod = async ( dex: Dex, fieldOverrides: RebalanceFieldInfo[], tokenAMint: Address, tokenBMint: Address, poolPrice?: Decimal ): Promise<RebalanceFieldInfo[]> => { const price = poolPrice ? poolPrice : new Decimal(await this.getPriceForPair(dex, tokenAMint, tokenBMint)); const defaultFields = getDefaultPeriodicRebalanceFieldInfos(price); let period: Decimal = new Decimal(defaultFields.find((x) => x.label === 'period')!.value); const periodInput = fieldOverrides.find((x) => x.label === 'period'); if (periodInput) { period = new Decimal(periodInput.value); } let lowerPriceDifferenceBPS = defaultFields.find((x) => x.label === 'lowerRangeBps')!.value; const lowerPriceDifferenceBPSInput = fieldOverrides.find((x) => x.label === 'lowerRangeBps'); if (lowerPriceDifferenceBPSInput) { lowerPriceDifferenceBPS = lowerPriceDifferenceBPSInput.value; } let upperPriceDifferenceBPS = defaultFields.find((x) => x.label === 'upperRangeBps')!.value; const upperPriceDifferenceBPSInput = fieldOverrides.find((x) => x.label === 'upperRangeBps'); if (upperPriceDifferenceBPSInput) { upperPriceDifferenceBPS = upperPriceDifferenceBPSInput.value; } return getPeriodicRebalanceRebalanceFieldInfos( price, period, new Decimal(lowerPriceDifferenceBPS), new Decimal(upperPriceDifferenceBPS) ); }; getFieldsForExpanderRebalanceMethod = async ( dex: Dex, fieldOverrides: RebalanceFieldInfo[], tokenAMint: Address, tokenBMint: Address, poolPrice?: Decimal ): Promise<RebalanceFieldInfo[]> => { const price = poolPrice ? poolPrice : new Decimal(await this.getPriceForPair(dex, tokenAMint, tokenBMint)); const defaultFields = getDefaultExpanderRebalanceFieldInfos(price); let lowerPriceDifferenceBPS = defaultFields.find((x) => x.label === 'lowerRangeBps')!.value; const lowerPriceDifferenceBPSInput = fieldOverrides.find((x) => x.label === 'lowerRangeBps'); if (lowerPriceDifferenceBPSInput) { lowerPriceDifferenceBPS = lowerPriceDifferenceBPSInput.value; } let upperPriceDifferenceBPS = defaultFields.find((x) => x.label === 'upperRangeBps')!.value; const upperPriceDifferenceBPSInput = fieldOverrides.find((x) => x.label === 'upperRangeBps'); if (upperPriceDifferenceBPSInput) { upperPriceDifferenceBPS = upperPriceDifferenceBPSInput.value; } let lowerResetPriceDifferenceBPS = defaultFields.find((x) => x.label === 'resetLowerRangeBps')!.value; const lowerResetPriceDifferenceBPSInput = fieldOverrides.find((x) => x.label === 'resetLowerRangeBps'); if (lowerResetPriceDifferenceBPSInput) { lowerResetPriceDifferenceBPS = lowerResetPriceDifferenceBPSInput.value; } let upperResetPriceDifferenceBPS = defaultFields.find((x) => x.label === 'resetUpperRangeBps')!.value; const upperResetPriceDifferenceBPSInput = fieldOverrides.find((x) => x.label === 'resetUpperRangeBps'); if (upperResetPriceDifferenceBPSInput) { upperResetPriceDifferenceBPS = upperResetPriceDifferenceBPSInput.value; } let expansionBPS = defaultFields.find((x) => x.label === 'expansionBps')!.value; const expansionBPSInput = fieldOverrides.find((x) => x.label === 'expansionBps'); if (expansionBPSInput) { expansionBPS = expansionBPSInput.value; } let maxNumberOfExpansions = defaultFields.find((x) => x.label === 'maxNumberOfExpansions')!.value; const maxNumberOfExpansionsInput = fieldOverrides.find((x) => x.label === 'maxNumberOfExpansions'); if (maxNumberOfExpansionsInput) { maxNumberOfExpansions = maxNumberOfExpansionsInput.value; } let swapUnevenAllowed = defaultFields.find((x) => x.label === 'swapUnevenAllowed')!.value; const swapUnevenAllowedInput = fieldOverrides.find((x) => x.label === 'swapUnevenAllowed'); if (swapUnevenAllowedInput) { swapUnevenAllowed = swapUnevenAllowedInput.value; } return getExpanderRebalanceFieldInfos( price, new Decimal(lowerPriceDifferenceBPS), new Decimal(upperPriceDifferenceBPS), new Decimal(lowerResetPriceDifferenceBPS), new Decimal(upperResetPriceDifferenceBPS), new Decimal(expansionBPS), new Decimal(maxNumberOfExpansions), new Decimal(swapUnevenAllowed) ); }; getFieldsForAutodriftRebalanceMethod = async ( dex: Dex, fieldOverrides: RebalanceFieldInfo[], tokenAMint: Address, tokenBMint: Address, tickSpacing: number, poolPrice?: Decimal ): Promise<RebalanceFieldInfo[]> => { const tokenADecimals = await getMintDecimals(this._rpc, tokenAMint); const tokenBDecimals = await getMintDecimals(this._rpc, tokenBMint); const price = poolPrice ? poolPrice : new Decimal(await this.getPriceForPair(dex, tokenAMint, tokenBMint)); // TODO: maybe we will need to get real staking price instead of pool price for this to be accurate. const defaultFields = getDefaultAutodriftRebalanceFieldInfos( dex, price, tokenADecimals, tokenBDecimals, tickSpacing ); const lastMidTick = defaultFields.find((x) => x.label === 'lastMidTick')!.value; let initDriftTicksPerEpoch = defaultFields.find((x) => x.label === 'initDriftTicksPerEpoch')!.value; const initDriftTicksPerEpochInput = fieldOverrides.find((x) => x.label === 'initDriftTicksPerEpoch'); if (initDriftTicksPerEpochInput) { initDriftTicksPerEpoch = initDriftTicksPerEpochInput.value; } let ticksBelowMid = defaultFields.find((x) => x.label === 'ticksBelowMid')!.value; const ticksBelowMidInput = fieldOverrides.find((x) => x.label === 'ticksBelowMid'); if (ticksBelowMidInput) { ticksBelowMid = ticksBelowMidInput.value; } let ticksAboveMid = defaultFields.find((x) => x.label === 'ticksAboveMid')!.value; const ticksAboveMidInput = fieldOverrides.find((x) => x.label === 'ticksAboveMid'); if (ticksAboveMidInput) { ticksAboveMid = ticksAboveMidInput.value; } let frontrunMultiplierBps = defaultFields.find((x) => x.label === 'frontrunMultiplierBps')!.value; const frontrunMultiplierBpsInput = fieldOverrides.find((x) => x.label === 'frontrunMultiplierBps'); if (frontrunMultiplierBpsInput) { frontrunMultiplierBps = frontrunMultiplierBpsInput.value; } let stakingRateASource = defaultFields.find((x) => x.label === 'stakingRateASource')!.value; const stakingRateASourceInput = fieldOverrides.find((x) => x.label === 'stakingRateASource'); if (stakingRateASourceInput) { stakingRateASource = stakingRateASourceInput.value; } let stakingRateBSource = defaultFields.find((x) => x.label === 'stakingRateBSource')!.value; const stakingRateBSourceInput = fieldOverrides.find((x) => x.label === 'stakingRateBSource'); if (stakingRateBSourceInput) { stakingRateBSource = stakingRateBSourceInput.value; } let initialDriftDirection = defaultFields.find((x) => x.label === 'initialDriftDirection')!.value; const initialDriftDirectionInput = fieldOverrides.find((x) => x.label === 'initialDriftDirection'); if (initialDriftDirectionInput) { initialDriftDirection = initialDriftDirectionInput.value; } const fieldInfos = getAutodriftRebalanceFieldInfos( dex, tokenADecimals, tokenBDecimals, tickSpacing, new Decimal(lastMidTick), new Decimal(initDriftTicksPerEpoch), new Decimal(ticksBelowMid), new Decimal(ticksAboveMid), new Decimal(frontrunMultiplierBps), new Decimal(stakingRateASource), new Decimal(stakingRateBSource), new Decimal(initialDriftDirection) ); return fieldInfos; }; /** * Get the price for a given pair of tokens in a given dex; The price comes from any pool having those tokens, not a specific one, so the price may not be exactly the same between different pools with the same tokens. For a specific pool price use getPoolPrice * @param strategy * @param amountA */ getPriceForPair = async (dex: Dex, poolTokenA: Address, poolTokenB: Address): Promise<number> => { if (dex === 'ORCA') { const pools = await this.getOrcaPoolsForTokens(poolTokenA, poolTokenB); if (pools.length === 0) { throw new Error(`No pool found for ${poolTokenA.toString()} and ${poolTokenB.toString()}`); } return Number(pools[0].price); } else if (dex === 'RAYDIUM') { const pools = await this.getRaydiumPoolsForTokens(poolTokenA, poolTokenB); if (pools.length === 0) { throw new Error(`No pool found for ${poolTokenA.toString()} and ${poolTokenB.toString()}`); } return pools[0].price; } else if (dex === 'METEORA') { const pools = await this.getMeteoraPoolsForTokens(poolTokenA, poolTokenB); if (pools.length === 0) { throw new Error(`No pool found for ${poolTokenA.toString()} and ${poolTokenB.toString()}`); } const decimalsX = await getMintDecimals(this._rpc, poolTokenA); const decimalsY = await getMintDecimals(this._rpc, poolTokenB); return getPriceOfBinByBinIdWithDecimals( pools[0].pool.activeId, pools[0].pool.binStep, decimalsX, decimalsY ).toNumber(); } else { throw new Error(`Dex ${dex} is not supported`); } }; getDefaultRebalanceFields = async ( dex: Dex, poolTokenA: Address, poolTokenB: Address, tickSpacing: number, rebalanceMethod: RebalanceMethod ): Promise<RebalanceFieldInfo[]> => { const price = new Decimal(await this.getPriceForPair(dex, poolTokenA, poolTokenB)); const tokenADecimals = await getMintDecimals(this._rpc, poolTokenA); const tokenBDecimals = await getMintDecimals(this._rpc, poolTokenB); switch (rebalanceMethod) { case ManualRebalanceMethod: return getDefaultManualRebalanceFieldInfos(price); case PricePercentageRebalanceMethod: return getDefaultPricePercentageRebalanceFieldInfos(price); case PricePercentageWithResetRangeRebalanceMethod: return getDefaultPricePercentageWithResetRebalanceFieldInfos(price); case DriftRebalanceMethod: return getDefaultDriftRebalanceFieldInfos(dex, tickSpacing, price, tokenADecimals, tokenBDecimals); case TakeProfitMethod: return getDefaultTakeProfitRebalanceFieldsInfos(price); case PeriodicRebalanceMethod: return getDefaultPeriodicRebalanceFieldInfos(price); case ExpanderMethod: return getDefaultExpanderRebalanceFieldInfos(price); case AutodriftMethod: return getDefaultAutodriftRebalanceFieldInfos(dex, price, tokenADecimals, tokenBDecimals, tickSpacing); default: throw new Error(`Rebalance method ${rebalanceMethod} is not supported`); } }; /** * Return a the pubkey of the pool in a given dex, for given mints and fee tier; if that pool doesn't exist, return default pubkey */ getPoolInitializedForDexPairTier = async ( dex: Dex, poolTokenA: Address, poolTokenB: Address, feeBPS: Decimal ): Promise<Address> => { if (dex === 'ORCA') { let pool: Address = DEFAULT_PUBLIC_KEY; const orcaPools = await this.getOrcaPoolsForTokens(poolTokenA, poolTokenB); orcaPools.forEach((element) => { if (element.feeRate * FullBPS === feeBPS.toNumber()) { pool = address(element.address); } }); return pool; } else if (dex === 'RAYDIUM') { const pools: Pool[] = []; const raydiumPools = await this.getRaydiumPoolsForTokens(poolTokenA, poolTokenB); raydiumPools.forEach((element) => { if (new Decimal(element.ammConfig.tradeFeeRate).div(FullBPS).div(FullPercentage).equals(feeBPS.div(FullBPS))) { pools.push(element); } }); if (pools.length === 0) { return DEFAULT_PUBLIC_KEY; } let pool = DEFAULT_PUBLIC_KEY; let tickSpacing = Number.MAX_VALUE; pools.forEach((element) => { if (element.ammConfig.tickSpacing < tickSpacing) { pool = address(element.id); tickSpacing = element.ammConfig.tickSpacing; } }); return pool; } else if (dex === 'METEORA') { let pool = DEFAULT_PUBLIC_KEY; const pools = await this.getMeteoraPoolsForTokens(poolTokenA, poolTokenB); pools.forEach((element) => { const feeRateBps = element.pool.parameters.baseFactor * element.pool.binStep; if (feeRateBps === feeBPS.toNumber()) { pool = address(element.key); } }); return pool; } else { throw new Error(`Dex ${dex} is not supported`); } }; /** * Return generic information for all pools in a given dex, for given mints and fee tier */ async getExistentPoolsForPair(dex: Dex, tokenMintA: Address, tokenMintB: Address): Promise<GenericPoolInfo[]> { if (dex === 'ORCA') { const pools = await this.getOrcaPoolsForTokens(tokenMintA, tokenMintB); const genericPoolInfos: GenericPoolInfo[] = await Promise.all( pools.map(async (pool: WhirlpoolAPIResponse) => { const positionsCount = new Decimal(await this.getPositionsCountForPool(dex, address(pool.address))); // read price from pool const poolData = await this._orcaService.getOrcaWhirlpool(address(pool.address)); if (!poolData) { throw new Error(`Pool ${pool.address} not found`); } const poolInfo: GenericPoolInfo = { dex, address: address(pool.address), price: new Decimal( orcaSqrtPriceToPrice(BigInt(poolData.sqrtPrice), pool.tokenA.decimals, pool.tokenB.decimals) ), tokenMintA: address(pool.tokenMintA), tokenMintB: address(pool.tokenMintB), tvl: pool.tvlUsdc ? new Decimal(pool.tvlUsdc) : undefined, feeRate: new Decimal(pool.feeRate).mul(FullBPS), volumeOnLast7d: pool.stats['7d'] ? new Decimal(pool.stats['7d'].volume) : undefined, tickSpacing: new Decimal(pool.tickSpacing), positions: positionsCount, }; return poolInfo; }) ); return genericPoolInfos; } else if (dex === 'RAYDIUM') { const pools = await this.getRaydiumPoolsForTokens(tokenMintA, tokenMintB); const genericPoolInfos: GenericPoolInfo[] = await Promise.all( pools.map(async (pool: Pool) => { const positionsCount = new Decimal(await this.getPositionsCountForPool(dex, address(pool.id))); const poolInfo: GenericPoolInfo = { dex, address: address(pool.id), price: new Decimal(pool.price), tokenMintA: address(pool.mintA), tokenMintB: address(pool.mintB), tvl: new Decimal(pool.tvl), feeRate: new Decimal(pool.ammConfig.tradeFeeRate).div(new Decimal(FullPercentage)), volumeOnLast7d: new Decimal(pool.week.volume), tickSpacing: new Decimal(pool.ammConfig.tickSpacing), positions: positionsCount, }; return poolInfo; }) ); return genericPoolInfos; } else if (dex === 'METEORA') { const pools = await this.getMeteoraPoolsForTokens(tokenMintA, tokenMintB); const genericPoolInfos: GenericPoolInfo[] = await Promise.all( pools.map(async (pool: MeteoraPool) => { const positionsCount = new Decimal(await this.getPositionsCountForPool(dex, pool.key)); const decimalsX = await getMintDecimals(this._rpc, pool.pool.tokenXMint); const decimalsY = await getMintDecimals(this._rpc, pool.pool.tokenYMint); const price = getPriceOfBinByBinIdWithDecimals(pool.pool.activeId, pool.pool.binStep, decimalsX, decimalsY); const poolInfo: GenericPoolInfo = { dex, address: pool.key, price, tokenMintA: pool.pool.tokenXMint, tokenMintB: pool.pool.tokenYMint, tvl: new Decimal(0), feeRate: computeMeteoraFee(pool.pool).div(1e2), // Transform it to rate volumeOnLast7d: new Decimal(0), tickSpacing: new Decimal(pool.pool.binStep), positions: positionsCount, }; return poolInfo; }) ); return genericPoolInfos; } else { throw new Error(`Dex ${dex} is not supported`); } } getOrcaPoolsForTokens = async (poolTokenA: Address, poolTokenB: Address): Promise<WhirlpoolAPIResponse[]> => { const pools: WhirlpoolAPIResponse[] = []; const poolTokenAString = poolTokenA.toString(); const poolTokenBString = poolTokenB.toString(); const whirlpools = await this._orcaService.getOrcaWhirlpools(); whirlpools.forEach((element) => { if ( (element.tokenMintA === poolTokenAString && element.tokenMintB === poolTokenBString) || (element.tokenMintA === poolTokenBString && element.tokenMintB === poolTokenAString) ) pools.push(element); }); return pools; }; getRaydiumPoolsForTokens = async (poolTokenA: Address, poolTokenB: Address): Promise<Pool[]> => { const pools: Pool[] = []; const poolTokenAString = poolTokenA.toString(); const poolTokenBString = poolTokenB.toString(); const raydiumPools = await this._raydiumService.getRaydiumWhirlpools(); raydiumPools.data.forEach((element) => { if ( (element.mintA === poolTokenAString && element.mintB === poolTokenBString) || (element.mintA === poolTokenBString && element.mintB === poolTokenAString) ) { pools.push(element); } }); return pools; }; getMeteoraPoolsForTokens = async (poolTokenA: Address, poolTokenB: Address): Promise<MeteoraPool[]> => { const pools: MeteoraPool[] = []; const meteoraPools = await this._meteoraService.getMeteoraPools(); meteoraPools.forEach((element) => { if ( (element.pool.tokenXMint === poolTokenA && element.pool.tokenYMint === poolTokenB) || (element.pool.tokenXMint === poolTokenB && element.pool.tokenYMint === poolTokenA) ) { pools.push(element); } }); return pools; }; /** * Return a list of all Kamino whirlpool strategies * @param strategies Limit results to these strategy addresses */ getStrategies = async (strategies?: Array<Address>): Promise<Array<WhirlpoolStrategy | null>> => { if (!strategies) { strategies = (await this.getAllStrategiesWithFilters({})).map((x) => x.address); } return await batchFetch(strategies, (chunk) => this.getWhirlpoolStrategies(chunk)); }; /** * Return a list of all Kamino whirlpool strategies with their addresses * @param strategies Limit results to these strategy addresses */ getStrategiesWithAddresses = async (strategies?: Array<Address>): Promise<Array<StrategyWithAddress>> => { if (!strategies) { return this.getAllStrategiesWithFilters({}); } const result: StrategyWithAddress[] = []; const states = await batchFetch(strategies, (chunk) => this.getWhirlpoolStrategies(chunk)); for (let i = 0; i < strategies.length; i++) { if (states[i]) { result.push({ address: strategies[i], strategy: states[i]! }); } else { throw Error(`Could not fetch strategy state for ${strategies[i].toString()}`); } } return result; }; private buildStrategiesGpaFilters( strategyFilters: StrategiesFilters ): (GetProgramAccountsDatasizeFilter | GetProgramAccountsMemcmpFilter)[] { const filters: (GetProgramAccountsDatasizeFilter | GetProgramAccountsMemcmpFilter)[] = []; filters.push({ dataSize: BigInt(getWhirlpoolStrategySize()), }); filters.push({ memcmp: { offset: 0n, bytes: bs58.encode(WHIRLPOOL_STRATEGY_DISCRIMINATOR) as Base58EncodedBytes, encoding: 'base58', }, }); if (strategyFilters.owner) { filters.push({ memcmp: { offset: 8n, bytes: strategyFilters.owner.toString() as Base58EncodedBytes, encoding: 'base58', }, }); } if (strategyFilters.strategyCreationStatus) { filters.push({ memcmp: { offset: 1625n, bytes: strategyCreationStatusToBase58(strategyFilters.strategyCreationStatus) as Base58EncodedBytes, encoding: 'base58', }, }); } if (strategyFilters.strategyType) { filters.push({ memcmp: { offset: 1120n, bytes: strategyTypeToBase58(strategyFilters.strategyType).toString() as Base58EncodedBytes, encoding: 'base58', }, }); } if (strategyFilters.isCommunity !== undefined && strategyFilters.isCommunity !== null) { const value = !strategyFilters.isCommunity ? '1' : '2'; filters.push({ memcmp: { offset: 1664n, bytes: value as Base58EncodedBytes, encoding: 'base58', }, }); } return filters; } /** * Streaming variant of {@link getAllStrategiesWithFilters}. Decodes one * strategy at a time and yields control to the event loop every * `yieldEvery` items (default 100) via `setImmediate` (or `setTimeout(0)` * outside Node) so other I/O can make progress while ~5600 strategies * decode. * * @param strategyFilters * @param