UNPKG

@marinade.finance/kamino-sdk

Version:
870 lines 216 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Kamino = exports.KAMINO_IDL = void 0; const hubble_config_1 = require("@hubbleprotocol/hubble-config"); const web3_js_1 = require("@solana/web3.js"); const programId_1 = require("./kamino-client/programId"); const accounts_1 = require("./kamino-client/accounts"); const decimal_js_1 = __importDefault(require("decimal.js")); const whirpools_client_1 = require("./whirpools-client"); const whirlpool_sdk_1 = require("@orca-so/whirlpool-sdk"); const orca_dal_1 = require("@orca-so/whirlpool-sdk/dist/dal/orca-dal"); const orca_position_1 = require("@orca-so/whirlpool-sdk/dist/position/orca-position"); const models_1 = require("./models"); const programId_2 = require("./whirpools-client/programId"); const scope_sdk_1 = require("@hubbleprotocol/scope-sdk"); const utils_1 = require("./utils"); const spl_token_1 = require("@solana/spl-token"); const instructions_1 = require("./kamino-client/instructions"); const bn_js_1 = __importDefault(require("bn.js")); const anchor_1 = require("@project-serum/anchor"); const StrategyStatus_1 = require("./kamino-client/types/StrategyStatus"); const constants_1 = require("./constants"); const types_1 = require("./kamino-client/types"); const raydium_client_1 = require("./raydium_client"); const programId_3 = require("./raydium_client/programId"); const raydium_sdk_1 = require("@raydium-io/raydium-sdk"); const kamino_json_1 = __importDefault(require("./kamino-client/kamino.json")); const services_1 = require("./services"); const add_liquidity_1 = require("@orca-so/whirlpool-sdk/dist/position/quotes/add-liquidity"); const instructions_2 = require("./kamino-client/instructions"); const StrategyConfigOption_1 = require("./kamino-client/types/StrategyConfigOption"); const DefaultStrategyConfig_1 = require("./constants/DefaultStrategyConfig"); const pubkeys_1 = require("./constants/pubkeys"); const CreationParameters_1 = require("./utils/CreationParameters"); const market_1 = require("@project-serum/serum/lib/market"); const deposit_method_1 = require("./constants/deposit_method"); const JupService_1 = require("./services/JupService"); const PoolSimulationService_1 = require("./services/PoolSimulationService"); const RebalanceType_1 = require("./kamino-client/types/RebalanceType"); const transactions_1 = require("./utils/transactions"); const manualRebalance_1 = require("./rebalance_methods/manualRebalance"); const pricePercentageRebalance_1 = require("./rebalance_methods/pricePercentageRebalance"); const pricePercentageWithResetRebalance_1 = require("./rebalance_methods/pricePercentageWithResetRebalance"); const driftRebalance_1 = require("./rebalance_methods/driftRebalance"); const rebalance_methods_1 = require("./rebalance_methods"); const priceReferenceTypes_1 = require("./utils/priceReferenceTypes"); const utils_2 = require("./rebalance_methods/utils"); const consts_1 = require("./rebalance_methods/consts"); exports.KAMINO_IDL = kamino_json_1.default; class Kamino { _cluster; _connection; _config; _globalConfig; _scope; _provider; _kaminoProgram; _kaminoProgramId; _orcaService; _raydiumService; _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, connection, globalConfig, programId, whirlpoolProgramId, raydiumProgramId) { this._cluster = cluster; this._connection = connection; this._config = (0, hubble_config_1.getConfigByCluster)(cluster); this._globalConfig = globalConfig ? globalConfig : new web3_js_1.PublicKey(this._config.kamino.globalConfig); this._provider = new anchor_1.Provider(connection, (0, utils_1.getReadOnlyWallet)(), { commitment: connection.commitment, }); this._kaminoProgramId = programId ? programId : this._config.kamino.programId; this._kaminoProgram = new anchor_1.Program(exports.KAMINO_IDL, this._kaminoProgramId, this._provider); this._scope = new scope_sdk_1.Scope(cluster, connection); (0, programId_1.setKaminoProgramId)(this._kaminoProgramId); if (whirlpoolProgramId) { (0, programId_2.setWhirlpoolsProgramId)(whirlpoolProgramId); } if (cluster === 'localnet') { if (raydiumProgramId) { (0, programId_3.setRaydiumProgramId)(raydiumProgramId); } } this._orcaService = new services_1.OrcaService(connection, cluster, this._globalConfig); this._raydiumService = new services_1.RaydiumService(connection, cluster); this._jupService = new JupService_1.JupService(connection, cluster); } getConnection = () => this._connection; getProgramID = () => this._kaminoProgramId; getProgram = () => this._kaminoProgram; setGlobalConfig = (globalConfig) => { this._globalConfig = globalConfig; }; getGlobalConfig = () => this._globalConfig; getDepositableTokens = async () => { const collateralInfos = await this.getCollateralInfos(); return collateralInfos.filter((x) => x.mint.toString() != web3_js_1.SystemProgram.programId.toString()); }; getCollateralInfos = async () => { const config = await accounts_1.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 = () => ['ORCA', 'RAYDIUM']; // todo: see if we can read this dynamically getFeeTiersForDex = (dex) => { if (dex == 'ORCA') { return [new decimal_js_1.default(0.0001), new decimal_js_1.default(0.0005), new decimal_js_1.default(0.003), new decimal_js_1.default(0.01)]; } else if (dex == 'RAYDIUM') { return [new decimal_js_1.default(0.0001), new decimal_js_1.default(0.0005), new decimal_js_1.default(0.0025), new decimal_js_1.default(0.01)]; } else { throw new Error(`Dex ${dex} is not supported`); } }; getRebalanceMethods = () => { return [ CreationParameters_1.ManualRebalanceMethod, CreationParameters_1.PricePercentageRebalanceMethod, CreationParameters_1.PricePercentageWithResetRangeRebalanceMethod, CreationParameters_1.DriftRebalanceMethod, CreationParameters_1.TakeProfitMethod, CreationParameters_1.PeriodicRebalanceMethod, CreationParameters_1.ExpanderMethod, ]; }; getPriceReferenceTypes = () => { return [priceReferenceTypes_1.PoolPriceReferenceType, priceReferenceTypes_1.TwapPriceReferenceType]; }; getDefaultRebalanceMethod = () => CreationParameters_1.PricePercentageRebalanceMethod; getDefaultParametersForNewVault = async () => { const dex = CreationParameters_1.DefaultDex; const tokenMintA = CreationParameters_1.DefaultMintTokenA; const tokenMintB = CreationParameters_1.DefaultMintTokenB; const rebalanceMethod = this.getDefaultRebalanceMethod(); const feeTier = CreationParameters_1.DefaultFeeTierOrca; let rebalancingParameters = await this.getDefaultRebalanceFields(dex, tokenMintA, tokenMintB, rebalanceMethod); let defaultParameters = { dex, tokenMintA, tokenMintB, feeTier, rebalancingParameters, }; return defaultParameters; }; getRebalanceTypeFromRebalanceFields = (rebalanceFields) => { return (0, utils_2.getRebalanceTypeFromRebalanceFields)(rebalanceFields); }; getRebalanceMethodFromRebalanceFields = (rebalanceFields) => { return (0, utils_2.getRebalanceMethodFromRebalanceFields)(rebalanceFields); }; getFieldsForRebalanceMethod = (rebalanceMethod, dex, fieldOverrides, tokenAMint, tokenBMint, poolPrice) => { switch (rebalanceMethod) { case CreationParameters_1.ManualRebalanceMethod: return this.getFieldsForManualRebalanceMethod(dex, fieldOverrides, tokenAMint, tokenBMint, poolPrice); case CreationParameters_1.PricePercentageRebalanceMethod: return this.getFieldsForPricePercentageMethod(dex, fieldOverrides, tokenAMint, tokenBMint, poolPrice); case CreationParameters_1.PricePercentageWithResetRangeRebalanceMethod: return this.getFieldsForPricePercentageWithResetMethod(dex, fieldOverrides, tokenAMint, tokenBMint, poolPrice); case CreationParameters_1.DriftRebalanceMethod: return this.getFieldsForDriftRebalanceMethod(dex, fieldOverrides, tokenAMint, tokenBMint, poolPrice); case CreationParameters_1.TakeProfitMethod: return this.getFieldsForTakeProfitRebalanceMethod(dex, fieldOverrides, tokenAMint, tokenBMint, poolPrice); case CreationParameters_1.PeriodicRebalanceMethod: return this.getFieldsForPeriodicRebalanceMethod(dex, fieldOverrides, tokenAMint, tokenBMint, poolPrice); case CreationParameters_1.ExpanderMethod: return this.getFieldsForExpanderRebalanceMethod(dex, fieldOverrides, tokenAMint, tokenBMint, poolPrice); default: throw new Error(`Rebalance method ${rebalanceMethod} is not supported`); } }; getFieldsForManualRebalanceMethod = async (dex, fieldOverrides, tokenAMint, tokenBMint, poolPrice) => { let price = poolPrice ? poolPrice : new decimal_js_1.default(await this.getPriceForPair(dex, tokenAMint, tokenBMint)); const defaultFields = (0, manualRebalance_1.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 (0, manualRebalance_1.getManualRebalanceFieldInfos)(new decimal_js_1.default(lowerPrice), new decimal_js_1.default(upperPrice)); }; getFieldsForPricePercentageMethod = async (dex, fieldOverrides, tokenAMint, tokenBMint, poolPrice) => { const price = poolPrice ? poolPrice : new decimal_js_1.default(await this.getPriceForPair(dex, tokenAMint, tokenBMint)); const defaultFields = (0, pricePercentageRebalance_1.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 (0, pricePercentageRebalance_1.getPricePercentageRebalanceFieldInfos)(price, new decimal_js_1.default(lowerPriceDifferenceBPS), new decimal_js_1.default(upperPriceDifferenceBPS)); }; getFieldsForPricePercentageWithResetMethod = async (dex, fieldOverrides, tokenAMint, tokenBMint, poolPrice) => { const price = poolPrice ? poolPrice : new decimal_js_1.default(await this.getPriceForPair(dex, tokenAMint, tokenBMint)); const defaultFields = (0, pricePercentageWithResetRebalance_1.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 (0, pricePercentageWithResetRebalance_1.getPricePercentageWithResetRebalanceFieldInfos)(price, new decimal_js_1.default(lowerPriceDifferenceBPS), new decimal_js_1.default(upperPriceDifferenceBPS), new decimal_js_1.default(lowerResetPriceDifferenceBPS), new decimal_js_1.default(upperResetPriceDifferenceBPS)); }; getFieldsForDriftRebalanceMethod = async (dex, fieldOverrides, tokenAMint, tokenBMint, poolPrice) => { const tokenADecimals = await (0, market_1.getMintDecimals)(this._connection, tokenAMint); const tokenBDecimals = await (0, market_1.getMintDecimals)(this._connection, tokenBMint); const price = poolPrice ? poolPrice : new decimal_js_1.default(await this.getPriceForPair(dex, tokenAMint, tokenBMint)); const defaultFields = (0, driftRebalance_1.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 = (0, driftRebalance_1.getDriftRebalanceFieldInfos)(dex, tokenADecimals, tokenBDecimals, new decimal_js_1.default(startMidTick), new decimal_js_1.default(ticksBelowMid), new decimal_js_1.default(ticksAboveMid), new decimal_js_1.default(secondsPerTick), new decimal_js_1.default(direction)); return fieldInfos; }; getFieldsForTakeProfitRebalanceMethod = async (dex, fieldOverrides, tokenAMint, tokenBMint, poolPrice) => { const price = poolPrice ? poolPrice : new decimal_js_1.default(await this.getPriceForPair(dex, tokenAMint, tokenBMint)); const defaultFields = (0, rebalance_methods_1.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 (0, rebalance_methods_1.getTakeProfitRebalanceFieldsInfos)(new decimal_js_1.default(lowerRangePrice), new decimal_js_1.default(upperRangePrice), new decimal_js_1.default(destinationToken), true); }; getFieldsForPeriodicRebalanceMethod = async (dex, fieldOverrides, tokenAMint, tokenBMint, poolPrice) => { const price = poolPrice ? poolPrice : new decimal_js_1.default(await this.getPriceForPair(dex, tokenAMint, tokenBMint)); const defaultFields = (0, rebalance_methods_1.getDefaultPeriodicRebalanceFieldInfos)(price); let period = new decimal_js_1.default(defaultFields.find((x) => x.label == 'period').value); let periodInput = fieldOverrides.find((x) => x.label == 'period'); if (periodInput) { period = new decimal_js_1.default(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 (0, rebalance_methods_1.getPeriodicRebalanceRebalanceFieldInfos)(price, period, new decimal_js_1.default(lowerPriceDifferenceBPS), new decimal_js_1.default(upperPriceDifferenceBPS)); }; getFieldsForExpanderRebalanceMethod = async (dex, fieldOverrides, tokenAMint, tokenBMint, poolPrice) => { const price = poolPrice ? poolPrice : new decimal_js_1.default(await this.getPriceForPair(dex, tokenAMint, tokenBMint)); const defaultFields = (0, rebalance_methods_1.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 (0, rebalance_methods_1.getExpanderRebalanceFieldInfos)(price, new decimal_js_1.default(lowerPriceDifferenceBPS), new decimal_js_1.default(upperPriceDifferenceBPS), new decimal_js_1.default(lowerResetPriceDifferenceBPS), new decimal_js_1.default(upperResetPriceDifferenceBPS), new decimal_js_1.default(expansionBPS), new decimal_js_1.default(maxNumberOfExpansions), new decimal_js_1.default(swapUnevenAllowed)); }; getPriceForPair = async (dex, poolTokenA, poolTokenB) => { 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, poolTokenA, poolTokenB, rebalanceMethod) => { let price = new decimal_js_1.default(await this.getPriceForPair(dex, poolTokenA, poolTokenB)); switch (rebalanceMethod) { case CreationParameters_1.ManualRebalanceMethod: return (0, manualRebalance_1.getDefaultManualRebalanceFieldInfos)(price); case CreationParameters_1.PricePercentageRebalanceMethod: return (0, pricePercentageRebalance_1.getDefaultPricePercentageRebalanceFieldInfos)(price); case CreationParameters_1.PricePercentageWithResetRangeRebalanceMethod: return (0, pricePercentageWithResetRebalance_1.getDefaultPricePercentageWithResetRebalanceFieldInfos)(price); case CreationParameters_1.DriftRebalanceMethod: let tokenADecimals = await (0, market_1.getMintDecimals)(this._connection, poolTokenA); let tokenBDecimals = await (0, market_1.getMintDecimals)(this._connection, poolTokenB); return (0, driftRebalance_1.getDefaultDriftRebalanceFieldInfos)(dex, price, tokenADecimals, tokenBDecimals); case CreationParameters_1.TakeProfitMethod: return (0, rebalance_methods_1.getDefaultTakeProfitRebalanceFieldsInfos)(price); case CreationParameters_1.PeriodicRebalanceMethod: return (0, rebalance_methods_1.getDefaultPeriodicRebalanceFieldInfos)(price); case CreationParameters_1.ExpanderMethod: return (0, rebalance_methods_1.getDefaultExpanderRebalanceFieldInfos)(price); default: throw new Error(`Rebalance method ${rebalanceMethod} is not supported`); } }; getPoolInitializedForDexPairTier = async (dex, poolTokenA, poolTokenB, feeBPS) => { if (dex == 'ORCA') { let pool = web3_js_1.PublicKey.default; let orcaPools = await this.getOrcaPoolsForTokens(poolTokenA, poolTokenB); orcaPools.forEach((element) => { if (element.lpFeeRate * CreationParameters_1.FullBPS == feeBPS.toNumber()) { pool = new web3_js_1.PublicKey(element.address); } }); return pool; } else if (dex == 'RAYDIUM') { let pool = web3_js_1.PublicKey.default; let raydiumPools = await this.getRaydiumPoolsForTokens(poolTokenA, poolTokenB); raydiumPools.forEach((element) => { if (new decimal_js_1.default(element.ammConfig.tradeFeeRate).div(CreationParameters_1.FullBPS).div(CreationParameters_1.FullPercentage).equals(feeBPS.div(CreationParameters_1.FullBPS))) { pool = new web3_js_1.PublicKey(element.id); } }); return pool; } else { throw new Error(`Dex ${dex} is not supported`); } }; async getExistentPoolsForPair(dex, tokenMintA, tokenMintB) { if (dex == 'ORCA') { let pools = await this.getOrcaPoolsForTokens(tokenMintA, tokenMintB); let genericPoolInfos = await Promise.all(pools.map(async (pool) => { let positionsCount = new decimal_js_1.default(await this.getPositionsCountForPool(dex, new web3_js_1.PublicKey(pool.address))); // read price from pool let poolData = await this._orcaService.getPool(new web3_js_1.PublicKey(pool.address)); if (!poolData) { throw new Error(`Pool ${pool.address} not found`); } let poolInfo = { dex, address: new web3_js_1.PublicKey(pool.address), price: poolData.price, tokenMintA: new web3_js_1.PublicKey(pool.tokenA.mint), tokenMintB: new web3_js_1.PublicKey(pool.tokenB.mint), tvl: pool.tvl ? new decimal_js_1.default(pool.tvl) : undefined, feeRate: new decimal_js_1.default(pool.lpFeeRate).mul(CreationParameters_1.FullBPS), volumeOnLast7d: pool.volume ? new decimal_js_1.default(pool.volume.week) : undefined, tickSpacing: new decimal_js_1.default(pool.tickSpacing), positions: positionsCount, }; return poolInfo; })); return genericPoolInfos; } else if (dex == 'RAYDIUM') { let pools = await this.getRaydiumPoolsForTokens(tokenMintA, tokenMintB); let genericPoolInfos = await Promise.all(pools.map(async (pool) => { let positionsCount = new decimal_js_1.default(await this.getPositionsCountForPool(dex, new web3_js_1.PublicKey(pool.id))); let poolInfo = { dex, address: new web3_js_1.PublicKey(pool.id), price: new decimal_js_1.default(pool.price), tokenMintA: new web3_js_1.PublicKey(pool.mintA), tokenMintB: new web3_js_1.PublicKey(pool.mintB), tvl: new decimal_js_1.default(pool.tvl), feeRate: new decimal_js_1.default(pool.ammConfig.tradeFeeRate).div(new decimal_js_1.default(CreationParameters_1.FullPercentage)), volumeOnLast7d: new decimal_js_1.default(pool.week.volume), tickSpacing: new decimal_js_1.default(pool.ammConfig.tickSpacing), positions: positionsCount, }; return poolInfo; })); return genericPoolInfos; } else { throw new Error(`Dex ${dex} is not supported`); } } getOrcaPoolsForTokens = async (poolTokenA, poolTokenB) => { let pools = []; 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, poolTokenB) => { let pools = []; 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) => { if (!strategies) { strategies = (await this.getAllStrategiesWithFilters({})).map((x) => x.address); } return await (0, utils_1.batchFetch)(strategies, (chunk) => accounts_1.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) => { if (!strategies) { return this.getAllStrategiesWithFilters({}); } const result = []; const states = await (0, utils_1.batchFetch)(strategies, (chunk) => accounts_1.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) => { let filters = []; filters.push({ dataSize: 4064, }); if (strategyFilters.strategyCreationStatus) { filters.push({ memcmp: { bytes: (0, utils_1.strategyCreationStatusToBase58)(strategyFilters.strategyCreationStatus), offset: 1625, }, }); } if (strategyFilters.strategyType) { filters.push({ memcmp: { bytes: (0, utils_1.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 = { strategy: new accounts_1.WhirlpoolStrategy(x.account), address: x.publicKey, }; return res; }); }; /** * Get a Kamino whirlpool strategy by its public key address * @param address */ getStrategyByAddress = (address) => accounts_1.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) => { 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 accounts_1.WhirlpoolStrategy(matchingStrategies[0].account); 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, scopePrices) => { const strategyState = await this.getStrategyStateIfNotFetched(strategy); const sharesFactor = decimal_js_1.default.pow(10, strategyState.strategy.sharesMintDecimals.toString()); const sharesIssued = new decimal_js_1.default(strategyState.strategy.sharesIssued.toString()); const balances = await this.getStrategyBalances(strategyState.strategy, scopePrices); if (sharesIssued.isZero()) { return { price: new decimal_js_1.default(1), balance: balances }; } else { return { price: balances.computedHoldings.totalSum.div(sharesIssued).mul(sharesFactor), balance: balances }; } }; getStrategiesForSharedData = async (strategyFilters) => { 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) => { return await this.getStrategiesShareDataWithFn(strategyFilters, this.getStrategiesForSharedData); }; getStrategiesShareDataWithFn = async (strategyFilters, getStrategiesFn) => { const result = []; const strategiesWithAddresses = await getStrategiesFn(strategyFilters); const fetchBalances = []; const allScopePrices = strategiesWithAddresses.map((x) => x.strategy.scopePrices); const scopePrices = await this._scope.getMultipleOraclePrices(allScopePrices); const scopePricesMap = scopePrices.reduce((map, [address, price]) => { map[address.toBase58()] = price; return map; }, {}); const raydiumStrategies = strategiesWithAddresses.filter((x) => x.strategy.strategyDex.toNumber() === (0, utils_1.dexToNumber)('RAYDIUM') && x.strategy.position.toString() !== web3_js_1.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() === (0, utils_1.dexToNumber)('ORCA') && x.strategy.position.toString() !== web3_js_1.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() === web3_js_1.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: (0, models_1.getEmptyShareData)({ ...strategyPrices, poolPrice: utils_1.ZERO, upperPrice: utils_1.ZERO, lowerPrice: utils_1.ZERO, }), }); } fetchBalances.push(...this.getBalance(raydiumStrategies, raydiumPools, raydiumPositions, this.getRaydiumBalances, collateralInfos, scopePricesMap)); fetchBalances.push(...this.getBalance(orcaStrategies, orcaPools, orcaPositions, this.getOrcaBalances, collateralInfos, scopePricesMap)); const strategyBalances = await Promise.all(fetchBalances); for (const { balance, strategyWithAddress } of strategyBalances) { const sharesFactor = decimal_js_1.default.pow(10, strategyWithAddress.strategy.sharesMintDecimals.toString()); const sharesIssued = new decimal_js_1.default(strategyWithAddress.strategy.sharesIssued.toString()); if (sharesIssued.isZero()) { result.push({ address: strategyWithAddress.address, strategy: strategyWithAddress.strategy, shareData: { price: new decimal_js_1.default(1), balance }, }); } else { result.push({ address: strategyWithAddress.address, strategy: strategyWithAddress.strategy, shareData: { price: balance.computedHoldings.totalSum.div(sharesIssued).mul(sharesFactor), balance }, }); } } return result; }; getBalance = (strategies, pools, positions, fetchBalance, collateralInfos, prices) => { const fetchBalances = []; 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, position, collateralInfos, prices ? prices[strategy.scopePrices.toBase58()] : undefined).then((balance) => { return { balance, strategyWithAddress: { strategy, address } }; })); } return fetchBalances; }; getRaydiumBalances = async (strategy, pool, position, collateralInfos, prices) => { const strategyPrices = await this.getStrategyPrices(strategy, collateralInfos, prices); const tokenHoldings = await this.getRaydiumTokensBalances(strategy, pool, position); let computedHoldings = this.getStrategyHoldingsUsd(tokenHoldings.available.a, tokenHoldings.available.b, tokenHoldings.invested.a, tokenHoldings.invested.b, new decimal_js_1.default(strategy.tokenAMintDecimals.toString()), new decimal_js_1.default(strategy.tokenBMintDecimals.toString()), strategyPrices.aPrice, strategyPrices.bPrice); let decimalsA = strategy.tokenAMintDecimals.toNumber(); let decimalsB = strategy.tokenBMintDecimals.toNumber(); const poolPrice = raydium_sdk_1.SqrtPriceMath.sqrtPriceX64ToPrice(pool.sqrtPriceX64, decimalsA, decimalsB); const upperPrice = raydium_sdk_1.SqrtPriceMath.sqrtPriceX64ToPrice(raydium_sdk_1.SqrtPriceMath.getSqrtPriceX64FromTick(position.tickUpperIndex), decimalsA, decimalsB); const lowerPrice = raydium_sdk_1.SqrtPriceMath.sqrtPriceX64ToPrice(raydium_sdk_1.SqrtPriceMath.getSqrtPriceX64FromTick(position.tickLowerIndex), decimalsA, decimalsB); const balance = { computedHoldings, prices: { ...strategyPrices, poolPrice, lowerPrice, upperPrice }, tokenAAmounts: tokenHoldings.available.a.plus(tokenHoldings.invested.a), tokenBAmounts: tokenHoldings.available.b.plus(tokenHoldings.invested.b), }; return balance; }; getRaydiumTokensBalances = async (strategy, pool, position) => { const lowerSqrtPriceX64 = raydium_sdk_1.SqrtPriceMath.getSqrtPriceX64FromTick(position.tickLowerIndex); const upperSqrtPriceX64 = raydium_sdk_1.SqrtPriceMath.getSqrtPriceX64FromTick(position.tickUpperIndex); const { amountA, amountB } = raydium_sdk_1.LiquidityMath.getAmountsFromLiquidity(pool.sqrtPriceX64, new bn_js_1.default(lowerSqrtPriceX64), new bn_js_1.default(upperSqrtPriceX64), position.liquidity, false // round down so the holdings are not overestimated ); const aAvailable = new decimal_js_1.default(strategy.tokenAAmounts.toString()); const bAvailable = new decimal_js_1.default(strategy.tokenBAmounts.toString()); const aInvested = new decimal_js_1.default(amountA.toString()); const bInvested = new decimal_js_1.default(amountB.toString()); let holdings = { available: { a: aAvailable, b: bAvailable, }, invested: { a: aInvested, b: bInvested, }, }; return holdings; }; getOrcaBalances = async (strategy, pool, position, collateralInfos, prices) => { const strategyPrices = await this.getStrategyPrices(strategy, collateralInfos, prices); let tokenHoldings = await this.getOrcaTokensBalances(strategy, pool, position); const computedHoldings = this.getStrategyHoldingsUsd(tokenHoldings.available.a, tokenHoldings.available.b, tokenHoldings.invested.a, tokenHoldings.invested.b, new decimal_js_1.default(strategy.tokenAMintDecimals.toString()), new decimal_js_1.default(strategy.tokenBMintDecimals.toString()), strategyPrices.aPrice, strategyPrices.bPrice); const decimalsA = strategy.tokenAMintDecimals.toNumber(); const decimalsB = strategy.tokenBMintDecimals.toNumber(); const poolPrice = (0, whirlpool_sdk_1.sqrtPriceX64ToPrice)(pool.sqrtPrice, decimalsA, decimalsB); const upperPrice = (0, whirlpool_sdk_1.tickIndexToPrice)(position.tickUpperIndex, decimalsA, decimalsB); const lowerPrice = (0, whirlpool_sdk_1.tickIndexToPrice)(position.tickLowerIndex, decimalsA, decimalsB); const balance = { computedHoldings, prices: { ...strategyPrices, poolPrice, upperPrice, lowerPrice }, tokenAAmounts: tokenHoldings.available.a.plus(tokenHoldings.invested.a), tokenBAmounts: tokenHoldings.available.b.plus(tokenHoldings.invested.b), }; return balance; }; getOrcaTokensBalances = async (strategy, pool, position) => { const quote = (0, whirlpool_sdk_1.getRemoveLiquidityQuote)({ positionAddress: strategy.position, liquidity: position.liquidity, slippageTolerance: whirlpool_sdk_1.Percentage.fromFraction(0, 1000), sqrtPrice: pool.sqrtPrice, tickLowerIndex: position.tickLowerIndex, tickUpperIndex: position.tickUpperIndex, tickCurrentIndex: pool.tickCurrentIndex, }); const aAvailable = new decimal_js_1.default(strategy.tokenAAmounts.toString()); const bAvailable = new decimal_js_1.default(strategy.tokenBAmounts.toString()); const aInvested = new decimal_js_1.default(quote.estTokenA.toString()); const bInvested = new decimal_js_1.default(quote.estTokenB.toString()); let holdings = { available: { a: aAvailable, b: bAvailable, }, invested: { a: aInvested, b: bInvested, }, }; return holdings; }; /** * Get the strategies share data (price + balances) of the Kamino whirlpool strategies that match the filters * @param strategyFilters */ getStrategyShareDataForStrategies = async (strategyFilters) => { // weird name of method, but want to keep this method backwards compatible and not rename it return this.getStrategiesShareData(strategyFilters); }; /** * Get the strategy share price of the specified Kamino whirlpool strategy * @param strategy */ getStrategySharePrice = async (strategy) => { const strategyState = await this.getStrategyStateIfNotFetched(strategy); const sharesFactor = decimal_js_1.default.pow(10, strategyState.strategy.sharesMintDecimals.toString()); const sharesIssued = new decimal_js_1.default(strategyState.strategy.sharesIssued.toString()); const balances = await this.getStrategyBalances(strategyState.strategy); if (sharesIssued.isZero()) { return new decimal_js_1.default(1); } else { return balances.computedHoldings.totalSum.div(sharesIssued).mul(sharesFactor); } }; getTokenAccountBalance = async (tokenAccount) => { const tokenAccountBalance = await this._connection.getTokenAccountBalance(tokenAccount); if (!tokenAccountBalance.value) { throw new Error(`Could not get token account balance for ${tokenAccount.toString()}.`); } return new decimal_js_1.default(tokenAccountBalance.value.uiAmountString); }; // getTokenAccountBalanceOrZero return 0 if the token balance is not initialized getTokenAccountBalanceOrZero = async (tokenAccount) => { let tokenAccountExists = await (0, transactions_1.checkIfAccountExists)(this._connection, tokenAccount); if (tokenAccountExists) { return await this.getTokenAccountBalance(tokenAccount); } else { return new decimal_js_1.default(0); } }; getStrategyBalances = async (strategy, scopePrices) => { const collateralInfos = await this.getCollateralInfos(); if (strategy.strategyDex.toNumber() == (0, utils_1.dexToNumber)('ORCA')) { return this.getStrategyBalancesOrca(strategy, collateralInfos, scopePrices); } else if (strategy.strategyDex.toNumber() == (0, utils_1.dexToNumber)('RAYDIUM')) { return this.getStrategyBalancesRaydium(strategy, collateralInfos, scopePrices); } else { throw new Error(`Invalid dex ${strategy.strategyDex.toString()}`); } }; getStrategyTokensBalances = async (strategy) => { if (strategy.strategyDex.toNumber() == (0, utils_1.dexToNumber)('ORCA')) { const [whirlpool, position] = await Promise.all([ whirpools_client_1.Whirlpool.fetch(this._connection, strategy.pool), whirpools_client_1.Position.fetch(this._connection, strategy.position), ]); if (!whirlpool) { throw Error(`Could not fetch whirlpool state with pubkey ${strategy.pool.toString()}`); } if (!position) { throw Error(`Could not fetch position state with pubkey ${strategy.position.toString()}`); } return this.getOrcaTokensBalances(strategy, whirlpool, position); } else if (strategy.strategyDex.toNumber() == (0, utils_1.dexToNumber)('RAYDIUM')) { const [poolState, position] = await Promise.all([ raydium_client_1.PoolState.fetch(this._connection, strategy.pool), raydium_client_1.PersonalPositionState.fetch(this._connection, strategy.position), ]); if (!poolState) { throw Error(`Could not fetch Raydium pool state with pubkey ${strategy.pool.toString()}`); } if (!position) { throw Error(`Could not fetch position state with pubkey ${strategy.position.toString()}`); } return this.getRaydiumTokensBalances(strategy, poolState, position); } else { throw new Error(`Invalid dex ${strategy.strategyDex.toString()}`); } }; /** * Get amount of specified token in all Kamino live strategies * @param tokenMint token mint pubkey */ getTotalTokensInStrategies = async (tokenMint) => { const strategies = await this.get