UNPKG

@kamino-finance/klend-sdk

Version:

Typescript SDK for interacting with the Kamino Lending (klend) protocol

281 lines 12 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.CONFIDENCE_FACTOR = exports.MAX_CONFIDENCE_PERCENTAGE = void 0; exports.getTokenOracleData = getTokenOracleData; exports.getAllOracleAccounts = getAllOracleAccounts; exports.cacheOrGetPythPrices = cacheOrGetPythPrices; exports.cacheOrGetSwitchboardPrice = cacheOrGetSwitchboardPrice; exports.cacheOrGetScopePrice = cacheOrGetScopePrice; const client_1 = require("@pythnetwork/client"); const web3_js_1 = require("@solana/web3.js"); const decimal_js_1 = __importDefault(require("decimal.js")); const scope_sdk_1 = require("@kamino-finance/scope-sdk"); const pubkey_1 = require("./pubkey"); const classes_1 = require("../classes"); const sbv2_lite_1 = __importDefault(require("@switchboard-xyz/sbv2-lite")); const kliquidity_sdk_1 = require("@kamino-finance/kliquidity-sdk"); const SWITCHBOARD_V2_PROGRAM_ID = new web3_js_1.PublicKey('SW1TCH7qEPTdLsDHRgPuMQjbQxKdH2aBStViMFnt64f'); // validate price confidence - confidence/price ratio should be less than 2% exports.MAX_CONFIDENCE_PERCENTAGE = new decimal_js_1.default('2'); /// Confidence factor is used to scale the confidence value to a value that can be compared to the price. exports.CONFIDENCE_FACTOR = new decimal_js_1.default('100').div(exports.MAX_CONFIDENCE_PERCENTAGE); const getScopeAddress = () => { return 'HFn8GnPADiny6XqUoWE8uRPPxb29ikn4yTuPa9MF2fWJ'; }; // TODO: Add freshness of the latest price to match sc logic async function getTokenOracleData(connection, reserves) { const allOracleAccounts = await getAllOracleAccounts(connection, reserves); const tokenOracleDataForReserves = []; const pythCache = new pubkey_1.PubkeyHashMap(); const switchboardCache = new pubkey_1.PubkeyHashMap(); const scopeCache = new pubkey_1.PubkeyHashMap(); let switchboardV2; for (const reserve of reserves) { let currentBest = undefined; const oracle = { pythAddress: reserve.config.tokenInfo.pythConfiguration.price, switchboardFeedAddress: reserve.config.tokenInfo.switchboardConfiguration.priceAggregator, switchboardTwapAddress: reserve.config.tokenInfo.switchboardConfiguration.twapAggregator, scopeOracleAddress: reserve.config.tokenInfo.scopeConfiguration.priceFeed, }; if ((0, pubkey_1.isNotNullPubkey)(oracle.pythAddress)) { const pythPrices = cacheOrGetPythPrices(oracle.pythAddress, pythCache, allOracleAccounts); if (pythPrices && pythPrices.spot) { currentBest = getBestPrice(currentBest, pythPrices.spot); } } if ((0, pubkey_1.isNotNullPubkey)(oracle.switchboardFeedAddress)) { if (!switchboardV2) { switchboardV2 = await sbv2_lite_1.default.loadMainnet(connection); } const switchboardPrice = cacheOrGetSwitchboardPrice(oracle.switchboardFeedAddress, switchboardCache, allOracleAccounts, switchboardV2); if (switchboardPrice) { currentBest = getBestPrice(currentBest, switchboardPrice); } } if ((0, pubkey_1.isNotNullPubkey)(oracle.scopeOracleAddress)) { const scopePrice = cacheOrGetScopePrice(oracle.scopeOracleAddress, scopeCache, allOracleAccounts, reserve.config.tokenInfo.scopeConfiguration.priceChain); if (scopePrice) { currentBest = getBestPrice(currentBest, scopePrice); } } if (!currentBest) { console.error(`No price found for reserve: ${(0, classes_1.parseTokenSymbol)(reserve.config.tokenInfo.name)}`); tokenOracleDataForReserves.push([reserve, undefined]); continue; } const tokenOracleData = { mintAddress: reserve.liquidity.mintPubkey, decimals: decimal_js_1.default.pow(10, reserve.liquidity.mintDecimals.toString()), price: new decimal_js_1.default(currentBest.price), timestamp: currentBest.timestamp, valid: currentBest.valid, }; tokenOracleDataForReserves.push([reserve, tokenOracleData]); } return tokenOracleDataForReserves; } async function getAllOracleAccounts(connection, reserves) { const allAccounts = []; reserves.forEach((reserve) => { if ((0, pubkey_1.isNotNullPubkey)(reserve.config.tokenInfo.pythConfiguration.price)) { allAccounts.push(reserve.config.tokenInfo.pythConfiguration.price); } if ((0, pubkey_1.isNotNullPubkey)(reserve.config.tokenInfo.switchboardConfiguration.priceAggregator)) { allAccounts.push(reserve.config.tokenInfo.switchboardConfiguration.priceAggregator); } if ((0, pubkey_1.isNotNullPubkey)(reserve.config.tokenInfo.switchboardConfiguration.twapAggregator)) { allAccounts.push(reserve.config.tokenInfo.switchboardConfiguration.twapAggregator); } if ((0, pubkey_1.isNotNullPubkey)(reserve.config.tokenInfo.scopeConfiguration.priceFeed)) { allAccounts.push(reserve.config.tokenInfo.scopeConfiguration.priceFeed); } }); const allAccountsDeduped = dedupKeys(allAccounts); const allAccs = await (0, kliquidity_sdk_1.batchFetch)(allAccountsDeduped, (chunk) => connection.getMultipleAccountsInfo(chunk)); const allAccsMap = new pubkey_1.PubkeyHashMap(); allAccs.forEach((acc, i) => { if (acc) { allAccsMap.set(allAccountsDeduped[i], acc); } }); return allAccsMap; } function dedupKeys(keys) { return new pubkey_1.PublicKeySet(keys).toArray(); } /** * Get pyth price from cache or fetch if not available * @param oracle oracle address * @param cache pyth cache * @param oracleAccounts all oracle accounts */ function cacheOrGetPythPrices(oracle, cache, oracleAccounts) { const prices = {}; const cached = cache.get(oracle); if (cached) { return cached; } else { const result = oracleAccounts.get(oracle); if (result) { try { const { price, timestamp, emaPrice, previousPrice, previousTimestamp, confidence } = (0, client_1.parsePriceData)(result.data); if (price) { const px = new decimal_js_1.default(price); prices.spot = { price: px, timestamp, valid: validatePythPx(px, confidence), }; } else { prices.spot = { price: new decimal_js_1.default(previousPrice), timestamp: previousTimestamp, valid: false, }; } if (emaPrice !== undefined && emaPrice !== null) { prices.twap = { price: new decimal_js_1.default(emaPrice.value), timestamp, valid: true, }; } if (prices.spot || prices.twap) { cache.set(oracle, prices); } } catch (error) { console.error(`Error parsing pyth price account ${oracle.toString()} data`, error); return null; } } else { return null; } } return prices; } /** * Get switchboard price from cache or fetch if not available * @param oracle oracle address * @param switchboardCache cache for oracle prices * @param oracleAccounts all oracle accounts * @param switchboardV2 loaded switchboard program */ function cacheOrGetSwitchboardPrice(oracle, switchboardCache, oracleAccounts, switchboardV2) { const cached = switchboardCache.get(oracle); if (cached) { return cached; } else { const info = oracleAccounts.get(oracle); if (info) { if (info.owner.equals(SWITCHBOARD_V2_PROGRAM_ID)) { const agg = switchboardV2.decodeAggregator(info); // @ts-ignore const result = switchboardV2.getLatestAggregatorValue(agg); if (result !== undefined && result !== null) { const switchboardPx = new decimal_js_1.default(result.toString()); const latestRoundTimestamp = agg.latestConfirmedRound.roundOpenTimestamp; const ts = BigInt(latestRoundTimestamp.toString()); const valid = validateSwitchboardV2Px(agg); return { price: switchboardPx, timestamp: ts, valid, }; } } else { console.error('Unrecognized switchboard owner address: ', info.owner.toString()); return null; } } } return null; } /** * Get scope price from cache or fetch if not available * @param oracle oracle address * @param scopeCache cache for oracle prices * @param allOracleAccounts all oracle accounts * @param chain scope chain */ function cacheOrGetScopePrice(oracle, scopeCache, allOracleAccounts, chain) { if (!(0, pubkey_1.isNotNullPubkey)(oracle) || !chain || !scope_sdk_1.Scope.isScopeChainValid(chain)) { return null; } const scopePrices = scopeCache.get(oracle); if (scopePrices) { return scopeChainToCandidatePrice(chain, scopePrices); } const info = allOracleAccounts.get(oracle); if (info) { const owner = info.owner.toString(); if (owner === getScopeAddress()) { const prices = scope_sdk_1.OraclePrices.decode(info.data); scopeCache.set(oracle, prices); return scopeChainToCandidatePrice(chain, prices); } else { console.error('Unrecognized scope owner address: ', owner); } } return null; } function getBestPrice(current, next) { if (isBetterPrice(current, next)) { return next; } return current; } function isBetterPrice(current, next) { if (!current) { return true; } if (current.valid && !next.valid) { return false; } if (!current.valid && next.valid) { return true; } return next.timestamp > current.timestamp; } function validatePythPx(price, confidence) { const conf50x = new decimal_js_1.default(confidence || 0).mul(exports.CONFIDENCE_FACTOR); return price.gt(conf50x); } function validateSwitchboardV2Px(agg) { const pxMantissa = new decimal_js_1.default(agg.latestConfirmedRound.result.mantissa.toString()); const pxScale = new decimal_js_1.default(agg.latestConfirmedRound.result.scale.toString()); const stDevMantissa = new decimal_js_1.default(agg.latestConfirmedRound.stdDeviation.mantissa.toString()); const stDevScale = new decimal_js_1.default(agg.latestConfirmedRound.stdDeviation.scale.toString()); let conf50xScaled; if (pxScale.gte(stDevScale)) { const scalingFactor = pxScale.sub(stDevScale); const conf50x = stDevMantissa.mul(exports.CONFIDENCE_FACTOR); conf50xScaled = conf50x.mul(scalingFactor); } else { const scalingFactor = stDevScale.sub(pxScale); const conf50x = stDevMantissa.mul(exports.CONFIDENCE_FACTOR); conf50xScaled = conf50x.div(scalingFactor); } return conf50xScaled.gte(pxMantissa); } function scopeChainToCandidatePrice(chain, prices) { const scopePx = scope_sdk_1.Scope.getPriceFromScopeChain(chain, prices); const valid = scopePx.timestamp.gt('0'); // scope prices are pre-validated return { price: scopePx.price, timestamp: BigInt(scopePx.timestamp.toString()), valid, }; } //# sourceMappingURL=oracle.js.map