@kamino-finance/klend-sdk
Version:
Typescript SDK for interacting with the Kamino Lending (klend) protocol
878 lines (877 loc) • 102 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.KaminoAction = exports.DEPOSITS_LIMIT = exports.BORROWS_LIMIT = exports.POSITION_LIMIT = void 0;
const web3_js_1 = require("@solana/web3.js");
const spl_token_1 = require("@solana/spl-token");
const bn_js_1 = __importDefault(require("bn.js"));
const decimal_js_1 = __importDefault(require("decimal.js"));
const instructions_1 = require("../idl_codegen/instructions");
const utils_1 = require("../utils");
const obligation_1 = require("./obligation");
const types_1 = require("../idl_codegen/types");
const farms_sdk_1 = require("@kamino-finance/farms-sdk");
const ObligationType_1 = require("../utils/ObligationType");
const lib_1 = require("../lib");
const scope_sdk_1 = require("@kamino-finance/scope-sdk");
exports.POSITION_LIMIT = 10;
exports.BORROWS_LIMIT = 5;
exports.DEPOSITS_LIMIT = 8;
const SOL_PADDING_FOR_INTEREST = new bn_js_1.default('1000000');
class KaminoAction {
kaminoMarket;
reserve;
outflowReserve;
owner;
payer;
obligation = null;
referrer;
userTokenAccountAddress;
userCollateralAccountAddress;
additionalTokenAccountAddress;
/**
* Null unless the obligation is not passed
*/
obligationType = null;
mint;
secondaryMint;
positions;
amount;
outflowAmount;
computeBudgetIxs;
computeBudgetIxsLabels;
setupIxs;
setupIxsLabels;
inBetweenIxs;
inBetweenIxsLabels;
lendingIxs;
lendingIxsLabels;
cleanupIxs;
cleanupIxsLabels;
preTxnIxs;
preTxnIxsLabels;
postTxnIxs;
postTxnIxsLabels;
refreshFarmsCleanupTxnIxs;
refreshFarmsCleanupTxnIxsLabels;
depositReserves;
borrowReserves;
preLoadedDepositReservesSameTx;
preLoadedBorrowReservesSameTx;
currentSlot;
constructor(kaminoMarket, owner, obligation, userTokenAccountAddress, userCollateralAccountAddress, mint, positions, amount, depositReserves, borrowReserves, reserveState, currentSlot, secondaryMint, additionalTokenAccountAddress, outflowReserveState, outflowAmount, referrer, payer) {
if (obligation instanceof obligation_1.KaminoObligation) {
this.obligation = obligation;
}
else if (obligation !== null) {
this.obligationType = obligation;
}
this.kaminoMarket = kaminoMarket;
this.owner = owner;
this.payer = payer ?? owner;
this.amount = new bn_js_1.default(amount);
this.mint = mint;
this.positions = positions;
this.userTokenAccountAddress = userTokenAccountAddress;
this.userCollateralAccountAddress = userCollateralAccountAddress;
this.computeBudgetIxs = [];
this.computeBudgetIxsLabels = [];
this.setupIxs = [];
this.setupIxsLabels = [];
this.inBetweenIxs = [];
this.inBetweenIxsLabels = [];
this.lendingIxs = [];
this.lendingIxsLabels = [];
this.cleanupIxs = [];
this.cleanupIxsLabels = [];
this.preTxnIxs = [];
this.preTxnIxsLabels = [];
this.postTxnIxs = [];
this.postTxnIxsLabels = [];
this.refreshFarmsCleanupTxnIxs = [];
this.refreshFarmsCleanupTxnIxsLabels = [];
this.depositReserves = depositReserves;
this.borrowReserves = borrowReserves;
this.additionalTokenAccountAddress = additionalTokenAccountAddress;
this.secondaryMint = secondaryMint;
this.reserve = reserveState;
this.outflowReserve = outflowReserveState;
this.outflowAmount = outflowAmount ? new bn_js_1.default(outflowAmount) : undefined;
this.preLoadedDepositReservesSameTx = [];
this.preLoadedBorrowReservesSameTx = [];
this.referrer = referrer ? referrer : web3_js_1.PublicKey.default;
this.currentSlot = currentSlot;
}
static async initialize(action, amount, mint, owner, kaminoMarket, obligation, referrer = web3_js_1.PublicKey.default, currentSlot = 0, payer) {
const reserve = kaminoMarket.getReserveByMint(mint);
if (reserve === undefined) {
throw new Error(`Reserve ${mint} not found in market ${kaminoMarket.getAddress().toBase58()}`);
}
const { userTokenAccountAddress, userCollateralAccountAddress } = KaminoAction.getUserAccountAddresses(payer ?? owner, reserve.state);
const { kaminoObligation, depositReserves, borrowReserves, distinctReserveCount } = await KaminoAction.loadObligation(action, kaminoMarket, owner, reserve.address, obligation);
const referrerKey = await this.getReferrerKey(kaminoMarket, owner, kaminoObligation, referrer);
return new KaminoAction(kaminoMarket, owner, kaminoObligation || obligation, userTokenAccountAddress, userCollateralAccountAddress, mint, distinctReserveCount, amount, depositReserves, borrowReserves, reserve, currentSlot, undefined, undefined, undefined, undefined, referrerKey, payer);
}
static getUserAccountAddresses(owner, reserve) {
const userTokenAccountAddress = (0, utils_1.getAssociatedTokenAddress)(reserve.liquidity.mintPubkey, owner, true, reserve.liquidity.tokenProgram, spl_token_1.ASSOCIATED_TOKEN_PROGRAM_ID);
const userCollateralAccountAddress = (0, utils_1.getAssociatedTokenAddress)(reserve.collateral.mintPubkey, owner, true, spl_token_1.TOKEN_PROGRAM_ID, spl_token_1.ASSOCIATED_TOKEN_PROGRAM_ID);
return { userTokenAccountAddress, userCollateralAccountAddress };
}
static async loadObligation(action, kaminoMarket, owner, reserve, obligation, outflowReserve) {
let kaminoObligation;
const depositReserves = [];
const borrowReserves = [];
if (obligation instanceof obligation_1.KaminoObligation) {
kaminoObligation = obligation;
}
else {
const obligationAddress = obligation.toPda(kaminoMarket.getAddress(), owner);
kaminoObligation = await obligation_1.KaminoObligation.load(kaminoMarket, obligationAddress);
}
if (kaminoObligation !== null) {
depositReserves.push(...[...kaminoObligation.deposits.keys()]);
borrowReserves.push(...[...kaminoObligation.borrows.keys()]);
}
if (!outflowReserve && action === 'depositAndBorrow') {
throw new Error(`Outflow reserve has not been set for depositAndBorrow`);
}
// Union of addresses
const distinctReserveCount = new utils_1.PublicKeySet([
...borrowReserves.map((e) => e),
...(action === 'borrow' ? [reserve] : []),
...(action === 'depositAndBorrow' ? [reserve] : []),
]).toArray().length +
new utils_1.PublicKeySet([
...depositReserves.map((e) => e),
...(action === 'deposit' ? [reserve] : []),
...(action === 'depositAndBorrow' ? [outflowReserve] : []),
]).toArray().length;
if (distinctReserveCount > exports.POSITION_LIMIT) {
throw Error(`Obligation already has max number of positions: ${exports.POSITION_LIMIT}`);
}
return {
kaminoObligation,
depositReserves,
borrowReserves,
distinctReserveCount,
};
}
static async buildRefreshObligationTxns(kaminoMarket, payer, obligation, extraComputeBudget = 1_000_000, // if > 0 then adds the ixn
currentSlot = 0) {
// placeholder for action initialization
const firstReserve = obligation.state.deposits[0].depositReserve;
const firstKaminoReserve = kaminoMarket.getReserveByAddress(firstReserve);
if (!firstKaminoReserve) {
throw new Error(`Reserve ${firstReserve.toBase58()} not found`);
}
const axn = await KaminoAction.initialize('refreshObligation', '0', firstKaminoReserve?.getLiquidityMint(), obligation.state.owner, kaminoMarket, obligation, undefined, currentSlot);
if (extraComputeBudget > 0) {
axn.addComputeBudgetIxn(extraComputeBudget);
}
axn.addRefreshObligation(payer);
return axn;
}
static async buildRequestElevationGroupTxns(kaminoMarket, payer, obligation, elevationGroup, extraComputeBudget = 1_000_000, // if > 0 then adds the ixn
currentSlot = 0) {
const firstReserve = obligation.state.deposits.find((x) => !x.depositReserve.equals(web3_js_1.PublicKey.default)).depositReserve;
const firstKaminoReserve = kaminoMarket.getReserveByAddress(firstReserve);
if (!firstKaminoReserve) {
throw new Error(`Reserve ${firstReserve.toBase58()} not found`);
}
const axn = await KaminoAction.initialize('requestElevationGroup', '0', firstKaminoReserve?.getLiquidityMint(), obligation.state.owner, kaminoMarket, obligation, undefined, currentSlot);
if (extraComputeBudget > 0) {
axn.addComputeBudgetIxn(extraComputeBudget);
}
axn.addRefreshObligation(payer);
axn.addRequestElevationIx(elevationGroup, 'setup');
return axn;
}
static async buildDepositTxns(kaminoMarket, amount, mint, owner, obligation, extraComputeBudget = 1_000_000, // if > 0 then adds the ixn
includeAtaIxns = true, // if true it includes create and close wsol and token atas,
requestElevationGroup = false, includeUserMetadata = true, // if true it includes user metadata
referrer = web3_js_1.PublicKey.default, currentSlot = 0, scopeRefresh = { includeScopeRefresh: false, scopeFeed: 'hubble' }, overrideElevationGroupRequest = undefined // if set, when an elevationgroup request is made, it will use this value
) {
const axn = await KaminoAction.initialize('deposit', amount, mint, owner, kaminoMarket, obligation, referrer, currentSlot);
const addInitObligationForFarm = true;
if (extraComputeBudget > 0) {
axn.addComputeBudgetIxn(extraComputeBudget);
}
const allReserves = new utils_1.PublicKeySet([
...axn.depositReserves,
...axn.borrowReserves,
axn.reserve.address,
]).toArray();
const tokenIds = axn.getTokenIdsForScopeRefresh(kaminoMarket, allReserves);
if (tokenIds.length > 0 && scopeRefresh.includeScopeRefresh) {
await axn.addScopeRefreshIxs(tokenIds, scopeRefresh.scopeFeed);
}
await axn.addSupportIxs('deposit', includeAtaIxns, requestElevationGroup, includeUserMetadata, addInitObligationForFarm, undefined, overrideElevationGroupRequest);
axn.addDepositIx();
axn.addRefreshFarmsCleanupTxnIxsToCleanupIxs();
return axn;
}
getTokenIdsForScopeRefresh(kaminoMarket, reserves) {
const tokenIds = [];
for (const reserveAddress of reserves) {
const reserve = kaminoMarket.getReserveByAddress(reserveAddress);
if (!reserve) {
throw new Error(`Reserve not found for reserve ${reserveAddress.toBase58()}`);
}
if (!reserve.state.config.tokenInfo.scopeConfiguration.priceFeed.equals(web3_js_1.PublicKey.default)) {
reserve.state.config.tokenInfo.scopeConfiguration.priceChain.map((x) => {
if (x !== scope_sdk_1.U16_MAX) {
tokenIds.push(x);
}
});
reserve.state.config.tokenInfo.scopeConfiguration.twapChain.map((x) => {
if (x !== scope_sdk_1.U16_MAX) {
tokenIds.push(x);
}
});
}
}
return tokenIds;
}
async addScopeRefreshIxs(tokens, feed = 'hubble') {
this.preTxnIxsLabels.unshift(`refreshScopePrices`);
this.preTxnIxs.unshift(await this.kaminoMarket.scope.refreshPriceListIx({
feed: feed,
}, tokens));
}
static async buildBorrowTxns(kaminoMarket, amount, mint, owner, obligation, extraComputeBudget = 1_000_000, // if > 0 then adds the ixn
includeAtaIxns = true, // if true it includes create and close wsol and token atas,
requestElevationGroup = false, includeUserMetadata = true, // if true it includes user metadata
referrer = web3_js_1.PublicKey.default, currentSlot = 0, scopeRefresh = { includeScopeRefresh: false, scopeFeed: 'hubble' }, overrideElevationGroupRequest = undefined // if set, when an elevationgroup request is made, it will use this value
) {
const axn = await KaminoAction.initialize('borrow', amount, mint, owner, kaminoMarket, obligation, referrer, currentSlot);
const addInitObligationForFarm = true;
if (extraComputeBudget > 0) {
axn.addComputeBudgetIxn(extraComputeBudget);
}
const allReserves = new utils_1.PublicKeySet([
...axn.depositReserves,
...axn.borrowReserves,
axn.reserve.address,
]).toArray();
const tokenIds = axn.getTokenIdsForScopeRefresh(kaminoMarket, allReserves);
if (tokenIds.length > 0 && scopeRefresh.includeScopeRefresh) {
await axn.addScopeRefreshIxs(tokenIds, scopeRefresh.scopeFeed);
}
await axn.addSupportIxs('borrow', includeAtaIxns, requestElevationGroup, includeUserMetadata, addInitObligationForFarm, undefined, overrideElevationGroupRequest);
axn.addBorrowIx();
axn.addRefreshFarmsCleanupTxnIxsToCleanupIxs();
return axn;
}
static async buildDepositReserveLiquidityTxns(kaminoMarket, amount, mint, owner, obligation, extraComputeBudget = 1_000_000, // if > 0 then adds the ixn
includeAtaIxns = true, // if true it includes create and close wsol and token atas
requestElevationGroup = false, includeUserMetadata = true, // if true it includes user metadata
referrer = web3_js_1.PublicKey.default, currentSlot = 0, scopeRefresh = { includeScopeRefresh: false, scopeFeed: 'hubble' }) {
const axn = await KaminoAction.initialize('mint', amount, mint, owner, kaminoMarket, obligation, referrer, currentSlot);
const addInitObligationForFarm = true;
if (extraComputeBudget > 0) {
axn.addComputeBudgetIxn(extraComputeBudget);
}
const allReserves = new utils_1.PublicKeySet([
...axn.depositReserves,
...axn.borrowReserves,
axn.reserve.address,
]).toArray();
const tokenIds = axn.getTokenIdsForScopeRefresh(kaminoMarket, allReserves);
if (tokenIds.length > 0 && scopeRefresh.includeScopeRefresh) {
await axn.addScopeRefreshIxs(tokenIds, scopeRefresh.scopeFeed);
}
await axn.addSupportIxs('mint', includeAtaIxns, requestElevationGroup, includeUserMetadata, addInitObligationForFarm);
axn.addDepositReserveLiquidityIx();
axn.addRefreshFarmsCleanupTxnIxsToCleanupIxs();
return axn;
}
static async buildRedeemReserveCollateralTxns(kaminoMarket, amount, mint, owner, obligation, extraComputeBudget = 1_000_000, // if > 0 then adds the ixn
includeAtaIxns = true, // if true it includes create and close wsol and token atas
requestElevationGroup = false, includeUserMetadata = true, // if true it includes user metadata,
referrer = web3_js_1.PublicKey.default, currentSlot = 0, scopeRefresh = { includeScopeRefresh: false, scopeFeed: 'hubble' }) {
const axn = await KaminoAction.initialize('redeem', amount, mint, owner, kaminoMarket, obligation, referrer, currentSlot);
const addInitObligationForFarm = true;
if (extraComputeBudget > 0) {
axn.addComputeBudgetIxn(extraComputeBudget);
}
const allReserves = new utils_1.PublicKeySet([
...axn.depositReserves,
...axn.borrowReserves,
axn.reserve.address,
]).toArray();
const tokenIds = axn.getTokenIdsForScopeRefresh(kaminoMarket, allReserves);
if (tokenIds.length > 0 && scopeRefresh.includeScopeRefresh) {
await axn.addScopeRefreshIxs(tokenIds, scopeRefresh.scopeFeed);
}
await axn.addSupportIxs('redeem', includeAtaIxns, requestElevationGroup, includeUserMetadata, addInitObligationForFarm);
axn.addRedeemReserveCollateralIx();
axn.addRefreshFarmsCleanupTxnIxsToCleanupIxs();
return axn;
}
static async buildDepositObligationCollateralTxns(kaminoMarket, amount, mint, owner, obligation, extraComputeBudget = 1_000_000, // if > 0 then adds the ixn
includeAtaIxns = true, // if true it includes create and close wsol and token atas
requestElevationGroup = false, includeUserMetadata = true, // if true it includes user metadata
referrer = web3_js_1.PublicKey.default, currentSlot = 0, scopeRefresh = { includeScopeRefresh: false, scopeFeed: 'hubble' }) {
const axn = await KaminoAction.initialize('depositCollateral', amount, mint, owner, kaminoMarket, obligation, referrer, currentSlot);
const addInitObligationForFarm = true;
if (extraComputeBudget > 0) {
axn.addComputeBudgetIxn(extraComputeBudget);
}
const allReserves = new utils_1.PublicKeySet([
...axn.depositReserves,
...axn.borrowReserves,
axn.reserve.address,
]).toArray();
const tokenIds = axn.getTokenIdsForScopeRefresh(kaminoMarket, allReserves);
if (tokenIds.length > 0 && scopeRefresh.includeScopeRefresh) {
await axn.addScopeRefreshIxs(tokenIds, scopeRefresh.scopeFeed);
}
await axn.addSupportIxs('depositCollateral', includeAtaIxns, requestElevationGroup, includeUserMetadata, addInitObligationForFarm);
axn.addDepositObligationCollateralIx();
axn.addRefreshFarmsCleanupTxnIxsToCleanupIxs();
return axn;
}
static async buildDepositAndBorrowTxns(kaminoMarket, depositAmount, depositMint, borrowAmount, borrowMint, payer, obligation, extraComputeBudget = 1_000_000, // if > 0 then adds the ixn
includeAtaIxns = true, // if true it includes create and close wsol and token atas,
requestElevationGroup = false, includeUserMetadata = true, // if true it includes user metadata,
referrer = web3_js_1.PublicKey.default, currentSlot = 0, scopeRefresh = { includeScopeRefresh: false, scopeFeed: 'hubble' }) {
const axn = await KaminoAction.initializeMultiTokenAction(kaminoMarket, 'depositAndBorrow', depositAmount, depositMint, borrowMint, payer, payer, obligation, borrowAmount, referrer, currentSlot);
const addInitObligationForFarmForDeposit = true;
const addInitObligationForFarmForBorrow = false;
const twoTokenAction = true;
if (extraComputeBudget > 0) {
axn.addComputeBudgetIxn(extraComputeBudget);
}
const allReserves = new utils_1.PublicKeySet([
...axn.depositReserves,
...axn.borrowReserves,
axn.reserve.address,
axn.outflowReserve.address,
]).toArray();
const tokenIds = axn.getTokenIdsForScopeRefresh(kaminoMarket, allReserves);
if (tokenIds.length > 0 && scopeRefresh.includeScopeRefresh) {
await axn.addScopeRefreshIxs(tokenIds, scopeRefresh.scopeFeed);
}
await axn.addSupportIxs('deposit', includeAtaIxns, requestElevationGroup, includeUserMetadata, addInitObligationForFarmForDeposit, twoTokenAction);
await axn.addDepositAndBorrowIx();
await axn.addInBetweenIxs('depositAndBorrow', includeAtaIxns, requestElevationGroup, addInitObligationForFarmForBorrow);
axn.addRefreshFarmsCleanupTxnIxsToCleanupIxs();
return axn;
}
static async buildRepayAndWithdrawTxns(kaminoMarket, repayAmount, repayMint, withdrawAmount, withdrawMint, payer, currentSlot, obligation, extraComputeBudget = 1_000_000, // if > 0 then adds the ixn
includeAtaIxns = true, // if true it includes create and close wsol and token atas,
requestElevationGroup = false, includeUserMetadata = true, // if true it includes user metadata,
isClosingPosition = false, referrer = web3_js_1.PublicKey.default, scopeRefresh = { includeScopeRefresh: false, scopeFeed: 'hubble' }) {
const axn = await KaminoAction.initializeMultiTokenAction(kaminoMarket, 'repayAndWithdraw', repayAmount, repayMint, withdrawMint, payer, payer, obligation, withdrawAmount, referrer, currentSlot);
const addInitObligationForFarmForRepay = true;
const addInitObligationForFarmForWithdraw = false;
const twoTokenAction = true;
if (extraComputeBudget > 0) {
axn.addComputeBudgetIxn(extraComputeBudget);
}
const allReserves = new utils_1.PublicKeySet([
...axn.depositReserves,
...axn.borrowReserves,
axn.reserve.address,
axn.outflowReserve.address,
]).toArray();
const tokenIds = axn.getTokenIdsForScopeRefresh(kaminoMarket, allReserves);
if (tokenIds.length > 0 && scopeRefresh.includeScopeRefresh) {
await axn.addScopeRefreshIxs(tokenIds, scopeRefresh.scopeFeed);
}
await axn.addSupportIxs('repay', includeAtaIxns, requestElevationGroup, includeUserMetadata, addInitObligationForFarmForRepay, twoTokenAction);
await axn.addRepayAndWithdrawIxs();
await axn.addInBetweenIxs('repayAndWithdraw', includeAtaIxns, requestElevationGroup, addInitObligationForFarmForWithdraw, isClosingPosition);
axn.addRefreshFarmsCleanupTxnIxsToCleanupIxs();
return axn;
}
static async buildWithdrawTxns(kaminoMarket, amount, mint, owner, obligation, extraComputeBudget = 1_000_000, // if > 0 then adds the ixn
includeAtaIxns = true, // if true it includes create and close wsol and token atas,
requestElevationGroup = false, includeUserMetadata = true, // if true it includes user metadata
referrer = web3_js_1.PublicKey.default, currentSlot = 0, scopeRefresh = { includeScopeRefresh: false, scopeFeed: 'hubble' }) {
const axn = await KaminoAction.initialize('withdraw', amount, mint, owner, kaminoMarket, obligation, referrer, currentSlot);
const addInitObligationForFarm = true;
if (extraComputeBudget > 0) {
axn.addComputeBudgetIxn(extraComputeBudget);
}
const allReserves = new utils_1.PublicKeySet([
...axn.depositReserves,
...axn.borrowReserves,
axn.reserve.address,
]).toArray();
const tokenIds = axn.getTokenIdsForScopeRefresh(kaminoMarket, allReserves);
if (tokenIds.length > 0 && scopeRefresh.includeScopeRefresh) {
await axn.addScopeRefreshIxs(tokenIds, scopeRefresh.scopeFeed);
}
await axn.addSupportIxs('withdraw', includeAtaIxns, requestElevationGroup, includeUserMetadata, addInitObligationForFarm);
await axn.addWithdrawIx();
axn.addRefreshFarmsCleanupTxnIxsToCleanupIxs();
return axn;
}
/**
*
* @param kaminoMarket
* @param amount
* @param mint
* @param owner
* @param obligation - obligation to repay or the PDA seeds
* @param currentSlot
* @param payer - if not set then owner is used
* @param extraComputeBudget - if > 0 then adds the ixn
* @param includeAtaIxns - if true it includes create and close wsol and token atas
* @param requestElevationGroup
* @param includeUserMetadata - if true it includes user metadata
* @param referrer
*/
static async buildRepayTxns(kaminoMarket, amount, mint, owner, obligation, currentSlot, payer = undefined, extraComputeBudget = 1_000_000, includeAtaIxns = true, requestElevationGroup = false, includeUserMetadata = true, referrer = web3_js_1.PublicKey.default, scopeRefresh = { includeScopeRefresh: false, scopeFeed: 'hubble' }) {
const axn = await KaminoAction.initialize('repay', amount, mint, owner, kaminoMarket, obligation, referrer, currentSlot, payer);
const addInitObligationForFarm = true;
if (extraComputeBudget > 0) {
axn.addComputeBudgetIxn(extraComputeBudget);
}
const allReserves = new utils_1.PublicKeySet([
...axn.depositReserves,
...axn.borrowReserves,
axn.reserve.address,
]).toArray();
const tokenIds = axn.getTokenIdsForScopeRefresh(kaminoMarket, allReserves);
if (tokenIds.length > 0 && scopeRefresh.includeScopeRefresh) {
await axn.addScopeRefreshIxs(tokenIds, scopeRefresh.scopeFeed);
}
await axn.addSupportIxs('repay', includeAtaIxns, requestElevationGroup, includeUserMetadata, addInitObligationForFarm);
await axn.addRepayIx();
axn.addRefreshFarmsCleanupTxnIxsToCleanupIxs();
return axn;
}
static async buildLiquidateTxns(kaminoMarket, amount, minCollateralReceiveAmount, repayTokenMint, withdrawTokenMint, liquidator, obligationOwner, obligation, extraComputeBudget = 1_000_000, // if > 0 then adds the ixn
includeAtaIxns = true, // if true it includes create and close wsol and token atas, and creates all other token atas if they don't exist
requestElevationGroup = false, includeUserMetadata = true, // if true it includes user metadata
referrer = web3_js_1.PublicKey.default, maxAllowedLtvOverridePercent = 0, currentSlot = 0, scopeRefresh = { includeScopeRefresh: false, scopeFeed: 'hubble' }) {
const axn = await KaminoAction.initializeMultiTokenAction(kaminoMarket, 'liquidate', amount, repayTokenMint, withdrawTokenMint, liquidator, obligationOwner, obligation, minCollateralReceiveAmount, referrer, currentSlot);
const addInitObligationForFarm = true;
if (extraComputeBudget > 0) {
axn.addComputeBudgetIxn(extraComputeBudget);
}
const allReserves = new utils_1.PublicKeySet([
...axn.depositReserves,
...axn.borrowReserves,
axn.reserve.address,
axn.outflowReserve.address,
]).toArray();
const tokenIds = axn.getTokenIdsForScopeRefresh(kaminoMarket, allReserves);
if (tokenIds.length > 0 && scopeRefresh.includeScopeRefresh) {
await axn.addScopeRefreshIxs(tokenIds, scopeRefresh.scopeFeed);
}
await axn.addSupportIxs('liquidate', includeAtaIxns, requestElevationGroup, includeUserMetadata, addInitObligationForFarm);
await axn.addLiquidateIx(maxAllowedLtvOverridePercent);
axn.addRefreshFarmsCleanupTxnIxsToCleanupIxs();
return axn;
}
static async buildWithdrawReferrerFeeTxns(owner, tokenMint, kaminoMarket, currentSlot = 0) {
const { axn, createAtaIxs } = await KaminoAction.initializeWithdrawReferrerFees(tokenMint, owner, kaminoMarket, currentSlot);
axn.preTxnIxs.push(...createAtaIxs);
axn.preTxnIxsLabels.push(`createAtasIxs[${axn.userTokenAccountAddress.toString()}]`);
axn.addRefreshReserveIxs([axn.reserve.address]);
axn.addWithdrawReferrerFeesIxs();
return axn;
}
async getTransactions() {
const txns = {
preLendingTxn: null,
lendingTxn: null,
postLendingTxn: null,
};
if (this.preTxnIxs.length) {
txns.preLendingTxn = new web3_js_1.Transaction({
feePayer: this.owner,
recentBlockhash: (await this.kaminoMarket.getConnection().getLatestBlockhash()).blockhash,
}).add(...this.preTxnIxs);
}
if (this.lendingIxs.length === 2) {
txns.lendingTxn = new web3_js_1.Transaction({
feePayer: this.owner,
recentBlockhash: (await this.kaminoMarket.getConnection().getLatestBlockhash()).blockhash,
}).add(...this.setupIxs, ...[this.lendingIxs[0]], ...this.inBetweenIxs, ...[this.lendingIxs[1]], ...this.cleanupIxs);
}
else {
txns.lendingTxn = new web3_js_1.Transaction({
feePayer: this.owner,
recentBlockhash: (await this.kaminoMarket.getConnection().getLatestBlockhash()).blockhash,
}).add(...this.setupIxs, ...this.lendingIxs, ...this.cleanupIxs);
}
if (this.postTxnIxs.length) {
txns.postLendingTxn = new web3_js_1.Transaction({
feePayer: this.owner,
recentBlockhash: (await this.kaminoMarket.getConnection().getLatestBlockhash()).blockhash,
}).add(...this.postTxnIxs);
}
return txns;
}
async sendTransactions(sendTransaction) {
const txns = await this.getTransactions();
await this.sendSingleTransaction(txns.preLendingTxn, sendTransaction);
const signature = await this.sendSingleTransaction(txns.lendingTxn, sendTransaction);
await this.sendSingleTransaction(txns.postLendingTxn, sendTransaction);
return signature;
}
async sendSingleTransaction(txn, sendTransaction) {
if (!txn)
return '';
const signature = await sendTransaction(txn, this.kaminoMarket.getConnection());
await this.kaminoMarket.getConnection().confirmTransaction(signature);
return signature;
}
async simulateTransactions(sendTransaction) {
const txns = await this.getTransactions();
await this.simulateSingleTransaction(txns.preLendingTxn, sendTransaction);
const signature = await this.simulateSingleTransaction(txns.lendingTxn, sendTransaction);
await this.simulateSingleTransaction(txns.postLendingTxn, sendTransaction);
return signature;
}
async simulateSingleTransaction(txn, sendTransaction) {
if (!txn)
return '';
return await sendTransaction(txn, this.kaminoMarket.getConnection());
}
addDepositIx() {
this.lendingIxsLabels.push(`depositReserveLiquidityAndObligationCollateral`);
this.lendingIxs.push((0, instructions_1.depositReserveLiquidityAndObligationCollateral)({
liquidityAmount: this.amount,
}, {
owner: this.owner,
obligation: this.getObligationPda(),
lendingMarket: this.kaminoMarket.getAddress(),
lendingMarketAuthority: this.kaminoMarket.getLendingMarketAuthority(),
reserve: this.reserve.address,
reserveLiquidityMint: this.reserve.getLiquidityMint(),
reserveLiquiditySupply: this.reserve.state.liquidity.supplyVault,
reserveCollateralMint: this.reserve.getCTokenMint(),
reserveDestinationDepositCollateral: this.reserve.state.collateral.supplyVault, // destinationCollateral
userSourceLiquidity: this.userTokenAccountAddress,
placeholderUserDestinationCollateral: this.kaminoMarket.programId,
collateralTokenProgram: spl_token_1.TOKEN_PROGRAM_ID,
liquidityTokenProgram: this.reserve.getLiquidityTokenProgram(),
instructionSysvarAccount: web3_js_1.SYSVAR_INSTRUCTIONS_PUBKEY,
}, this.kaminoMarket.programId));
}
addDepositReserveLiquidityIx() {
this.lendingIxsLabels.push(`depositReserveLiquidity`);
this.lendingIxs.push((0, instructions_1.depositReserveLiquidity)({
liquidityAmount: this.amount,
}, {
owner: this.owner,
lendingMarket: this.kaminoMarket.getAddress(),
lendingMarketAuthority: this.kaminoMarket.getLendingMarketAuthority(),
reserve: this.reserve.address,
reserveLiquidityMint: this.reserve.getLiquidityMint(),
reserveLiquiditySupply: this.reserve.state.liquidity.supplyVault,
reserveCollateralMint: this.reserve.getCTokenMint(),
userSourceLiquidity: this.userTokenAccountAddress,
userDestinationCollateral: this.userCollateralAccountAddress,
collateralTokenProgram: spl_token_1.TOKEN_PROGRAM_ID,
liquidityTokenProgram: this.reserve.getLiquidityTokenProgram(),
instructionSysvarAccount: web3_js_1.SYSVAR_INSTRUCTIONS_PUBKEY,
}, this.kaminoMarket.programId));
}
addRedeemReserveCollateralIx() {
this.lendingIxsLabels.push(`redeemReserveCollateral`);
this.lendingIxs.push((0, instructions_1.redeemReserveCollateral)({
collateralAmount: this.amount,
}, {
owner: this.owner,
lendingMarket: this.kaminoMarket.getAddress(),
lendingMarketAuthority: this.kaminoMarket.getLendingMarketAuthority(),
reserve: this.reserve.address,
reserveLiquidityMint: this.reserve.getLiquidityMint(),
reserveLiquiditySupply: this.reserve.state.liquidity.supplyVault,
reserveCollateralMint: this.reserve.getCTokenMint(),
userSourceCollateral: this.userCollateralAccountAddress,
userDestinationLiquidity: this.userTokenAccountAddress,
collateralTokenProgram: spl_token_1.TOKEN_PROGRAM_ID,
liquidityTokenProgram: this.reserve.getLiquidityTokenProgram(),
instructionSysvarAccount: web3_js_1.SYSVAR_INSTRUCTIONS_PUBKEY,
}, this.kaminoMarket.programId));
}
addDepositObligationCollateralIx() {
this.lendingIxsLabels.push(`depositObligationCollateral`);
this.lendingIxs.push((0, instructions_1.depositObligationCollateral)({
collateralAmount: this.amount,
}, {
owner: this.owner,
obligation: this.getObligationPda(),
lendingMarket: this.kaminoMarket.getAddress(),
depositReserve: this.reserve.address,
reserveDestinationCollateral: this.reserve.state.collateral.supplyVault,
userSourceCollateral: this.userCollateralAccountAddress,
tokenProgram: spl_token_1.TOKEN_PROGRAM_ID,
instructionSysvarAccount: web3_js_1.SYSVAR_INSTRUCTIONS_PUBKEY,
}, this.kaminoMarket.programId));
}
addBorrowIx() {
this.lendingIxsLabels.push(`borrowObligationLiquidity`);
const depositReservesList = this.getAdditionalDepositReservesList();
const depositReserveAccountMetas = depositReservesList.map((reserve) => {
return { pubkey: reserve, isSigner: false, isWritable: true };
});
const borrowIx = (0, instructions_1.borrowObligationLiquidity)({
liquidityAmount: this.amount,
}, {
owner: this.owner,
obligation: this.getObligationPda(),
lendingMarket: this.kaminoMarket.getAddress(),
lendingMarketAuthority: this.kaminoMarket.getLendingMarketAuthority(),
borrowReserve: this.reserve.address,
borrowReserveLiquidityMint: this.reserve.getLiquidityMint(),
reserveSourceLiquidity: this.reserve.state.liquidity.supplyVault,
userDestinationLiquidity: this.userTokenAccountAddress,
borrowReserveLiquidityFeeReceiver: this.reserve.state.liquidity.feeVault,
referrerTokenState: (0, utils_1.referrerTokenStatePda)(this.referrer, this.reserve.address, this.kaminoMarket.programId)[0],
tokenProgram: this.reserve.getLiquidityTokenProgram(),
instructionSysvarAccount: web3_js_1.SYSVAR_INSTRUCTIONS_PUBKEY,
}, this.kaminoMarket.programId);
borrowIx.keys =
this.obligation.state.elevationGroup > 0 || this.obligation.refreshedStats.potentialElevationGroupUpdate > 0
? borrowIx.keys.concat([...depositReserveAccountMetas])
: borrowIx.keys;
this.lendingIxs.push(borrowIx);
}
async addDepositAndBorrowIx() {
this.lendingIxsLabels.push(`depositReserveLiquidityAndObligationCollateral`);
this.lendingIxsLabels.push(`borrowObligationLiquidity`);
this.lendingIxs.push((0, instructions_1.depositReserveLiquidityAndObligationCollateral)({
liquidityAmount: this.amount,
}, {
owner: this.owner,
obligation: this.getObligationPda(),
lendingMarket: this.kaminoMarket.getAddress(),
lendingMarketAuthority: this.kaminoMarket.getLendingMarketAuthority(),
reserve: this.reserve.address,
reserveLiquidityMint: this.reserve.getLiquidityMint(),
reserveLiquiditySupply: this.reserve.state.liquidity.supplyVault,
reserveCollateralMint: this.reserve.getCTokenMint(),
reserveDestinationDepositCollateral: this.reserve.state.collateral.supplyVault, // destinationCollateral
userSourceLiquidity: this.userTokenAccountAddress,
placeholderUserDestinationCollateral: this.kaminoMarket.programId,
collateralTokenProgram: spl_token_1.TOKEN_PROGRAM_ID,
liquidityTokenProgram: this.reserve.getLiquidityTokenProgram(),
instructionSysvarAccount: web3_js_1.SYSVAR_INSTRUCTIONS_PUBKEY,
}, this.kaminoMarket.programId));
if (!this.outflowReserve) {
throw new Error(`outflowReserve not set`);
}
if (!this.additionalTokenAccountAddress) {
throw new Error(`additionalTokenAccountAddress not set`);
}
if (!this.outflowAmount) {
throw new Error(`outflowAmount not set`);
}
const depositReservesList = this.getAdditionalDepositReservesList();
if (depositReservesList.length === 0) {
depositReservesList.push(this.reserve.address);
}
const depositReserveAccountMetas = depositReservesList.map((reserve) => {
return { pubkey: reserve, isSigner: false, isWritable: true };
});
const borrowIx = (0, instructions_1.borrowObligationLiquidity)({
liquidityAmount: this.outflowAmount,
}, {
owner: this.owner,
obligation: this.getObligationPda(),
lendingMarket: this.kaminoMarket.getAddress(),
lendingMarketAuthority: this.kaminoMarket.getLendingMarketAuthority(),
borrowReserve: this.outflowReserve.address,
borrowReserveLiquidityMint: this.outflowReserve.getLiquidityMint(),
reserveSourceLiquidity: this.outflowReserve.state.liquidity.supplyVault,
userDestinationLiquidity: this.additionalTokenAccountAddress,
borrowReserveLiquidityFeeReceiver: this.outflowReserve.state.liquidity.feeVault,
referrerTokenState: (0, utils_1.referrerTokenStatePda)(this.referrer, this.outflowReserve.address, this.kaminoMarket.programId)[0],
tokenProgram: this.outflowReserve.getLiquidityTokenProgram(),
instructionSysvarAccount: web3_js_1.SYSVAR_INSTRUCTIONS_PUBKEY,
}, this.kaminoMarket.programId);
borrowIx.keys = borrowIx.keys.concat([...depositReserveAccountMetas]);
this.lendingIxs.push(borrowIx);
}
async addRepayAndWithdrawIxs() {
this.lendingIxsLabels.push(`repayObligationLiquidity(reserve=${this.reserve.address})(obligation=${this.getObligationPda()})`);
this.lendingIxsLabels.push(`withdrawObligationCollateralAndRedeemReserveCollateral`);
const depositReservesList = this.getAdditionalDepositReservesList();
const depositReserveAccountMetas = depositReservesList.map((reserve) => {
return { pubkey: reserve, isSigner: false, isWritable: true };
});
const repayIx = (0, instructions_1.repayObligationLiquidity)({
liquidityAmount: this.amount,
}, {
owner: this.owner,
obligation: this.getObligationPda(),
lendingMarket: this.kaminoMarket.getAddress(),
repayReserve: this.reserve.address,
reserveLiquidityMint: this.reserve.getLiquidityMint(),
userSourceLiquidity: this.userTokenAccountAddress,
reserveDestinationLiquidity: this.reserve.state.liquidity.supplyVault,
tokenProgram: this.reserve.getLiquidityTokenProgram(),
instructionSysvarAccount: web3_js_1.SYSVAR_INSTRUCTIONS_PUBKEY,
}, this.kaminoMarket.programId);
repayIx.keys = repayIx.keys.concat([...depositReserveAccountMetas]);
this.lendingIxs.push(repayIx);
if (!this.outflowReserve) {
throw new Error(`outflowReserve not set`);
}
if (!this.additionalTokenAccountAddress) {
throw new Error(`additionalTokenAccountAddress not set`);
}
if (!this.outflowAmount) {
throw new Error(`outflowAmount not set`);
}
const collateralExchangeRate = this.outflowReserve.getEstimatedCollateralExchangeRate(this.currentSlot, this.kaminoMarket.state.referralFeeBps);
this.lendingIxs.push((0, instructions_1.withdrawObligationCollateralAndRedeemReserveCollateral)({
collateralAmount: this.outflowAmount.eq(new bn_js_1.default(utils_1.U64_MAX))
? this.outflowAmount
: new bn_js_1.default(new decimal_js_1.default(this.outflowAmount.toString()).mul(collateralExchangeRate).ceil().toString()),
}, {
owner: this.owner,
obligation: this.getObligationPda(),
lendingMarket: this.kaminoMarket.getAddress(),
lendingMarketAuthority: this.kaminoMarket.getLendingMarketAuthority(),
withdrawReserve: this.outflowReserve.address,
reserveLiquidityMint: this.outflowReserve.getLiquidityMint(),
reserveCollateralMint: this.outflowReserve.getCTokenMint(),
reserveLiquiditySupply: this.outflowReserve.state.liquidity.supplyVault,
reserveSourceCollateral: this.outflowReserve.state.collateral.supplyVault,
userDestinationLiquidity: this.additionalTokenAccountAddress,
placeholderUserDestinationCollateral: this.kaminoMarket.programId,
collateralTokenProgram: spl_token_1.TOKEN_PROGRAM_ID,
liquidityTokenProgram: this.outflowReserve.getLiquidityTokenProgram(),
instructionSysvarAccount: web3_js_1.SYSVAR_INSTRUCTIONS_PUBKEY,
}, this.kaminoMarket.programId));
}
async addWithdrawIx() {
const collateralExchangeRate = this.reserve.getEstimatedCollateralExchangeRate(this.currentSlot, this.kaminoMarket.state.referralFeeBps);
const collateralAmount = this.amount.eq(new bn_js_1.default(utils_1.U64_MAX))
? this.amount
: new bn_js_1.default(new decimal_js_1.default(this.amount.toString()).mul(collateralExchangeRate).ceil().toString());
this.lendingIxsLabels.push(`withdrawObligationCollateralAndRedeemReserveCollateral`);
this.lendingIxs.push((0, instructions_1.withdrawObligationCollateralAndRedeemReserveCollateral)({
collateralAmount,
}, {
owner: this.owner,
obligation: this.getObligationPda(),
lendingMarket: this.kaminoMarket.getAddress(),
lendingMarketAuthority: this.kaminoMarket.getLendingMarketAuthority(),
withdrawReserve: this.reserve.address,
reserveLiquidityMint: this.reserve.getLiquidityMint(),
reserveCollateralMint: this.reserve.getCTokenMint(),
reserveLiquiditySupply: this.reserve.state.liquidity.supplyVault,
reserveSourceCollateral: this.reserve.state.collateral.supplyVault,
userDestinationLiquidity: this.userTokenAccountAddress,
placeholderUserDestinationCollateral: this.kaminoMarket.programId,
collateralTokenProgram: spl_token_1.TOKEN_PROGRAM_ID,
liquidityTokenProgram: this.reserve.getLiquidityTokenProgram(),
instructionSysvarAccount: web3_js_1.SYSVAR_INSTRUCTIONS_PUBKEY,
}, this.kaminoMarket.programId));
}
async addRepayIx() {
this.lendingIxsLabels.push(`repayObligationLiquidity(reserve=${this.reserve.address})(obligation=${this.getObligationPda()})`);
const depositReservesList = this.getAdditionalDepositReservesList();
const depositReserveAccountMetas = depositReservesList.map((reserve) => {
return { pubkey: reserve, isSigner: false, isWritable: true };
});
const repayIx = (0, instructions_1.repayObligationLiquidity)({
liquidityAmount: this.amount,
}, {
owner: this.payer,
obligation: this.getObligationPda(),
lendingMarket: this.kaminoMarket.getAddress(),
repayReserve: this.reserve.address,
reserveLiquidityMint: this.reserve.getLiquidityMint(),
userSourceLiquidity: this.userTokenAccountAddress,
reserveDestinationLiquidity: this.reserve.state.liquidity.supplyVault,
tokenProgram: this.reserve.getLiquidityTokenProgram(),
instructionSysvarAccount: web3_js_1.SYSVAR_INSTRUCTIONS_PUBKEY,
}, this.kaminoMarket.programId);
repayIx.keys =
this.obligation.state.elevationGroup > 0 ? repayIx.keys.concat([...depositReserveAccountMetas]) : repayIx.keys;
this.lendingIxs.push(repayIx);
}
async addLiquidateIx(maxAllowedLtvOverridePercent = 0) {
this.lendingIxsLabels.push(`liquidateObligationAndRedeemReserveCollateral`);
if (!this.outflowReserve) {
throw Error(`Withdraw reserve during liquidation is not defined`);
}
if (!this.additionalTokenAccountAddress) {
throw Error(`Liquidating token account address is not defined`);
}
const depositReservesList = this.getAdditionalDepositReservesList();
const depositReserveAccountMetas = depositReservesList.map((reserve) => {
return { pubkey: reserve, isSigner: false, isWritable: true };
});
const liquidateIx = (0, instructions_1.liquidateObligationAndRedeemReserveCollateral)({
liquidityAmount: this.amount,
// TODO: Configure this when updating liquidator with new interface
minAcceptableReceivedLiquidityAmount: this.outflowAmount || new bn_js_1.default(0),
maxAllowedLtvOverridePercent: new bn_js_1.default(maxAllowedLtvOverridePercent),
}, {
liquidator: this.owner,
obligation: this.getObligationPda(),
lendingMarket: this.kaminoMarket.getAddress(),
lendingMarketAuthority: this.kaminoMarket.getLendingMarketAuthority(),
repayReserve: this.reserve.address,
repayReserveLiquidityMint: this.reserve.getLiquidityMint(),
repayReserveLiquiditySupply: this.reserve.state.liquidity.supplyVault,
withdrawReserve: this.outflowReserve.address,
withdrawReserveLiquidityMint: this.outflowReserve.getLiquidityMint(),
withdrawReserveCollateralMint: this.outflowReserve.getCTokenMint(),
withdrawReserveCollateralSupply: this.outflowReserve.state.collateral.supplyVault,
withdrawReserveLiquiditySupply: this.outflowReserve.state.liquidity.supplyVault,
userSourceLiquidity: this.additionalTokenAccountAddress,
userDestinationCollateral: this.userCollateralAccountAddress,
userDestinationLiquidity: this.userTokenAccountAddress,
withdrawReserveLiquidityFeeReceiver: this.outflowReserve.state.liquidity.feeVault,
collateralTokenProgram: spl_token_1.TOKEN_PROGRAM_ID,
repayLiquidityTokenProgram: this.reserve.getLiquidityTokenProgram(),
withdrawLiquidityTokenProgram: this.outflowReserve.getLiquidityTokenProgram(),
instructionSysvarAccount: web3_js_1.SYSVAR_INSTRUCTIONS_PUBKEY,
}, this.kaminoMarket.programId);
liquidateIx.keys =
this.obligation.state.elevationGroup > 0
? liquidateIx.keys.concat([...depositReserveAccountMetas])
: liquidateIx.keys;
this.lendingIxs.push(liquidateIx);
}
async addInBetweenIxs(action, includeAtaIxns, requestElevationGroup, addInitObligationForFarm, isClosingPosition = false) {
await this.addSupportIxsWithoutInitObligation(action, includeAtaIxns, 'inBetween', requestElevationGroup, addInitObligationForFarm, isClosingPosition);
}
addRefreshObligation(crank) {
const uniqueReserveAddresses = new utils_1.PublicKeySet(this.depositReserves.concat(this.borrowReserves)).toArray();
const addAllToSetupIxns = 'setup';
// Union of addresses
const allReservesExcludingCurrent = [...uniqueReserveAddresses];
this.addRefreshReserveIxs(allReservesExcludingCurrent, addAllToSetupIxns);
this.addRefreshFarmsForReserve(this.depositReserves.map((r) => this.kaminoMarket.getReserveByAddress(r)), addAllToSetupIxns, types_1.ReserveFarmKind.Collateral, crank);
this.addRefreshFarmsForReserve(this.borrowReserves.map((r) => this.kaminoMarket.getReserveByAddress(r)), addAllToSetupIxns, types_1.ReserveFarmKind.Debt, crank);
this.addRefreshObligationIx(addAllToSetupIxns, false);
}
async addSupportIxsWithoutInitObligation(action, includeAtaIxns, addAsSupportIx = 'setup', requestElevationGroup = false, addInitObligationForFarm = false, isClosingPosition = false, twoTokenAction = false, overrideElevationGroupRequest) {
// TODO: why are we not doing this first?
if (includeAtaIxns) {
await this.addAtaIxs(action);
}
if ([
'depositCollateral',
'deposit',
'withdraw',
'borrow',
'liquidate',
'repay',
'depositAndBorrow',
'repayAndWithdraw',
'refreshObligation',
].includes(action)) {
// The support ixns in order are:
// 0. Init obligation ixn
// 0. Token Ata ixns
// 0. Init obligation for farm
// 1. Ixns to refresh the reserves of the obligation not related to the current action
// 2. Ixn to refresh the reserve of the current action
// 3. Ixn to refresh the obligation