UNPKG

kamino-sdk-beta

Version:

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

1,326 lines (1,219 loc) 303 kB
import { getConfigByCluster, HubbleConfig, SolanaCluster } from '@hubbleprotocol/hubble-config'; import { AccountInfo, Connection, GetProgramAccountsFilter, PublicKey, SystemProgram, SYSVAR_INSTRUCTIONS_PUBKEY, SYSVAR_RENT_PUBKEY, TransactionInstruction, AddressLookupTableAccount, MessageV0, TransactionMessage, Keypair, AddressLookupTableProgram, Transaction, ParsedAccountData, } from '@solana/web3.js'; import bs58 from 'bs58'; import { setKaminoProgramId } from './kamino-client/programId'; import { GlobalConfig, TermsSignature, WhirlpoolStrategy, CollateralInfos } from './kamino-client/accounts'; import Decimal from 'decimal.js'; import { initializeTickArray, InitializeTickArrayAccounts, InitializeTickArrayArgs, Position, TickArray, Whirlpool, } from './whirlpools-client'; import { AddLiquidityQuote, AddLiquidityQuoteParam, defaultSlippagePercentage, getNearestValidTickIndexFromTickIndex, getNextValidTickIndex, getStartTickIndex, OrcaNetwork, OrcaWhirlpoolClient, Percentage, priceToTickIndex, sqrtPriceX64ToPrice, tickIndexToPrice, } from '@orca-so/whirlpool-sdk'; import { OrcaDAL } from '@orca-so/whirlpool-sdk/dist/dal/orca-dal'; import { OrcaPosition } from '@orca-so/whirlpool-sdk/dist/position/orca-position'; import { Data, getEmptyShareData, Holdings, KaminoPosition, KaminoStrategyWithShareMint, MintToPriceMap, ShareData, ShareDataWithAddress, StrategyBalances, StrategyBalanceWithAddress, StrategyHolder, StrategyProgramAddress, StrategyVaultTokens, StrategyWithPendingFees, TokenAmounts, TokenHoldings, TotalStrategyVaultTokens, TreasuryFeeVault, } from './models'; import { setWhirlpoolsProgramId } from './whirlpools-client/programId'; import { OraclePrices, Scope } from '@kamino-finance/scope-sdk'; import { batchFetch, collToLamportsDecimal, createAssociatedTokenAccountInstruction, DepositAmountsForSwap, Dex, dexToNumber, GenericPoolInfo, GenericPositionRangeInfo, getAssociatedTokenAddress, getAssociatedTokenAddressAndData, getDexProgramId, getReadOnlyWallet, buildStrategyRebalanceParams, getUpdateStrategyConfigIx, LiquidityDistribution, numberToRebalanceType, RebalanceFieldInfo, sendTransactionWithLogs, StrategiesFilters, strategyCreationStatusToBase58, strategyTypeToBase58, VaultParameters, ZERO, PositionRange, numberToDex, TokensBalances, isSOLMint, SwapperIxBuilder, lamportsToNumberDecimal, DECIMALS_SOL, InstructionsWithLookupTables, ProfiledFunctionExecution, noopProfiledFunctionExecution, MaybeTokensBalances, PerformanceFees, PriceReferenceType, InputRebalanceFieldInfo, getTickArray, rebalanceFieldsDictToInfo, InitStrategyIxs, WithdrawShares, MetadataProgramAddressesOrca, MetadataProgramAddressesRaydium, LowerAndUpperTickPubkeys, isVaultInitialized, WithdrawAllAndCloseIxns, InitPoolTickIfNeeded, numberToReferencePriceType, stripTwapZeros, getTokenNameFromCollateralInfo, keyOrDefault, getMintDecimals, } from './utils'; import { ASSOCIATED_TOKEN_PROGRAM_ID, createCloseAccountInstruction, TOKEN_PROGRAM_ID, unpackMint, } from '@solana/spl-token'; import { checkExpectedVaultsBalances, CheckExpectedVaultsBalancesAccounts, CheckExpectedVaultsBalancesArgs, closeStrategy, CloseStrategyAccounts, collectFeesAndRewards, CollectFeesAndRewardsAccounts, deposit, DepositAccounts, DepositArgs, executiveWithdraw, ExecutiveWithdrawAccounts, ExecutiveWithdrawArgs, initializeStrategy, InitializeStrategyAccounts, InitializeStrategyArgs, invest, InvestAccounts, openLiquidityPosition, OpenLiquidityPositionAccounts, OpenLiquidityPositionArgs, singleTokenDepositWithMin, SingleTokenDepositWithMinAccounts, SingleTokenDepositWithMinArgs, updateRewardMapping, UpdateRewardMappingAccounts, UpdateRewardMappingArgs, updateStrategyConfig, UpdateStrategyConfigAccounts, UpdateStrategyConfigArgs, withdraw, WithdrawAccounts, WithdrawArgs, withdrawFromTopup, WithdrawFromTopupAccounts, WithdrawFromTopupArgs, } from './kamino-client/instructions'; import BN from 'bn.js'; import StrategyWithAddress from './models/StrategyWithAddress'; import { Idl, Program, AnchorProvider } from '@coral-xyz/anchor'; import { Rebalancing, Uninitialized } from './kamino-client/types/StrategyStatus'; import { FRONTEND_KAMINO_STRATEGY_URL, METADATA_PROGRAM_ID, U64_MAX } from './constants'; import { CollateralInfo, ExecutiveWithdrawActionKind, RebalanceType, RebalanceTypeKind, ReferencePriceTypeKind, StrategyConfigOption, StrategyStatusKind, } from './kamino-client/types'; import { AmmConfig, PersonalPositionState, PoolState } from './raydium_client'; import { setRaydiumProgramId } from './raydium_client/programId'; import { getPdaProtocolPositionAddress, i32ToBytes, LiquidityMath, SqrtPriceMath, TickMath, TickUtils, } from '@raydium-io/raydium-sdk'; import KaminoIdl from './kamino-client/idl.json'; import { OrcaService, RaydiumService, Whirlpool as OrcaPool, WhirlpoolAprApy } from './services'; import { getAddLiquidityQuote, InternalAddLiquidityQuote, InternalAddLiquidityQuoteParam, } from '@orca-so/whirlpool-sdk/dist/position/quotes/add-liquidity'; import { signTerms, SignTermsAccounts, SignTermsArgs } from './kamino-client/instructions'; import { Pool } from './services/RaydiumPoolsResponse'; import { UpdateWithdrawFee, UpdateReward0Fee, UpdateReward1Fee, UpdateReward2Fee, UpdateCollectFeesFee, UpdateRebalanceType, UpdateLookupTable, UpdateReferencePriceType, } from './kamino-client/types/StrategyConfigOption'; import { DefaultPerformanceFeeBps } from './constants/DefaultStrategyConfig'; import { ADDRESS_LUT_PROGRAM_ID, CONSENSUS_ID, LUT_OWNER_KEY, STAGING_GLOBAL_CONFIG, STAGING_KAMINO_PROGRAM_ID, MEMO_PROGRAM_ID, TOKEN_2022_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 { DOLAR_BASED, PROPORTION_BASED } from './constants/deposit_method'; import { JupService } from './services/JupService'; import { simulateManualPool, simulatePercentagePool, SimulationPercentagePoolParameters, } from './services/PoolSimulationService'; import { Expander, Manual, PricePercentage, PricePercentageWithReset, Drift, TakeProfit, PeriodicRebalance, Autodrift, } from './kamino-client/types/RebalanceType'; import { checkIfAccountExists, createWsolAtaIfMissing, getAtasWithCreateIxnsIfMissing, MAX_ACCOUNTS_PER_TRANSACTION, removeBudgetAndAtaIxns, } from './utils/transactions'; import { SwapResponse } from '@jup-ag/api'; import { StrategyPrices } from './models'; import { getDefaultManualRebalanceFieldInfos, getManualRebalanceFieldInfos } from './rebalance_methods'; import { deserializePricePercentageRebalanceFromOnchainParams, deserializePricePercentageRebalanceWithStateOverride, getDefaultPricePercentageRebalanceFieldInfos, getPositionRangeFromPercentageRebalanceParams, getPricePercentageRebalanceFieldInfos, readPricePercentageRebalanceParamsFromStrategy, readRawPricePercentageRebalanceStateFromStrategy, } from './rebalance_methods'; import { deserializePricePercentageWithResetRebalanceFromOnchainParams, deserializePricePercentageWithResetRebalanceWithStateOverride, getDefaultPricePercentageWithResetRebalanceFieldInfos, getPositionRangeFromPricePercentageWithResetParams, getPricePercentageWithResetRebalanceFieldInfos, readPricePercentageWithResetRebalanceParamsFromStrategy, readRawPricePercentageWithResetRebalanceStateFromStrategy, } from './rebalance_methods'; import { deserializeDriftRebalanceFromOnchainParams, deserializeDriftRebalanceWithStateOverride, getDefaultDriftRebalanceFieldInfos, getDriftRebalanceFieldInfos, getPositionRangeFromDriftParams, readDriftRebalanceParamsFromStrategy, readRawDriftRebalanceStateFromStrategy, } from './rebalance_methods'; import { deserializeExpanderRebalanceWithStateOverride, deserializePeriodicRebalanceFromOnchainParams, deserializeTakeProfitRebalanceFromOnchainParams, getDefaultExpanderRebalanceFieldInfos, getDefaultPeriodicRebalanceFieldInfos, getDefaultTakeProfitRebalanceFieldsInfos, getExpanderRebalanceFieldInfos, getPeriodicRebalanceRebalanceFieldInfos, getPositionRangeFromExpanderParams, getPositionRangeFromPeriodicRebalanceParams, getTakeProfitRebalanceFieldsInfos, readExpanderRebalanceFieldInfosFromStrategy, readExpanderRebalanceParamsFromStrategy, readPeriodicRebalanceRebalanceParamsFromStrategy, readPeriodicRebalanceRebalanceStateFromStrategy, readRawExpanderRebalanceStateFromStrategy, 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 { deserializeAutodriftRebalanceWithStateOverride, deserializeAutodriftRebalanceFromOnchainParams, readAutodriftRebalanceParamsFromStrategy, readRawAutodriftRebalanceStateFromStrategy, getAutodriftRebalanceFieldInfos, getDefaultAutodriftRebalanceFieldInfos, getPositionRangeFromAutodriftParams, } from './rebalance_methods/autodriftRebalance'; import { KaminoPrices, OraclePricesAndCollateralInfos } from './models'; import { getRemoveLiquidityQuote } from './whirlpools-client/shim/remove-liquidity'; import { setMeteoraProgramId } from './meteora_client/programId'; import { computeMeteoraFee, MeteoraPool, MeteoraService } from './services/MeteoraService'; import { binIdToBinArrayIndex, deriveBinArray, getBinFromBinArray, getBinFromBinArrays, getBinIdFromPriceWithDecimals, getPriceOfBinByBinIdWithDecimals, MeteoraPosition, } from './utils/meteora'; import { BinArray, LbPair, PositionV2 } from './meteora_client/accounts'; import LbPairWithAddress from './models/LbPairWithAddress'; import { initializeBinArray, InitializeBinArrayAccounts, InitializeBinArrayArgs } from './meteora_client/instructions'; import { PubkeyHashMap } from './utils/pubkey'; export const KAMINO_IDL = KaminoIdl; export class Kamino { private readonly _cluster: SolanaCluster; private readonly _connection: Connection; readonly _config: HubbleConfig; private _globalConfig: PublicKey; private readonly _scope: Scope; private readonly _provider: AnchorProvider; private readonly _kaminoProgram: Program; private readonly _kaminoProgramId: PublicKey; private readonly _orcaService: OrcaService; private readonly _raydiumService: RaydiumService; private readonly _meteoraService: MeteoraService; /** * Create a new instance of the Kamino SDK class. * @param cluster Name of the Solana cluster * @param connection 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 */ constructor( cluster: SolanaCluster, connection: Connection, globalConfig?: PublicKey, programId?: PublicKey, whirlpoolProgramId?: PublicKey, raydiumProgramId?: PublicKey, meteoraProgramId?: PublicKey ) { this._cluster = cluster; this._connection = connection; this._config = getConfigByCluster(cluster); this._provider = new AnchorProvider(connection, getReadOnlyWallet(), { commitment: connection.commitment, }); if (programId && programId.equals(STAGING_KAMINO_PROGRAM_ID)) { this._kaminoProgramId = programId; this._globalConfig = STAGING_GLOBAL_CONFIG; } else { this._kaminoProgramId = programId ? programId : this._config.kamino.programId; this._globalConfig = globalConfig ? globalConfig : new PublicKey(this._config.kamino.globalConfig); } this._kaminoProgram = new Program(KAMINO_IDL as Idl, this._kaminoProgramId, this._provider); this._scope = new Scope(cluster, connection); setKaminoProgramId(this._kaminoProgramId); if (whirlpoolProgramId) { setWhirlpoolsProgramId(whirlpoolProgramId); } if (raydiumProgramId) { setRaydiumProgramId(raydiumProgramId); } if (meteoraProgramId) { setMeteoraProgramId(meteoraProgramId); } this._orcaService = new OrcaService(connection, cluster, whirlpoolProgramId); this._raydiumService = new RaydiumService(connection, raydiumProgramId); this._meteoraService = new MeteoraService(connection, meteoraProgramId); } getConnection = () => this._connection; getProgramID = () => this._kaminoProgramId; getProgram = () => this._kaminoProgram; setGlobalConfig = (globalConfig: PublicKey) => { this._globalConfig = globalConfig; }; getGlobalConfig = () => this._globalConfig; getDepositableTokens = async (): Promise<CollateralInfo[]> => { const collateralInfos = await this.getCollateralInfos(); return collateralInfos.filter((x) => !x.mint.equals(SystemProgram.programId)); }; 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); }; 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: PublicKey | StrategyWithAddress): Promise<PriceReferenceType> => { const strategyWithAddress = await this.getStrategyStateIfNotFetched(strategy); return numberToReferencePriceType(strategyWithAddress.strategy.rebalanceRaw.referencePriceType); }; getFieldsForRebalanceMethod = ( rebalanceMethod: RebalanceMethod, dex: Dex, fieldOverrides: RebalanceFieldInfo[], tokenAMint: PublicKey, tokenBMint: PublicKey, 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: PublicKey, tokenBMint: PublicKey, 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: PublicKey, tokenBMint: PublicKey, 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: PublicKey, tokenBMint: PublicKey, 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: PublicKey, tokenBMint: PublicKey, poolPrice?: Decimal ): Promise<RebalanceFieldInfo[]> => { const tokenADecimals = await getMintDecimals(this._connection, tokenAMint); const tokenBDecimals = await getMintDecimals(this._connection, 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: PublicKey, tokenBMint: PublicKey, 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: PublicKey, tokenBMint: PublicKey, 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: PublicKey, tokenBMint: PublicKey, 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: PublicKey, tokenBMint: PublicKey, tickSpacing: number, poolPrice?: Decimal ): Promise<RebalanceFieldInfo[]> => { const tokenADecimals = await getMintDecimals(this._connection, tokenAMint); const tokenBDecimals = await getMintDecimals(this._connection, 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: PublicKey, poolTokenB: PublicKey): 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 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._connection, poolTokenA); const decimalsY = await getMintDecimals(this._connection, 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: PublicKey, poolTokenB: PublicKey, tickSpacing: number, rebalanceMethod: RebalanceMethod ): Promise<RebalanceFieldInfo[]> => { const price = new Decimal(await this.getPriceForPair(dex, poolTokenA, poolTokenB)); const tokenADecimals = await getMintDecimals(this._connection, poolTokenA); const tokenBDecimals = await getMintDecimals(this._connection, 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: PublicKey, poolTokenB: PublicKey, feeBPS: Decimal ): Promise<PublicKey> => { if (dex == 'ORCA') { let pool = PublicKey.default; const orcaPools = await this.getOrcaPoolsForTokens(poolTokenA, poolTokenB); orcaPools.forEach((element) => { if (element.lpFeeRate * FullBPS == feeBPS.toNumber()) { pool = new PublicKey(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 PublicKey.default; } let pool = PublicKey.default; let tickSpacing = Number.MAX_VALUE; pools.forEach((element) => { if (element.ammConfig.tickSpacing < tickSpacing) { pool = new PublicKey(element.id); tickSpacing = element.ammConfig.tickSpacing; } }); return pool; } else if (dex == 'METEORA') { let pool = PublicKey.default; const pools = await this.getMeteoraPoolsForTokens(poolTokenA, poolTokenB); pools.forEach((element) => { const feeRateBps = element.pool.parameters.baseFactor * element.pool.binStep; if (feeRateBps == feeBPS.toNumber()) { pool = new PublicKey(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: PublicKey, tokenMintB: PublicKey): Promise<GenericPoolInfo[]> { if (dex == 'ORCA') { const pools = await this.getOrcaPoolsForTokens(tokenMintA, tokenMintB); const genericPoolInfos: GenericPoolInfo[] = await Promise.all( pools.map(async (pool: OrcaPool) => { const positionsCount = new Decimal(await this.getPositionsCountForPool(dex, new PublicKey(pool.address))); // read price from pool const poolData = await this._orcaService.getPool(new PublicKey(pool.address)); if (!poolData) { throw new Error(`Pool ${pool.address} not found`); } const poolInfo: GenericPoolInfo = { dex, address: new PublicKey(pool.address), price: poolData.price, tokenMintA: new PublicKey(pool.tokenA.mint), tokenMintB: new PublicKey(pool.tokenB.mint), tvl: pool.tvl ? new Decimal(pool.tvl) : undefined, feeRate: new Decimal(pool.lpFeeRate).mul(FullBPS), volumeOnLast7d: pool.volume ? new Decimal(pool.volume.week) : 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, new PublicKey(pool.id))); const poolInfo: GenericPoolInfo = { dex, address: new PublicKey(pool.id), price: new Decimal(pool.price), tokenMintA: new PublicKey(pool.mintA), tokenMintB: new PublicKey(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._connection, pool.pool.tokenXMint); const decimalsY = await getMintDecimals(this._connection, 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: PublicKey, poolTokenB: PublicKey): Promise<OrcaPool[]> => { const pools: OrcaPool[] = []; const poolTokenAString = poolTokenA.toString(); const poolTokenBString = poolTokenB.toString(); const whirlpools = await this._orcaService.getOrcaWhirlpools(); whirlpools.whirlpools.forEach((element) => { if ( (element.tokenA.mint == poolTokenAString && element.tokenB.mint == poolTokenBString) || (element.tokenA.mint == poolTokenBString && element.tokenB.mint == poolTokenAString) ) pools.push(element); }); return pools; }; getRaydiumPoolsForTokens = async (poolTokenA: PublicKey, poolTokenB: PublicKey): 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: PublicKey, poolTokenB: PublicKey): Promise<MeteoraPool[]> => { const pools: MeteoraPool[] = []; const meteoraPools = await this._meteoraService.getMeteoraPools(); meteoraPools.forEach((element) => { if ( (element.pool.tokenXMint.equals(poolTokenA) && element.pool.tokenYMint.equals(poolTokenB)) || (element.pool.tokenXMint.equals(poolTokenB) && element.pool.tokenYMint.equals(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<PublicKey>): 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<PublicKey>): 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: GetProgramAccountsFilter[] = []; filters.push({ dataSize: WhirlpoolStrategy.layout.span + 8, }); filters.push({ memcmp: { offset: 0, bytes: bs58.encode(WhirlpoolStrategy.discriminator), }, }); if (strategyFilters.owner) { filters.push({ memcmp: { bytes: strategyFilters.owner.toBase58(), offset: 8, }, }); } if (strategyFilters.strategyCreationStatus) { filters.push({ memcmp: { bytes: strategyCreationStatusToBase58(strategyFilters.strategyCreationStatus), offset: 1625, }, }); } if (strategyFilters.strategyType) { filters.push({ memcmp: { bytes: strategyTypeToBase58(strategyFilters.strategyType).toString(), offset: 1120, }, }); } if (strategyFilters.isCommunity !== undefined && strategyFilters.isCommunity !== null) { const value = !strategyFilters.isCommunity ? '1' : '2'; filters.push({ memcmp: { bytes: value, offset: 1664, }, }); } return ( await this._connection.getProgramAccounts(this._kaminoProgramId, { filters, }) ).map((x) => { const res: StrategyWithAddress = { strategy: WhirlpoolStrategy.decode(x.account.data), address: x.pubkey, }; return res; }); }; /** * Get a Kamino whirlpool strategy by its public key address * @param address */ getStrategyByAddress = (address: PublicKey) => this.getWhirlpoolStrategy(address); /** * Get a Kamino whirlpool strategy by its kToken mint address * @param kTokenMint - mint address of the kToken */ getStrategyByKTokenMint = async (kTokenMint: PublicKey): Promise<StrategyWithAddress | null> => { const matchingStrategies = await this._connection.getProgramAccounts(this._kaminoProgram.programId, { filters: [ { dataSize: WhirlpoolStrategy.layout.span + 8, }, { memcmp: { offset: 0, bytes: bs58.encode(WhirlpoolStrategy.discriminator), }, }, { memcmp: { bytes: kTokenMint.toBase58(), offset: 720, }, }, ], }); if (matchingStrategies.length === 0) { return null; } if (matchingStrategies.length > 1) { throw new Error( `Multiple strategies found for kToken mint: ${kTokenMint.toBase58()}. Strategies found: ${