UNPKG

@kamino-finance/klend-sdk

Version:

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

1,016 lines 53.1 kB
"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.getReservesForMarket = getReservesForMarket; exports.getSingleReserve = getSingleReserve; exports.getReservesActive = getReservesActive; exports.getTokenIdsForScopeRefresh = getTokenIdsForScopeRefresh; exports.getReserveFromMintAndMarket = getReserveFromMintAndMarket; const web3_js_1 = require("@solana/web3.js"); const obligation_1 = require("./obligation"); const reserve_1 = require("./reserve"); const accounts_1 = require("../idl_codegen/accounts"); const utils_1 = require("../utils"); const bs58_1 = __importDefault(require("bs58")); const anchor_1 = require("@coral-xyz/anchor"); const decimal_js_1 = __importDefault(require("decimal.js")); const farms_sdk_1 = require("@kamino-finance/farms-sdk"); const programId_1 = require("../idl_codegen/programId"); const bs58_2 = __importDefault(require("bs58")); 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 sbv2_lite_1 = __importDefault(require("@switchboard-xyz/sbv2-lite")); const zero_padding_1 = require("../idl_codegen/zero_padding"); const utils_3 = require("../utils"); const validations_1 = require("../utils/validations"); class KaminoMarket { connection; address; state; reserves; reservesActive; programId; recentSlotDurationMs; constructor(connection, state, marketAddress, reserves, recentSlotDurationMs, programId = programId_1.PROGRAM_ID) { this.address = marketAddress; this.connection = connection; this.state = state; this.reserves = reserves; this.reservesActive = getReservesActive(this.reserves); this.programId = programId; this.recentSlotDurationMs = recentSlotDurationMs; } /** * Load a new market with all of its associated reserves * @param connection * @param marketAddress * @param recentSlotDurationMs * @param programId * @param withReserves * @param setupLocalTest * @param withReserves */ static async load(connection, marketAddress, recentSlotDurationMs, programId = programId_1.PROGRAM_ID, withReserves = true) { const market = await accounts_1.LendingMarket.fetch(connection, marketAddress, programId); if (market === null) { return null; } if (recentSlotDurationMs <= 0) { throw new Error('Recent slot duration cannot be 0'); } const reserves = withReserves ? await getReservesForMarket(marketAddress, connection, programId, recentSlotDurationMs) : new Map(); return new KaminoMarket(connection, market, marketAddress.toString(), reserves, recentSlotDurationMs, programId); } static loadWithReserves(connection, market, reserves, marketAddress, recentSlotDurationMs, programId = programId_1.PROGRAM_ID) { return new KaminoMarket(connection, market, marketAddress.toString(), reserves, recentSlotDurationMs, programId); } async reload() { const market = await accounts_1.LendingMarket.fetch(this.connection, this.getAddress(), this.programId); if (market === null) { return; } this.state = market; this.reserves = await getReservesForMarket(this.getAddress(), this.connection, this.programId, this.recentSlotDurationMs); this.reservesActive = getReservesActive(this.reserves); } async reloadSingleReserve(reservePk, accountData) { const reserve = await getSingleReserve(reservePk, this.connection, this.recentSlotDurationMs, accountData); this.reserves.set(reservePk, reserve); this.reservesActive.set(reservePk, reserve); } /** * Get the address of this market * @return market address public key */ getAddress() { return new web3_js_1.PublicKey(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 */ getLendingMarketAuthority() { return (0, utils_1.lendingMarketAuthPda)(this.getAddress(), this.programId)[0]; } getName() { return (0, utils_2.parseZeroPaddedUtf8)(this.state.name); } async getObligationDepositByWallet(owner, mint, obligationType) { const obligation = await this.getObligationByWallet(owner, obligationType); return obligation?.getDepositByMint(mint)?.amount ?? new decimal_js_1.default(0); } async getObligationBorrowByWallet(owner, mint, obligationType) { const obligation = await this.getObligationByWallet(owner, obligationType); return obligation?.getBorrowByMint(mint)?.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(collTokenMint, debtTokenMint) { const { maxLtv: maxCollateralLtv, borrowFactor } = this.getMaxAndLiquidationLtvAndBorrowFactorForPair(collTokenMint, debtTokenMint); 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 anchor_1.BN(0)) && this.state.elevationGroups[item - 1].debtReserve.equals(debtReserve.address)); } getMaxAndLiquidationLtvAndBorrowFactorForPair(collTokenMint, debtTokenMint) { const collReserve = this.getReserveByMint(collTokenMint); const debtReserve = this.getReserveByMint(debtTokenMint); if (!collReserve || !debtReserve) { throw Error('Could not find one of the reserves.'); } 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) { let obligations = (await this.getAllObligationsForMarket(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.getDepositByMint(mint) !== undefined); 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.getDepositByMint(collMint) !== undefined && obligation.getBorrowByMint(debtMint) !== 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() { return (await this.getAllObligationsForMarket()) .filter((obligation) => obligation.refreshedStats.userTotalBorrow.gt(0) || obligation.refreshedStats.userTotalDeposit.gt(0)) .reduce((acc, _obligation) => acc + 1, 0); } async getObligationByWallet(publicKey, obligationType) { const { address } = this; if (!address) { throw Error('Market must be initialized to call initialize.'); } const obligationAddress = obligationType.toPda(this.getAddress(), publicKey); return obligation_1.KaminoObligation.load(this, obligationAddress); } /** * @returns The max borrowable amount for leverage positions */ async getMaxLeverageBorrowableAmount(collReserve, debtReserve, slot, requestElevationGroup, obligation) { return obligation ? obligation.getMaxBorrowAmount(this, debtReserve.getLiquidityMint(), slot, requestElevationGroup) : debtReserve.getMaxBorrowAmountWithCollReserve(this, collReserve, slot); } async loadReserves() { const addresses = [...this.reserves.keys()]; const reserveAccounts = await this.connection.getMultipleAccountsInfo(addresses, 'processed'); const deserializedReserves = reserveAccounts.map((reserve, i) => { if (reserve === null) { // maybe reuse old here throw new Error(`Reserve account ${addresses[i].toBase58()} was not found`); } const reserveAccount = accounts_1.Reserve.decode(reserve.data); if (!reserveAccount) { throw Error(`Could not parse reserve ${addresses[i].toBase58()}`); } return reserveAccount; }); const reservesAndOracles = await (0, utils_1.getTokenOracleData)(this.connection, deserializedReserves); const kaminoReserves = new utils_1.PubkeyHashMap(); reservesAndOracles.forEach(([reserve, oracle], index) => { if (!oracle) { throw Error(`Could not find oracle for ${(0, utils_2.parseTokenSymbol)(reserve.config.tokenInfo.name)} reserve`); } const kaminoReserve = reserve_1.KaminoReserve.initialize(reserveAccounts[index], addresses[index], reserve, oracle, this.connection, this.recentSlotDurationMs); 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`); } getReserveByMint(address) { for (const reserve of this.reserves.values()) { if (reserve.getLiquidityMint().equals(address)) { return reserve; } } return undefined; } /** * Returns this market's reserve of the given mint address, or throws an error (including the given description) if * such reserve does not exist. */ getExistingReserveByMint(address, description = 'Requested') { return (0, validations_1.checkDefined)(this.getReserveByMint(address), `${description} reserve with mint ${address} not found`); } getReserveBySymbol(symbol) { for (const reserve of this.reserves.values()) { if (reserve.symbol === symbol) { return reserve; } } return undefined; } /** * Returns this market's reserve of the given symbol, or throws an error (including the given description) if * such reserve does not exist. */ getExistingReserveBySymbol(symbol, description = 'Requested') { return (0, validations_1.checkDefined)(this.getReserveBySymbol(symbol), `${description} reserve with symbol ${symbol} not found`); } getReserveMintBySymbol(symbol) { return this.getReserveBySymbol(symbol)?.getLiquidityMint(); } async getReserveFarmInfo(mint, 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.getReserveByMint(mint); if (!kaminoReserve) { throw Error(`Could not find reserve. ${mint}`); } 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: web3_js_1.PublicKey.default, 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: web3_js_1.PublicKey.default, 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 farms_sdk_1.FarmState.fetch(this.connection, farmAddress); if (!farmState) { throw Error(`Could not parse farm state. ${farmAddress}`); } const { token, rewardsAvailable, rewardScheduleCurve } = farmState.rewardInfos[0]; // TODO: marius fix const rewardPerSecondLamports = rewardScheduleCurve.points[0].rewardPerTimeUnit.toNumber(); const { mint, decimals: rewardDecimals } = token; const rewardPriceUsd = await getRewardPrice(mint); const rewardApr = this.calculateRewardAPR(rewardPerSecondLamports, rewardPriceUsd, totalInvestmentUsd, rewardDecimals.toNumber()); return { rewardsPerSecond: new decimal_js_1.default(rewardPerSecondLamports).dividedBy(10 ** rewardDecimals.toNumber()), rewardsRemaining: new decimal_js_1.default(rewardsAvailable.toNumber()).dividedBy(10 ** rewardDecimals.toNumber()), rewardApr: rewardsAvailable.toNumber() > 0 ? 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(tag) { const filters = [ { dataSize: accounts_1.Obligation.layout.span + 8, }, { memcmp: { offset: 32, bytes: this.address, }, }, ]; if (tag !== undefined) { filters.push({ memcmp: { offset: 8, bytes: bs58_1.default.encode(new anchor_1.BN(tag).toBuffer()), }, }); } const collateralExchangeRates = new utils_1.PubkeyHashMap(); const cumulativeBorrowRates = new utils_1.PubkeyHashMap(); const [slot, obligations] = await Promise.all([ this.connection.getSlot(), (0, utils_3.getProgramAccounts)(this.connection, this.programId, zero_padding_1.ObligationZP.layout.span + 8, { commitment: this.connection.commitment ?? 'processed', filters, dataSlice: { offset: 0, length: zero_padding_1.ObligationZP.layout.span + 8 }, // truncate the padding }), ]); return obligations.map((obligation) => { if (obligation.account === null) { throw new Error('Invalid account'); } const obligationAccount = zero_padding_1.ObligationZP.decode(obligation.account.data); if (!obligationAccount) { throw Error('Could not parse obligation.'); } obligation_1.KaminoObligation.addRatesForObligation(this, obligationAccount, collateralExchangeRates, cumulativeBorrowRates, slot); return new obligation_1.KaminoObligation(this, obligation.pubkey, 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(tag) { const filters = [ { dataSize: accounts_1.Obligation.layout.span + 8, }, { memcmp: { offset: 32, bytes: this.address, }, }, ]; if (tag !== undefined) { filters.push({ memcmp: { offset: 8, bytes: bs58_1.default.encode(new anchor_1.BN(tag).toBuffer()), }, }); } const collateralExchangeRates = new utils_1.PubkeyHashMap(); const cumulativeBorrowRates = new utils_1.PubkeyHashMap(); const [obligationPubkeys, slot] = await Promise.all([ this.connection.getProgramAccounts(this.programId, { filters, dataSlice: { offset: 0, length: 0 }, }), this.connection.getSlot(), ]); for (const batch of (0, kliquidity_sdk_1.chunks)(obligationPubkeys.map((x) => x.pubkey), 100)) { const obligationAccounts = await this.connection.getMultipleAccountsInfo(batch); const obligationsBatch = []; for (let i = 0; i < obligationAccounts.length; i++) { const obligation = obligationAccounts[i]; const pubkey = batch[i]; if (obligation === null) { continue; } const obligationAccount = accounts_1.Obligation.decode(obligation.data); if (!obligationAccount) { throw Error(`Could not decode obligation ${pubkey.toString()}`); } obligation_1.KaminoObligation.addRatesForObligation(this, obligationAccount, collateralExchangeRates, cumulativeBorrowRates, slot); obligationsBatch.push(new obligation_1.KaminoObligation(this, pubkey, obligationAccount, collateralExchangeRates, cumulativeBorrowRates)); } yield obligationsBatch; } } async getAllObligationsByTag(tag, market) { const [slot, obligations] = await Promise.all([ this.connection.getSlot(), this.connection.getProgramAccounts(this.programId, { filters: [ { dataSize: accounts_1.Obligation.layout.span + 8, }, { memcmp: { offset: 8, bytes: bs58_1.default.encode(new anchor_1.BN(tag).toBuffer()), }, }, { memcmp: { offset: 32, bytes: market.toBase58(), }, }, ], }), ]); const collateralExchangeRates = new utils_1.PubkeyHashMap(); const cumulativeBorrowRates = new utils_1.PubkeyHashMap(); return obligations.map((obligation) => { if (obligation.account === null) { throw new Error('Invalid account'); } if (!obligation.account.owner.equals(this.programId)) { throw new Error("account doesn't belong to this program"); } const obligationAccount = accounts_1.Obligation.decode(obligation.account.data); if (!obligationAccount) { throw Error('Could not parse obligation.'); } obligation_1.KaminoObligation.addRatesForObligation(this, obligationAccount, collateralExchangeRates, cumulativeBorrowRates, slot); return new obligation_1.KaminoObligation(this, obligation.pubkey, obligationAccount, collateralExchangeRates, cumulativeBorrowRates); }); } async getAllObligationsByDepositedReserve(reserve) { const finalObligations = []; for (let i = 0; i < utils_1.DEPOSITS_LIMIT; i++) { const [slot, obligations] = await Promise.all([ this.connection.getSlot(), this.connection.getProgramAccounts(this.programId, { filters: [ { dataSize: accounts_1.Obligation.layout.span + 8, }, { memcmp: { offset: 96 + 136 * i, bytes: reserve.toBase58(), }, }, { memcmp: { offset: 32, bytes: this.address, }, }, ], }), ]); const collateralExchangeRates = new utils_1.PubkeyHashMap(); const cumulativeBorrowRates = new utils_1.PubkeyHashMap(); const obligationsBatch = obligations.map((obligation) => { if (obligation.account === null) { throw new Error('Invalid account'); } if (!obligation.account.owner.equals(this.programId)) { throw new Error("account doesn't belong to this program"); } const obligationAccount = accounts_1.Obligation.decode(obligation.account.data); if (!obligationAccount) { throw Error('Could not parse obligation.'); } obligation_1.KaminoObligation.addRatesForObligation(this, obligationAccount, collateralExchangeRates, cumulativeBorrowRates, slot); return new obligation_1.KaminoObligation(this, obligation.pubkey, obligationAccount, collateralExchangeRates, cumulativeBorrowRates); }); finalObligations.push(...obligationsBatch); } return finalObligations; } async getAllUserObligations(user, commitment = this.connection.commitment) { const [currentSlot, obligations] = await Promise.all([ this.connection.getSlot(), this.connection.getProgramAccounts(this.programId, { filters: [ { dataSize: accounts_1.Obligation.layout.span + 8, }, { memcmp: { offset: 0, bytes: bs58_2.default.encode(accounts_1.Obligation.discriminator), }, }, { memcmp: { offset: 64, bytes: user.toBase58(), }, }, { memcmp: { offset: 32, bytes: this.address, }, }, ], commitment, }), ]); const collateralExchangeRates = new utils_1.PubkeyHashMap(); const cumulativeBorrowRates = new utils_1.PubkeyHashMap(); return obligations.map((obligation) => { if (obligation.account === null) { throw new Error('Invalid account'); } if (!obligation.account.owner.equals(this.programId)) { throw new Error("account doesn't belong to this program"); } const obligationAccount = accounts_1.Obligation.decode(obligation.account.data); if (!obligationAccount) { throw Error('Could not parse obligation.'); } obligation_1.KaminoObligation.addRatesForObligation(this, obligationAccount, collateralExchangeRates, cumulativeBorrowRates, currentSlot); return new obligation_1.KaminoObligation(this, obligation.pubkey, obligationAccount, collateralExchangeRates, cumulativeBorrowRates); }); } async getAllUserObligationsForReserve(user, reserve) { const obligationAddresses = []; obligationAddresses.push(new utils_1.VanillaObligation(this.programId).toPda(this.getAddress(), user)); const targetReserve = new utils_1.PubkeyHashMap(Array.from(this.reserves.entries())).get(reserve); if (!targetReserve) { throw Error('Could not find reserve.'); } for (const [key, kaminoReserve] of this.reserves) { if (targetReserve.address.equals(key)) { // skip target reserve continue; } obligationAddresses.push(new utils_1.MultiplyObligation(targetReserve.getLiquidityMint(), kaminoReserve.getLiquidityMint(), this.programId).toPda(this.getAddress(), user)); obligationAddresses.push(new utils_1.MultiplyObligation(kaminoReserve.getLiquidityMint(), targetReserve.getLiquidityMint(), this.programId).toPda(this.getAddress(), user)); obligationAddresses.push(new utils_1.LeverageObligation(targetReserve.getLiquidityMint(), kaminoReserve.getLiquidityMint(), this.programId).toPda(this.getAddress(), user)); obligationAddresses.push(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)); obligations.forEach((obligation) => { if (obligation !== null) { for (const deposits of obligation.deposits.keys()) { if (deposits.equals(reserve)) { finalObligations.push(obligation); } } for (const borrows of obligation.borrows.keys()) { if (borrows.equals(reserve)) { finalObligations.push(obligation); } } } }); } return finalObligations; } async getUserVanillaObligation(user) { const vanillaObligationAddress = 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.'); } return obligation; } isReserveInObligation(obligation, reserve) { for (const deposits of obligation.deposits.keys()) { if (deposits.equals(reserve)) { return true; } } for (const borrows of obligation.borrows.keys()) { if (borrows.equals(reserve)) { return true; } } return false; } async getUserObligationsByTag(tag, user) { const [currentSlot, obligations] = await Promise.all([ this.connection.getSlot(), this.connection.getProgramAccounts(this.programId, { filters: [ { dataSize: accounts_1.Obligation.layout.span + 8, }, { memcmp: { offset: 8, bytes: bs58_1.default.encode(new anchor_1.BN(tag).toBuffer()), }, }, { memcmp: { offset: 32, bytes: this.address, }, }, { memcmp: { offset: 64, bytes: user.toBase58(), }, }, ], }), ]); const collateralExchangeRates = new utils_1.PubkeyHashMap(); const cumulativeBorrowRates = new utils_1.PubkeyHashMap(); return obligations.map((obligation) => { if (obligation.account === null) { throw new Error('Invalid account'); } if (!obligation.account.owner.equals(this.programId)) { throw new Error("account doesn't belong to this program"); } const obligationAccount = accounts_1.Obligation.decode(obligation.account.data); if (!obligationAccount) { throw Error('Could not parse obligation.'); } obligation_1.KaminoObligation.addRatesForObligation(this, obligationAccount, 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) { return obligation_1.KaminoObligation.loadAll(this, addresses); } /** * 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] = (0, utils_1.userMetadataPda)(user, this.programId); const userMetadata = await accounts_1.UserMetadata.fetch(this.connection, address, this.programId); return [address, userMetadata]; } async getReferrerTokenStateForReserve(referrer, reserve) { const [address, _bump] = (0, utils_1.referrerTokenStatePda)(referrer, reserve, this.programId); const referrerTokenState = await accounts_1.ReferrerTokenState.fetch(this.connection, address, this.programId); return [address, referrerTokenState]; } async getAllReferrerTokenStates(referrer) { const referrerTokenStates = await this.connection.getProgramAccounts(this.programId, { filters: [ { dataSize: accounts_1.ReferrerTokenState.layout.span + 8, }, { memcmp: { offset: 8, bytes: referrer.toBase58(), }, }, ], }); const referrerTokenStatesForMints = new utils_1.PubkeyHashMap(); referrerTokenStates.forEach((referrerTokenState) => { if (referrerTokenState.account === null) { throw new Error('Invalid account'); } if (!referrerTokenState.account.owner.equals(this.programId)) { throw new Error("account doesn't belong to this program"); } const referrerTokenStateDecoded = accounts_1.ReferrerTokenState.decode(referrerTokenState.account.data); 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 utils_1.PubkeyHashMap(); 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 utils_1.PubkeyHashMap(); for (const mint of referrerTokenStatesForMints.keys()) { referrerFeesCumulativeForMints.set(mint, new fraction_1.Fraction(referrerTokenStatesForMints.get(mint).amountUnclaimedSf).toDecimal()); } return referrerFeesCumulativeForMints; } async getReferrerUrl(baseUrl, referrer) { return baseUrl + this.encodeReferrer(referrer); } async getReferrerFromUrl(baseUrl, url) { return this.decodeReferrer(url.split(baseUrl)[1]); } async encodeReferrer(referrer) { return bs58_2.default.encode(referrer.toBuffer()); } async decodeReferrer(encoded_referrer) { const referrer_buffer = bs58_2.default.decode(encoded_referrer); return new web3_js_1.PublicKey(referrer_buffer.toString()); } /** * Get the underlying connection passed when instantiating this market * @return connection */ getConnection() { return this.connection; } /** * Get all Scope prices used by all the market reserves */ async getAllScopePrices(scope, oraclePrices) { if (!oraclePrices) { oraclePrices = await scope.getOraclePrices(); } 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); if (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 (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() { const klendPrices = { scope: { spot: {}, twap: {} }, pyth: { spot: {}, twap: {} }, switchboard: { spot: {}, twap: {} }, }; const allOracleAccounts = await (0, utils_1.getAllOracleAccounts)(this.connection, this.getReserves().map((x) => x.state)); const pythCache = new utils_1.PubkeyHashMap(); const switchboardCache = new utils_1.PubkeyHashMap(); const scopeCache = new utils_1.PubkeyHashMap(); const switchboardV2 = await sbv2_lite_1.default.loadMainnet(this.connection); 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, switchboardV2), twap: (0, utils_1.isNotNullPubkey)(switchboardTwapOracle) ? (0, utils_1.cacheOrGetSwitchboardPrice)(switchboardTwapOracle, switchboardCache, allOracleAccounts, switchboardV2) : null, }; this.setPriceIfExist(klendPrices.switchboard, switchboardPrices.spot, switchboardPrices.twap, tokenMint, tokenName); } } return klendPrices; } getCumulativeBorrowRatesByReserve(slot) { const cumulativeBorrowRates = new utils_1.PubkeyHashMap(); for (const reserve of this.reserves.values()) { cumulativeBorrowRates.set(reserve.address, reserve.getEstimatedCumulativeBorrowRate(slot, this.state.referralFeeBps)); } return cumulativeBorrowRates; } getCollateralExchangeRatesByReserve(slot) { const collateralExchangeRates = new utils_1.PubkeyHashMap(); 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 utils_1.PublicKeySet([]), collateralLiquidityMints: new utils_1.PublicKeySet([]), debtReserve: elevationGroup.debtReserve, debtLiquidityMint: web3_js_1.PublicKey.default, 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.equals(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.contains(mint)) && (debtLiquidityMint == undefined || debtLiquidityMint.equals(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.contains(mint)) && (debtReserve == undefined || debtReserve.equals(elevationGroupDescription.debtReserve))); }); } } exports.KaminoMarket = KaminoMarket; async function getReservesForMarket(marketAddress, connection, programId, recentSlotDurationMs) { const reserves = await connection.getProgramAccounts(programId, { filters: [ { dataSize: accounts_1.Reserve.layout.span + 8, }, { memcmp: { offset: 32, bytes: marketAddress.toBase58(), }, }, ], }); const deserializedReserves = reserves.map((reserve) => { if (reserve.account === null) { throw new Error(`Reserve account ${reserve.pubkey.toBase58()} does not exist`); } const reserveAccount = accounts_1.Reserve.decode(reserve.account.data); if (!reserveAccount) { throw Error(`Could not parse reserve ${reserve.pubkey.toBase58()}`); } return reserveAccount; }); const allBuffers = reserves.map((reserve) => reserve.account); const reservesAndOracles = await (0, utils_1.getTokenOracleData)(connection, deserializedReserves); const reservesByAddress = new utils_1.PubkeyHashMap(); reservesAndOracles.forEach(([reserve, oracle], index) => { if (!oracle) { throw Error(`Could not find oracle for ${(0, utils_2.parseTokenSymbol)(reserve.config.tokenInfo.name)} reserve`); } const kaminoReserve = reserve_1.KaminoReserve.initialize(allBuffers[index], reserves[index].pubkey, reserve, oracle, connection, recentSlotDurationMs); reservesByAddress.set(kaminoReserve.address, kaminoReserve); }); return reservesByAddress; } async function getSingleReserve(reservePk, connection, recentSlotDurationMs, accountData) { const reserve = accountData ? accountData : await connection.getAccountInfo(reservePk); if (r