@marinade.finance/kamino-sdk
Version:
1,347 lines (1,230 loc) • 216 kB
text/typescript
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