"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.KaminoMarket = void 0;
exports.getReserveStatesForMarket = getReserveStatesForMarket;
exports.getReservesForMarket = getReservesForMarket;
exports.getSingleReserve = getSingleReserve;
exports.getReservesActive = getReservesActive;
exports.getTokenIdsForScopeRefresh = getTokenIdsForScopeRefresh;
const kit_1 = require("@solana/kit");
const obligation_1 = require("./obligation");
const reserve_1 = require("./reserve");
const accounts_1 = require("../@codegen/klend/accounts");
const utils_1 = require("../utils");
const bn_js_1 = __importDefault(require("bn.js"));
const decimal_js_1 = __importDefault(require("decimal.js"));
const farms_sdk_1 = require("@kamino-finance/farms-sdk");
const farm_utils_1 = require("./farm_utils");
const programId_1 = require("../@codegen/klend/programId");
const scope_sdk_1 = require("@kamino-finance/scope-sdk");
const fraction_1 = require("./fraction");
const kliquidity_sdk_1 = require("@kamino-finance/kliquidity-sdk");
const utils_2 = require("./utils");
const permission_1 = require("./permission");
const zero_padding_1 = require("../@codegen/klend/zero_padding");
const validations_1 = require("../utils/validations");
const buffer_1 = require("buffer");
const cdnClient_1 = require("./cdnClient");
const base58Decoder = (0, kit_1.getBase58Decoder)();
class KaminoMarket {
    rpc;
    address;
    state;
    reserves;
    reservesActive;
    programId;
    farmsProgramId;
    recentSlotDurationMs;
    // scope feeds used by all market reserves
    scopeFeeds;
    constructor(rpc, state, marketAddress, reserves, recentSlotDurationMs, programId = programId_1.PROGRAM_ID, farmsProgramId = farms_sdk_1.FARMS_PROGRAM_ADDRESS) {
        if (recentSlotDurationMs <= 0) {
            throw new Error('Recent slot duration cannot be 0');
        }
        this.address = marketAddress;
        this.rpc = rpc;
        this.state = state;
        this.reserves = reserves;
        this.reservesActive = getReservesActive(this.reserves);
        this.programId = programId;
        this.farmsProgramId = farmsProgramId;
        this.recentSlotDurationMs = recentSlotDurationMs;
        this.scopeFeeds = new Set(Array.from(this.reserves.values())
            .filter((r) => (0, utils_1.isNotNullPubkey)(r.state.config.tokenInfo.scopeConfiguration.priceFeed))
            .map((r) => r.state.config.tokenInfo.scopeConfiguration.priceFeed));
    }
    /**
     * TESTING ONLY!
     *
     * Used to create mock markets for testing
     */
    static createMarket(rpc, state, marketAddress, reserves, recentSlotDurationMs, programId = programId_1.PROGRAM_ID) {
        return new KaminoMarket(rpc, state, marketAddress, reserves, recentSlotDurationMs, programId);
    }
    /**
     * Load a new market with all of its associated reserves
     * @param rpc
     * @param marketAddress
     * @param recentSlotDurationMs
     * @param programId
     * @param withReserves
     */
    static async load(rpc, marketAddress, recentSlotDurationMs, programId = programId_1.PROGRAM_ID, withReserves = true, farmsProgramId = farms_sdk_1.FARMS_PROGRAM_ADDRESS) {
        const market = await accounts_1.LendingMarket.fetch(rpc, marketAddress, programId);
        if (market === null) {
            return null;
        }
        const reserves = withReserves
            ? await getReservesForMarket(marketAddress, rpc, programId, recentSlotDurationMs)
            : new Map();
        return new KaminoMarket(rpc, market, marketAddress, reserves, recentSlotDurationMs, programId, farmsProgramId);
    }
    static loadWithReserves(connection, market, reserves, marketAddress, recentSlotDurationMs, programId = programId_1.PROGRAM_ID, farmsProgramId = farms_sdk_1.FARMS_PROGRAM_ADDRESS) {
        return new KaminoMarket(connection, market, marketAddress, reserves, recentSlotDurationMs, programId, farmsProgramId);
    }
    static async loadMultiple(connection, markets, recentSlotDurationMs, programId = programId_1.PROGRAM_ID, withReserves = true, oracleAccounts, farmsProgramId = farms_sdk_1.FARMS_PROGRAM_ADDRESS) {
        const marketStates = await (0, kliquidity_sdk_1.batchFetch)(markets, (market) => accounts_1.LendingMarket.fetchMultiple(connection, market, programId));
        const marketReservesByMarket = withReserves
            ? await getReservesForMarkets(markets, connection, programId, recentSlotDurationMs, oracleAccounts)
            : new Map();
        const kaminoMarkets = new Map();
        for (let i = 0; i < markets.length; i++) {
            const market = marketStates[i];
            const marketAddress = markets[i];
            if (market === null) {
                throw Error(`Could not fetch LendingMarket account state for market ${marketAddress}`);
            }
            const marketReserves = withReserves
                ? marketReservesByMarket.get(marketAddress) ?? new Map()
                : new Map();
            kaminoMarkets.set(marketAddress, new KaminoMarket(connection, market, marketAddress, marketReserves, recentSlotDurationMs, programId, farmsProgramId));
        }
        return kaminoMarkets;
    }
    static async loadMultipleWithReserves(connection, markets, reserves, recentSlotDurationMs, programId = programId_1.PROGRAM_ID, farmsProgramId = farms_sdk_1.FARMS_PROGRAM_ADDRESS) {
        const marketStates = await (0, kliquidity_sdk_1.batchFetch)(markets, (market) => accounts_1.LendingMarket.fetchMultiple(connection, market, programId));
        const kaminoMarkets = new Map();
        for (let i = 0; i < markets.length; i++) {
            const market = marketStates[i];
            const marketAddress = markets[i];
            if (market === null) {
                throw Error(`Could not fetch LendingMarket account state for market ${marketAddress}`);
            }
            const marketReserves = reserves.get(marketAddress);
            if (!marketReserves) {
                throw Error(`Could not get reserves for market ${marketAddress} from the reserves map argument supplied to this method`);
            }
            kaminoMarkets.set(marketAddress, new KaminoMarket(connection, market, marketAddress, marketReserves, recentSlotDurationMs, programId, farmsProgramId));
        }
        return kaminoMarkets;
    }
    async reload() {
        const market = await accounts_1.LendingMarket.fetch(this.rpc, this.getAddress(), this.programId);
        if (market === null) {
            return;
        }
        this.state = market;
        this.reserves = await getReservesForMarket(this.getAddress(), this.rpc, this.programId, this.recentSlotDurationMs);
        this.reservesActive = getReservesActive(this.reserves);
    }
    async reloadSingleReserve(reservePk, reserveData) {
        const reserve = await getSingleReserve(reservePk, this.rpc, this.recentSlotDurationMs, reserveData);
        this.reserves.set(reservePk, reserve);
        this.reservesActive.set(reservePk, reserve);
    }
    /**
     * Get the address of this market
     * @return market address public key
     */
    getAddress() {
        return this.address;
    }
    /**
     * Get a list of reserves for this market
     */
    getReserves() {
        return [...this.reserves.values()];
    }
    getElevationGroup(elevationGroup) {
        return this.state.elevationGroups[elevationGroup - 1];
    }
    /**
     * Returns this market's elevation group of the given ID, or `null` for the default group `0`, or throws an error
     * (including the given description) if the requested group does not exist.
     */
    getExistingElevationGroup(elevationGroupId, description = 'Requested') {
        if (elevationGroupId === 0) {
            return null;
        }
        return (0, validations_1.checkDefined)(this.getMarketElevationGroupDescriptions().find((candidate) => candidate.elevationGroup === elevationGroupId), `${description} elevation group ${elevationGroupId} not found`);
    }
    getMinNetValueObligation() {
        return new fraction_1.Fraction(this.state.minNetValueInObligationSf).toDecimal();
    }
    /**
     * Get the authority PDA of this market
     * @return market authority public key
     */
    async getLendingMarketAuthority() {
        return (await (0, utils_1.lendingMarketAuthPda)(this.getAddress(), this.programId))[0];
    }
    getName() {
        return (0, utils_2.parseZeroPaddedUtf8)(this.state.name);
    }
    /** True if any op in `op` is gated by the market's permissioning authority. */
    requiresPermissioner(op) {
        return permission_1.PermissionedOp.fromBN(this.state.permissionedOps).intersects(op);
    }
    /** Address authorized to sign for permissioned ops on this market. */
    getPermissioningAuthority() {
        return this.state.permissioningAuthority === utils_1.DEFAULT_PUBLIC_KEY ? undefined : this.state.permissioningAuthority;
    }
    async getObligationDepositByWallet(owner, depositReserveAddress, obligationType) {
        const obligation = await this.getObligationByWallet(owner, obligationType);
        return obligation?.getDepositByReserve(depositReserveAddress)?.amount ?? new decimal_js_1.default(0);
    }
    async getObligationBorrowByWallet(owner, borrowReserveAddress, obligationType) {
        const obligation = await this.getObligationByWallet(owner, obligationType);
        return obligation?.getBorrowByReserve(borrowReserveAddress)?.amount ?? new decimal_js_1.default(0);
    }
    getTotalDepositTVL() {
        let tvl = new decimal_js_1.default(0);
        for (const reserve of this.reserves.values()) {
            tvl = tvl.add(reserve.getDepositTvl());
        }
        return tvl;
    }
    getTotalBorrowTVL() {
        let tvl = new decimal_js_1.default(0);
        for (const reserve of this.reserves.values()) {
            tvl = tvl.add(reserve.getBorrowTvl());
        }
        return tvl;
    }
    getMaxLeverageForPair(collReserveAddress, debtReserveAddress) {
        const { maxLtv: maxCollateralLtv, borrowFactor } = this.getMaxAndLiquidationLtvAndBorrowFactorForPair(collReserveAddress, debtReserveAddress);
        const maxLeverage = 
        // const ltv = (coll * ltv_factor) / (debt * borrow_factor);
        1 / (1 - (maxCollateralLtv * 100) / (borrowFactor * 100));
        return maxLeverage;
    }
    getCommonElevationGroupsForPair(collReserve, debtReserve) {
        const groupsColl = new Set(collReserve.state.config.elevationGroups);
        const groupsDebt = new Set(debtReserve.state.config.elevationGroups);
        return [...groupsColl].filter((item) => groupsDebt.has(item) &&
            item !== 0 &&
            this.state.elevationGroups[item - 1].allowNewLoans !== 0 &&
            collReserve.state.config.borrowLimitAgainstThisCollateralInElevationGroup[item - 1].gt(new bn_js_1.default(0)) &&
            this.state.elevationGroups[item - 1].debtReserve === debtReserve.address);
    }
    /**
     * The elevation group an on-chain deposit+borrow of this (collateral, debt) pair auto-selects: the common
     * elevation group with the highest LTV, or `0` (no emode / default) when the pair shares no usable group.
     *
     * Single source of truth for the "preferred group for a borrow pair" rule — used by `KaminoAction` when building
     * deposit+borrow ixs and by the swap-debt migration preview/validation so all three stay consistent.
     */
    getPreferredElevationGroupForBorrowPair(collReserve, debtReserve) {
        const commonElevationGroups = this.getCommonElevationGroupsForPair(collReserve, debtReserve);
        if (commonElevationGroups.length === 0) {
            return 0;
        }
        let selectedId = 0;
        let selectedMaxLtvPct = -1;
        for (const group of this.state.elevationGroups) {
            if (commonElevationGroups.includes(group.id) && group.ltvPct > selectedMaxLtvPct) {
                selectedId = group.id;
                selectedMaxLtvPct = group.ltvPct;
            }
        }
        return selectedId;
    }
    getMaxAndLiquidationLtvAndBorrowFactorForPair(collReserveAddress, debtReserveAddress, elevationGroup) {
        const collReserve = this.getReserveByAddress(collReserveAddress);
        const debtReserve = this.getReserveByAddress(debtReserveAddress);
        if (!collReserve || !debtReserve) {
            throw Error('Could not find one of the reserves.');
        }
        // When the caller pins a specific elevation group, evaluate LTV/borrow-factor AT THAT group: `0` means no emode
        // (the reserves' own config), otherwise the requested group's parameters (borrow factor is always 1 in emode).
        // With no group pinned, fall back to the best common group (highest max LTV) — the group an on-chain
        // deposit+borrow auto-selects.
        if (elevationGroup !== undefined) {
            if (elevationGroup === 0) {
                return {
                    maxLtv: collReserve.state.config.loanToValuePct / 100,
                    liquidationLtv: collReserve.state.config.liquidationThresholdPct / 100,
                    borrowFactor: debtReserve.state.config.borrowFactorPct.toNumber() / 100,
                };
            }
            const group = (0, validations_1.checkDefined)(this.state.elevationGroups[elevationGroup - 1], `Elevation group ${elevationGroup} not found`);
            return {
                maxLtv: group.ltvPct / 100,
                liquidationLtv: group.liquidationThresholdPct / 100,
                borrowFactor: 1,
            };
        }
        const commonElevationGroups = this.getCommonElevationGroupsForPair(collReserve, debtReserve);
        // Ltv factor for coll token
        const maxCollateralLtv = commonElevationGroups.length === 0
            ? collReserve.state.config.loanToValuePct
            : this.state.elevationGroups
                .filter((e) => commonElevationGroups.includes(e.id))
                .reduce((acc, elem) => Math.max(acc, elem.ltvPct), 0);
        const liquidationLtv = commonElevationGroups.length === 0
            ? collReserve.state.config.liquidationThresholdPct
            : this.state.elevationGroups
                .filter((e) => commonElevationGroups.includes(e.id))
                .reduce((acc, elem) => Math.max(acc, elem.liquidationThresholdPct), 0);
        const borrowFactor = commonElevationGroups.length === 0 ? debtReserve?.state.config.borrowFactorPct.toNumber() / 100 : 1;
        return { maxLtv: maxCollateralLtv / 100, liquidationLtv: liquidationLtv / 100, borrowFactor };
    }
    async getTotalProductTvl(productType, slot) {
        let obligations = (await this.getAllObligationsForMarket(slot, productType.toArgs().tag)).filter((obligation) => obligation.refreshedStats.userTotalBorrow.gt(0) || obligation.refreshedStats.userTotalDeposit.gt(0));
        switch (productType.toArgs().tag) {
            case utils_1.VanillaObligation.tag: {
                break;
            }
            case utils_1.LendingObligation.tag: {
                const mint = productType.toArgs().seed1;
                obligations = obligations.filter((obligation) => obligation.getDepositsByMint(mint).length > 0);
                break;
            }
            case utils_1.MultiplyObligation.tag:
            case utils_1.LeverageObligation.tag: {
                const collMint = productType.toArgs().seed1;
                const debtMint = productType.toArgs().seed2;
                obligations = obligations.filter((obligation) => obligation.getDepositsByMint(collMint).length > 0 && obligation.getBorrowsByMint(debtMint).length > 0);
                break;
            }
            case utils_1.LendingObligationFixedRate.tag: {
                const reserveAddress = productType.toArgs().seed1;
                obligations = obligations.filter((obligation) => obligation.getDepositByReserve(reserveAddress) !== undefined);
                break;
            }
            case utils_1.LeverageObligationFixedRate.tag:
            case utils_1.MultiplyObligationFixedRate.tag: {
                const collReserveAddress = productType.toArgs().seed1;
                const debtReserveAddress = productType.toArgs().seed2;
                obligations = obligations.filter((obligation) => {
                    const collDeposit = obligation.getDepositByReserve(collReserveAddress);
                    const debtBorrow = obligation.getBorrowByReserve(debtReserveAddress);
                    return collDeposit !== undefined && debtBorrow !== undefined;
                });
                break;
            }
            default:
                throw new Error('Invalid obligation type');
        }
        const deposits = obligations.reduce((acc, obligation) => acc.plus(obligation.refreshedStats.userTotalDeposit), new decimal_js_1.default(0));
        const borrows = obligations.reduce((acc, obligation) => acc.plus(obligation.refreshedStats.userTotalBorrow), new decimal_js_1.default(0));
        const avgLeverage = obligations.reduce((acc, obligations) => acc.plus(obligations.refreshedStats.leverage), new decimal_js_1.default(0));
        return { tvl: deposits.sub(borrows), deposits, borrows, avgLeverage: avgLeverage.div(obligations.length) };
    }
    /**
     *
     * @returns Number of active obligations in the market
     */
    async getNumberOfObligations(slot) {
        return (await this.getAllObligationsForMarket(slot))
            .filter((obligation) => obligation.refreshedStats.userTotalBorrow.gt(0) || obligation.refreshedStats.userTotalDeposit.gt(0))
            .reduce((acc, _obligation) => acc + 1, 0);
    }
    async getObligationByWallet(Address, obligationType) {
        const { address } = this;
        if (!address) {
            throw Error('Market must be initialized to call initialize.');
        }
        const obligationAddress = await obligationType.toPda(this.getAddress(), Address);
        return obligation_1.KaminoObligation.load(this, obligationAddress);
    }
    /**
     * @returns The max borrowable amount for leverage positions
     */
    getMaxLeverageBorrowableAmount(collReserve, debtReserve, slot, requestElevationGroup, obligation) {
        return obligation
            ? obligation.getMaxBorrowAmount(this, debtReserve.address, slot, requestElevationGroup)
            : debtReserve.getMaxBorrowAmountWithCollReserve(this, collReserve);
    }
    async loadReserves(oracleAccounts) {
        const addresses = [...this.reserves.keys()];
        if (addresses.length === 0) {
            return;
        }
        const reserveAccounts = await this.rpc
            .getMultipleAccounts(addresses, { commitment: 'processed', encoding: 'base64' })
            .send();
        const deserializedReserves = reserveAccounts.value.map((reserve, i) => {
            if (reserve === null) {
                // maybe reuse old here
                throw new Error(`Reserve account ${addresses[i]} was not found`);
            }
            const reserveAccount = accounts_1.Reserve.decode(buffer_1.Buffer.from(reserve.data[0], 'base64'));
            if (!reserveAccount) {
                throw Error(`Could not parse reserve ${addresses[i]}`);
            }
            return {
                address: addresses[i],
                state: reserveAccount,
            };
        });
        const [reservesAndOracles, cdnResourcesData] = await Promise.all([
            (0, utils_1.getTokenOracleData)(this.getRpc(), deserializedReserves, oracleAccounts),
            cdnClient_1.kaminoCdn.getData(),
        ]);
        const kaminoReserves = new Map();
        reservesAndOracles.forEach(([{ address: reserveAddress, state: reserve }, oracle]) => {
            if (!oracle) {
                throw Error(`Could not find oracle for ${(0, utils_2.parseTokenSymbol)(reserve.config.tokenInfo.name)} (${reserveAddress}) reserve in market ${reserve.lendingMarket}`);
            }
            const kaminoReserve = reserve_1.KaminoReserve.initialize(reserveAddress, reserve, oracle, this.rpc, this.recentSlotDurationMs, cdnResourcesData);
            kaminoReserves.set(kaminoReserve.address, kaminoReserve);
        });
        this.reserves = kaminoReserves;
        this.reservesActive = getReservesActive(this.reserves);
    }
    async refreshAll() {
        const promises = [this.getReserves().every((reserve) => reserve.stats) ? this.loadReserves() : null].filter((x) => x);
        await Promise.all(promises);
        this.reservesActive = getReservesActive(this.reserves);
    }
    getReserveByAddress(address) {
        return this.reserves.get(address);
    }
    /**
     * Returns this market's reserve of the given address, or throws an error (including the given description) if such
     * reserve does not exist.
     */
    getExistingReserveByAddress(address, description = 'Requested') {
        return (0, validations_1.checkDefined)(this.getReserveByAddress(address), `${description} reserve ${address} not found`);
    }
    /**
     * Returns all reserves for the given mint address (both float rate and fixed rate).
     *
     * @param mint The liquidity mint address
     * @returns Array of all reserves for this mint
     */
    getReservesByMint(address) {
        const reserves = [];
        for (const reserve of this.reserves.values()) {
            if (reserve.getLiquidityMint() === address) {
                reserves.push(reserve);
            }
        }
        return reserves;
    }
    getExistingReservesByMint(address, description = 'Requested') {
        return (0, validations_1.checkArrayNotEmpty)(this.getReservesByMint(address), `${description} reserve with mint ${address} not found`);
    }
    getLiquidityTokenProgramByMint(mint) {
        const reserves = this.getExistingReservesByMint(mint);
        const tokenProgram = reserves[0].getLiquidityTokenProgram();
        for (let i = 1; i < reserves.length; i++) {
            const otherTokenProgram = reserves[i].getLiquidityTokenProgram();
            if (otherTokenProgram !== tokenProgram) {
                throw new Error(`Inconsistent token programs for mint ${mint}: reserve ${reserves[0].address} has ${tokenProgram}, reserve ${reserves[i].address} has ${otherTokenProgram}`);
            }
        }
        return tokenProgram;
    }
    /**
     * Returns this market's reserve matching the given mint address and reserve kind.
     * Since a market can have multiple reserves for the same mint (with different terms),
     * the reserve kind specifies which reserve to select.
     *
     * Example for fixed-term reserves:
     * const fixedReserveKind = new FixedReserveKind(new BN(30 * 24 * 60 * 60), 500); // 30 days, 5% borrow rate
     * const fixedReserve = market.getReserveByMintAndKind(tokenMint, fixedReserveKind)!;
  
     * @param mint The liquidity mint address
     * @param reserveKind The reserve kind to match (e.g., FloatRateReserveKind or FixedRateReserveKind)
     */
    getReserveByMintAndKind(mint, reserveKind) {
        for (const reserve of this.reserves.values()) {
            if (reserve.getLiquidityMint() === mint && reserveKind.matches(reserve)) {
                return reserve;
            }
        }
        return undefined;
    }
    /**
     * Returns this market's reserve matching the given mint address and reserve kind,
     * or throws an error if not found.
     *
     * @param mint The liquidity mint address
     * @param reserveKind The reserve kind to match
     * @param description Optional description for the error message
     */
    getExistingReserveByMintAndKind(mint, reserveKind, description = 'Requested') {
        return (0, validations_1.checkDefined)(this.getReserveByMintAndKind(mint, reserveKind), `${description} reserve with mint ${mint} and kind ${reserveKind.toString()} not found`);
    }
    /**
     * Returns this market's float rate reserve for the given mint address.
     * Float rate reserves are reserves without a fixed term (debtTermSeconds = 0).
     *
     * @param mint The liquidity mint address
     * @returns The float rate reserve, or undefined if not found
     */
    getFloatRateReserveByMint(mint) {
        return this.getReserveByMintAndKind(mint, new utils_1.FloatRateReserveKind());
    }
    /**
     * Returns this market's float rate reserve for the given mint address,
     * or throws an error if not found.
     *
     * @param mint The liquidity mint address
     * @param description Optional description for the error message
     * @returns The float rate reserve
     */
    getExistingFloatRateReserveByMint(mint, description = 'Requested') {
        return (0, validations_1.checkDefined)(this.getFloatRateReserveByMint(mint), `${description} float rate reserve with mint ${mint} not found`);
    }
    /**
     * Returns all fixed rate reserves for the given mint address.
     * Fixed rate reserves have a non-zero debt term (debtTermSeconds > 0).
     *
     * @param mint The liquidity mint address
     * @returns Array of all fixed rate reserves for this mint
     */
    getFixedRateReservesByMint(mint) {
        const fixedRateReserves = [];
        for (const reserve of this.reserves.values()) {
            if (reserve.getLiquidityMint() === mint && reserve.getKind().isFixedRate()) {
                fixedRateReserves.push(reserve);
            }
        }
        return fixedRateReserves;
    }
    /**
     * Returns all maturity-timestamp reserves for the given mint address.
     *
     * @param mint The liquidity mint address
     * @returns Array of all maturity-timestamp reserves for this mint
     */
    getMaturityTimestampReservesByMint(mint) {
        const reserves = [];
        for (const reserve of this.reserves.values()) {
            if (reserve.getLiquidityMint() === mint && reserve.getKind().isMaturityTimestampKind()) {
                reserves.push(reserve);
            }
        }
        return reserves;
    }
    /**
     * Returns all reserves for the given symbol (both float rate and fixed rate).
     *
     * @param symbol The reserve symbol
     * @returns Array of all reserves for this symbol
     */
    getReservesBySymbol(symbol) {
        const reserves = [];
        for (const reserve of this.reserves.values()) {
            if (reserve.symbol === symbol) {
                reserves.push(reserve);
            }
        }
        return reserves;
    }
    getExistingReservesBySymbol(symbol, description = 'Requested') {
        return (0, validations_1.checkArrayNotEmpty)(this.getReservesBySymbol(symbol), `${description} reserve with symbol ${symbol} not found`);
    }
    /**
     * Returns this market's reserve matching the given symbol and reserve kind.
     * Since a market can have multiple reserves for the same symbol (with different terms),
     * the reserve kind specifies which reserve to select.
     *
     * @param symbol The reserve symbol
     * @param reserveKind The reserve kind to match (e.g., FloatRateReserveKind or FixedRateReserveKind)
     */
    getReserveBySymbolAndKind(symbol, reserveKind) {
        for (const reserve of this.reserves.values()) {
            if (reserve.symbol === symbol && reserveKind.matches(reserve)) {
                return reserve;
            }
        }
        return undefined;
    }
    /**
     * Returns this market's reserve matching the given symbol and reserve kind,
     * or throws an error if not found.
     *
     * @param symbol The reserve symbol
     * @param reserveKind The reserve kind to match
     * @param description Optional description for the error message
     */
    getExistingReserveBySymbolAndKind(symbol, reserveKind, description = 'Requested') {
        return (0, validations_1.checkDefined)(this.getReserveBySymbolAndKind(symbol, reserveKind), `${description} reserve with symbol ${symbol} and kind ${reserveKind.toString()} not found`);
    }
    /**
     * Returns this market's float rate reserve for the given symbol.
     * Float rate reserves are reserves without a fixed term (debtTermSeconds = 0).
     *
     * @param symbol The reserve symbol
     * @returns The float rate reserve, or undefined if not found
     */
    getFloatRateReserveBySymbol(symbol) {
        return this.getReserveBySymbolAndKind(symbol, new utils_1.FloatRateReserveKind());
    }
    /**
     * Returns this market's float rate reserve for the given symbol,
     * or throws an error if not found.
     *
     * @param symbol The reserve symbol
     * @param description Optional description for the error message
     * @returns The float rate reserve
     */
    getExistingFloatRateReserveBySymbol(symbol, description = 'Requested') {
        return (0, validations_1.checkDefined)(this.getFloatRateReserveBySymbol(symbol), `${description} float rate reserve with symbol ${symbol} not found`);
    }
    /**
     * Returns all fixed rate reserves for the given symbol.
     * Fixed rate reserves have a non-zero debt term (debtTermSeconds > 0).
     *
     * @param symbol The reserve symbol
     * @returns Array of all fixed rate reserves for this symbol
     */
    getFixedRateReservesBySymbol(symbol) {
        const fixedRateReserves = [];
        for (const reserve of this.reserves.values()) {
            if (reserve.symbol === symbol && reserve.getKind().isFixedRate()) {
                fixedRateReserves.push(reserve);
            }
        }
        return fixedRateReserves;
    }
    /**
     * Returns all maturity-timestamp reserves for the given symbol.
     *
     * @param symbol The reserve symbol
     * @returns Array of all maturity-timestamp reserves for this symbol
     */
    getMaturityTimestampReservesBySymbol(symbol) {
        const reserves = [];
        for (const reserve of this.reserves.values()) {
            if (reserve.symbol === symbol && reserve.getKind().isMaturityTimestampKind()) {
                reserves.push(reserve);
            }
        }
        return reserves;
    }
    getReserveMintBySymbol(symbol) {
        const reserves = this.getReservesBySymbol(symbol);
        if (reserves.length === 0) {
            throw new Error(`No reserves found for symbol ${symbol}`);
        }
        return reserves[0].getLiquidityMint();
    }
    async getReserveFarmInfo(reserveAddress, getRewardPrice) {
        const { address } = this;
        if (!address) {
            throw Error('Market must be initialized to call initialize.');
        }
        if (!this.getReserves().every((reserve) => reserve.stats)) {
            await this.loadReserves();
        }
        // Find the reserve
        const kaminoReserve = this.getReserveByAddress(reserveAddress);
        if (!kaminoReserve) {
            throw Error(`Could not find reserve ${reserveAddress}`);
        }
        const totalDepositAmount = lamportsToNumberDecimal(kaminoReserve.getLiquidityAvailableAmount(), kaminoReserve.stats.decimals);
        const totalBorrowAmount = lamportsToNumberDecimal(kaminoReserve.getBorrowedAmount(), kaminoReserve.stats.decimals);
        const collateralFarmAddress = kaminoReserve.state.farmCollateral;
        const debtFarmAddress = kaminoReserve.state.farmDebt;
        const result = {
            borrowingRewards: {
                rewardsPerSecond: new decimal_js_1.default(0),
                rewardsRemaining: new decimal_js_1.default(0),
                rewardApr: new decimal_js_1.default(0),
                rewardMint: utils_1.DEFAULT_PUBLIC_KEY,
                totalInvestmentUsd: new decimal_js_1.default(0),
                rewardPrice: 0,
            },
            depositingRewards: {
                rewardsPerSecond: new decimal_js_1.default(0),
                rewardsRemaining: new decimal_js_1.default(0),
                rewardApr: new decimal_js_1.default(0),
                rewardMint: utils_1.DEFAULT_PUBLIC_KEY,
                totalInvestmentUsd: new decimal_js_1.default(0),
                rewardPrice: 0,
            },
        };
        if ((0, utils_1.isNotNullPubkey)(collateralFarmAddress)) {
            result.depositingRewards = await this.getRewardInfoForFarm(collateralFarmAddress, totalDepositAmount, getRewardPrice);
        }
        if ((0, utils_1.isNotNullPubkey)(debtFarmAddress)) {
            result.borrowingRewards = await this.getRewardInfoForFarm(debtFarmAddress, totalBorrowAmount, getRewardPrice);
        }
        return result;
    }
    async getRewardInfoForFarm(farmAddress, totalInvestmentUsd, getRewardPrice) {
        const farmState = await (0, farm_utils_1.fetchFarmStateOrNull)(this.getRpc(), farmAddress);
        if (!farmState) {
            throw Error(`Could not parse farm state. ${farmAddress}`);
        }
        const { token, rewardsAvailable, rewardScheduleCurve } = farmState.rewardInfos[0];
        // TODO: marius fix
        const rewardPerSecondLamports = Number(rewardScheduleCurve.points[0].rewardPerTimeUnit);
        const { mint, decimals: rewardDecimals } = token;
        const rewardPriceUsd = await getRewardPrice(mint);
        const rewardApr = this.calculateRewardAPR(rewardPerSecondLamports, rewardPriceUsd, totalInvestmentUsd, Number(rewardDecimals));
        return {
            rewardsPerSecond: new decimal_js_1.default(rewardPerSecondLamports).dividedBy(10 ** Number(rewardDecimals)),
            rewardsRemaining: new decimal_js_1.default(rewardsAvailable.toString()).dividedBy(10 ** Number(rewardDecimals)),
            rewardApr: rewardsAvailable > 0n ? rewardApr : new decimal_js_1.default(0),
            rewardMint: mint,
            totalInvestmentUsd,
            rewardPrice: rewardPriceUsd,
        };
    }
    calculateRewardAPR(rewardPerSecondLamports, rewardPriceUsd, totalInvestmentUsd, rewardDecimals) {
        const rewardsPerYear = new decimal_js_1.default(rewardPerSecondLamports)
            .dividedBy(10 ** rewardDecimals)
            .times(365 * 24 * 60 * 60)
            .times(rewardPriceUsd);
        return rewardsPerYear.dividedBy(totalInvestmentUsd);
    }
    /**
     * Get all obligations for lending market, optionally filter by obligation tag
     * This function will likely require an RPC capable of returning more than the default 100k rows in a single scan
     *
     * @param tag
     */
    async getAllObligationsForMarket(slot, tag) {
        const filters = [
            {
                dataSize: BigInt(accounts_1.Obligation.layout.span + 8),
            },
            {
                memcmp: {
                    offset: 32n,
                    bytes: this.address.toString(),
                    encoding: 'base58',
                },
            },
        ];
        if (tag !== undefined) {
            filters.push({
                memcmp: {
                    offset: 8n,
                    bytes: base58Decoder.decode(new bn_js_1.default(tag).toBuffer()),
                    encoding: 'base58',
                },
            });
        }
        const collateralExchangeRates = new Map();
        const cumulativeBorrowRates = new Map();
        const obligations = await (0, utils_1.getProgramAccounts)(this.rpc, this.programId, zero_padding_1.ObligationZP.layout.span + 8, filters, { offset: 0, length: zero_padding_1.ObligationZP.layout.span + 8 } // truncate the padding
        );
        return obligations.map((obligation) => {
            if (obligation.data === null) {
                throw new Error('Invalid account');
            }
            const obligationAccount = zero_padding_1.ObligationZP.decode(obligation.data);
            if (!obligationAccount) {
                throw Error('Could not parse obligation.');
            }
            obligation_1.KaminoObligation.addRatesForObligation(this, obligationAccount.deposits, obligationAccount.borrows, collateralExchangeRates, cumulativeBorrowRates, slot);
            return new obligation_1.KaminoObligation(this, obligation.address, obligationAccount, collateralExchangeRates, cumulativeBorrowRates);
        });
    }
    /**
     * Get all obligations for lending market from an async generator filled with batches of 100 obligations each
     * @param tag
     * @example
     * const obligationsGenerator = market.batchGetAllObligationsForMarket();
     * for await (const obligations of obligationsGenerator) {
     *   console.log('got a batch of # obligations:', obligations.length);
     * }
     */
    async *batchGetAllObligationsForMarket(slot, tag) {
        const filters = [
            {
                dataSize: BigInt(accounts_1.Obligation.layout.span + 8),
            },
            {
                memcmp: {
                    offset: 32n,
                    bytes: this.address.toString(),
                    encoding: 'base58',
                },
            },
        ];
        if (tag !== undefined) {
            filters.push({
                memcmp: {
                    offset: 8n,
                    bytes: base58Decoder.decode(new bn_js_1.default(tag).toBuffer()),
                    encoding: 'base58',
                },
            });
        }
        const collateralExchangeRates = new Map();
        const cumulativeBorrowRates = new Map();
        const obligationPubkeys = await this.rpc
            .getProgramAccounts(this.programId, {
            filters,
            encoding: 'base64',
            dataSlice: { offset: 0, length: 0 },
        })
            .send();
        for (const batch of (0, kliquidity_sdk_1.chunks)(obligationPubkeys.map((x) => x.pubkey), 100)) {
            const obligationAccounts = await this.rpc.getMultipleAccounts(batch, { encoding: 'base64' }).send();
            const obligationsBatch = [];
            for (let i = 0; i < obligationAccounts.value.length; i++) {
                const obligation = obligationAccounts.value[i];
                const pubkey = batch[i];
                if (obligation === null) {
                    continue;
                }
                const obligationAccount = accounts_1.Obligation.decode(buffer_1.Buffer.from(obligation.data[0], 'base64'));
                if (!obligationAccount) {
                    throw Error(`Could not decode obligation ${pubkey.toString()}`);
                }
                obligation_1.KaminoObligation.addRatesForObligation(this, obligationAccount.deposits, obligationAccount.borrows, collateralExchangeRates, cumulativeBorrowRates, slot);
                obligationsBatch.push(new obligation_1.KaminoObligation(this, pubkey, obligationAccount, collateralExchangeRates, cumulativeBorrowRates));
            }
            yield obligationsBatch;
        }
    }
    async getAllObligationsByTag(tag, market, slot) {
        const obligations = await this.rpc
            .getProgramAccounts(this.programId, {
            filters: [
                {
                    dataSize: BigInt(accounts_1.Obligation.layout.span + 8),
                },
                {
                    memcmp: {
                        offset: 8n,
                        bytes: base58Decoder.decode(new bn_js_1.default(tag).toBuffer()),
                        encoding: 'base58',
                    },
                },
                {
                    memcmp: {
                        offset: 32n,
                        bytes: market.toString(),
                        encoding: 'base58',
                    },
                },
            ],
            encoding: 'base64',
        })
            .send();
        const collateralExchangeRates = new Map();
        const cumulativeBorrowRates = new Map();
        return obligations.map((obligation) => {
            if (obligation.account === null) {
                throw new Error('Invalid account');
            }
            if (obligation.account.owner !== this.programId) {
                throw new Error("account doesn't belong to this program");
            }
            const obligationAccount = accounts_1.Obligation.decode(buffer_1.Buffer.from(obligation.account.data[0], 'base64'));
            if (!obligationAccount) {
                throw Error('Could not parse obligation.');
            }
            obligation_1.KaminoObligation.addRatesForObligation(this, obligationAccount.deposits, obligationAccount.borrows, collateralExchangeRates, cumulativeBorrowRates, slot);
            return new obligation_1.KaminoObligation(this, obligation.pubkey, obligationAccount, collateralExchangeRates, cumulativeBorrowRates);
        });
    }
    /**
     * Retrieves all obligations that have deposited into the specified reserve.
     *
     * Iterates through all possible deposit slots up to DEPOSITS_LIMIT, applying filters to fetch obligations
     * from the program accounts where the deposited reserve matches the provided address. For each matching
     * obligation, it decodes the account data, validates ownership, and constructs KaminoObligation instances
     * with calculated rates.
     *
     * @param {Address} reserve - The address of the reserve to filter deposited obligations by.
     * @returns {Promise<KaminoObligation[]>} A promise that resolves to an array of KaminoObligation objects representing all obligations that have deposited into the specified reserve.
     * @throws {Error} If an account is invalid or does not belong to this program, or if obligation parsing fails.
     */
    async getAllObligationsByDepositedReserve(reserve, slot) {
        const finalObligations = [];
        for (let i = 0; i < utils_1.DEPOSITS_LIMIT; i++) {
            const obligations = await this.rpc
                .getProgramAccounts(this.programId, {
                filters: [
                    {
                        dataSize: BigInt(accounts_1.Obligation.layout.span + 8),
                    },
                    {
                        memcmp: {
                            offset: 96n + 136n * BigInt(i), // the offset for the borrows array in the obligation account
                            bytes: reserve.toString(),
                            encoding: 'base58',
                        },
                    },
                    {
                        memcmp: {
                            offset: 32n,
                            bytes: this.address.toString(),
                            encoding: 'base58',
                        },
                    },
                ],
                encoding: 'base64',
            })
                .send();
            const collateralExchangeRates = new Map();
            const cumulativeBorrowRates = new Map();
            const obligationsBatch = obligations.map((obligation) => {
                if (obligation.account === null) {
                    throw new Error('Invalid account');
                }
                if (obligation.account.owner !== this.programId) {
                    throw new Error("account doesn't belong to this program");
                }
                const obligationAccount = accounts_1.Obligation.decode(buffer_1.Buffer.from(obligation.account.data[0], 'base64'));
                if (!obligationAccount) {
                    throw Error('Could not parse obligation.');
                }
                obligation_1.KaminoObligation.addRatesForObligation(this, obligationAccount.deposits, obligationAccount.borrows, collateralExchangeRates, cumulativeBorrowRates, slot);
                return new obligation_1.KaminoObligation(this, obligation.pubkey, obligationAccount, collateralExchangeRates, cumulativeBorrowRates);
            });
            finalObligations.push(...obligationsBatch);
        }
        return finalObligations;
    }
    /**
     * Retrieves all obligations that have borrowed from the specified reserve.
     *
     * Iterates through all possible borrow slots up to BORROWS_LIMIT, applying filters to fetch obligations
     * from the program accounts where the borrowed reserve matches the provided address. For each matching
     * obligation, it decodes the account data, validates ownership, and constructs KaminoObligation instances
     * with calculated rates.
     *
     * @param {Address} reserve - The address of the reserve to filter borrowed obligations by.
     * @returns {Promise<KaminoObligation[]>} A promise that resolves to an array of KaminoObligation objects
     *   representing all obligations that have borrowed from the specified reserve.
     * @throws {Error} If an account is invalid or does not belong to this program, or if obligation parsing fails.
     */
    async getAllObligationsByBorrowedReserve(reserve, slot) {
        const finalObligations = [];
        for (let i = 0; i < utils_1.BORROWS_LIMIT; i++) {
            const obligations = await this.rpc
                .getProgramAccounts(this.programId, {
                filters: [
                    {
                        dataSize: BigInt(accounts_1.Obligation.layout.span + 8),
                    },
                    {
                        memcmp: {
                            offset: 96n + 136n * 8n + 24n + 200n * BigInt(i), // the offset for the borrows array in the obligation account
                            bytes: reserve.toString(),
                            encoding: 'base58',
                        },
                    },
                    {
                        memcmp: {
                            offset: 32n, // lendingMarket address
                            bytes: this.address.toString(),
                            encoding: 'base58',
                        },
                    },
                ],
                encoding: 'base64',
            })
                .send();
            const collateralExchangeRates = new Map();
            const cumulativeBorrowRates = new Map();
            const obligationsBatch = obligations.map((obligation) => {
                if (obligation.account === null) {
                    throw new Error('Invalid account');
                }
                if (obligation.account.owner !== this.programId) {
                    throw new Error("account doesn't belong to this program");
                }
                const obligationAccount = accounts_1.Obligation.decode(buffer_1.Buffer.from(obligation.account.data[0], 'base64'));
                if (!obligationAccount) {
                    throw Error('Could not parse obligation.');
                }
                obligation_1.KaminoObligation.addRatesForObligation(this, obligationAccount.deposits, obligationAccount.borrows, collateralExchangeRates, cumulativeBorrowRates, slot);
                return new obligation_1.KaminoObligation(this, obligation.pubkey, obligationAccount, collateralExchangeRates, cumulativeBorrowRates);
            });
            finalObligations.push(...obligationsBatch);
        }
        return finalObligations;
    }
    async getAllUserObligations(user, slot, commitment = 'processed') {
        const [currentSlot, obligations] = await Promise.all([
            Promise.resolve(slot),
            this.rpc
                .getProgramAccounts(this.programId, {
                filters: [
                    {
                        dataSize: BigInt(accounts_1.Obligation.layout.span + 8),
                    },
                    {
                        memcmp: {
                            offset: 0n,
                            bytes: base58Decoder.decode(accounts_1.Obligation.discriminator),
                            encoding: 'base58',
                        },
                    },
                    {
                        memcmp: {
                            offset: 64n,
                            bytes: user.toString(),
                            encoding: 'base58',
                        },
                    },
                    {
                        memcmp: {
                            offset: 32n,
                            bytes: this.address.toString(),
                            encoding: 'base58',
                        },
                    },
                ],
                encoding: 'base64',
                commitment,
            })
                .send(),
        ]);
        const collateralExchangeRates = new Map();
        const cumulativeBorrowRates = new Map();
        return obligations.map((obligation) => {
            if (obligation.account.owner !== this.programId) {
                throw new Error("account doesn't belong to this program");
            }
            const obligationAccount = accounts_1.Obligation.decode(buffer_1.Buffer.from(obligation.account.data[0], 'base64'));
            if (!obligationAccount) {
                throw Error('Could not parse obligation.');
            }
            obligation_1.KaminoObligation.addRatesForObligation(this, obligationAccount.deposits, obligationAccount.borrows, collateralExchangeRates, cumulativeBorrowRates, currentSlot);
            return new obligation_1.KaminoObligation(this, obligation.pubkey, obligationAccount, collateralExchangeRates, cumulativeBorrowRates);
        });
    }
    async getAllUserObligationsForReserve(user, reserve, slot) {
        const obligationAddresses = [];
        obligationAddresses.push(await new utils_1.VanillaObligation(this.programId).toPda(this.getAddress(), user));
        const targetReserve = new Map(Array.from(this.reserves.entries())).get(reserve);
        if (!targetReserve) {
            throw Error(`Could not find reserve ${reserve}`);
        }
        for (const [key, kaminoReserve] of this.reserves) {
            if (targetReserve.address === key) {
                // skip target reserve
                continue;
            }
            obligationAddresses.push(await new utils_1.MultiplyObligation(targetReserve.getLiquidityMint(), kaminoReserve.getLiquidityMint(), this.programId).toPda(this.getAddress(), user));
            obligationAddresses.push(await new utils_1.MultiplyObligation(kaminoReserve.getLiquidityMint(), targetReserve.getLiquidityMint(), this.programId).toPda(this.getAddress(), user));
            obligationAddresses.push(await new utils_1.LeverageObligation(targetReserve.getLiquidityMint(), kaminoReserve.getLiquidityMint(), this.programId).toPda(this.getAddress(), user));
            obligationAddresses.push(await new utils_1.LeverageObligation(kaminoReserve.getLiquidityMint(), targetReserve.getLiquidityMint(), this.programId).toPda(this.getAddress(), user));
        }
        const batchSize = 100;
        const finalObligations = [];
        for (let batchStart = 0; batchStart < obligationAddresses.length; batchStart += batchSize) {
            const obligations = await this.getMultipleObligationsByAddress(obligationAddresses.slice(batchStart, batchStart + batchSize), slot);
            obligations.forEach((obligation) => {
                if (obligation !== null) {
                    for (const deposits of obligation.deposits.keys()) {
                        if (deposits === reserve) {
                            finalObligations.push(obligation);
                        }
                    }
                    for (const borrows of obligation.borrows.keys()) {
                        if (borrows === reserve) {
                            finalObligations.push(obligation);
                        }
                    }
                }
            });
        }
        return finalObligations;
    }
    async getUserVanillaObligation(user) {
        const vanillaObligationAddress = await new utils_1.VanillaObligation(this.programId).toPda(this.getAddress(), user);
        const obligation = await this.getObligationByAddress(vanillaObligationAddress);
        if (!obligation) {
            throw new Error(`Could not find vanilla obligation ${vanillaObligationAddress}`);
        }
        return obligation;
    }
    isReserveInObligation(obligation, reserve) {
        for (const deposits of obligation.deposits.keys()) {
            if (deposits === reserve) {
                return true;
            }
        }
        for (const borrows of obligation.borrows.keys()) {
            if (borrows === reserve) {
                return true;
            }
        }
        return false;
    }
    async getUserObligationsByTag(tag, user, currentSlot) {
        const obligations = await this.rpc
            .getProgramAccounts(this.programId, {
            filters: [
                {
                    dataSize: BigInt(accounts_1.Obligation.layout.span + 8),
                },
                {
                    memcmp: {
                        offset: 8n,
                        bytes: base58Decoder.decode(new bn_js_1.default(tag).toBuffer()),
                        encoding: 'base58',
                    },
                },
                {
                    memcmp: {
                        offset: 32n,
                        bytes: this.address.toString(),
                        encoding: 'base58',
                    },
                },
                {
                    memcmp: {
                        offset: 64n,
                        bytes: user.toString(),
                        encoding: 'base58',
                    },
                },
            ],
            encoding: 'base64',
        })
            .send();
        const collateralExchangeRates = new Map();
        const cumulativeBorrowRates = new Map();
        return obligations.map((obligation) => {
            if (obligation.account.owner !== this.programId) {
                throw new Error("account doesn't belong to this program");
            }
            const obligationAccount = accounts_1.Obligation.decode(buffer_1.Buffer.from(obligation.account.data[0], 'base64'));
            if (!obligationAccount) {
                throw Error('Could not parse obligation.');
            }
            obligation_1.KaminoObligation.addRatesForObligation(this, obligationAccount.deposits, obligationAccount.borrows, collateralExchangeRates, cumulativeBorrowRates, currentSlot);
            return new obligation_1.KaminoObligation(this, obligation.pubkey, obligationAccount, collateralExchangeRates, cumulativeBorrowRates);
        });
    }
    async getObligationByAddress(address) {
        if (!this.getReserves().every((reserve) => reserve.stats)) {
            await this.loadReserves();
        }
        return obligation_1.KaminoObligation.load(this, address);
    }
    async getMultipleObligationsByAddress(addresses, slot) {
        return obligation_1.KaminoObligation.loadAll(this, addresses, slot);
    }
    /**
     * Get the user metadata PDA and fetch and return the user metadata state if it exists
     * @return [address, userMetadataState] - The address of the user metadata PDA and the user metadata state, or null if it doesn't exist
     */
    async getUserMetadata(user) {
        const [address, _bump] = await (0, utils_1.userMetadataPda)(user, this.programId);
        const userMetadata = await accounts_1.UserMetadata.fetch(this.rpc, address, this.programId);
        return [address, userMetadata];
    }
    async getReferrerTokenStateForReserve(referrer, reserve) {
        const address = await (0, utils_1.referrerTokenStatePda)(referrer, reserve, this.programId);
        const referrerTokenState = await accounts_1.ReferrerTokenState.fetch(this.rpc, address, this.programId);
        return [address, referrerTokenState];
    }
    async getAllReferrerTokenStates(referrer) {
        const referrerTokenStates = await this.rpc
            .getProgramAccounts(this.programId, {
            filters: [
                {
                    dataSize: BigInt(accounts_1.ReferrerTokenState.layout.span + 8),
                },
                {
                    memcmp: {
                        offset: 8n,
                        bytes: referrer.toString(),
                        encoding: 'base58',
                    },
                },
            ],
            encoding: 'base64',
        })
            .send();
        const referrerTokenStatesForMints = new Map();
        referrerTokenStates.forEach((referrerTokenState) => {
            if (referrerTokenState.account === null) {
                throw new Error('Invalid account');
            }
            if (referrerTokenState.account.owner !== this.programId) {
                throw new Error("account doesn't belong to this program");
            }
            const referrerTokenStateDecoded = accounts_1.ReferrerTokenState.decode(buffer_1.Buffer.from(referrerTokenState.account.data[0], 'base64'));
            if (!referrerTokenStateDecoded) {
                throw Error('Could not parse obligation.');
            }
            referrerTokenStatesForMints.set(referrerTokenStateDecoded.mint, referrerTokenStateDecoded);
        });
        return referrerTokenStatesForMints;
    }
    async getAllReferrerFeesUnclaimed(referrer) {
        const referrerTokenStatesForMints = await this.getAllReferrerTokenStates(referrer);
        const referrerFeesUnclaimedForMints = new Map();
        for (const mint of referrerTokenStatesForMints.keys()) {
            referrerFeesUnclaimedForMints.set(mint, new fraction_1.Fraction(referrerTokenStatesForMints.get(mint).amountUnclaimedSf).toDecimal());
        }
        return referrerFeesUnclaimedForMints;
    }
    async getReferrerFeesUnclaimedForReserve(referrer, reserve) {
        const [, referrerTokenState] = await this.getReferrerTokenStateForReserve(referrer, reserve.address);
        return referrerTokenState ? new fraction_1.Fraction(referrerTokenState.amountUnclaimedSf).toDecimal() : new decimal_js_1.default(0);
    }
    async getReferrerFeesCumulativeForReserve(referrer, reserve) {
        const [, referrerTokenState] = await this.getReferrerTokenStateForReserve(referrer, reserve.address);
        return referrerTokenState ? new fraction_1.Fraction(referrerTokenState.amountCumulativeSf).toDecimal() : new decimal_js_1.default(0);
    }
    async getAllReferrerFeesCumulative(referrer) {
        const referrerTokenStatesForMints = await this.getAllReferrerTokenStates(referrer);
        const referrerFeesCumulativeForMints = new Map();
        for (const mint of referrerTokenStatesForMints.keys()) {
            referrerFeesCumulativeForMints.set(mint, new fraction_1.Fraction(referrerTokenStatesForMints.get(mint).amountUnclaimedSf).toDecimal());
        }
        return referrerFeesCumulativeForMints;
    }
    getReferrerUrl(baseUrl, referrer) {
        return `${baseUrl}${referrer.toString()}`;
    }
    getReferrerFromUrl(baseUrl, url) {
        return (0, kit_1.address)(url.split(baseUrl)[1]);
    }
    /**
     * Get the underlying rpc passed when instantiating this market
     * @return rpc
     */
    getRpc() {
        return this.rpc;
    }
    /**
     * Get all scope OraclePrices accounts for all market reserves
     * @param scope
     */
    async getReserveOraclePrices(scope) {
        const reserveOraclePrices = new Map();
        const oraclePrices = await scope.getMultipleOraclePrices(Array.from(this.scopeFeeds.keys()));
        const oraclePriceMap = new Map();
        for (const [feed, account] of oraclePrices) {
            oraclePriceMap.set(feed, account);
        }
        for (const [reserveAddress, reserve] of this.reserves) {
            reserveOraclePrices.set(reserveAddress, oraclePriceMap.get(reserve.state.config.tokenInfo.scopeConfiguration.priceFeed));
        }
        return reserveOraclePrices;
    }
    /**
     * Get all Scope prices used by all the market reserves
     */
    async getAllScopePrices(scope, allOraclePrices) {
        const spot = {};
        const twaps = {};
        for (const reserve of this.reserves.values()) {
            const tokenMint = reserve.getLiquidityMint().toString();
            const tokenName = reserve.getTokenSymbol();
            const oracle = reserve.state.config.tokenInfo.scopeConfiguration.priceFeed;
            const chain = reserve.state.config.tokenInfo.scopeConfiguration.priceChain;
            const twapChain = reserve.state.config.tokenInfo.scopeConfiguration.twapChain.filter((x) => x > 0);
            const oraclePrices = allOraclePrices.get(oracle);
            if (oraclePrices && oracle && (0, utils_1.isNotNullPubkey)(oracle) && chain && scope_sdk_1.Scope.isScopeChainValid(chain)) {
                const spotPrice = await scope.getPriceFromChain(chain, oraclePrices);
                spot[tokenMint] = { price: spotPrice.price, name: tokenName };
            }
            if (oraclePrices && oracle && (0, utils_1.isNotNullPubkey)(oracle) && twapChain && scope_sdk_1.Scope.isScopeChainValid(twapChain)) {
                const twap = await scope.getPriceFromChain(twapChain, oraclePrices);
                twaps[tokenMint] = { price: twap.price, name: tokenName };
            }
        }
        return { spot, twap: twaps };
    }
    /**
     * Get all Scope/Pyth/Switchboard prices used by all the market reserves
     */
    async getAllPrices(oracleAccounts) {
        const klendPrices = {
            scope: { spot: {}, twap: {} },
            pyth: { spot: {}, twap: {} },
            switchboard: { spot: {}, twap: {} },
        };
        const allOracleAccounts = oracleAccounts ??
            (await (0, utils_1.getAllOracleAccounts)(this.rpc, this.getReserves().map((x) => x.state)));
        const pythCache = new Map();
        const switchboardCache = new Map();
        const scopeCache = new Map();
        for (const reserve of this.reserves.values()) {
            const tokenMint = reserve.getLiquidityMint().toString();
            const tokenName = reserve.getTokenSymbol();
            const scopeOracle = reserve.state.config.tokenInfo.scopeConfiguration.priceFeed;
            const spotChain = reserve.state.config.tokenInfo.scopeConfiguration.priceChain;
            const twapChain = reserve.state.config.tokenInfo.scopeConfiguration.twapChain.filter((x) => x > 0);
            const pythOracle = reserve.state.config.tokenInfo.pythConfiguration.price;
            const switchboardSpotOracle = reserve.state.config.tokenInfo.switchboardConfiguration.priceAggregator;
            const switchboardTwapOracle = reserve.state.config.tokenInfo.switchboardConfiguration.twapAggregator;
            if ((0, utils_1.isNotNullPubkey)(scopeOracle)) {
                const scopePrices = {
                    spot: (0, utils_1.cacheOrGetScopePrice)(scopeOracle, scopeCache, allOracleAccounts, spotChain),
                    twap: (0, utils_1.cacheOrGetScopePrice)(scopeOracle, scopeCache, allOracleAccounts, twapChain),
                };
                this.setPriceIfExist(klendPrices.scope, scopePrices.spot, scopePrices.twap, tokenMint, tokenName);
            }
            if ((0, utils_1.isNotNullPubkey)(pythOracle)) {
                const pythPrices = (0, utils_1.cacheOrGetPythPrices)(pythOracle, pythCache, allOracleAccounts);
                this.setPriceIfExist(klendPrices.pyth, pythPrices?.spot, pythPrices?.twap, tokenMint, tokenName);
            }
            if ((0, utils_1.isNotNullPubkey)(switchboardSpotOracle)) {
                const switchboardPrices = {
                    spot: (0, utils_1.cacheOrGetSwitchboardPrice)(switchboardSpotOracle, switchboardCache, allOracleAccounts),
                    twap: (0, utils_1.isNotNullPubkey)(switchboardTwapOracle)
                        ? (0, utils_1.cacheOrGetSwitchboardPrice)(switchboardTwapOracle, switchboardCache, allOracleAccounts)
                        : null,
                };
                this.setPriceIfExist(klendPrices.switchboard, switchboardPrices.spot, switchboardPrices.twap, tokenMint, tokenName);
            }
        }
        return klendPrices;
    }
    getCumulativeBorrowRatesByReserve(slot) {
        const cumulativeBorrowRates = new Map();
        for (const reserve of this.reserves.values()) {
            cumulativeBorrowRates.set(reserve.address, reserve.getEstimatedCumulativeBorrowRate(slot, this.state.referralFeeBps));
        }
        return cumulativeBorrowRates;
    }
    getCollateralExchangeRatesByReserve(slot) {
        const collateralExchangeRates = new Map();
        for (const reserve of this.reserves.values()) {
            collateralExchangeRates.set(reserve.address, reserve.getEstimatedCollateralExchangeRate(slot, this.state.referralFeeBps));
        }
        return collateralExchangeRates;
    }
    setPriceIfExist(prices, spot, twap, mint, tokenName) {
        if (spot) {
            prices.spot[mint] = { price: spot.price, name: tokenName };
        }
        if (twap) {
            prices.twap[mint] = { price: twap.price, name: tokenName };
        }
    }
    getRecentSlotDurationMs() {
        return this.recentSlotDurationMs;
    }
    /* Returns all elevation groups except the default one  */
    getMarketElevationGroupDescriptions() {
        const elevationGroups = [];
        // Partially build
        for (const elevationGroup of this.state.elevationGroups) {
            if (elevationGroup.id === 0) {
                continue;
            }
            elevationGroups.push({
                collateralReserves: new Set([]),
                collateralLiquidityMints: new Set([]),
                debtReserve: elevationGroup.debtReserve,
                debtLiquidityMint: utils_1.DEFAULT_PUBLIC_KEY,
                elevationGroup: elevationGroup.id,
                maxReservesAsCollateral: elevationGroup.maxReservesAsCollateral,
            });
        }
        // Fill the remaining
        for (const reserve of this.reserves.values()) {
            const reserveLiquidityMint = reserve.getLiquidityMint();
            const reserveAddress = reserve.address;
            const reserveElevationGroups = reserve.state.config.elevationGroups;
            for (const elevationGroupId of reserveElevationGroups) {
                if (elevationGroupId === 0) {
                    continue;
                }
                const elevationGroupDescription = elevationGroups[elevationGroupId - 1];
                if (elevationGroupDescription) {
                    if (reserveAddress === elevationGroupDescription.debtReserve) {
                        elevationGroups[elevationGroupId - 1].debtLiquidityMint = reserveLiquidityMint;
                    }
                    else {
                        elevationGroups[elevationGroupId - 1].collateralReserves.add(reserveAddress);
                        elevationGroups[elevationGroupId - 1].collateralLiquidityMints.add(reserveLiquidityMint);
                    }
                }
                else {
                    throw new Error(`Invalid elevation group id ${elevationGroupId} at reserve ${reserveAddress.toString()}`);
                }
            }
        }
        return elevationGroups;
    }
    /* Returns all elevation groups for a given combination of liquidity mints, except the default one */
    getElevationGroupsForMintsCombination(collLiquidityMints, debtLiquidityMint) {
        const allElevationGroups = this.getMarketElevationGroupDescriptions();
        return allElevationGroups.filter((elevationGroupDescription) => {
            return (collLiquidityMints.every((mint) => elevationGroupDescription.collateralLiquidityMints.has(mint)) &&
                (debtLiquidityMint == undefined || debtLiquidityMint === elevationGroupDescription.debtLiquidityMint));
        });
    }
    /* Returns all elevation groups for a given combination of reserves, except the default one */
    getElevationGroupsForReservesCombination(collReserves, debtReserve) {
        const allElevationGroups = this.getMarketElevationGroupDescriptions();
        return allElevationGroups.filter((elevationGroupDescription) => {
            return (collReserves.every((mint) => elevationGroupDescription.collateralReserves.has(mint)) &&
                (debtReserve == undefined || debtReserve === elevationGroupDescription.debtReserve));
        });
    }
}
exports.KaminoMarket = KaminoMarket;
async function getReserveStatesForMarket(marketAddress, rpc, programId) {
    const reserves = await rpc
        .getProgramAccounts(programId, {
        filters: [
            {
                dataSize: BigInt(accounts_1.Reserve.layout.span + 8),
            },
            {
                memcmp: {
                    offset: 32n,
                    bytes: marketAddress.toString(),
                    encoding: 'base58',
                },
            },
            {
                // Match Reserve's 8-byte Anchor discriminator at offset 0 so uninitialized
                // klend-owned accounts with the same 8624-byte layout (e.g. pre-allocated
                // placeholders awaiting init_reserve) are filtered out server-side and
                // never reach Reserve.decode below.
                memcmp: {
                    offset: 0n,
                    bytes: base58Decoder.decode(accounts_1.Reserve.discriminator),
                    encoding: 'base58',
                },
            },
        ],
        encoding: 'base64',
    })
        .send();
    return reserves.map((reserve) => {
        if (reserve.account === null) {
            throw new Error(`Reserve account ${reserve.pubkey} does not exist`);
        }
        const reserveAccount = accounts_1.Reserve.decode(buffer_1.Buffer.from(reserve.account.data[0], 'base64'));
        if (!reserveAccount) {
            throw Error(`Could not parse reserve ${reserve.pubkey}`);
        }
        return {
            address: reserve.pubkey,
            state: reserveAccount,
        };
    });
}
async function getReservesForMarkets(marketAddresses, rpc, programId, recentSlotDurationMs, oracleAccounts) {
    const requestedMarkets = new Set(marketAddresses);
    const reservesByMarket = new Map();
    marketAddresses.forEach((marketAddress) => {
        reservesByMarket.set(marketAddress, new Map());
    });
    if (requestedMarkets.size === 0) {
        return reservesByMarket;
    }
    const reserves = await rpc
        .getProgramAccounts(programId, {
        filters: [
            {
                dataSize: BigInt(accounts_1.Reserve.layout.span + 8),
            },
            {
                memcmp: {
                    offset: 0n,
                    bytes: base58Decoder.decode(accounts_1.Reserve.discriminator),
                    encoding: 'base58',
                },
            },
        ],
        encoding: 'base64',
    })
        .send();
    const deserializedReserves = reserves
        .map((reserve) => {
        if (reserve.account === null) {
            throw new Error(`Reserve account ${reserve.pubkey} does not exist`);
        }
        const reserveAccount = accounts_1.Reserve.decode(buffer_1.Buffer.from(reserve.account.data[0], 'base64'));
        if (!reserveAccount) {
            throw Error(`Could not parse reserve ${reserve.pubkey}`);
        }
        return {
            address: reserve.pubkey,
            state: reserveAccount,
        };
    })
        .filter((reserve) => requestedMarkets.has(reserve.state.lendingMarket));
    if (deserializedReserves.length === 0) {
        return reservesByMarket;
    }
    const kaminoReserves = await initializeKaminoReserves(deserializedReserves, rpc, recentSlotDurationMs, oracleAccounts);
    kaminoReserves.forEach((kaminoReserve) => {
        const marketReserves = reservesByMarket.get(kaminoReserve.state.lendingMarket);
        if (marketReserves) {
            marketReserves.set(kaminoReserve.address, kaminoReserve);
        }
    });
    return reservesByMarket;
}
async function getReservesForMarket(marketAddress, rpc, programId, recentSlotDurationMs, oracleAccounts) {
    const deserializedReserves = await getReserveStatesForMarket(marketAddress, rpc, programId);
    const kaminoReserves = await initializeKaminoReserves(deserializedReserves, rpc, recentSlotDurationMs, oracleAccounts);
    const reservesByAddress = new Map();
    kaminoReserves.forEach((kaminoReserve) => {
        reservesByAddress.set(kaminoReserve.address, kaminoReserve);
    });
    return reservesByAddress;
}
async function initializeKaminoReserves(reserves, rpc, recentSlotDurationMs, oracleAccounts) {
    const [reservesAndOracles, cdnResourcesData] = await Promise.all([
        (0, utils_1.getTokenOracleData)(rpc, reserves, oracleAccounts),
        cdnClient_1.kaminoCdn.getData(),
    ]);
    return reservesAndOracles.map(([{ address: reserveAddress, state: reserve }, oracle]) => {
        if (!oracle) {
            throw Error(`Could not find oracle for ${(0, utils_2.parseTokenSymbol)(reserve.config.tokenInfo.name)} (${reserveAddress}) reserve in market ${reserve.lendingMarket}`);
        }
        return reserve_1.KaminoReserve.initialize(reserveAddress, reserve, oracle, rpc, recentSlotDurationMs, cdnResourcesData);
    });
}
async function getSingleReserve(reservePk, rpc, recentSlotDurationMs, reserveData, oracleAccounts) {
    const reserve = reserveData ?? (await accounts_1.Reserve.fetch(rpc, reservePk));
    if (reserve === null) {
        throw new Error(`Reserve account ${reservePk} does not exist`);
    }
    const [reservesAndOracles, cdnResourcesData] = await Promise.all([
        (0, utils_1.getTokenOracleData)(rpc, [{ address: reservePk, state: reserve }], oracleAccounts),
        cdnClient_1.kaminoCdn.getData(),
    ]);
    const [, oracle] = reservesAndOracles[0];
    if (!oracle) {
        throw Error(`Could not find oracle for ${(0, utils_2.parseTokenSymbol)(reserve.config.tokenInfo.name)} (${reservePk}) reserve in market ${reserve.lendingMarket}`);
    }
    return reserve_1.KaminoReserve.initialize(reservePk, reserve, oracle, rpc, recentSlotDurationMs, cdnResourcesData);
}
function getReservesActive(reserves) {
    const reservesActive = new Map();
    for (const [key, reserve] of reserves) {
        if (reserve.state.config.status === 0) {
            reservesActive.set(key, reserve);
        }
    }
    return reservesActive;
}
/**
 *
 * @param kaminoMarket
 * @param reserves
 */
