@kamino-finance/klend-sdk
Version:
Typescript SDK for interacting with the Kamino Lending (klend) protocol
1,012 lines • 60.2 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.KaminoMarket = void 0;
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 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 zero_padding_1 = require("../@codegen/klend/zero_padding");
const validations_1 = require("../utils/validations");
const buffer_1 = require("buffer");
const base58Decoder = (0, kit_1.getBase58Decoder)();
class KaminoMarket {
rpc;
address;
state;
reserves;
reservesActive;
programId;
recentSlotDurationMs;
// scope feeds used by all market reserves
scopeFeeds;
constructor(rpc, state, marketAddress, reserves, recentSlotDurationMs, programId = programId_1.PROGRAM_ID) {
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.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));
}
/**
* 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) {
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);
}
static loadWithReserves(connection, market, reserves, marketAddress, recentSlotDurationMs, programId = programId_1.PROGRAM_ID) {
return new KaminoMarket(connection, market, marketAddress, reserves, recentSlotDurationMs, programId);
}
static async loadMultiple(connection, markets, recentSlotDurationMs, programId = programId_1.PROGRAM_ID, withReserves = true, oracleAccounts) {
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 = withReserves
? await getReservesForMarket(marketAddress, connection, programId, recentSlotDurationMs, oracleAccounts)
: new Map();
kaminoMarkets.set(marketAddress, new KaminoMarket(connection, market, marketAddress, marketReserves, recentSlotDurationMs, programId));
}
return kaminoMarkets;
}
static async loadMultipleWithReserves(connection, markets, reserves, recentSlotDurationMs, programId = programId_1.PROGRAM_ID) {
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));
}
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);
}
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 bn_js_1.default(0)) &&
this.state.elevationGroups[item - 1].debtReserve === 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(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.getLiquidityMint(), slot, requestElevationGroup)
: debtReserve.getMaxBorrowAmountWithCollReserve(this, collReserve, slot);
}
async loadReserves(oracleAccounts) {
const addresses = [...this.reserves.keys()];
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 = await (0, utils_1.getTokenOracleData)(this.getRpc(), deserializedReserves, oracleAccounts);
const kaminoReserves = new Map();
reservesAndOracles.forEach(([reserve, oracle], index) => {
if (!oracle) {
throw Error(`Could not find oracle for ${(0, utils_2.parseTokenSymbol)(reserve.config.tokenInfo.name)} (${addresses[index]}) reserve in market ${reserve.lendingMarket}`);
}
const kaminoReserve = reserve_1.KaminoReserve.initialize(addresses[index], reserve, oracle, this.rpc, 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() === 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: 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 farms_sdk_1.FarmState.fetch(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 = 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: 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 [slot, obligations] = await Promise.all([
this.rpc.getSlot().send(),
(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, 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(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, slot] = await Promise.all([
this.rpc
.getProgramAccounts(this.programId, {
filters,
encoding: 'base64',
dataSlice: { offset: 0, length: 0 },
})
.send(),
this.rpc.getSlot().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, 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.rpc.getSlot().send(),
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, 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) {
const finalObligations = [];
for (let i = 0; i < utils_1.DEPOSITS_LIMIT; i++) {
const [slot, obligations] = await Promise.all([
this.rpc.getSlot().send(),
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, 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) {
const finalObligations = [];
for (let i = 0; i < utils_1.BORROWS_LIMIT; i++) {
const [slot, obligations] = await Promise.all([
this.rpc.getSlot().send(),
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, collateralExchangeRates, cumulativeBorrowRates, slot);
return new obligation_1.KaminoObligation(this, obligation.pubkey, obligationAccount, collateralExchangeRates, cumulativeBorrowRates);
});
finalObligations.push(...obligationsBatch);
}
return finalObligations;
}
async getAllUserObligations(user, commitment = 'processed', slot) {
const [currentSlot, obligations] = await Promise.all([
slot !== undefined ? Promise.resolve(slot) : this.rpc.getSlot().send(),
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, collateralExchangeRates, cumulativeBorrowRates, currentSlot);
return new obligation_1.KaminoObligation(this, obligation.pubkey, obligationAccount, collateralExchangeRates, cumulativeBorrowRates);
});
}
async getAllUserObligationsForReserve(user, reserve) {
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));
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) {
const [currentSlot, obligations] = await Promise.all([
this.rpc.getSlot().send(),
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, 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] = 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.twap