UNPKG

@kamino-finance/kliquidity-sdk

Version:

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

1,352 lines (1,246 loc) 309 kB
import { getConfigByCluster, HubbleConfig, SolanaCluster } from '@hubbleprotocol/hubble-config'; import { Account, AccountRole, address, Address, Base58EncodedBytes, fetchEncodedAccounts, generateKeyPairSigner, getAddressEncoder, GetProgramAccountsDatasizeFilter, GetProgramAccountsMemcmpFilter, getProgramDerivedAddress, IInstruction, isAddress, isSome, JsonParsedTokenAccount, none, Option, Rpc, Slot, SolanaRpcApi, some, TransactionSigner, } from '@solana/kit'; import bs58 from 'bs58'; import { CollateralInfos, GlobalConfig, TermsSignature, WhirlpoolStrategy } from './@codegen/kliquidity/accounts'; import Decimal from 'decimal.js'; import { initializeTickArray, InitializeTickArrayAccounts, InitializeTickArrayArgs, } from './@codegen/whirlpools/instructions'; import { Position as OrcaPosition, TickArray, Whirlpool } 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, OraclePricesAndCollateralInfos, 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, strategyCreationStatusToBase58, strategyTypeToBase58, stripTwapZeros, SwapperIxBuilder, TokensBalances, VaultParameters, WithdrawAllAndCloseIxns, WithdrawShares, ZERO, getNearestValidTickIndexFromTickIndex as orcaGetNearestValidTickIndexFromTickIndex, getIncreaseLiquidityQuote, defaultSlippagePercentageBPS, } from './utils'; import { checkExpectedVaultsBalances, CheckExpectedVaultsBalancesAccounts, CheckExpectedVaultsBalancesArgs, closeStrategy, CloseStrategyAccounts, collectFeesAndRewards, CollectFeesAndRewardsAccounts, deposit, DepositAccounts, DepositArgs, executiveWithdraw, ExecutiveWithdrawAccounts, ExecutiveWithdrawArgs, initializeStrategy, InitializeStrategyAccounts, InitializeStrategyArgs, invest, InvestAccounts, openLiquidityPosition, OpenLiquidityPositionAccounts, OpenLiquidityPositionArgs, signTerms, SignTermsAccounts, SignTermsArgs, singleTokenDepositWithMin, SingleTokenDepositWithMinAccounts, SingleTokenDepositWithMinArgs, updateRewardMapping, UpdateRewardMappingAccounts, UpdateRewardMappingArgs, updateStrategyConfig, UpdateStrategyConfigAccounts, UpdateStrategyConfigArgs, withdraw, WithdrawAccounts, WithdrawArgs, withdrawFromTopup, WithdrawFromTopupAccounts, WithdrawFromTopupArgs, } from './@codegen/kliquidity/instructions'; import BN from 'bn.js'; import StrategyWithAddress from './models/StrategyWithAddress'; import { Rebalancing, Uninitialized } from './@codegen/kliquidity/types/StrategyStatus'; import { FRONTEND_KAMINO_STRATEGY_URL, METADATA_PROGRAM_ID, U64_MAX, ZERO_BN } from './constants'; import { CollateralInfo, ExecutiveWithdrawActionKind, RebalanceType, RebalanceTypeKind, ReferencePriceTypeKind, StrategyConfigOption, StrategyStatusKind, } from './@codegen/kliquidity/types'; import { AmmConfig, PersonalPositionState, PoolState } from './@codegen/raydium/accounts'; import { OrcaService, RaydiumService, Whirlpool as WhirlpoolAPIResponse, WhirlpoolAprApy } from './services'; import { Pool } from './services/RaydiumPoolsResponse'; import { UpdateCollectFeesFee, UpdateLookupTable, UpdateRebalanceType, UpdateReferencePriceType, UpdateReward0Fee, UpdateReward1Fee, UpdateReward2Fee, UpdateWithdrawFee, } from './@codegen/kliquidity/types/StrategyConfigOption'; 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 { DEFAULT_JUP_API_ENDPOINT, JupService } from './services/JupService'; import { simulateManualPool, simulatePercentagePool, SimulationPercentagePoolParameters, } from './services/PoolSimulationService'; import { Autodrift, Drift, Expander, Manual, PeriodicRebalance, PricePercentage, PricePercentageWithReset, TakeProfit, } from './@codegen/kliquidity/types/RebalanceType'; 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 { BinArray, LbPair, PositionV2 } from './@codegen/meteora/accounts'; import LbPairWithAddress from './models/LbPairWithAddress'; import { initializeBinArray, InitializeBinArrayAccounts, InitializeBinArrayArgs, } from './@codegen/meteora/instructions'; import { getPdaProtocolPositionAddress, i32ToBytes, LiquidityMath as RaydiumLiquidityMath, SqrtPriceMath as RaydiumSqrtPriceMath, TickMath as RaydiumTickMath, TickUtils as RaydiumTickUtils, } from '@raydium-io/raydium-sdk-v2/lib'; 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 { Connection } from '@solana/web3.js'; import { toLegacyPublicKey } from './utils/compat'; import { IncreaseLiquidityQuoteParam } from '@orca-so/whirlpools'; const addressEncoder = getAddressEncoder(); export class Kamino { private readonly _cluster: SolanaCluster; private readonly _rpc: Rpc<SolanaRpcApi>; private readonly _legacyConnection: Connection; 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 _jupBaseAPI: string = DEFAULT_JUP_API_ENDPOINT; /** * Create a new instance of the Kamino SDK class. * @param cluster Name of the Solana cluster * @param rpc Connection to the Solana cluster * @param legacyConnection 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>, legacyConnection: Connection, globalConfig?: Address, programId?: Address, whirlpoolProgramId?: Address, raydiumProgramId?: Address, meteoraProgramId?: Address, jupBaseAPI?: string ) { this._cluster = cluster; this._rpc = rpc; this._legacyConnection = legacyConnection; 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._scope = new Scope(cluster, rpc); this._orcaService = new OrcaService(rpc, legacyConnection, whirlpoolProgramId); this._raydiumService = new RaydiumService(rpc, legacyConnection, raydiumProgramId); this._meteoraService = new MeteoraService(rpc, meteoraProgramId); if (jupBaseAPI) { this._jupBaseAPI = jupBaseAPI; } } getConnection = () => this._rpc; getLegacyConnection = () => this._legacyConnection; 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); }; getDisabledTokensPrices = async (collateralInfos?: CollateralInfo[]) => { const collInfos = collateralInfos ? collateralInfos : await this.getCollateralInfos(); const disabledTokens = collInfos.filter((x) => x.disabled && x.mint !== DEFAULT_PUBLIC_KEY); return JupService.getDollarPrices( disabledTokens.map((x) => x.mint), this._jupBaseAPI ); }; 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[]): RebalanceTypeKind => { 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; }; getAllStrategiesWithFilters = async (strategyFilters: StrategiesFilters): Promise<Array<StrategyWithAddress>> => { const filters: (GetProgramAccountsDatasizeFilter | GetProgramAccountsMemcmpFilter)[] = []; filters.push({ dataSize: BigInt(WhirlpoolStrategy.layout.span + 8), }); filters.push({ memcmp: { offset: 0n, bytes: bs58.encode(WhirlpoolStrategy.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 ( await this._rpc .getProgramAccounts(this.getProgramID(), { filters, encoding: 'base64', }) .send() ).map((x) => { const res: StrategyWithAddress = { strategy: WhirlpoolStrategy.decode(Buffer.from(x.account.data[0], 'base64')), address: x.pubkey, }; return res; }); }; /** * Get a Kamino whirlpool strategy by its public key address * @param address */ getStrategyByAddress = (address: Address) => this.getWhirlpoolStrategy(address); /** * Get a Kamino whirlpool strategy by its kToken mint address * @param kTokenMint - mint address of the kToken */ getStrategyByKTokenMint = async (kTokenMint: Address): Promise<StrategyWithAddress | null> => { const filters: (GetProgramAccountsDatasizeFilter | GetProgramAccountsMemcmpFilter)[] = [ { dataSize: BigInt(WhirlpoolStrategy.layout.span + 8), }, { memcmp: { offset: 0n, bytes: bs58.encode(WhirlpoolStrategy.discriminator) as Base58EncodedBytes, encoding: 'base58', }, }, { memcmp: { bytes: kTokenMint.toString() as Base58EncodedBytes, offset: 720n, encoding: 'base58', }, }, ]; const matchingStrategies = await this._rpc .getProgramAccounts(this.getProgramID(), { filters, encoding: 'base64', }) .send(); if (matchingStrategies.length === 0) { return null; } if (matchingStrategies.length > 1) { throw new Error( `Multiple strategies found for kToken mint: ${kTokenMint}. Strategies found: ${matchingStrategies.map( (x) => x.pubkey )}` ); } const decodedStrategy = WhirlpoolStrategy.decode(Buffer.from(matchingStrategies[0].account.data[