UNPKG

@kamino-finance/scope-sdk

Version:
474 lines 20.5 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Scope = void 0; const kit_1 = require("@solana/kit"); const decimal_js_1 = __importDefault(require("decimal.js")); const accounts_1 = require("./@codegen/scope/accounts"); const types_1 = require("./@codegen/scope/types"); const constants_1 = require("./constants"); const ScopeIx = __importStar(require("./@codegen/scope/instructions")); const utils_1 = require("./utils"); const model_1 = require("./model"); const accounts_2 = require("./@codegen/kliquidity/accounts"); const accounts_3 = require("./@codegen/jupiter-perps/accounts"); const programId_1 = require("./@codegen/jupiter-perps/programId"); const system_1 = require("@solana-program/system"); const sysvars_1 = require("@solana/sysvars"); class Scope { _rpc; _config; /** * Create a new instance of the Scope SDK class. * @param cluster Name of the Solana cluster * @param rpc Connection to the Solana rpc */ constructor(cluster, rpc) { this._rpc = rpc; switch (cluster) { case 'localnet': this._config = constants_1.SCOPE_LOCALNET_CONFIG; break; case 'devnet': this._config = constants_1.SCOPE_DEVNET_CONFIG; break; case 'mainnet-beta': { this._config = constants_1.SCOPE_MAINNET_CONFIG; break; } default: { throw Error('Invalid cluster'); } } } static priceToDecimal(price) { return new decimal_js_1.default(price.value.toString()).mul(new decimal_js_1.default(10).pow(new decimal_js_1.default(-price.exp.toString()))); } /** * Get the deserialised OraclePrices account for a given feed * @param feed - either the feed PDA seed or the configuration account address * @returns OraclePrices */ async getOraclePrices(feed) { (0, model_1.validatePricesParam)(feed); let oraclePrices; if (feed?.feed || feed?.config) { const [, configAccount] = await this.getFeedConfiguration(feed); oraclePrices = configAccount.oraclePrices; } else if (feed?.prices) { oraclePrices = feed.prices; } else { oraclePrices = this._config.oraclePrices; } const prices = await accounts_1.OraclePrices.fetch(this._rpc, oraclePrices, this._config.programId); if (!prices) { throw Error(`Could not get scope oracle prices`); } return prices; } /** * Get the deserialised OraclePrices accounts for a given `OraclePrices` account pubkeys * Optimised to filter duplicate keys from the network request but returns the same size response as requested in the same order * @throws Error if any of the accounts cannot be fetched * @param prices - public keys of the `OraclePrices` accounts * @returns [Address, OraclePrices][] */ async getMultipleOraclePrices(prices) { const priceStrings = prices.map((price) => price); const uniqueScopePrices = [...new Set(priceStrings)]; if (uniqueScopePrices.length === 1) { return [[uniqueScopePrices[0], await this.getOraclePrices({ prices: uniqueScopePrices[0] })]]; } const oraclePrices = await accounts_1.OraclePrices.fetchMultiple(this._rpc, uniqueScopePrices, this._config.programId); const oraclePricesMap = oraclePrices .map((price, i) => { if (price === null) { throw Error(`Could not get scope oracle prices for ${uniqueScopePrices[i]}`); } return price; }) .reduce((map, price, i) => { map[uniqueScopePrices[i]] = price; return map; }, {}); return prices.map((price) => [price, oraclePricesMap[price]]); } /** * Get the deserialised Configuration account for a given feed * @param feedParam - either the feed PDA seed or the configuration account address * @returns [configuration account address, deserialised configuration] */ async getFeedConfiguration(feedParam) { (0, model_1.validateFeedParam)(feedParam); const { feed, config } = feedParam || {}; let configPubkey; if (feed) { configPubkey = await (0, utils_1.getConfigurationPda)(feed); } else if (config) { configPubkey = config; } else { configPubkey = this._config.configurationAccount; } const configAccount = await accounts_1.Configuration.fetch(this._rpc, configPubkey, this._config.programId); if (!configAccount) { throw new Error(`Could not find configuration account for ${feed || configPubkey}`); } return [configPubkey, configAccount]; } /** * Get the deserialised OracleMappings account for a given feed * @param feed - either the feed PDA seed or the configuration account address * @returns OracleMappings */ async getOracleMappings(feed) { const [config, configAccount] = await this.getFeedConfiguration(feed); return this.getOracleMappingsFromConfig(feed, config, configAccount); } /** * Get the deserialized OracleMappings account for a given feed and config * @param feed - either the feed PDA seed or the configuration account address * @param config - the configuration account address * @param configAccount - the deserialized configuration account * @returns OracleMappings */ async getOracleMappingsFromConfig(feed, config, configAccount) { const oracleMappings = await accounts_1.OracleMappings.fetch(this._rpc, configAccount.oracleMappings, this._config.programId); if (!oracleMappings) { throw Error(`Could not get scope oracle mappings account for feed ${JSON.stringify(feed)}, config ${config}`); } return oracleMappings; } /** * Get the price of a token from a chain of token prices * @param chain * @param prices */ static getPriceFromScopeChain(chain, prices) { // Protect from bad defaults if (chain.every((tokenId) => tokenId === 0)) { throw new Error('Token chain cannot be all 0s'); } // Protect from bad defaults const filteredChain = chain.filter((tokenId) => tokenId !== constants_1.U16_MAX); if (filteredChain.length === 0) { throw new Error(`Token chain cannot be all ${constants_1.U16_MAX}s (u16 max)`); } let oldestTimestamp = new decimal_js_1.default('0'); const priceChain = filteredChain.map((tokenId) => { const datedPrice = prices.prices[tokenId]; if (!datedPrice) { throw Error(`Could not get price for token ${tokenId}`); } const currentPxTs = new decimal_js_1.default(datedPrice.unixTimestamp.toString()); if (oldestTimestamp.eq(new decimal_js_1.default('0'))) { oldestTimestamp = currentPxTs; } else if (!currentPxTs.eq(new decimal_js_1.default('0'))) { oldestTimestamp = decimal_js_1.default.min(oldestTimestamp, currentPxTs); } const priceInfo = datedPrice.price; return Scope.priceToDecimal(priceInfo); }); if (priceChain.length === 1) { return { price: priceChain[0], timestamp: oldestTimestamp, }; } // Compute token value by multiplying all values of the chain const pxFromChain = priceChain.reduce((acc, price) => acc.mul(price), new decimal_js_1.default(1)); return { price: pxFromChain, timestamp: oldestTimestamp, }; } /** * Verify if the scope chain is valid * @param chain */ static isScopeChainValid(chain) { return !(chain.length === 0 || chain.every((tokenId) => tokenId === 0) || chain.every((tokenId) => tokenId === constants_1.U16_MAX)); } /** * Get the price of a token from a chain of token prices * @param chain * @param oraclePrices */ async getPriceFromChain(chain, oraclePrices) { let prices; if (oraclePrices) { prices = oraclePrices; } else { prices = await this.getOraclePrices(); } return Scope.getPriceFromScopeChain(chain, prices); } /** * Create a new scope price feed * @param admin * @param feed */ async initialise(admin, feed) { const config = await (0, utils_1.getConfigurationPda)(feed); const oraclePrices = await (0, kit_1.generateKeyPairSigner)(); const createOraclePricesIx = (0, system_1.getCreateAccountInstruction)({ payer: admin, newAccount: oraclePrices, lamports: await this._rpc.getMinimumBalanceForRentExemption(utils_1.ORACLE_PRICES_LEN).send(), space: utils_1.ORACLE_PRICES_LEN, programAddress: this._config.programId, }); const oracleMappings = await (0, kit_1.generateKeyPairSigner)(); const createOracleMappingsIx = (0, system_1.getCreateAccountInstruction)({ payer: admin, newAccount: oracleMappings, lamports: await this._rpc.getMinimumBalanceForRentExemption(utils_1.ORACLE_MAPPINGS_LEN).send(), space: utils_1.ORACLE_MAPPINGS_LEN, programAddress: this._config.programId, }); const tokenMetadatas = await (0, kit_1.generateKeyPairSigner)(); const createTokenMetadatasIx = (0, system_1.getCreateAccountInstruction)({ payer: admin, newAccount: tokenMetadatas, lamports: await this._rpc.getMinimumBalanceForRentExemption(utils_1.TOKEN_METADATAS_LEN).send(), space: utils_1.TOKEN_METADATAS_LEN, programAddress: this._config.programId, }); const oracleTwaps = await (0, kit_1.generateKeyPairSigner)(); const createOracleTwapsIx = (0, system_1.getCreateAccountInstruction)({ payer: admin, newAccount: oracleTwaps, lamports: await this._rpc.getMinimumBalanceForRentExemption(utils_1.ORACLE_TWAPS_LEN).send(), space: utils_1.ORACLE_TWAPS_LEN, programAddress: this._config.programId, }); const initScopeIx = ScopeIx.initialize({ feedName: feed }, { admin: admin, configuration: config, oracleMappings: oracleMappings.address, oracleTwaps: oracleTwaps.address, tokenMetadatas: tokenMetadatas.address, oraclePrices: oraclePrices.address, systemProgram: system_1.SYSTEM_PROGRAM_ADDRESS, }, this._config.programId); return [ [createOraclePricesIx, createOracleMappingsIx, createOracleTwapsIx, createTokenMetadatasIx, initScopeIx], [admin, oraclePrices, oracleMappings, oracleTwaps, tokenMetadatas], { configuration: config, oracleMappings: oracleMappings.address, oraclePrices: oraclePrices.address, oracleTwaps: oracleTwaps.address, }, ]; } /** * Update the price mapping of a token * @param admin * @param feed * @param index * @param oracleType * @param mapping * @param twapEnabled * @param twapSource * @param refPriceIndex * @param genericData */ async updateFeedMapping(admin, feed, index, oracleType, mapping, twapEnabled = false, twapSource = 0, refPriceIndex = 65_535, genericData = Array(20).fill(0)) { const [config, configAccount] = await this.getFeedConfiguration({ feed }); const updateIx = ScopeIx.updateMapping({ feedName: feed, token: index, priceType: oracleType.discriminator, twapEnabled, twapSource, refPriceIndex, genericData, }, { admin: admin, configuration: config, oracleMappings: configAccount.oracleMappings, priceInfo: (0, kit_1.some)(mapping), }, this._config.programId); return updateIx; } async refreshPriceList(feed, tokens) { const [, configAccount] = await this.getFeedConfiguration(feed); let refreshIx = ScopeIx.refreshPriceList({ tokens, }, { oracleMappings: configAccount.oracleMappings, oraclePrices: configAccount.oraclePrices, oracleTwaps: configAccount.oracleTwaps, instructionSysvarAccountInfo: sysvars_1.SYSVAR_INSTRUCTIONS_ADDRESS, }, this._config.programId); const mappings = await this.getOracleMappings(feed); for (const token of tokens) { refreshIx = { ...refreshIx, accounts: refreshIx.accounts?.concat(await Scope.getRefreshAccounts(this._rpc, configAccount, this._config.kliquidityProgramId, mappings, token)), }; } return refreshIx; } async refreshPriceListIx(feed, tokens) { const [config, configAccount] = await this.getFeedConfiguration(feed); const mappings = await this.getOracleMappingsFromConfig(feed, config, configAccount); return this.refreshPriceListIxWithAccounts(tokens, configAccount, mappings); } async refreshPriceListIxWithAccounts(tokens, configAccount, mappings) { let refreshIx = ScopeIx.refreshPriceList({ tokens, }, { oracleMappings: configAccount.oracleMappings, oraclePrices: configAccount.oraclePrices, oracleTwaps: configAccount.oracleTwaps, instructionSysvarAccountInfo: sysvars_1.SYSVAR_INSTRUCTIONS_ADDRESS, }, this._config.programId); for (const token of tokens) { refreshIx = { ...refreshIx, accounts: refreshIx.accounts?.concat(await Scope.getRefreshAccounts(this._rpc, configAccount, this._config.kliquidityProgramId, mappings, token)), }; } return refreshIx; } static async getRefreshAccounts(connection, configAccount, kaminoProgramId, mappings, token) { const keys = []; keys.push({ role: kit_1.AccountRole.READONLY, address: mappings.priceInfoAccounts[token], }); switch (mappings.priceTypes[token]) { case types_1.OracleType.KToken.discriminator: { keys.push(...(await Scope.getKTokenRefreshAccounts(connection, kaminoProgramId, mappings, token))); return keys; } case new types_1.OracleType.JupiterLpFetch().discriminator: { const lpMint = await (0, utils_1.getJlpMintPda)(mappings.priceInfoAccounts[token]); keys.push({ role: kit_1.AccountRole.READONLY, address: lpMint, }); return keys; } case types_1.OracleType.JupiterLpCompute.discriminator: { const lpMint = await (0, utils_1.getJlpMintPda)(mappings.priceInfoAccounts[token]); const jlpRefreshAccounts = await this.getJlpRefreshAccounts(connection, configAccount, mappings, token, 'compute'); jlpRefreshAccounts.unshift({ role: kit_1.AccountRole.READONLY, address: lpMint, }); keys.push(...jlpRefreshAccounts); return keys; } case types_1.OracleType.JupiterLpScope.discriminator: { const lpMint = await (0, utils_1.getJlpMintPda)(mappings.priceInfoAccounts[token]); const jlpRefreshAccounts = await this.getJlpRefreshAccounts(connection, configAccount, mappings, token, 'scope'); jlpRefreshAccounts.unshift({ role: kit_1.AccountRole.READONLY, address: lpMint, }); keys.push(...jlpRefreshAccounts); return keys; } default: { return keys; } } } static async getJlpRefreshAccounts(rpc, configAccount, mappings, token, fetchingMechanism) { const pool = await accounts_3.Pool.fetch(rpc, mappings.priceInfoAccounts[token], programId_1.PROGRAM_ID); if (!pool) { throw Error(`Could not get Jupiter pool ${mappings.priceInfoAccounts[token]} to refresh token index ${token}`); } const extraAccounts = []; if (fetchingMechanism === 'scope') { const mintsToScopeChain = await (0, utils_1.getMintsToScopeChainPda)(configAccount.oraclePrices, mappings.priceInfoAccounts[token], token); extraAccounts.push({ role: kit_1.AccountRole.READONLY, address: mintsToScopeChain, }); } extraAccounts.push(...pool.custodies.map((custody) => { return { role: kit_1.AccountRole.READONLY, address: custody, }; })); if (fetchingMechanism === 'compute') { for (const custodyPk of pool.custodies) { const custody = await accounts_3.Custody.fetch(rpc, custodyPk, programId_1.PROGRAM_ID); if (!custody) { throw Error(`Could not get Jupiter custody ${custodyPk} to refresh token index ${token}`); } extraAccounts.push({ role: kit_1.AccountRole.READONLY, address: custody.oracle.oracleAccount, }); } } return extraAccounts; } static async getKTokenRefreshAccounts(connection, kaminoProgramId, mappings, token) { const strategy = await accounts_2.WhirlpoolStrategy.fetch(connection, mappings.priceInfoAccounts[token], kaminoProgramId); if (!strategy) { throw Error(`Could not get Kamino strategy ${mappings.priceInfoAccounts[token]} to refresh token index ${token}`); } const globalConfig = await accounts_2.GlobalConfig.fetch(connection, strategy.globalConfig, kaminoProgramId); if (!globalConfig) { throw Error(`Could not get global config for Kamino strategy ${mappings.priceInfoAccounts[token]} to refresh token index ${token}`); } return [strategy.globalConfig, globalConfig.tokenInfos, strategy.pool, strategy.position, strategy.scopePrices].map((acc) => { return { role: kit_1.AccountRole.READONLY, address: acc, }; }); } } exports.Scope = Scope; exports.default = Scope; //# sourceMappingURL=Scope.js.map