function getTokenIdsForScopeRefresh(kaminoMarket, reserves) {
    const tokenIds = new Map();
    for (const reserveAddress of reserves) {
        const reserve = kaminoMarket.getReserveByAddress(reserveAddress);
        if (!reserve) {
            throw new Error(`Reserve not found for reserve ${reserveAddress}`);
        }
        const { scopeConfiguration } = reserve.state.config.tokenInfo;
        if (scopeConfiguration.priceFeed !== utils_1.DEFAULT_PUBLIC_KEY) {
            let x = 0;
            while (scopeConfiguration.priceChain[x] !== scope_sdk_1.U16_MAX) {
                (0, utils_1.setOrAppend)(tokenIds, scopeConfiguration.priceFeed, scopeConfiguration.priceChain[x]);
                x++;
            }
            x = 0;
            while (scopeConfiguration.twapChain[x] !== scope_sdk_1.U16_MAX) {
                (0, utils_1.setOrAppend)(tokenIds, scopeConfiguration.priceFeed, scopeConfiguration.twapChain[x]);
                x++;
            }
        }
    }
    //TODO: remove code below
    // - currently Scope program does not allow multiple refreshPricesList instructions in one tx
    // - temporary fix is to only refresh one scope feed at this time
    const firstFeed = tokenIds.entries().next();
    tokenIds.clear();
    if (!firstFeed.done) {
        const [key, value] = firstFeed.value;
        tokenIds.set(key, value);
    }
    return tokenIds;
}
const lamportsToNumberDecimal = (amount, decimals) => {
    const factor = 10 ** decimals;
    return new decimal_js_1.default(amount).div(factor);
};
//# sourceMappingURL=market.js.map