@kamino-finance/scope-sdk
Version:
Scope Oracle SDK
474 lines • 20.5 kB
JavaScript
;
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