"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.getTokenOracleDataSync = getTokenOracleDataSync;
exports.getTokenOracleData = getTokenOracleData;
exports.getAllOracleAccounts = getAllOracleAccounts;
exports.cacheOrGetPythPrices = cacheOrGetPythPrices;
exports.cacheOrGetSwitchboardPrice = cacheOrGetSwitchboardPrice;
exports.cacheOrGetScopePrice = cacheOrGetScopePrice;
const kit_1 = require("@solana/kit");
const decimal_js_1 = __importDefault(require("decimal.js"));
const scope_sdk_1 = require("@kamino-finance/scope-sdk");
const OraclePrices_1 = require("@kamino-finance/scope-sdk/dist/@codegen/scope/accounts/OraclePrices");
const pubkey_1 = require("./pubkey");
const classes_1 = require("../classes");
const kliquidity_sdk_1 = require("@kamino-finance/kliquidity-sdk");
const accounts_1 = require("../@codegen/pyth_rec/accounts");
const accounts_2 = require("../@codegen/switchboard_v2/accounts");
const buffer_1 = require("buffer");
const switchboard_1 = require("./switchboard");
const programId_1 = require("../@codegen/switchboard_v2/programId");
// 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 (0, kit_1.address)('HFn8GnPADiny6XqUoWE8uRPPxb29ikn4yTuPa9MF2fWJ');
};
function getTokenOracleDataSync(allOracleAccounts, reserves) {
    const tokenOracleDataForReserves = [];
    const pythCache = new Map();
    const switchboardCache = new Map();
    const scopeCache = new Map();
    for (const reserveWithAddress of reserves) {
        const { address, state: reserve } = reserveWithAddress;
        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)) {
            const switchboardPrice = cacheOrGetSwitchboardPrice(oracle.switchboardFeedAddress, switchboardCache, allOracleAccounts);
            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) {
            const reserveSymbol = (0, classes_1.parseTokenSymbol)(reserve.config.tokenInfo.name);
            console.error(`No price found for reserve: ${reserveSymbol ?? 'unknown'} (${address}) in market: ${reserve.lendingMarket}`);
            tokenOracleDataForReserves.push([reserveWithAddress, 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([reserveWithAddress, tokenOracleData]);
    }
    return tokenOracleDataForReserves;
}
// TODO: Add freshness of the latest price to match sc logic
async function getTokenOracleData(rpc, reserves, oracleAccounts) {
    const allOracleAccounts = oracleAccounts ??
        (await getAllOracleAccounts(rpc, reserves.map((r) => r.state)));
    return getTokenOracleDataSync(allOracleAccounts, reserves);
}
async function getAllOracleAccounts(rpc, 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, async (chunk) => (await rpc.getMultipleAccounts(chunk).send()).value);
    const allAccsMap = new Map();
    allAccs.forEach((acc, i) => {
        if (acc !== null) {
            allAccsMap.set(allAccountsDeduped[i], { ...acc, programAddress: acc.owner, address: allAccountsDeduped[i] });
        }
    });
    return allAccsMap;
}
function dedupKeys(keys) {
    return [...new Set(keys)];
}
/**
 * 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 { priceMessage } = accounts_1.priceUpdateV2.decode(buffer_1.Buffer.from(result.data[0], 'base64'));
                const { price, exponent, conf: confidence, publishTime: timestamp, emaPrice } = priceMessage;
                if (price) {
                    const px = new decimal_js_1.default(price.toString()).div(10 ** Math.abs(exponent));
                    const conf = new decimal_js_1.default(confidence.toString());
                    prices.spot = {
                        price: px,
                        timestamp: BigInt(timestamp.toString()),
                        valid: validatePythPx(px, conf),
                    };
                }
                if (emaPrice !== undefined && emaPrice !== null) {
                    const emaPx = new decimal_js_1.default(emaPrice.toString()).div(10 ** Math.abs(exponent));
                    prices.twap = {
                        price: emaPx,
                        timestamp: BigInt(timestamp.toString()),
                        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
 */
function cacheOrGetSwitchboardPrice(oracle, switchboardCache, oracleAccounts) {
    const cached = switchboardCache.get(oracle);
    if (cached) {
        return cached;
    }
    else {
        const info = oracleAccounts.get(oracle);
        if (info) {
            if (info.programAddress === programId_1.PROGRAM_ID) {
                const agg = accounts_2.AggregatorAccountData.decode(buffer_1.Buffer.from(info.data[0], 'base64'));
                const result = (0, switchboard_1.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.programAddress);
                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.programAddress;
        if (owner === getScopeAddress()) {
            try {
                const prices = OraclePrices_1.OraclePrices.decode(buffer_1.Buffer.from(info.data[0], 'base64'));
                scopeCache.set(oracle, prices);
                return scopeChainToCandidatePrice(chain, prices);
            }
            catch (error) {
                console.debug(`Error parsing scope price account ${oracle.toString()} data`, error);
                return null;
            }
        }
        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 = confidence.mul(exports.CONFIDENCE_FACTOR);
    return !price.isZero() && 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