@kamino-finance/klend-sdk
Version:
Typescript SDK for interacting with the Kamino Lending (klend) protocol
239 lines (199 loc) • 6.83 kB
text/typescript
import { SLOTS_PER_SECOND, SLOTS_PER_YEAR } from '../utils';
import Decimal from 'decimal.js';
import { AccountInfo, PublicKey } from '@solana/web3.js';
import { SOL_MINTS } from '../lib';
import { AccountLayout } from '@solana/spl-token';
type ObligationFarmScoreType = {
obligationId: string;
balance: string;
debt: string;
score: string;
lastSlot: number;
tokenMint: string;
side: 'supply' | 'borrow';
};
type RewardRate = {
beginningSlot: number;
rewardRate: string;
name?: string;
};
function getLatestRewardRate(
rewardRates: Array<{
beginningSlot: number;
rewardRate: string;
name?: string;
}>,
slot: number
) {
return rewardRates
.filter((rr) => slot >= rr.beginningSlot)
.reduce((v1, v2) => (v1.beginningSlot > v2.beginningSlot ? v1 : v2), {
beginningSlot: 0,
rewardRate: '0',
});
}
export const calculateNewScore = (
rewardStat: {
lastSlot: number;
rewardRates: Array<RewardRate>;
rewardsPerShare: string;
totalBalance: string;
},
pool: ObligationFarmScoreType,
rewardRate: string,
endSlot: number,
startSlot: number
) => {
const { balance, debt, score } = pool;
const { rewardsPerShare, totalBalance } = rewardStat;
const oldDebt = new Decimal(debt);
const oldScore = new Decimal(score);
const oldRewardsPerShare = new Decimal(rewardsPerShare);
const oldBalance = new Decimal(balance);
const totalBalanceVal = new Decimal(totalBalance);
const newRewardsPerShare = !totalBalanceVal.isZero()
? oldRewardsPerShare.plus(
new Decimal(endSlot)
.minus(new Decimal(startSlot.toString()))
.times(new Decimal(rewardRate))
.div(totalBalanceVal)
.div(new Decimal(SLOTS_PER_YEAR))
)
: new Decimal(0);
return oldScore.plus(newRewardsPerShare.times(oldBalance).minus(oldDebt));
};
export const estimateCurrentScore = (
rewardStat: {
lastSlot: number;
rewardRates: Array<RewardRate>;
rewardsPerShare: string;
totalBalance: string;
},
rewardScore: ObligationFarmScoreType,
mostRecentSlot: number,
mostRecentSlotTime: number
) => {
const { lastSlot, rewardRates } = rewardStat;
const estimatedCurrentSlot = mostRecentSlot + SLOTS_PER_SECOND * (Date.now() / 1000 - mostRecentSlotTime);
const { rewardRate } = getLatestRewardRate(rewardRates, estimatedCurrentSlot);
const currentScore = calculateNewScore(rewardStat, rewardScore, rewardRate, estimatedCurrentSlot, lastSlot);
return currentScore;
};
export const interpolate = (x: number, x0: number, x1: number, y0: number, y1: number) => {
if (x > x1) {
throw 'Cannot do extrapolation';
}
return y0 + ((x - x0) * (y1 - y0)) / (x1 - x0);
};
export const getBorrowRate = (currentUtilization: number, curve: [number, number][]): number => {
let [x0, y0, x1, y1] = [0, 0, 0, 0];
if (curve.length < 2) {
throw 'Invalid borrow rate curve, only one point';
}
if (currentUtilization > 1) {
currentUtilization = 1;
}
for (let i = 1; i < curve.length; i++) {
const [pointUtilization, pointRate] = curve[i];
if (pointUtilization === currentUtilization) {
return pointRate;
}
if (currentUtilization <= pointUtilization) {
x0 = curve[i - 1][0];
y0 = curve[i - 1][1];
x1 = curve[i][0];
y1 = curve[i][1];
break;
}
}
if (x0 === 0 && y0 === 0 && x1 === 0 && y1 === 0) {
console.log('All are 0');
throw 'Invalid borrow rate curve, could not identify the interpolation points.';
}
if (x0 >= x1 || y0 > y1) {
console.log('(x0, y0), (x1, y1)', x0, y0, x1, y1);
throw 'Invalid borrow rate curve, curve is not uniformly increasing';
}
return interpolate(currentUtilization, x0, x1, y0, y1);
};
export const parseTokenSymbol = (tokenSymbol: number[]): string => {
return String.fromCharCode(...tokenSymbol.filter((x) => x > 0));
};
export function parseZeroPaddedUtf8(utf8Array: number[]): string {
for (let last = utf8Array.length - 1; last >= 0; last--) {
const trailing_zero = utf8Array[last];
if (trailing_zero != 0) {
const encoding = new Uint8Array(last + 1);
for (let i = 0; i <= last; i++) {
encoding[i] = utf8Array[i];
}
break;
}
}
return new TextDecoder().decode();
}
export function renderZeroPaddedUtf8(str: string, utf8ArrayLength: number): number[] {
const encoding = new TextEncoder().encode(str);
const result = new Array<number>(utf8ArrayLength);
for (let i = 0; i < result.length; i++) {
result[i] = i < encoding.length ? encoding[i] : 0;
}
return result;
}
export function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
export function numberToLamportsDecimal(amount: Decimal.Value, decimals: number): Decimal {
const factor = 10 ** decimals;
return new Decimal(amount).mul(factor);
}
export function lamportsToNumberDecimal(amount: Decimal.Value, decimals: number): Decimal {
const factor = 10 ** decimals;
return new Decimal(amount).div(factor);
}
export function lamportsToDecimal(amount: Decimal.Value, decimals: Decimal.Value): Decimal {
const factor = new Decimal(10).pow(decimals);
return new Decimal(amount).div(factor);
}
export const isSolMint = (mint: PublicKey): boolean => {
return SOL_MINTS.filter((m) => m.equals(mint)).length > 0;
};
export const valueOrZero = (value: Decimal): Decimal => {
const zero = new Decimal(0);
if (value.isNaN() || !value.isFinite()) {
return zero;
} else {
return value;
}
};
export const isEmptyObject = (obj: any) => {
return Object.keys(obj).length === 0 && obj.constructor === Object;
};
export const positiveOrZero = (value: Decimal): Decimal => {
const zero = new Decimal(0);
return Decimal.max(value, zero);
};
export function calculateAPYFromAPR(apr: number) {
const apy = new Decimal(1).plus(new Decimal(apr).dividedBy(SLOTS_PER_YEAR)).toNumber() ** SLOTS_PER_YEAR - 1;
return apy;
}
export function calculateAPRFromAPY(apy: Decimal.Value) {
return new Decimal(apy)
.plus(1)
.pow(1 / SLOTS_PER_YEAR)
.minus(1)
.times(SLOTS_PER_YEAR);
}
export function sameLengthArrayEquals(left: Array<number>, right: Array<number>): boolean {
if (left.length != right.length) {
throw new Error(`Not same length: ${left.length} != ${left.length}`);
}
return left.every((value, index) => value === right[index]);
}
export function getTokenBalanceFromAccountInfoLamports(accountInfo: AccountInfo<Buffer>): Decimal {
// Decode the buffer using the AccountLayout from @solana/spl-token
const tokenAccountData = AccountLayout.decode(accountInfo.data);
// Extract the balance from the `amount` field, which is a 64-bit unsigned integer
const balance = tokenAccountData.amount;
return new Decimal(balance.toString());
}