@kamino-finance/kliquidity-sdk
Version:
Typescript SDK for interacting with the Kamino Liquidity (kliquidity) protocol
1,352 lines (1,246 loc) • 309 kB
text/typescript
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[