UNPKG

@marinade.finance/kamino-sdk

Version:
1,347 lines (1,230 loc) 216 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, } from '@solana/web3.js'; import { setKaminoProgramId } from './kamino-client/programId'; import { GlobalConfig, TermsSignature, WhirlpoolStrategy, WhirlpoolStrategyFields, CollateralInfos, } from './kamino-client/accounts'; import Decimal from 'decimal.js'; import { initializeTickArray, InitializeTickArrayAccounts, InitializeTickArrayArgs, Position, TickArray, Whirlpool, } from './whirpools-client'; import { AddLiquidityQuote, AddLiquidityQuoteParam, defaultSlippagePercentage, getNearestValidTickIndexFromTickIndex, getNextValidTickIndex, getRemoveLiquidityQuote, 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, ShareData, ShareDataWithAddress, StrategyBalances, StrategyBalanceWithAddress, StrategyHolder, StrategyProgramAddress, StrategyVaultTokens, TokenAmounts, TokenHoldings, TotalStrategyVaultTokens, TreasuryFeeVault, } from './models'; import { PROGRAM_ID_CLI as WHIRLPOOL_PROGRAM_ID, setWhirlpoolsProgramId } from './whirpools-client/programId'; import { OraclePrices, Scope } from '@hubbleprotocol/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, ProportionalMintingMethod, PerformanceFees, PriceReferenceType, InputRebalanceFieldInfo, getTickArray, rebalanceFieldsDictToInfo, isVaultInitialized, WithdrawAllAndCloseIxns, } from './utils'; import { ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID } 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, } from './kamino-client/instructions'; import BN from 'bn.js'; import StrategyWithAddress from './models/StrategyWithAddress'; import { Idl, Program, Provider } from '@project-serum/anchor'; import { Rebalancing, Uninitialized } from './kamino-client/types/StrategyStatus'; import { FRONTEND_KAMINO_STRATEGY_URL, METADATA_PROGRAM_ID } from './constants'; import { CollateralInfo, ExecutiveWithdrawActionKind, RebalanceType, RebalanceTypeKind, ReferencePriceTypeKind, StrategyConfigOption, StrategyStatusKind, } from './kamino-client/types'; import { AmmConfig, PersonalPositionState, PoolState } from './raydium_client'; import { PROGRAM_ID as RAYDIUM_PROGRAM_ID, setRaydiumProgramId } from './raydium_client/programId'; import { i32ToBytes, LiquidityMath, SqrtPriceMath, TickMath, TickUtils } from '@raydium-io/raydium-sdk'; import KaminoIdl from './kamino-client/kamino.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 { UpdateDepositCap, UpdateDepositCapIxn, UpdateWithdrawFee, UpdateDepositFee, UpdateReward0Fee, UpdateReward1Fee, UpdateReward2Fee, UpdateCollectFeesFee, UpdateRebalanceType, UpdateLookupTable, UpdateDepositMintingMethod, UpdateReferencePriceType, } from './kamino-client/types/StrategyConfigOption'; import { DefaultDepositCap, DefaultDepositCapPerIx, DefaultPerformanceFeeBps, DefaultWithdrawFeeBps, } from './constants/DefaultStrategyConfig'; import { DEVNET_GLOBAL_LOOKUP_TABLE, MAINNET_GLOBAL_LOOKUP_TABLE } from './constants/pubkeys'; import { DefaultDex, DefaultFeeTierOrca, DefaultMintTokenA, DefaultMintTokenB, DriftRebalanceMethod, ExpanderMethod, FullBPS, FullPercentage, ManualRebalanceMethod, PeriodicRebalanceMethod, PricePercentageRebalanceMethod, PricePercentageWithResetRangeRebalanceMethod, RebalanceMethod, TakeProfitMethod, } from './utils/CreationParameters'; import { getMintDecimals } from '@project-serum/serum/lib/market'; 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, } from './kamino-client/types/RebalanceType'; import { checkIfAccountExists, createWsolAtaIfMissing, decodeSerializedTransaction, getAtasWithCreateIxnsIfMissing, MAX_ACCOUNTS_PER_TRANSACTION, removeBudgetAndAtaIxns, } from './utils/transactions'; import { RouteInfo } from '@jup-ag/core'; import { SwapResponse } from '@jup-ag/api'; import { StrategyPrices } from './models/StrategyPrices'; import { getDefaultManualRebalanceFieldInfos, getManualRebalanceFieldInfos } from './rebalance_methods/manualRebalance'; import { deserializePricePercentageRebalanceFromOnchainParams, deserializePricePercentageRebalanceWithStateOverride, getDefaultPricePercentageRebalanceFieldInfos, getPositionRangeFromPercentageRebalanceParams, getPricePercentageRebalanceFieldInfos, readPricePercentageRebalanceParamsFromStrategy, readPricePercentageRebalanceStateFromStrategy, readRawPricePercentageRebalanceStateFromStrategy, } from './rebalance_methods/pricePercentageRebalance'; import { deserializePricePercentageWithResetRebalanceFromOnchainParams, deserializePricePercentageWithResetRebalanceWithStateOverride, getDefaultPricePercentageWithResetRebalanceFieldInfos, getPositionRangeFromPricePercentageWithResetParams, getPricePercentageWithResetRebalanceFieldInfos, readPricePercentageWithResetRebalanceParamsFromStrategy, readRawPricePercentageWithResetRebalanceStateFromStrategy, } from './rebalance_methods/pricePercentageWithResetRebalance'; import { deserializeDriftRebalanceFromOnchainParams, deserializeDriftRebalanceWithStateOverride, getDefaultDriftRebalanceFieldInfos, getDriftRebalanceFieldInfos, getPositionRangeFromDriftParams, readDriftRebalanceParamsFromStrategy, readRawDriftRebalanceStateFromStrategy, } from './rebalance_methods/driftRebalance'; 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 { getRebalanceMethodFromRebalanceFields, getRebalanceTypeFromRebalanceFields } from './rebalance_methods/utils'; import { RebalanceTypeLabelName } from './rebalance_methods/consts'; import WhirlpoolWithAddress from './models/WhirlpoolWithAddress'; import RaydiumPoollWithAddress from './models/RaydiumPoolWithAddress'; 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: Provider; private readonly _kaminoProgram: Program; private readonly _kaminoProgramId: PublicKey; private readonly _orcaService: OrcaService; private readonly _raydiumService: RaydiumService; private readonly _jupService: JupService; /** * 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 ) { this._cluster = cluster; this._connection = connection; this._config = getConfigByCluster(cluster); this._globalConfig = globalConfig ? globalConfig : new PublicKey(this._config.kamino.globalConfig); this._provider = new Provider(connection, getReadOnlyWallet(), { commitment: connection.commitment, }); this._kaminoProgramId = programId ? programId : this._config.kamino.programId; 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 (cluster === 'localnet') { if (raydiumProgramId) { setRaydiumProgramId(raydiumProgramId); } } this._orcaService = new OrcaService(connection, cluster, this._globalConfig); this._raydiumService = new RaydiumService(connection, cluster); this._jupService = new JupService(connection, cluster); } 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.toString() != SystemProgram.programId.toString()); }; getCollateralInfos = async () => { const config = await GlobalConfig.fetch(this._connection, this._globalConfig); if (!config) { throw Error(`Could not fetch globalConfig with pubkey ${this.getGlobalConfig().toString()}`); } return this.getCollateralInfo(config.tokenInfos); }; getSupportedDexes = (): Dex[] => ['ORCA', 'RAYDIUM']; // 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 { throw new Error(`Dex ${dex} is not supported`); } }; getRebalanceMethods = (): RebalanceMethod[] => { return [ ManualRebalanceMethod, PricePercentageRebalanceMethod, PricePercentageWithResetRangeRebalanceMethod, DriftRebalanceMethod, TakeProfitMethod, PeriodicRebalanceMethod, ExpanderMethod, ]; }; 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; let rebalancingParameters = await this.getDefaultRebalanceFields(dex, tokenMintA, tokenMintB, rebalanceMethod); let defaultParameters: VaultParameters = { dex, tokenMintA, tokenMintB, feeTier, rebalancingParameters, }; return defaultParameters; }; getRebalanceTypeFromRebalanceFields = (rebalanceFields: RebalanceFieldInfo[]): RebalanceTypeKind => { return getRebalanceTypeFromRebalanceFields(rebalanceFields); }; getRebalanceMethodFromRebalanceFields = (rebalanceFields: RebalanceFieldInfo[]): RebalanceMethod => { return getRebalanceMethodFromRebalanceFields(rebalanceFields); }; getFieldsForRebalanceMethod = ( rebalanceMethod: RebalanceMethod, dex: Dex, fieldOverrides: RebalanceFieldInfo[], tokenAMint: PublicKey, tokenBMint: PublicKey, 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, 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); default: throw new Error(`Rebalance method ${rebalanceMethod} is not supported`); } }; getFieldsForManualRebalanceMethod = async ( dex: Dex, fieldOverrides: RebalanceFieldInfo[], tokenAMint: PublicKey, tokenBMint: PublicKey, poolPrice?: Decimal ): Promise<RebalanceFieldInfo[]> => { let 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; let lowerPriceDifferenceBPSInput = fieldOverrides.find((x) => x.label == 'lowerRangeBps'); if (lowerPriceDifferenceBPSInput) { lowerPriceDifferenceBPS = lowerPriceDifferenceBPSInput.value; } let upperPriceDifferenceBPS = defaultFields.find((x) => x.label == 'upperRangeBps')!.value; let upperPriceDifferenceBPSInput = fieldOverrides.find((x) => x.label == 'upperRangeBps'); if (upperPriceDifferenceBPSInput) { upperPriceDifferenceBPS = upperPriceDifferenceBPSInput.value; } let lowerResetPriceDifferenceBPS = defaultFields.find((x) => x.label == 'resetLowerRangeBps')!.value; let lowerResetPriceDifferenceBPSInput = fieldOverrides.find((x) => x.label == 'resetLowerRangeBps'); if (lowerResetPriceDifferenceBPSInput) { lowerResetPriceDifferenceBPS = lowerResetPriceDifferenceBPSInput.value; } let upperResetPriceDifferenceBPS = defaultFields.find((x) => x.label == 'resetUpperRangeBps')!.value; let 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[], 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, price, tokenADecimals, tokenBDecimals); let startMidTick = defaultFields.find((x) => x.label == 'startMidTick')!.value; let startMidTickInput = fieldOverrides.find((x) => x.label == 'startMidTick'); if (startMidTickInput) { startMidTick = startMidTickInput.value; } let ticksBelowMid = defaultFields.find((x) => x.label == 'ticksBelowMid')!.value; let ticksBelowMidInput = fieldOverrides.find((x) => x.label == 'ticksBelowMid'); if (ticksBelowMidInput) { ticksBelowMid = ticksBelowMidInput.value; } let ticksAboveMid = defaultFields.find((x) => x.label == 'ticksAboveMid')!.value; let ticksAboveMidInput = fieldOverrides.find((x) => x.label == 'ticksAboveMid'); if (ticksAboveMidInput) { ticksAboveMid = ticksAboveMidInput.value; } let secondsPerTick = defaultFields.find((x) => x.label == 'secondsPerTick')!.value; let secondsPerTickInput = fieldOverrides.find((x) => x.label == 'secondsPerTick'); if (secondsPerTickInput) { secondsPerTick = secondsPerTickInput.value; } let direction = defaultFields.find((x) => x.label == 'direction')!.value; let directionInput = fieldOverrides.find((x) => x.label == 'direction'); if (directionInput) { direction = directionInput.value; } let fieldInfos = getDriftRebalanceFieldInfos( dex, tokenADecimals, tokenBDecimals, 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); // pub lower_range_price: u128, // pub upper_range_price: u128, // // Which token we want the full amount in // // Will wait until the position is fully in this token (0 or 1, representing A or B) // pub destination_token: RebalanceTakeProfitToken, let lowerRangePrice = defaultFields.find((x) => x.label == 'rangePriceLower')!.value; let lowerRangePriceInput = fieldOverrides.find((x) => x.label == 'rangePriceLower'); if (lowerRangePriceInput) { lowerRangePrice = lowerRangePriceInput.value; } let upperRangePrice = defaultFields.find((x) => x.label == 'rangePriceUpper')!.value; let upperRangePriceInput = fieldOverrides.find((x) => x.label == 'rangePriceUpper'); if (upperRangePriceInput) { upperRangePrice = upperRangePriceInput.value; } let destinationToken = defaultFields.find((x) => x.label == 'destinationToken')!.value; let 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); let periodInput = fieldOverrides.find((x) => x.label == 'period'); if (periodInput) { period = new Decimal(periodInput.value); } let lowerPriceDifferenceBPS = defaultFields.find((x) => x.label == 'lowerRangeBps')!.value; let lowerPriceDifferenceBPSInput = fieldOverrides.find((x) => x.label == 'lowerRangeBps'); if (lowerPriceDifferenceBPSInput) { lowerPriceDifferenceBPS = lowerPriceDifferenceBPSInput.value; } let upperPriceDifferenceBPS = defaultFields.find((x) => x.label == 'upperRangeBps')!.value; let 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; let lowerPriceDifferenceBPSInput = fieldOverrides.find((x) => x.label == 'lowerRangeBps'); if (lowerPriceDifferenceBPSInput) { lowerPriceDifferenceBPS = lowerPriceDifferenceBPSInput.value; } let upperPriceDifferenceBPS = defaultFields.find((x) => x.label == 'upperRangeBps')!.value; let upperPriceDifferenceBPSInput = fieldOverrides.find((x) => x.label == 'upperRangeBps'); if (upperPriceDifferenceBPSInput) { upperPriceDifferenceBPS = upperPriceDifferenceBPSInput.value; } let lowerResetPriceDifferenceBPS = defaultFields.find((x) => x.label == 'resetLowerRangeBps')!.value; let lowerResetPriceDifferenceBPSInput = fieldOverrides.find((x) => x.label == 'resetLowerRangeBps'); if (lowerResetPriceDifferenceBPSInput) { lowerResetPriceDifferenceBPS = lowerResetPriceDifferenceBPSInput.value; } let upperResetPriceDifferenceBPS = defaultFields.find((x) => x.label == 'resetUpperRangeBps')!.value; let upperResetPriceDifferenceBPSInput = fieldOverrides.find((x) => x.label == 'resetUpperRangeBps'); if (upperResetPriceDifferenceBPSInput) { upperResetPriceDifferenceBPS = upperResetPriceDifferenceBPSInput.value; } let expansionBPS = defaultFields.find((x) => x.label == 'expansionBps')!.value; let expansionBPSInput = fieldOverrides.find((x) => x.label == 'expansionBps'); if (expansionBPSInput) { expansionBPS = expansionBPSInput.value; } let maxNumberOfExpansions = defaultFields.find((x) => x.label == 'maxNumberOfExpansions')!.value; let maxNumberOfExpansionsInput = fieldOverrides.find((x) => x.label == 'maxNumberOfExpansions'); if (maxNumberOfExpansionsInput) { maxNumberOfExpansions = maxNumberOfExpansionsInput.value; } let swapUnevenAllowed = defaultFields.find((x) => x.label == 'swapUnevenAllowed')!.value; let 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) ); }; getPriceForPair = async (dex: Dex, poolTokenA: PublicKey, poolTokenB: PublicKey): Promise<number> => { if (dex == 'ORCA') { let 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') { let 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 { throw new Error(`Dex ${dex} is not supported`); } }; getDefaultRebalanceFields = async ( dex: Dex, poolTokenA: PublicKey, poolTokenB: PublicKey, rebalanceMethod: RebalanceMethod ): Promise<RebalanceFieldInfo[]> => { let price = new Decimal(await this.getPriceForPair(dex, poolTokenA, poolTokenB)); switch (rebalanceMethod) { case ManualRebalanceMethod: return getDefaultManualRebalanceFieldInfos(price); case PricePercentageRebalanceMethod: return getDefaultPricePercentageRebalanceFieldInfos(price); case PricePercentageWithResetRangeRebalanceMethod: return getDefaultPricePercentageWithResetRebalanceFieldInfos(price); case DriftRebalanceMethod: let tokenADecimals = await getMintDecimals(this._connection, poolTokenA); let tokenBDecimals = await getMintDecimals(this._connection, poolTokenB); return getDefaultDriftRebalanceFieldInfos(dex, price, tokenADecimals, tokenBDecimals); case TakeProfitMethod: return getDefaultTakeProfitRebalanceFieldsInfos(price); case PeriodicRebalanceMethod: return getDefaultPeriodicRebalanceFieldInfos(price); case ExpanderMethod: return getDefaultExpanderRebalanceFieldInfos(price); default: throw new Error(`Rebalance method ${rebalanceMethod} is not supported`); } }; getPoolInitializedForDexPairTier = async ( dex: Dex, poolTokenA: PublicKey, poolTokenB: PublicKey, feeBPS: Decimal ): Promise<PublicKey> => { if (dex == 'ORCA') { let pool = PublicKey.default; let 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') { let pool = PublicKey.default; let raydiumPools = await this.getRaydiumPoolsForTokens(poolTokenA, poolTokenB); raydiumPools.forEach((element) => { if (new Decimal(element.ammConfig.tradeFeeRate).div(FullBPS).div(FullPercentage).equals(feeBPS.div(FullBPS))) { pool = new PublicKey(element.id); } }); return pool; } else { throw new Error(`Dex ${dex} is not supported`); } }; async getExistentPoolsForPair(dex: Dex, tokenMintA: PublicKey, tokenMintB: PublicKey): Promise<GenericPoolInfo[]> { if (dex == 'ORCA') { let pools = await this.getOrcaPoolsForTokens(tokenMintA, tokenMintB); let genericPoolInfos: GenericPoolInfo[] = await Promise.all( pools.map(async (pool: OrcaPool) => { let positionsCount = new Decimal(await this.getPositionsCountForPool(dex, new PublicKey(pool.address))); // read price from pool let poolData = await this._orcaService.getPool(new PublicKey(pool.address)); if (!poolData) { throw new Error(`Pool ${pool.address} not found`); } let 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') { let pools = await this.getRaydiumPoolsForTokens(tokenMintA, tokenMintB); let genericPoolInfos: GenericPoolInfo[] = await Promise.all( pools.map(async (pool: Pool) => { let positionsCount = new Decimal(await this.getPositionsCountForPool(dex, new PublicKey(pool.id))); let 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 { throw new Error(`Dex ${dex} is not supported`); } } getOrcaPoolsForTokens = async (poolTokenA: PublicKey, poolTokenB: PublicKey): Promise<OrcaPool[]> => { let pools: OrcaPool[] = []; let whirlpools = await this._orcaService.getOrcaWhirlpools(); whirlpools.whirlpools.forEach((element) => { if ( (element.tokenA.mint.toString() == poolTokenA.toString() && element.tokenB.mint.toString() == poolTokenB.toString()) || (element.tokenA.mint.toString() == poolTokenB.toString() && element.tokenB.mint.toString() == poolTokenA.toString()) ) pools.push(element); }); return pools; }; getRaydiumPoolsForTokens = async (poolTokenA: PublicKey, poolTokenB: PublicKey): Promise<Pool[]> => { let pools: Pool[] = []; let raydiumPools = await this._raydiumService.getRaydiumWhirlpools(); raydiumPools.data.forEach((element) => { if ( (element.mintA.toString() == poolTokenA.toString() && element.mintB.toString() == poolTokenB.toString()) || (element.mintA.toString() == poolTokenB.toString() && element.mintB.toString() == poolTokenA.toString()) ) { 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) => WhirlpoolStrategy.fetchMultiple(this._connection, 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) => WhirlpoolStrategy.fetchMultiple(this._connection, 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>> => { let filters: GetProgramAccountsFilter[] = []; filters.push({ dataSize: 4064, }); 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) { let value = !strategyFilters.isCommunity ? '1' : '2'; filters.push({ memcmp: { bytes: value, offset: 1664, }, }); } return (await this._kaminoProgram.account.whirlpoolStrategy.all(filters)).map((x) => { const res: StrategyWithAddress = { strategy: new WhirlpoolStrategy(x.account as WhirlpoolStrategyFields), address: x.publicKey, }; return res; }); }; /** * Get a Kamino whirlpool strategy by its public key address * @param address */ getStrategyByAddress = (address: PublicKey) => WhirlpoolStrategy.fetch(this._connection, 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._kaminoProgram.account.whirlpoolStrategy.all([ { 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: ${matchingStrategies.map( (x) => x.publicKey.toBase58() )}` ); } const decodedStrategy = new WhirlpoolStrategy(matchingStrategies[0].account as WhirlpoolStrategyFields); return { address: matchingStrategies[0].publicKey, strategy: decodedStrategy, }; }; /** * Get the strategy share data (price + balances) of the specified Kamino whirlpool strategy * @param strategy * @param scopePrices */ getStrategyShareData = async ( strategy: PublicKey | StrategyWithAddress, scopePrices?: OraclePrices ): Promise<ShareData> => { const strategyState = await this.getStrategyStateIfNotFetched(strategy); const sharesFactor = Decimal.pow(10, strategyState.strategy.sharesMintDecimals.toString()); const sharesIssued = new Decimal(strategyState.strategy.sharesIssued.toString()); const balances = await this.getStrategyBalances(strategyState.strategy, scopePrices); if (sharesIssued.isZero()) { return { price: new Decimal(1), balance: balances }; } else { return { price: balances.computedHoldings.totalSum.div(sharesIssued).mul(sharesFactor), balance: balances }; } }; getStrategiesForSharedData = async ( strategyFilters: StrategiesFilters | PublicKey[] ): Promise<Array<StrategyWithAddress>> => { return Array.isArray(strategyFilters) ? await this.getStrategiesWithAddresses(strategyFilters) : await this.getAllStrategiesWithFilters(strategyFilters); }; /** * Batch fetch share data for all or a filtered list of strategies * @param strategyFilters strategy filters or a list of strategy public keys */ getStrategiesShareData = async ( strategyFilters: StrategiesFilters | PublicKey[] ): Promise<Array<ShareDataWithAddress>> => { return await this.getStrategiesShareDataWithFn(strategyFilters, this.getStrategiesForSharedData); }; getStrategiesShareDataWithFn = async ( strategyFilters: StrategiesFilters | PublicKey[], getStrategiesFn: (strategyFilters: StrategiesFilters | PublicKey[]) => Promise<Array<StrategyWithAddress>> ): Promise<Array<ShareDataWithAddress>> => { const result: Array<ShareDataWithAddress> = []; const strategiesWithAddresses = await getStrategiesFn(strategyFilters); const fetchBalances: Promise<StrategyBalanceWithAddress>[] = []; const allScopePrices = strategiesWithAddresses.map((x) => x.strategy.scopePrices); const scopePrices = await this._scope.getMultipleOraclePrices(allScopePrices); const scopePricesMap: Record<string, OraclePrices> = scopePrices.reduce((map, [address, price]) => { map[address.toBase58()] = price; return map; }, {}); const raydiumStrategies = strategiesWithAddresses.filter( (x) => x.strategy.strategyDex.toNumber() === dexToNumber('RAYDIUM') && x.strategy.position.toString() !== PublicKey.default.toString() ); const raydiumPools = await this.getRaydiumPools(raydiumStrategies.map((x) => x.strategy.pool)); const raydiumPositions = await this.getRaydiumPositions(raydiumStrategies.map((x) => x.strategy.position)); const orcaStrategies = strategiesWithAddresses.filter( (x) => x.strategy.strategyDex.toNumber() === dexToNumber('ORCA') && x.strategy.position.toString() !== PublicKey.default.toString() ); const orcaPools = await this.getWhirlpools(orcaStrategies.map((x) => x.strategy.pool)); const orcaPositions = await this.getOrcaPositions(orcaStrategies.map((x) => x.strategy.position)); const inactiveStrategies = strategiesWithAddresses.filter( (x) => x.strategy.position.toString() === PublicKey.default.toString() ); const collateralInfos = await this.getCollateralInfos(); for (const { strategy, address } of inactiveStrategies) { const strategyPrices = await this.getStrategyPrices( strategy, collateralInfos, scopePricesMap[strategy.scopePrices.toBase58()] ); result.push({ address, strategy, shareData: getEmptyShareData({ ...strategyPrices, poolPrice: ZERO, upperPrice: ZERO, lowerPrice: ZERO, }), }); } fetchBalances.push( ...this.getBalance<PoolState, PersonalPositionState>( raydiumStrategies, raydiumPools, raydiumPositions, this.getRaydiumBalances, collateralInfos, scopePricesMap ) ); fetchBalances.push( ...this.getBalance<Whirlpool, Position>( orcaStrategies, orcaPools, orcaPositions, this.getOrcaBalances, collateralInfos, scopePricesMap ) ); const strategyBalances = await Promise.all(fetchBalances); for (const { balance, strategyWithAddress } of strategyBalances) { const sharesFactor = Decimal.pow(10, strategyWithAddress.strategy.sharesMintDecimals.toString()); const sharesIssued = new Decimal(strategyWithAddress.strategy.sharesIssued.toString()); if (sharesIssued.isZero()) { result.push({ address: strategyWithAddress.address, strategy: strategyWithAddress.strategy, shareData: { price: new Decimal(1), balance }, }); } else { result.push({ address: strategyWithAddress.address, strategy: strategyWithAddress.strategy, shareData: { price: balance.computedHoldings.totalSum.div(sharesIssued).mul(sharesFactor), balance }, }); } } return result; }; private getBalance = <PoolT, PositionT>( strategies: StrategyWithAddress[], pools: (PoolT | null)[], positions: (PositionT | null)[], fetchBalance: ( strategy: WhirlpoolStrategy, pool: PoolT, position: PositionT, collateralInfos: CollateralInfo[], prices?: OraclePrices ) => Promise<StrategyBalances>, collateralInfos: CollateralInfo[], prices?: Record<string, OraclePrices> ): Promise<StrategyBalanceWithAddress>[] => { const fetchBalances: Promise<StrategyBalanceWithAddress>[] = []; for (let i = 0; i < strategies.length; i++) { const { strategy, address } = strategies[i]; const pool = pools[i]; const position = positions[i]; if (!pool) { throw new Error(`Pool ${strategy.pool.toString()} could not be found.`); } if (!position) { throw new Error(`Position ${strategy.position.toString()} could not be found.`); } fetchBalances.push( fetchBalance( strategy, pool as PoolT, position as PositionT, collateralInfos, prices ? prices[strategy.scopePrices.toBase58()] : undefined ).then((balance) => { return { balance, strategyWithAddress: { strategy, address } }; }) ); } return fetchBalances; }; private getRaydiumBalances = async ( strategy: WhirlpoolStrategy, pool: PoolState, position: PersonalPositionState, collateralInfos: CollateralInfo[], prices?: OraclePrices ) => { const strategyPrices = await this.getStrategyPrices(strategy, collateralInfos, prices); const tokenHoldings = await this.getRaydiumTokensBalances(strategy, pool, position); let computedHoldings: Holdings = this.getStrategyHoldingsUsd( tokenHoldings.available.a, tokenHoldings.available.b, tokenHoldings.invested.a, tokenHoldings.invested.b, new Decimal(strategy.tokenAMintDecimals.toString()), new Decimal(strategy.tokenBMintDecimals.toString()), strategyPrices.aPrice, strategyPrices.bPrice ); let decimalsA = strategy.tokenAMintDecimals.toNumber(); let decimalsB = strategy.tokenBMintDecimals.toNumber(); const poolPrice = SqrtPriceMath.sqrtPriceX64ToPrice(pool.sqrtPriceX64, decimalsA, decimalsB); const upperPrice = SqrtPriceMath.sqrtPriceX64ToPrice( SqrtPriceMath.getSqrtPriceX64FromTick(position.tickUpperIndex), decimalsA, decimalsB ); const lowerPrice = SqrtPriceMath.sqrtPriceX64ToPrice( SqrtPriceMath.getSqrtPriceX64FromTick(position.tickLowerIndex), decimalsA, decimalsB ); const balance: StrategyBalances = { computedHoldings, prices: { ...strategyPrices, poolPrice, lowerPrice, upperPrice }, tokenAAmounts: tokenHoldings.available.a.plus(tokenHoldings.invested.a), tokenBAmounts: tokenHoldings.available.b.plus(tokenHoldings.invested.b), }; return balance; }; private getRaydiumTokensBalances = async ( strategy: WhirlpoolStrategy, pool: PoolState, position: PersonalPositionState ) => { const lowerSqrtPriceX64 = SqrtPriceMath.getSqrtPriceX64FromTick(position.tickLowerIndex); const upperSqrtPriceX64 = SqrtPriceMath.getSqrtPriceX64FromTick(position.tickUpperIndex); const { amountA, amountB } = LiquidityMath.getAmountsFromLiquidity( pool.sqrtPriceX64, new BN(lowerSqrtPriceX64), new BN(upperSqrtPriceX64), position.liquidity, false // round down so the holdings are not overestimated ); const aAvailable = new Decimal(strategy.tokenAAmounts.toString()); const bAvailable = new Decimal(strategy.tokenBAmounts.toString()); const aInvested = new Decimal(amountA.toString()); const bInvested = new Decimal(amountB.toString()); let holdings: TokenHoldings = { available: { a: aAvailable, b: bAvailable, }, invested: { a: aInvested, b: bInvested, }, }; return holdings; }; private getOrcaBalances = async ( strategy: WhirlpoolStrategy, pool: Whirlpool, position: Position, collateralInfos: CollateralInfo[], prices?: OraclePrices ) => { const strategyPrices = await this.getStrategyPrices(strategy, collateralInfos, prices); let tokenHoldings = await this.getOrcaTokensBalances(strategy, pool, position); const computedHoldings: Holdings = this.getStrategyHoldingsUsd( tokenHoldings.available.a, tokenHoldings.available.b, tokenHoldings.invested.a, tokenHoldings.invested.b, new Decimal(strategy.tokenAMintDecimals.toString()), new Decimal(strategy.tokenBMintDecimals.toString()), strategyPrices.aPrice, strategyPrices.bPrice ); const decimalsA = strategy.tokenAMintDecimals.toNumber(); const decimalsB = strategy.tokenBMintDecimals.toNumber(); const poolPrice = sqrtPriceX64ToPrice(pool.sqrtPrice, decimalsA, decimalsB); const upperPrice = tickIndexToPrice(position.tickUpperIndex, decimalsA, decimalsB); const lowerPrice = tickIndexToPrice(position.tickLowerIndex, decimalsA, decimalsB); const balance: StrategyBalances = { computedHoldings, prices: { ...strategyPrices, poolPrice, upperPrice, lowerPrice }, tokenAAmounts: tokenHoldings.available.a.plus(tokenHoldings.invested.a), tokenBAmounts: tokenHoldings.available.b.plus(tokenHoldings.invested.b), }; return balance; }; private getOrcaTokensBalances = async ( strategy: WhirlpoolStrategy, pool: Whirlpool, position: Position ): Promise<TokenHoldings> => { const quote = getRemoveLiquidityQuote({ positionAddress: strategy.position, liquidity: position.liquidity, slippageTolerance: Percentage.fromFraction(0, 1000), sqrtPrice: pool.sqrtPrice, tickLowerIndex: position.tickLowerIndex, tickUpperIndex: position.tickUpperIndex, tickCurrentIndex: pool.tickCurrentIndex, }); const aAvailable = new Decimal(strategy.tokenAAmounts.toString()); const bAvailable = new Decimal(strategy.tokenBAmounts.toString()); const aInvested = new Decimal(quote.estTokenA.toString()); const bInvested = new Decimal(quote.estTokenB.toString()); let holdi