@kamino-finance/klend-sdk
Version:
Typescript SDK for interacting with the Kamino Lending (klend) protocol
400 lines • 20 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.StakeAccount = exports.SmallStakeAccountInfo = exports.StakeInfo = exports.StakeMeta = exports.STAKE_POOL_PROGRAM_ID = exports.STAKE_ACCOUNT_RENT_EXEMPTION = exports.TRANSIENT_STAKE_SEED_PREFIX = void 0;
exports.getStandardPoolState = getStandardPoolState;
exports.getValidatorList = getValidatorList;
exports.maybeGetStakedPoolByMint = maybeGetStakedPoolByMint;
exports.getStandardPoolMintRemainingAccounts = getStandardPoolMintRemainingAccounts;
exports.getWithdrawCandidates = getWithdrawCandidates;
exports.findWithdrawAuthorityProgramAddress = findWithdrawAuthorityProgramAddress;
exports.findStakeProgramAddress = findStakeProgramAddress;
exports.findTransientStakeProgramAddress = findTransientStakeProgramAddress;
exports.calcLamportsWithdrawAmount = calcLamportsWithdrawAmount;
const spl_stake_pool_1 = require("@solana/spl-stake-pool");
const spl_stake_pool_2 = require("@solana/spl-stake-pool");
const unstakingPool_1 = require("./unstakingPool");
const borsh = __importStar(require("@coral-xyz/borsh")); // eslint-disable-line @typescript-eslint/no-unused-vars
const bn_js_1 = __importDefault(require("bn.js"));
const assert_1 = __importDefault(require("assert"));
const kit_1 = require("@solana/kit");
const compat_1 = require("@solana/compat");
const utils_1 = require("../utils");
const utils_2 = require("@kamino-finance/farms-sdk/dist/utils");
const utils_3 = require("../@codegen/unstaking_pool/utils");
exports.TRANSIENT_STAKE_SEED_PREFIX = Buffer.from('transient');
exports.STAKE_ACCOUNT_RENT_EXEMPTION = new bn_js_1.default(2_282_880);
exports.STAKE_POOL_PROGRAM_ID = (0, compat_1.fromLegacyPublicKey)(spl_stake_pool_1.STAKE_POOL_PROGRAM_ID);
const MINIMUM_ACTIVE_STAKE = new bn_js_1.default(1_000_000);
// This represents the minimum each validator stake account must have and cannot be withdrawn
const TRANSIENT_STAKE_ACCOUNT_RENT_EXEMPTION = exports.STAKE_ACCOUNT_RENT_EXEMPTION.add(MINIMUM_ACTIVE_STAKE);
async function getStandardPoolState(rpc, address) {
const accountInfo = await (0, kit_1.fetchEncodedAccount)(rpc, address);
if (!accountInfo.exists) {
throw new Error(`Cannot fetch standard stake pool account ${address.toString()}`);
}
return spl_stake_pool_2.StakePoolLayout.decode(Buffer.from(accountInfo.data));
}
async function getValidatorList(rpc, address) {
const accountInfo = await (0, kit_1.fetchEncodedAccount)(rpc, address);
if (!accountInfo.exists) {
throw new Error(`Cannot fetch standard stake pool account ${address.toString()}`);
}
return spl_stake_pool_1.ValidatorListLayout.decode(Buffer.from(accountInfo.data));
}
async function maybeGetStakedPoolByMint(rpc, mint) {
const results = await (0, utils_1.getProgramAccounts)(rpc, exports.STAKE_POOL_PROGRAM_ID, unstakingPool_1.STAKE_POOL_SIZE, [
{ memcmp: { offset: 162n, bytes: mint.toString(), encoding: 'base58' } },
]);
// There should be only 1 stake pool for mint
if (results.length === 0) {
return undefined;
}
if (results.length === 1) {
return [spl_stake_pool_2.StakePoolLayout.decode(results[0].data), results[0].address];
}
// This should not happen
throw new Error(`Got ${results.length} stake pools for mint ${mint.toString()} and not sure which one is correct.`);
}
async function getStandardPoolMintRemainingAccounts(rpc, stakedSolPool, stakedSolPoolPk, stakedSolToDeposit) {
const withdrawAuthority = await findWithdrawAuthorityProgramAddress(exports.STAKE_POOL_PROGRAM_ID, stakedSolPoolPk);
const remainingAccounts = [
{ address: stakedSolPoolPk, role: 1 },
{ address: (0, compat_1.fromLegacyPublicKey)(stakedSolPool.validatorList), role: 1 },
{ address: withdrawAuthority, role: 1 },
{ address: (0, compat_1.fromLegacyPublicKey)(stakedSolPool.managerFeeAccount), role: 1 },
{ address: unstakingPool_1.CLOCK_PROGRAM_ID, role: 0 },
{ address: unstakingPool_1.STAKE_PROGRAM_ID, role: 0 },
{ address: exports.STAKE_POOL_PROGRAM_ID, role: 0 },
];
const withdrawCandidates = await getWithdrawCandidates(rpc, stakedSolPool, stakedSolPoolPk, stakedSolToDeposit);
// Each withdraw candidate should also create a new keypair for the stake account
const withdrawCandidatesTo = [];
for (const withdrawCandidateFrom of withdrawCandidates) {
remainingAccounts.push({ address: withdrawCandidateFrom, role: 1 });
const withdrawCandidateTo = await (0, kit_1.generateKeyPairSigner)();
remainingAccounts.push({ address: withdrawCandidateTo.address, signer: withdrawCandidateTo, role: 3 });
withdrawCandidatesTo.push(withdrawCandidateTo);
}
return [remainingAccounts, withdrawCandidatesTo];
}
async function getAllWithdrawCandidatesSorted(rpc, stakedSolPool, stakedSolPoolPk) {
const reserveStake = (0, compat_1.fromLegacyPublicKey)(stakedSolPool.reserveStake);
const activeValidators = [];
const transientValidators = [];
const validatorList = await getValidatorList(rpc, (0, compat_1.fromLegacyPublicKey)(stakedSolPool.validatorList));
const accountsToFetch = [];
// Add all accounts to be fetched to an array so that we can use getMultipleAccounts
for (const validator of validatorList.validators) {
const stakeAccount = await findStakeProgramAddress(exports.STAKE_POOL_PROGRAM_ID, (0, compat_1.fromLegacyPublicKey)(validator.voteAccountAddress), stakedSolPoolPk, validator.transientSeedSuffixStart.toNumber());
const transientAccount = await findTransientStakeProgramAddress(exports.STAKE_POOL_PROGRAM_ID, (0, compat_1.fromLegacyPublicKey)(validator.voteAccountAddress), stakedSolPoolPk, validator.transientSeedSuffixEnd);
accountsToFetch.push(stakeAccount);
accountsToFetch.push(transientAccount);
}
let accountsBalances = [];
// TODO: if this is still too slow we can also start all getMultipleAccounts in parallel and do Promise.all
for (let i = 0; i < accountsToFetch.length; i += 100) {
const accountInfos = await (0, kit_1.fetchEncodedAccounts)(rpc, accountsToFetch.slice(i, i + 100));
accountsBalances = accountsBalances.concat(accountInfos.map((accountInfo) => (accountInfo.exists ? new bn_js_1.default(accountInfo.lamports.toString()) : new bn_js_1.default(0))));
}
(0, assert_1.default)(accountsBalances.length === accountsToFetch.length);
let i = 0;
for (const validator of validatorList.validators) {
const isPreferred = stakedSolPool.preferredWithdrawValidatorVoteAddress
? validator.voteAccountAddress.equals(stakedSolPool.preferredWithdrawValidatorVoteAddress)
: false;
const stakeAccount = accountsToFetch[i];
const stakeAccountBalance = accountsBalances[i].sub(TRANSIENT_STAKE_ACCOUNT_RENT_EXEMPTION);
if (stakeAccountBalance.gt(new bn_js_1.default(0))) {
activeValidators.push({ isPreferred, balance: stakeAccountBalance, pk: stakeAccount });
}
const transientAccount = accountsToFetch[i + 1];
const transientAccountBalance = accountsBalances[i + 1].sub(TRANSIENT_STAKE_ACCOUNT_RENT_EXEMPTION);
if (transientAccountBalance.gt(new bn_js_1.default(0))) {
transientValidators.push({ isPreferred, balance: transientAccountBalance, pk: transientAccount });
}
i += 2;
}
// Sorting descending based on balance, but preferred validators should always be used first
const byPreferrenceAndBalance = (a, b) => {
// First, sort by isPreferred (preferred validators come first)
if (a.isPreferred !== b.isPreferred) {
return a.isPreferred ? -1 : 1;
}
// If both have the same preference status, sort by balance (descending)
return b.balance.cmp(a.balance);
};
activeValidators.sort(byPreferrenceAndBalance);
transientValidators.sort(byPreferrenceAndBalance);
const allCandidates = activeValidators.concat(transientValidators);
// Add reserve stake account at the end as that should be used only if no validators have enough stake
const reserveStakeBalance = new bn_js_1.default(await (0, utils_2.getSolBalanceInLamports)(rpc, reserveStake)).sub(exports.STAKE_ACCOUNT_RENT_EXEMPTION);
if (reserveStakeBalance.gt(new bn_js_1.default(0))) {
allCandidates.push({
isPreferred: false,
balance: reserveStakeBalance,
pk: reserveStake,
});
}
return allCandidates;
}
async function getWithdrawCandidates(rpc, stakedSolPool, stakedSolPoolPk, stakedSolToDeposit) {
const allCandidates = await getAllWithdrawCandidatesSorted(rpc, stakedSolPool, stakedSolPoolPk);
let stakedSolRemaining = stakedSolToDeposit;
let solToWithdraw = new bn_js_1.default(0);
const withdrawCandidates = [];
const reserveStake = (0, compat_1.fromLegacyPublicKey)(stakedSolPool.reserveStake);
// Try to withdraw all of the SOL from validators' active/transient accounts
for (const candidate of allCandidates) {
if (stakedSolRemaining.isZero()) {
break;
}
// See how much the validator balance is worth in staked sol
// but limit to amount of needed stake sol
let stakedSolAmountToWithdraw = bn_js_1.default.min(stakedSolRemaining, solToStakePoolTokensWithInverseFee(stakedSolPool, new bn_js_1.default(candidate.balance)));
// Convert it back to staked sol so we get the real amount
let actualSolAmount = calcLamportsWithdrawAmount(stakedSolPool, stakedSolAmountToWithdraw);
const remainingSolAmount = calcLamportsWithdrawAmount(stakedSolPool, stakedSolRemaining.sub(stakedSolAmountToWithdraw));
// If the current validator uses up all of the remaining staked sol except some minimum that we need
// in order to split_stake, then leave at least the minimum required to be consumed by another validator
if (!remainingSolAmount.isZero() && remainingSolAmount < new bn_js_1.default(MINIMUM_ACTIVE_STAKE)) {
stakedSolAmountToWithdraw = stakedSolAmountToWithdraw.sub(solToStakePoolTokensWithInverseFee(stakedSolPool, new bn_js_1.default(MINIMUM_ACTIVE_STAKE)));
actualSolAmount = calcLamportsWithdrawAmount(stakedSolPool, stakedSolAmountToWithdraw);
}
if (actualSolAmount < new bn_js_1.default(MINIMUM_ACTIVE_STAKE) && candidate.pk != reserveStake) {
// Skip if the amount to withdraw is less than the minimum required for a valid stake
continue;
}
// Update stake_pool so simulation stays true to what happens on chain
stakedSolRemaining = stakedSolRemaining.sub(stakedSolAmountToWithdraw);
solToWithdraw = solToWithdraw.add(actualSolAmount);
stakedSolPool.totalLamports = stakedSolPool.totalLamports.sub(actualSolAmount);
stakedSolPool.poolTokenSupply = stakedSolPool.poolTokenSupply.sub(stakePoolTokensMinusFee(stakedSolPool, stakedSolAmountToWithdraw));
withdrawCandidates.push(candidate.pk);
}
return withdrawCandidates;
}
function calcPoolTokensStakeWithdrawalFee(stakedSolPool, stakedSolAmountToWithdraw) {
const denominator = stakedSolPool.stakeWithdrawalFee.denominator;
if (denominator.isZero()) {
return new bn_js_1.default(0);
}
const numerator = stakedSolAmountToWithdraw.mul(stakedSolPool.stakeWithdrawalFee.numerator);
const poolTokens = numerator.add(denominator).sub(new bn_js_1.default(1)).div(denominator);
return poolTokens;
}
function stakePoolTokensMinusFee(stakedSolPool, stakedSolAmountToWithdraw) {
const stakedSolFee = calcPoolTokensStakeWithdrawalFee(stakedSolPool, stakedSolAmountToWithdraw);
return stakedSolAmountToWithdraw.sub(stakedSolFee);
}
function solToStakePoolTokensWithInverseFee(stakedSolPool, sol) {
let poolTokens = calcPoolTokensForDeposit(stakedSolPool, sol);
if (!stakedSolPool.stakeWithdrawalFee.numerator.isZero()) {
const numerator = poolTokens.mul(stakedSolPool.stakeWithdrawalFee.denominator);
const denominator = stakedSolPool.stakeWithdrawalFee.denominator.sub(stakedSolPool.stakeWithdrawalFee.numerator);
if (denominator.isZero()) {
// If the pool has 100% fee for some reason just fail it, we cannot compute the inverse
throw new Error('Pool fee cannot be 100%');
}
poolTokens = numerator.div(denominator);
}
return poolTokens;
}
// Below functions/types are not exported from spl-stake-pool
const addressEncoder = (0, kit_1.getAddressEncoder)();
/**
* Generates the withdraw authority program address for the stake pool
*/
async function findWithdrawAuthorityProgramAddress(programId, stakePoolAddress) {
const [publicKey] = await (0, kit_1.getProgramDerivedAddress)({
seeds: [addressEncoder.encode(stakePoolAddress), Buffer.from('withdraw')],
programAddress: programId,
});
return publicKey;
}
async function findStakeProgramAddress(programId, voteAccountAddress, stakedSolPoolPk, seed) {
const [publicKey] = await (0, kit_1.getProgramDerivedAddress)({
seeds: [
addressEncoder.encode(voteAccountAddress),
addressEncoder.encode(stakedSolPoolPk),
seed ? new bn_js_1.default(seed).toArrayLike(Buffer, 'le', 4) : Buffer.alloc(0),
],
programAddress: programId,
});
return publicKey;
}
async function findTransientStakeProgramAddress(programId, voteAccountAddress, stakePoolAddress, seed) {
const [publicKey] = await (0, kit_1.getProgramDerivedAddress)({
seeds: [
exports.TRANSIENT_STAKE_SEED_PREFIX,
addressEncoder.encode(voteAccountAddress),
addressEncoder.encode(stakePoolAddress),
seed.toArrayLike(Buffer, 'le', 8),
],
programAddress: programId,
});
return publicKey;
}
function calcPoolTokensForDeposit(stakePool, stakeLamports) {
if (stakePool.poolTokenSupply.isZero() || stakePool.totalLamports.isZero()) {
return stakeLamports;
}
const numerator = stakeLamports.mul(stakePool.poolTokenSupply);
return numerator.div(stakePool.totalLamports);
}
function calcLamportsWithdrawAmount(stakePool, poolTokens) {
const numerator = poolTokens.mul(stakePool.totalLamports);
const denominator = stakePool.poolTokenSupply;
if (numerator.lt(denominator)) {
return new bn_js_1.default(0);
}
return numerator.div(denominator);
}
class StakeMeta {
rentExemptReserve;
authorizedStaker;
authorizedWithdrawer;
lockupUnixTimestamp;
lockupEpoch;
lockupCustodian;
static layout(property) {
return borsh.struct([
borsh.u64('rentExemptReserve'),
(0, utils_3.borshAddress)('authorizedStaker'),
(0, utils_3.borshAddress)('authorizedWithdrawer'),
borsh.i64('lockupUnixTimestamp'),
borsh.u64('lockupEpoch'),
(0, utils_3.borshAddress)('lockupCustodian'),
], property);
}
constructor(fields) {
this.rentExemptReserve = fields.rentExemptReserve;
this.authorizedStaker = fields.authorizedStaker;
this.authorizedWithdrawer = fields.authorizedWithdrawer;
this.lockupUnixTimestamp = fields.lockupUnixTimestamp;
this.lockupEpoch = fields.lockupEpoch;
this.lockupCustodian = fields.lockupCustodian;
}
static decode(data) {
const dec = StakeMeta.layout().decode(data);
return new StakeMeta({
rentExemptReserve: dec.rentExemptReserve,
authorizedStaker: dec.authorizedStaker,
authorizedWithdrawer: dec.authorizedWithdrawer,
lockupUnixTimestamp: dec.lockupUnixTimestamp,
lockupEpoch: dec.lockupEpoch,
lockupCustodian: dec.lockupCustodian,
});
}
}
exports.StakeMeta = StakeMeta;
class StakeInfo {
delegationVoter;
delegationStake;
delegationActivationEpoch;
delegationDeactivationEpoch;
delegationWarmupCooldownRate;
creditsObserved;
static layout(property) {
return borsh.struct([
(0, utils_3.borshAddress)('delegationVoter'),
borsh.u64('delegationStake'),
borsh.u64('delegationActivationEpoch'),
borsh.u64('delegationDeactivationEpoch'),
borsh.f64('delegationWarmupCooldownRate'),
borsh.u64('creditsObserved'),
], property);
}
constructor(fields) {
this.delegationVoter = fields.delegationVoter;
this.delegationStake = fields.delegationStake;
this.delegationActivationEpoch = fields.delegationActivationEpoch;
this.delegationDeactivationEpoch = fields.delegationDeactivationEpoch;
this.delegationWarmupCooldownRate = fields.delegationWarmupCooldownRate;
this.creditsObserved = fields.creditsObserved;
}
static decode(data) {
const dec = StakeInfo.layout().decode(data);
return new StakeInfo({
delegationVoter: dec.delegationVoter,
delegationStake: dec.delegationStake,
delegationActivationEpoch: dec.delegationActivationEpoch,
delegationDeactivationEpoch: dec.delegationDeactivationEpoch,
delegationWarmupCooldownRate: dec.delegationWarmupCooldownRate,
creditsObserved: dec.creditsObserved,
});
}
}
exports.StakeInfo = StakeInfo;
class SmallStakeAccountInfo {
meta;
stake;
static layout(property) {
return borsh.struct([StakeMeta.layout('meta'), StakeInfo.layout('stake')], property);
}
constructor(fields) {
this.meta = fields.meta;
this.stake = fields.stake;
}
static decode(data) {
const dec = SmallStakeAccountInfo.layout().decode(data);
return new SmallStakeAccountInfo({
meta: dec.meta,
stake: dec.stake,
});
}
}
exports.SmallStakeAccountInfo = SmallStakeAccountInfo;
class StakeAccount {
type;
info;
static layout = borsh.struct([borsh.u32('type'), SmallStakeAccountInfo.layout('info')]);
constructor(fields) {
this.type = fields.type;
this.info = fields.info;
}
static decode(data) {
const dec = StakeAccount.layout.decode(data);
return new StakeAccount({
type: dec.type,
info: dec.info,
});
}
}
exports.StakeAccount = StakeAccount;
//# sourceMappingURL=standardStakePool.js.map