@kamino-finance/klend-sdk
Version:
Typescript SDK for interacting with the Kamino Lending (klend) protocol
281 lines • 12 kB
JavaScript
"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