UNPKG

@kamino-finance/klend-sdk

Version:

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

400 lines 20 kB
"use strict"; 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