UNPKG

@kamino-finance/kliquidity-sdk

Version:

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

892 lines (891 loc) 330 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.KSWAP_BASE_API = exports.KAMINO_SCOPE_FEED_ID = exports.HUBBLE_SCOPE_FEED_ID = void 0; const hubble_config_1 = require("@hubbleprotocol/hubble-config"); const kit_1 = require("@solana/kit"); const accounts_1 = require("./@codegen/kliquidity/accounts"); const codamaHelpers_1 = require("./utils/codamaHelpers"); const decimal_js_1 = __importDefault(require("decimal.js")); const instructions_1 = require("./@codegen/whirlpools/instructions"); const accounts_2 = require("./@codegen/whirlpools/accounts"); const whirlpools_core_1 = require("@orca-so/whirlpools-core"); const models_1 = require("./models"); const scope_sdk_1 = require("@kamino-finance/scope-sdk"); const utils_1 = require("./utils"); const bytes_1 = require("./utils/bytes"); const instructions_2 = require("./@codegen/kliquidity/instructions"); const bn_js_1 = __importDefault(require("bn.js")); const raydiumBridge_1 = require("./utils/raydiumBridge"); const strategyStatus_1 = require("./@codegen/kliquidity/types/strategyStatus"); const constants_1 = require("./constants"); const types_1 = require("./@codegen/kliquidity/types"); const accounts_3 = require("./@codegen/raydium/accounts"); const services_1 = require("./services"); // StrategyConfigOption variants are now accessed as StrategyConfigOption.UpdateCollectFeesFee, etc. const DefaultStrategyConfig_1 = require("./constants/DefaultStrategyConfig"); const pubkeys_1 = require("./constants/pubkeys"); const CreationParameters_1 = require("./utils/CreationParameters"); const deposit_method_1 = require("./constants/deposit_method"); const JupService_1 = require("./services/JupService"); const PoolSimulationService_1 = require("./services/PoolSimulationService"); // RebalanceType variants are now accessed as RebalanceType.Manual, RebalanceType.PricePercentage, etc. const transactions_1 = require("./utils/transactions"); 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"); const autodriftRebalance_1 = require("./rebalance_methods/autodriftRebalance"); const whirlpools_1 = require("./utils/whirlpools"); const MeteoraService_1 = require("./services/MeteoraService"); const meteora_1 = require("./utils/meteora"); const accounts_4 = require("./@codegen/meteora/accounts"); const instructions_3 = require("./@codegen/meteora/instructions"); const raydiumSdkCompat_1 = require("./utils/raydiumSdkCompat"); const token_2022_1 = require("@solana-program/token-2022"); const token_1 = require("@solana-program/token"); const system_1 = require("@solana-program/system"); const sysvars_1 = require("@solana/sysvars"); const compat_1 = require("@solana/compat"); const address_lookup_table_1 = require("@solana-program/address-lookup-table"); const lookupTable_1 = require("./utils/lookupTable"); const compat_2 = require("./utils/compat"); const kSwap_1 = require("./services/kSwap"); const addressEncoder = (0, kit_1.getAddressEncoder)(); const base58Decoder = (0, kit_1.getBase58Decoder)(); exports.HUBBLE_SCOPE_FEED_ID = (0, kit_1.address)('3NJYftD5sjVfxSnUdZ1wVML8f3aC6mp1CXCL6L7TnU8C'); exports.KAMINO_SCOPE_FEED_ID = (0, kit_1.address)('3t4JZcueEzTbVP6kLxXrL3VpWx45jDer4eqysweBchNH'); exports.KSWAP_BASE_API = 'https://api.kamino.finance/kswap'; /** Append extra account metas to a codama instruction without losing type safety */ function appendAccounts(ix, extra) { return { ...ix, accounts: [...(ix.accounts || []), ...extra] }; } /** Unwrap an Option<T> to T | undefined */ function unwrapOption(opt) { return opt.__option === 'Some' ? opt.value : undefined; } function yieldToEventLoop() { return new Promise((resolve) => { if (typeof setImmediate !== 'undefined') { setImmediate(resolve); } else { setTimeout(resolve, 0); } }); } class Kamino { _cluster; _rpc; _config; _globalConfig; _scope; _kliquidityProgramId; _orcaService; _raydiumService; _meteoraService; _kSwapBaseAPI = exports.KSWAP_BASE_API; logger; /** * Create a new instance of the Kamino SDK class. * @param cluster Name of the Solana cluster * @param rpc Connection to the Solana cluster * @param globalConfig override kamino global config * @param programId override kamino program id * @param whirlpoolProgramId override whirlpool program id * @param raydiumProgramId override raydium program id * @param meteoraProgramId * @param jupBaseAPI */ constructor(cluster, rpc, globalConfig, programId, whirlpoolProgramId, raydiumProgramId, meteoraProgramId, jupBaseAPI, // unused, kept for backwards compatibility kSwapBaseAPI, logger) { this._cluster = cluster; this._rpc = rpc; this._config = (0, hubble_config_1.getConfigByCluster)(cluster); if (programId && programId === pubkeys_1.STAGING_KAMINO_PROGRAM_ID) { this._kliquidityProgramId = programId; this._globalConfig = pubkeys_1.STAGING_GLOBAL_CONFIG; } else { this._kliquidityProgramId = programId ? programId : (0, compat_1.fromLegacyPublicKey)(this._config.kamino.programId); this._globalConfig = globalConfig ? globalConfig : (0, compat_1.fromLegacyPublicKey)(this._config.kamino.globalConfig); } this.logger = logger ? logger : console; this._scope = new scope_sdk_1.Scope(cluster, rpc); this._orcaService = new services_1.OrcaService(rpc, whirlpoolProgramId, this.logger); this._raydiumService = new services_1.RaydiumService(rpc, raydiumProgramId, this.logger); this._meteoraService = new MeteoraService_1.MeteoraService(rpc, meteoraProgramId, this.logger); if (kSwapBaseAPI) { this._kSwapBaseAPI = kSwapBaseAPI; } } getConnection = () => this._rpc; getProgramID = () => this._kliquidityProgramId; setGlobalConfig = (globalConfig) => { this._globalConfig = globalConfig; }; getGlobalConfig = () => this._globalConfig; getDepositableTokens = async () => { const collateralInfos = await this.getCollateralInfos(); return collateralInfos.filter((x) => x.mint !== pubkeys_1.DEFAULT_PUBLIC_KEY); }; getCollateralInfos = async () => { const config = await this.getGlobalConfigState(this._globalConfig); if (!config) { throw Error(`Could not fetch globalConfig with pubkey ${this.getGlobalConfig().toString()}`); } return this.getCollateralInfo(config.tokenInfos); }; /** * Get the prices of all disabled tokens in the specified collateral infos * @param collateralInfos * @returns {Map<Address, Decimal>} - token prices by mint address */ getDisabledTokensPrices = async (collateralInfos) => { const collInfos = collateralInfos ? collateralInfos : await this.getCollateralInfos(); const disabledTokensMints = collInfos.filter((x) => x.disabled && x.mint !== pubkeys_1.DEFAULT_PUBLIC_KEY).map((x) => x.mint); if (disabledTokensMints.length === 0) { return new Map(); } return (0, kSwap_1.getTokensPrices)(this._kSwapBaseAPI, disabledTokensMints, this.logger); }; getSupportedDexes = () => ['ORCA', 'RAYDIUM', 'METEORA']; // 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 if (dex === 'METEORA') { 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, CreationParameters_1.AutodriftMethod, ]; }; getEnabledRebalanceMethods = () => { return this.getRebalanceMethods().filter((x) => x.enabled); }; 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; const tickSpacing = CreationParameters_1.DefaultTickSpacing; const rebalancingParameters = await this.getDefaultRebalanceFields(dex, tokenMintA, tokenMintB, tickSpacing, rebalanceMethod); const defaultParameters = { dex, tokenMintA, tokenMintB, feeTier, rebalancingParameters, }; return defaultParameters; }; /** * Retunrs what type of rebalance method the fields represent */ getRebalanceTypeFromRebalanceFields = (rebalanceFields) => { return (0, utils_2.getRebalanceTypeFromRebalanceFields)(rebalanceFields); }; /** * Retunrs the rebalance method the fields represent with more details (description, enabled, etc) */ getRebalanceMethodFromRebalanceFields = (rebalanceFields) => { return (0, utils_2.getRebalanceMethodFromRebalanceFields)(rebalanceFields); }; getReferencePriceTypeForStrategy = async (strategy) => { const strategyWithAddress = await this.getStrategyStateIfNotFetched(strategy); return (0, utils_1.numberToReferencePriceType)(strategyWithAddress.strategy.rebalanceRaw.referencePriceType); }; getFieldsForRebalanceMethod = (rebalanceMethod, dex, fieldOverrides, tokenAMint, tokenBMint, tickSpacing, 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, tickSpacing, 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); case CreationParameters_1.AutodriftMethod: return this.getFieldsForAutodriftRebalanceMethod(dex, fieldOverrides, tokenAMint, tokenBMint, tickSpacing, poolPrice); default: throw new Error(`Rebalance method ${rebalanceMethod} is not supported`); } }; getFieldsForManualRebalanceMethod = 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.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, rebalance_methods_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, rebalance_methods_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, rebalance_methods_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, rebalance_methods_1.getDefaultPricePercentageWithResetRebalanceFieldInfos)(price); let lowerPriceDifferenceBPS = defaultFields.find((x) => x.label === 'lowerRangeBps').value; const lowerPriceDifferenceBPSInput = fieldOverrides.find((x) => x.label === 'lowerRangeBps'); if (lowerPriceDifferenceBPSInput) { lowerPriceDifferenceBPS = lowerPriceDifferenceBPSInput.value; } let upperPriceDifferenceBPS = defaultFields.find((x) => x.label === 'upperRangeBps').value; const upperPriceDifferenceBPSInput = fieldOverrides.find((x) => x.label === 'upperRangeBps'); if (upperPriceDifferenceBPSInput) { upperPriceDifferenceBPS = upperPriceDifferenceBPSInput.value; } let lowerResetPriceDifferenceBPS = defaultFields.find((x) => x.label === 'resetLowerRangeBps').value; const lowerResetPriceDifferenceBPSInput = fieldOverrides.find((x) => x.label === 'resetLowerRangeBps'); if (lowerResetPriceDifferenceBPSInput) { lowerResetPriceDifferenceBPS = lowerResetPriceDifferenceBPSInput.value; } let upperResetPriceDifferenceBPS = defaultFields.find((x) => x.label === 'resetUpperRangeBps').value; const upperResetPriceDifferenceBPSInput = fieldOverrides.find((x) => x.label === 'resetUpperRangeBps'); if (upperResetPriceDifferenceBPSInput) { upperResetPriceDifferenceBPS = upperResetPriceDifferenceBPSInput.value; } return (0, rebalance_methods_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, tickSpacing, tokenAMint, tokenBMint, poolPrice) => { const tokenADecimals = await (0, utils_1.getMintDecimals)(this._rpc, tokenAMint); const tokenBDecimals = await (0, utils_1.getMintDecimals)(this._rpc, tokenBMint); const price = poolPrice ? poolPrice : new decimal_js_1.default(await this.getPriceForPair(dex, tokenAMint, tokenBMint)); const defaultFields = (0, rebalance_methods_1.getDefaultDriftRebalanceFieldInfos)(dex, tickSpacing, price, tokenADecimals, tokenBDecimals); let startMidTick = defaultFields.find((x) => x.label === 'startMidTick').value; const startMidTickInput = fieldOverrides.find((x) => x.label === 'startMidTick'); if (startMidTickInput) { startMidTick = startMidTickInput.value; } let ticksBelowMid = defaultFields.find((x) => x.label === 'ticksBelowMid').value; const ticksBelowMidInput = fieldOverrides.find((x) => x.label === 'ticksBelowMid'); if (ticksBelowMidInput) { ticksBelowMid = ticksBelowMidInput.value; } let ticksAboveMid = defaultFields.find((x) => x.label === 'ticksAboveMid').value; const ticksAboveMidInput = fieldOverrides.find((x) => x.label === 'ticksAboveMid'); if (ticksAboveMidInput) { ticksAboveMid = ticksAboveMidInput.value; } let secondsPerTick = defaultFields.find((x) => x.label === 'secondsPerTick').value; const secondsPerTickInput = fieldOverrides.find((x) => x.label === 'secondsPerTick'); if (secondsPerTickInput) { secondsPerTick = secondsPerTickInput.value; } let direction = defaultFields.find((x) => x.label === 'direction').value; const directionInput = fieldOverrides.find((x) => x.label === 'direction'); if (directionInput) { direction = directionInput.value; } const fieldInfos = (0, rebalance_methods_1.getDriftRebalanceFieldInfos)(dex, tokenADecimals, tokenBDecimals, tickSpacing, 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); let lowerRangePrice = defaultFields.find((x) => x.label === 'rangePriceLower').value; const lowerRangePriceInput = fieldOverrides.find((x) => x.label === 'rangePriceLower'); if (lowerRangePriceInput) { lowerRangePrice = lowerRangePriceInput.value; } let upperRangePrice = defaultFields.find((x) => x.label === 'rangePriceUpper').value; const upperRangePriceInput = fieldOverrides.find((x) => x.label === 'rangePriceUpper'); if (upperRangePriceInput) { upperRangePrice = upperRangePriceInput.value; } let destinationToken = defaultFields.find((x) => x.label === 'destinationToken').value; const destinationTokenInput = fieldOverrides.find((x) => x.label === 'destinationToken'); if (destinationTokenInput) { destinationToken = destinationTokenInput.value; } return (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); const 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; 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, 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; const lowerPriceDifferenceBPSInput = fieldOverrides.find((x) => x.label === 'lowerRangeBps'); if (lowerPriceDifferenceBPSInput) { lowerPriceDifferenceBPS = lowerPriceDifferenceBPSInput.value; } let upperPriceDifferenceBPS = defaultFields.find((x) => x.label === 'upperRangeBps').value; const upperPriceDifferenceBPSInput = fieldOverrides.find((x) => x.label === 'upperRangeBps'); if (upperPriceDifferenceBPSInput) { upperPriceDifferenceBPS = upperPriceDifferenceBPSInput.value; } let lowerResetPriceDifferenceBPS = defaultFields.find((x) => x.label === 'resetLowerRangeBps').value; const lowerResetPriceDifferenceBPSInput = fieldOverrides.find((x) => x.label === 'resetLowerRangeBps'); if (lowerResetPriceDifferenceBPSInput) { lowerResetPriceDifferenceBPS = lowerResetPriceDifferenceBPSInput.value; } let upperResetPriceDifferenceBPS = defaultFields.find((x) => x.label === 'resetUpperRangeBps').value; const upperResetPriceDifferenceBPSInput = fieldOverrides.find((x) => x.label === 'resetUpperRangeBps'); if (upperResetPriceDifferenceBPSInput) { upperResetPriceDifferenceBPS = upperResetPriceDifferenceBPSInput.value; } let expansionBPS = defaultFields.find((x) => x.label === 'expansionBps').value; const expansionBPSInput = fieldOverrides.find((x) => x.label === 'expansionBps'); if (expansionBPSInput) { expansionBPS = expansionBPSInput.value; } let maxNumberOfExpansions = defaultFields.find((x) => x.label === 'maxNumberOfExpansions').value; const maxNumberOfExpansionsInput = fieldOverrides.find((x) => x.label === 'maxNumberOfExpansions'); if (maxNumberOfExpansionsInput) { maxNumberOfExpansions = maxNumberOfExpansionsInput.value; } let swapUnevenAllowed = defaultFields.find((x) => x.label === 'swapUnevenAllowed').value; const swapUnevenAllowedInput = fieldOverrides.find((x) => x.label === 'swapUnevenAllowed'); if (swapUnevenAllowedInput) { swapUnevenAllowed = swapUnevenAllowedInput.value; } return (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)); }; getFieldsForAutodriftRebalanceMethod = async (dex, fieldOverrides, tokenAMint, tokenBMint, tickSpacing, poolPrice) => { const tokenADecimals = await (0, utils_1.getMintDecimals)(this._rpc, tokenAMint); const tokenBDecimals = await (0, utils_1.getMintDecimals)(this._rpc, tokenBMint); const price = poolPrice ? poolPrice : new decimal_js_1.default(await this.getPriceForPair(dex, tokenAMint, tokenBMint)); // TODO: maybe we will need to get real staking price instead of pool price for this to be accurate. const defaultFields = (0, autodriftRebalance_1.getDefaultAutodriftRebalanceFieldInfos)(dex, price, tokenADecimals, tokenBDecimals, tickSpacing); const lastMidTick = defaultFields.find((x) => x.label === 'lastMidTick').value; let initDriftTicksPerEpoch = defaultFields.find((x) => x.label === 'initDriftTicksPerEpoch').value; const initDriftTicksPerEpochInput = fieldOverrides.find((x) => x.label === 'initDriftTicksPerEpoch'); if (initDriftTicksPerEpochInput) { initDriftTicksPerEpoch = initDriftTicksPerEpochInput.value; } let ticksBelowMid = defaultFields.find((x) => x.label === 'ticksBelowMid').value; const ticksBelowMidInput = fieldOverrides.find((x) => x.label === 'ticksBelowMid'); if (ticksBelowMidInput) { ticksBelowMid = ticksBelowMidInput.value; } let ticksAboveMid = defaultFields.find((x) => x.label === 'ticksAboveMid').value; const ticksAboveMidInput = fieldOverrides.find((x) => x.label === 'ticksAboveMid'); if (ticksAboveMidInput) { ticksAboveMid = ticksAboveMidInput.value; } let frontrunMultiplierBps = defaultFields.find((x) => x.label === 'frontrunMultiplierBps').value; const frontrunMultiplierBpsInput = fieldOverrides.find((x) => x.label === 'frontrunMultiplierBps'); if (frontrunMultiplierBpsInput) { frontrunMultiplierBps = frontrunMultiplierBpsInput.value; } let stakingRateASource = defaultFields.find((x) => x.label === 'stakingRateASource').value; const stakingRateASourceInput = fieldOverrides.find((x) => x.label === 'stakingRateASource'); if (stakingRateASourceInput) { stakingRateASource = stakingRateASourceInput.value; } let stakingRateBSource = defaultFields.find((x) => x.label === 'stakingRateBSource').value; const stakingRateBSourceInput = fieldOverrides.find((x) => x.label === 'stakingRateBSource'); if (stakingRateBSourceInput) { stakingRateBSource = stakingRateBSourceInput.value; } let initialDriftDirection = defaultFields.find((x) => x.label === 'initialDriftDirection').value; const initialDriftDirectionInput = fieldOverrides.find((x) => x.label === 'initialDriftDirection'); if (initialDriftDirectionInput) { initialDriftDirection = initialDriftDirectionInput.value; } const fieldInfos = (0, autodriftRebalance_1.getAutodriftRebalanceFieldInfos)(dex, tokenADecimals, tokenBDecimals, tickSpacing, new decimal_js_1.default(lastMidTick), new decimal_js_1.default(initDriftTicksPerEpoch), new decimal_js_1.default(ticksBelowMid), new decimal_js_1.default(ticksAboveMid), new decimal_js_1.default(frontrunMultiplierBps), new decimal_js_1.default(stakingRateASource), new decimal_js_1.default(stakingRateBSource), new decimal_js_1.default(initialDriftDirection)); return fieldInfos; }; /** * Get the price for a given pair of tokens in a given dex; The price comes from any pool having those tokens, not a specific one, so the price may not be exactly the same between different pools with the same tokens. For a specific pool price use getPoolPrice * @param strategy * @param amountA */ getPriceForPair = async (dex, poolTokenA, poolTokenB) => { if (dex === 'ORCA') { const pools = await this.getOrcaPoolsForTokens(poolTokenA, poolTokenB); if (pools.length === 0) { throw new Error(`No pool found for ${poolTokenA.toString()} and ${poolTokenB.toString()}`); } return Number(pools[0].price); } else if (dex === 'RAYDIUM') { const pools = await this.getRaydiumPoolsForTokens(poolTokenA, poolTokenB); if (pools.length === 0) { throw new Error(`No pool found for ${poolTokenA.toString()} and ${poolTokenB.toString()}`); } return pools[0].price; } else if (dex === 'METEORA') { const pools = await this.getMeteoraPoolsForTokens(poolTokenA, poolTokenB); if (pools.length === 0) { throw new Error(`No pool found for ${poolTokenA.toString()} and ${poolTokenB.toString()}`); } const decimalsX = await (0, utils_1.getMintDecimals)(this._rpc, poolTokenA); const decimalsY = await (0, utils_1.getMintDecimals)(this._rpc, poolTokenB); return (0, meteora_1.getPriceOfBinByBinIdWithDecimals)(pools[0].pool.activeId, pools[0].pool.binStep, decimalsX, decimalsY).toNumber(); } else { throw new Error(`Dex ${dex} is not supported`); } }; getDefaultRebalanceFields = async (dex, poolTokenA, poolTokenB, tickSpacing, rebalanceMethod) => { const price = new decimal_js_1.default(await this.getPriceForPair(dex, poolTokenA, poolTokenB)); const tokenADecimals = await (0, utils_1.getMintDecimals)(this._rpc, poolTokenA); const tokenBDecimals = await (0, utils_1.getMintDecimals)(this._rpc, poolTokenB); switch (rebalanceMethod) { case CreationParameters_1.ManualRebalanceMethod: return (0, rebalance_methods_1.getDefaultManualRebalanceFieldInfos)(price); case CreationParameters_1.PricePercentageRebalanceMethod: return (0, rebalance_methods_1.getDefaultPricePercentageRebalanceFieldInfos)(price); case CreationParameters_1.PricePercentageWithResetRangeRebalanceMethod: return (0, rebalance_methods_1.getDefaultPricePercentageWithResetRebalanceFieldInfos)(price); case CreationParameters_1.DriftRebalanceMethod: return (0, rebalance_methods_1.getDefaultDriftRebalanceFieldInfos)(dex, tickSpacing, 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); case CreationParameters_1.AutodriftMethod: return (0, autodriftRebalance_1.getDefaultAutodriftRebalanceFieldInfos)(dex, price, tokenADecimals, tokenBDecimals, tickSpacing); default: throw new Error(`Rebalance method ${rebalanceMethod} is not supported`); } }; /** * Return a the pubkey of the pool in a given dex, for given mints and fee tier; if that pool doesn't exist, return default pubkey */ getPoolInitializedForDexPairTier = async (dex, poolTokenA, poolTokenB, feeBPS) => { if (dex === 'ORCA') { let pool = pubkeys_1.DEFAULT_PUBLIC_KEY; const orcaPools = await this.getOrcaPoolsForTokens(poolTokenA, poolTokenB); orcaPools.forEach((element) => { if (element.feeRate * CreationParameters_1.FullBPS === feeBPS.toNumber()) { pool = (0, kit_1.address)(element.address); } }); return pool; } else if (dex === 'RAYDIUM') { const pools = []; const 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))) { pools.push(element); } }); if (pools.length === 0) { return pubkeys_1.DEFAULT_PUBLIC_KEY; } let pool = pubkeys_1.DEFAULT_PUBLIC_KEY; let tickSpacing = Number.MAX_VALUE; pools.forEach((element) => { if (element.ammConfig.tickSpacing < tickSpacing) { pool = (0, kit_1.address)(element.id); tickSpacing = element.ammConfig.tickSpacing; } }); return pool; } else if (dex === 'METEORA') { let pool = pubkeys_1.DEFAULT_PUBLIC_KEY; const pools = await this.getMeteoraPoolsForTokens(poolTokenA, poolTokenB); pools.forEach((element) => { const feeRateBps = element.pool.parameters.baseFactor * element.pool.binStep; if (feeRateBps === feeBPS.toNumber()) { pool = (0, kit_1.address)(element.key); } }); return pool; } else { throw new Error(`Dex ${dex} is not supported`); } }; /** * Return generic information for all pools in a given dex, for given mints and fee tier */ async getExistentPoolsForPair(dex, tokenMintA, tokenMintB) { if (dex === 'ORCA') { const pools = await this.getOrcaPoolsForTokens(tokenMintA, tokenMintB); const genericPoolInfos = await Promise.all(pools.map(async (pool) => { const positionsCount = new decimal_js_1.default(await this.getPositionsCountForPool(dex, (0, kit_1.address)(pool.address))); // read price from pool const poolData = await this._orcaService.getOrcaWhirlpool((0, kit_1.address)(pool.address)); if (!poolData) { throw new Error(`Pool ${pool.address} not found`); } const poolInfo = { dex, address: (0, kit_1.address)(pool.address), price: new decimal_js_1.default((0, whirlpools_core_1.sqrtPriceToPrice)(BigInt(poolData.sqrtPrice), pool.tokenA.decimals, pool.tokenB.decimals)), tokenMintA: (0, kit_1.address)(pool.tokenMintA), tokenMintB: (0, kit_1.address)(pool.tokenMintB), tvl: pool.tvlUsdc ? new decimal_js_1.default(pool.tvlUsdc) : undefined, feeRate: new decimal_js_1.default(pool.feeRate).mul(CreationParameters_1.FullBPS), volumeOnLast7d: pool.stats['7d'] ? new decimal_js_1.default(pool.stats['7d'].volume) : undefined, tickSpacing: new decimal_js_1.default(pool.tickSpacing), positions: positionsCount, }; return poolInfo; })); return genericPoolInfos; } else if (dex === 'RAYDIUM') { const pools = await this.getRaydiumPoolsForTokens(tokenMintA, tokenMintB); const genericPoolInfos = await Promise.all(pools.map(async (pool) => { const positionsCount = new decimal_js_1.default(await this.getPositionsCountForPool(dex, (0, kit_1.address)(pool.id))); const poolInfo = { dex, address: (0, kit_1.address)(pool.id), price: new decimal_js_1.default(pool.price), tokenMintA: (0, kit_1.address)(pool.mintA), tokenMintB: (0, kit_1.address)(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 if (dex === 'METEORA') { const pools = await this.getMeteoraPoolsForTokens(tokenMintA, tokenMintB); const genericPoolInfos = await Promise.all(pools.map(async (pool) => { const positionsCount = new decimal_js_1.default(await this.getPositionsCountForPool(dex, pool.key)); const decimalsX = await (0, utils_1.getMintDecimals)(this._rpc, pool.pool.tokenXMint); const decimalsY = await (0, utils_1.getMintDecimals)(this._rpc, pool.pool.tokenYMint); const price = (0, meteora_1.getPriceOfBinByBinIdWithDecimals)(pool.pool.activeId, pool.pool.binStep, decimalsX, decimalsY); const poolInfo = { dex, address: pool.key, price, tokenMintA: pool.pool.tokenXMint, tokenMintB: pool.pool.tokenYMint, tvl: new decimal_js_1.default(0), feeRate: (0, MeteoraService_1.computeMeteoraFee)(pool.pool).div(1e2), // Transform it to rate volumeOnLast7d: new decimal_js_1.default(0), tickSpacing: new decimal_js_1.default(pool.pool.binStep), positions: positionsCount, }; return poolInfo; })); return genericPoolInfos; } else { throw new Error(`Dex ${dex} is not supported`); } } getOrcaPoolsForTokens = async (poolTokenA, poolTokenB) => { const pools = []; const poolTokenAString = poolTokenA.toString(); const poolTokenBString = poolTokenB.toString(); const whirlpools = await this._orcaService.getOrcaWhirlpools(); whirlpools.forEach((element) => { if ((element.tokenMintA === poolTokenAString && element.tokenMintB === poolTokenBString) || (element.tokenMintA === poolTokenBString && element.tokenMintB === poolTokenAString)) pools.push(element); }); return pools; }; getRaydiumPoolsForTokens = async (poolTokenA, poolTokenB) => { const pools = []; const poolTokenAString = poolTokenA.toString(); const poolTokenBString = poolTokenB.toString(); const raydiumPools = await this._raydiumService.getRaydiumWhirlpools(); raydiumPools.data.forEach((element) => { if ((element.mintA === poolTokenAString && element.mintB === poolTokenBString) || (element.mintA === poolTokenBString && element.mintB === poolTokenAString)) { pools.push(element); } }); return pools; }; getMeteoraPoolsForTokens = async (poolTokenA, poolTokenB) => { const pools = []; const meteoraPools = await this._meteoraService.getMeteoraPools(); meteoraPools.forEach((element) => { if ((element.pool.tokenXMint === poolTokenA && element.pool.tokenYMint === poolTokenB) || (element.pool.tokenXMint === poolTokenB && element.pool.tokenYMint === poolTokenA)) { pools.push(element); } }); return pools; }; /** * Return a list of all Kamino whirlpool strategies * @param strategies Limit results to these strategy addresses */ getStrategies = async (strategies) => { if (!strategies) { strategies = (await this.getAllStrategiesWithFilters({})).map((x) => x.address); } return await (0, utils_1.batchFetch)(strategies, (chunk) => this.getWhirlpoolStrategies(chunk)); }; /** * Return a list of all Kamino whirlpool strategies with their addresses * @param strategies Limit results to these strategy addresses */ getStrategiesWithAddresses = async (strategies) => { if (!strategies) { return this.getAllStrategiesWithFilters({}); } const result = []; const states = await (0, utils_1.batchFetch)(strategies, (chunk) => this.getWhirlpoolStrategies(chunk)); for (let i = 0; i < strategies.length; i++) { if (states[i]) { result.push({ address: strategies[i], strategy: states[i] }); } else { throw Error(`Could not fetch strategy state for ${strategies[i].toString()}`); } } return result; }; buildStrategiesGpaFilters(strategyFilters) { const filters = []; filters.push({ dataSize: BigInt((0, accounts_1.getWhirlpoolStrategySize)()), }); filters.push({ memcmp: { offset: 0n, bytes: base58Decoder.decode(accounts_1.WHIRLPOOL_STRATEGY_DISCRIMINATOR), encoding: 'base58', }, }); if (strategyFilters.owner) { filters.push({ memcmp: { offset: 8n, bytes: strategyFilters.owner.toString(), encoding: 'base58', }, }); } if (strategyFilters.strategyCreationStatus) { filters.push({ memcmp: { offset: 1625n, bytes: (0, utils_1.strategyCreationStatusToBase58)(strategyFilters.strategyCreationStatus), encoding: 'base58', }, }); } if (strategyFilters.strategyType) { filters.push({ memcmp: { offset: 1120n, bytes: (0, utils_1.strategyTypeToBase58)(strategyFilters.strategyType).toString(), encoding: 'base58', }, }); } if (strategyFilters.isCommunity !== undefined && strategyFilters.isCommunity !== null) { const value = !strategyFilters.isCommunity ? '1' : '2'; filters.push({ memcmp: { offset: 1664n, bytes: value, encoding: 'base58', }, }); } return filters; } /** * Streaming variant of {@link getAllStrategiesWithFilters}. Decodes one * strategy at a time and yields control to the event loop every * `yieldEvery` items (default 100) via `setImmediate` (or `setTimeout(0)` * outside Node) so other I/O can make progress while ~5600 strategies * decode. * * @param strategyFilters * @param options.yieldEvery decode this many strategies before yielding to * the event loop. Default 100. Set to `Infinity` to never yield. */ async *getAllStrategiesWithFiltersStream(strategyFilters, options) { const yieldEvery = options?.yieldEvery ?? 100; const filters = this.buildStrategiesGpaFilters(strategyFilters); const accounts = await this._rpc .getProgramAccounts(this.getProgramID(), { filters, encoding: 'base64', }) .send(); for (let i = 0; i < accounts.length; i++) { const x = accounts[i]; yield { strategy: (0, accounts_1.getWhirlpoolStrategyDecoder)().decode((0, bytes_1.base64ToBytes)(x.account.data[0])), address: x.pubkey, }; if ((i + 1) % yieldEvery === 0 && i + 1 < accounts.length) { await yieldToEventLoop(); } } } getAllStrategiesWithFilters = async (strategyFilters) => { const result = []; for await (const item of this.getAllStrategiesWithFiltersStream(strategyFilters)) { result.push(item); } return result; }; /** * Get a Kamino whirlpool strategy by its public key address * @param address */ getStrategyByAddress = (address) => this.getWhirlpoolStrategy(address); /** * Get a Kamino whirlpool strategy by its kToken mint address * @param kTokenMint - mint address of the kToken */ getStrategyByKTokenMint = async (kTokenMint) => { const filters = [ { dataSize: BigInt((0, accounts_1.getWhirlpoolStrategySize)()), }, { memcmp: { offset: 0n, bytes: base58Decoder.decode(accounts_1.WHIRLPOOL_STRATEGY_DISCRIMINATOR), encoding: 'base58', }, }, { memcmp: { bytes: kTokenMint.toString(), offset: 720n, encoding: 'base58', }, }, ]; const matchingStrategies = await this._rpc .getProgramAccounts(this.getProgramID(), { filters, encoding: 'base64', }) .send(); if (matchingStrategies.length === 0) { return null; } if (matchingStrategies.length > 1) { throw new Error(`Multiple strategies found for kToken mint: ${kTokenMint}. Strategies found: ${matchingStrategies.map((x) => x.pubkey)}`); } const decodedStrategy = (0, accounts_1.getWhirlpoolStrategyDecoder)().decode((0, bytes_1.base64ToBytes)(matchingStrategies[0].account.data[0])); return { address: matchingStrategies[0].pubkey, 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 collateralInfos = scopePrices ? await this.getCollateralInfos() : undefined; const scopePricesMap = collateralInfos ? await this.getScopePricesMap(this.strategyTokenScopeFeedsToArray(this.getAllScopePriceFeedsForStrategy(strategyState.strategy, collateralInfos)), scopePrices) : undefined; 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, scopePricesMap); 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 }; } }; /** * Get the token A and B per share for the specified Kamino whirlpool strategy * @param strategy */ getTokenAAndBPerShare = async (strategy) => { const strategyState = await this.getStrategyStateIfNotFetched(strategy); const sharesIssued = new decimal_js_1.default(strategyState.strategy.sharesIssued.toString()); const balances = await this.getStrategyBalances(strategyState.strategy); if (sharesIssued.isZero()) { return { a: new decimal_js_1.default(0), b: new decimal_js_1.default(0) }; } return { a: balances.tokenAAmounts.div(sharesIssued), b: balances.tokenBAmounts.div(sharesIssued) }; }; /** * Get all scope prices feeds (token A, token B and rewards) for the specified strategy * @param strategy * @param collateralInfos the collateral infos array * @returns the scope price feeds for the specified strategy */ getAllScopePriceFeedsForStrategy = (strategy, collateralInfos) => { const res = { tokenAFeed: collateralInfos[Number(strategy.tokenACollateralId)].scopeFeed, tokenBFeed: collateralInfos[Number(strategy.tokenBCollateralId)].scopeFeed, rewardsFeeds: [undefined, undefined, undefined], kaminoRewardsFeeds: [undefined, undefined, undefined], }; if (strategy.reward0Vault !== pubkeys_1.DEFAULT_PUBLIC_KEY && strategy.reward0Vault !== strategy.baseVaultAuthority) { res.rewardsFeeds[0] = collateralInfos[Number(strategy.reward0CollateralId)].scopeFeed;