UNPKG

@kamino-finance/klend-sdk

Version:

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

1,539 lines (1,372 loc) 104 kB
import { Connection, PublicKey, RpcResponseAndContext, SimulatedTransactionResponse, SystemProgram, SYSVAR_INSTRUCTIONS_PUBKEY, SYSVAR_RENT_PUBKEY, Transaction, TransactionInstruction, TransactionSignature, } from '@solana/web3.js'; import { ASSOCIATED_TOKEN_PROGRAM_ID, NATIVE_MINT, TOKEN_PROGRAM_ID, createCloseAccountInstruction, createSyncNativeInstruction, } from '@solana/spl-token'; import BN from 'bn.js'; import Decimal from 'decimal.js'; import { borrowObligationLiquidity, depositObligationCollateral, depositReserveLiquidity, depositReserveLiquidityAndObligationCollateral, initObligation, initObligationFarmsForReserve, InitObligationFarmsForReserveAccounts, InitObligationFarmsForReserveArgs, initReferrerTokenState, initUserMetadata, liquidateObligationAndRedeemReserveCollateral, redeemReserveCollateral, refreshObligation, refreshObligationFarmsForReserve, RefreshObligationFarmsForReserveAccounts, RefreshObligationFarmsForReserveArgs, refreshReserve, repayObligationLiquidity, requestElevationGroup, RequestElevationGroupAccounts, RequestElevationGroupArgs, withdrawObligationCollateralAndRedeemReserveCollateral, withdrawReferrerFees, } from '../idl_codegen/instructions'; import { buildComputeBudgetIx, createAssociatedTokenAccountIdempotentInstruction, ObligationType, U64_MAX, referrerTokenStatePda, userMetadataPda, createLookupTableIx, isNotNullPubkey, PublicKeySet, WRAPPED_SOL_MINT, getAssociatedTokenAddress, ScopeRefresh, createAtasIdempotent, } from '../utils'; import { KaminoMarket } from './market'; import { KaminoObligation } from './obligation'; import { KaminoReserve } from './reserve'; import { ReserveFarmKind } from '../idl_codegen/types'; import { farmsId } from '@kamino-finance/farms-sdk'; import { Reserve } from '../idl_codegen/accounts'; import { VanillaObligation } from '../utils/ObligationType'; import { PROGRAM_ID } from '../lib'; import { U16_MAX } from '@kamino-finance/scope-sdk'; export const POSITION_LIMIT = 10; export const BORROWS_LIMIT = 5; export const DEPOSITS_LIMIT = 8; const SOL_PADDING_FOR_INTEREST = new BN('1000000'); export type ActionType = | 'deposit' | 'borrow' | 'withdraw' | 'repay' | 'mint' | 'redeem' | 'depositCollateral' | 'liquidate' | 'depositAndBorrow' | 'repayAndWithdraw' | 'refreshObligation' | 'requestElevationGroup' | 'withdrawReferrerFees'; export type AuxiliaryIx = 'setup' | 'inBetween' | 'cleanup'; export class KaminoAction { kaminoMarket: KaminoMarket; reserve: KaminoReserve; outflowReserve: KaminoReserve | undefined; owner: PublicKey; payer: PublicKey; obligation: KaminoObligation | null = null; referrer: PublicKey; userTokenAccountAddress: PublicKey; userCollateralAccountAddress: PublicKey; additionalTokenAccountAddress?: PublicKey; /** * Null unless the obligation is not passed */ obligationType: ObligationType | null = null; mint: PublicKey; secondaryMint?: PublicKey; positions?: number; amount: BN; outflowAmount?: BN; computeBudgetIxs: Array<TransactionInstruction>; computeBudgetIxsLabels: Array<string>; setupIxs: Array<TransactionInstruction>; setupIxsLabels: Array<string>; inBetweenIxs: Array<TransactionInstruction>; inBetweenIxsLabels: Array<string>; lendingIxs: Array<TransactionInstruction>; lendingIxsLabels: Array<string>; cleanupIxs: Array<TransactionInstruction>; cleanupIxsLabels: Array<string>; preTxnIxs: Array<TransactionInstruction>; preTxnIxsLabels: Array<string>; postTxnIxs: Array<TransactionInstruction>; postTxnIxsLabels: Array<string>; refreshFarmsCleanupTxnIxs: Array<TransactionInstruction>; refreshFarmsCleanupTxnIxsLabels: Array<string>; depositReserves: Array<PublicKey>; borrowReserves: Array<PublicKey>; preLoadedDepositReservesSameTx: Array<PublicKey>; preLoadedBorrowReservesSameTx: Array<PublicKey>; currentSlot: number; private constructor( kaminoMarket: KaminoMarket, owner: PublicKey, obligation: KaminoObligation | ObligationType | null, userTokenAccountAddress: PublicKey, userCollateralAccountAddress: PublicKey, mint: PublicKey, positions: number, amount: string | BN, depositReserves: Array<PublicKey>, borrowReserves: Array<PublicKey>, reserveState: KaminoReserve, currentSlot: number, secondaryMint?: PublicKey, additionalTokenAccountAddress?: PublicKey, outflowReserveState?: KaminoReserve, outflowAmount?: string | BN, referrer?: PublicKey, payer?: PublicKey ) { if (obligation instanceof 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(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(outflowAmount) : undefined; this.preLoadedDepositReservesSameTx = []; this.preLoadedBorrowReservesSameTx = []; this.referrer = referrer ? referrer : PublicKey.default; this.currentSlot = currentSlot; } static async initialize( action: ActionType, amount: string | BN, mint: PublicKey, owner: PublicKey, kaminoMarket: KaminoMarket, obligation: KaminoObligation | ObligationType, referrer: PublicKey = PublicKey.default, currentSlot: number = 0, payer?: PublicKey ) { 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 ); } private static getUserAccountAddresses(owner: PublicKey, reserve: Reserve) { const userTokenAccountAddress = getAssociatedTokenAddress( reserve.liquidity.mintPubkey, owner, true, reserve.liquidity.tokenProgram, ASSOCIATED_TOKEN_PROGRAM_ID ); const userCollateralAccountAddress = getAssociatedTokenAddress( reserve.collateral.mintPubkey, owner, true, TOKEN_PROGRAM_ID, ASSOCIATED_TOKEN_PROGRAM_ID ); return { userTokenAccountAddress, userCollateralAccountAddress }; } private static async loadObligation( action: ActionType, kaminoMarket: KaminoMarket, owner: PublicKey, reserve: PublicKey, obligation: KaminoObligation | ObligationType, outflowReserve?: PublicKey ) { let kaminoObligation: KaminoObligation | null; const depositReserves: Array<PublicKey> = []; const borrowReserves: Array<PublicKey> = []; if (obligation instanceof KaminoObligation) { kaminoObligation = obligation; } else { const obligationAddress = obligation.toPda(kaminoMarket.getAddress(), owner); kaminoObligation = await 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 PublicKeySet<PublicKey>([ ...borrowReserves.map((e) => e), ...(action === 'borrow' ? [reserve] : []), ...(action === 'depositAndBorrow' ? [reserve] : []), ]).toArray().length + new PublicKeySet<PublicKey>([ ...depositReserves.map((e) => e), ...(action === 'deposit' ? [reserve] : []), ...(action === 'depositAndBorrow' ? [outflowReserve!] : []), ]).toArray().length; if (distinctReserveCount > POSITION_LIMIT) { throw Error(`Obligation already has max number of positions: ${POSITION_LIMIT}`); } return { kaminoObligation, depositReserves, borrowReserves, distinctReserveCount, }; } static async buildRefreshObligationTxns( kaminoMarket: KaminoMarket, payer: PublicKey, obligation: KaminoObligation, extraComputeBudget: number = 1_000_000, // if > 0 then adds the ixn currentSlot: number = 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: KaminoMarket, payer: PublicKey, obligation: KaminoObligation, elevationGroup: number, extraComputeBudget: number = 1_000_000, // if > 0 then adds the ixn currentSlot: number = 0 ) { const firstReserve = obligation.state.deposits.find( (x) => !x.depositReserve.equals(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: KaminoMarket, amount: string | BN, mint: PublicKey, owner: PublicKey, obligation: KaminoObligation | ObligationType, extraComputeBudget: number = 1_000_000, // if > 0 then adds the ixn includeAtaIxns: boolean = true, // if true it includes create and close wsol and token atas, requestElevationGroup: boolean = false, includeUserMetadata: boolean = true, // if true it includes user metadata referrer: PublicKey = PublicKey.default, currentSlot: number = 0, scopeRefresh: ScopeRefresh = { includeScopeRefresh: false, scopeFeed: 'hubble' }, overrideElevationGroupRequest: number | undefined = 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 PublicKeySet<PublicKey>([ ...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: KaminoMarket, reserves: PublicKey[]): number[] { const tokenIds: number[] = []; 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(PublicKey.default)) { reserve.state.config.tokenInfo.scopeConfiguration.priceChain.map((x) => { if (x !== U16_MAX) { tokenIds.push(x); } }); reserve.state.config.tokenInfo.scopeConfiguration.twapChain.map((x) => { if (x !== U16_MAX) { tokenIds.push(x); } }); } } return tokenIds; } async addScopeRefreshIxs(tokens: number[], feed: string = 'hubble') { this.preTxnIxsLabels.unshift(`refreshScopePrices`); this.preTxnIxs.unshift( await this.kaminoMarket.scope.refreshPriceListIx( { feed: feed, }, tokens ) ); } static async buildBorrowTxns( kaminoMarket: KaminoMarket, amount: string | BN, mint: PublicKey, owner: PublicKey, obligation: KaminoObligation | ObligationType, extraComputeBudget: number = 1_000_000, // if > 0 then adds the ixn includeAtaIxns: boolean = true, // if true it includes create and close wsol and token atas, requestElevationGroup: boolean = false, includeUserMetadata: boolean = true, // if true it includes user metadata referrer: PublicKey = PublicKey.default, currentSlot: number = 0, scopeRefresh: ScopeRefresh = { includeScopeRefresh: false, scopeFeed: 'hubble' }, overrideElevationGroupRequest: number | undefined = 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 PublicKeySet<PublicKey>([ ...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: KaminoMarket, amount: string | BN, mint: PublicKey, owner: PublicKey, obligation: KaminoObligation | ObligationType, extraComputeBudget: number = 1_000_000, // if > 0 then adds the ixn includeAtaIxns: boolean = true, // if true it includes create and close wsol and token atas requestElevationGroup: boolean = false, includeUserMetadata: boolean = true, // if true it includes user metadata referrer: PublicKey = PublicKey.default, currentSlot: number = 0, scopeRefresh: 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 PublicKeySet<PublicKey>([ ...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: KaminoMarket, amount: string | BN, mint: PublicKey, owner: PublicKey, obligation: KaminoObligation | ObligationType, extraComputeBudget: number = 1_000_000, // if > 0 then adds the ixn includeAtaIxns: boolean = true, // if true it includes create and close wsol and token atas requestElevationGroup: boolean = false, includeUserMetadata: boolean = true, // if true it includes user metadata, referrer: PublicKey = PublicKey.default, currentSlot: number = 0, scopeRefresh: 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 PublicKeySet<PublicKey>([ ...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: KaminoMarket, amount: string | BN, mint: PublicKey, owner: PublicKey, obligation: KaminoObligation | ObligationType, extraComputeBudget: number = 1_000_000, // if > 0 then adds the ixn includeAtaIxns: boolean = true, // if true it includes create and close wsol and token atas requestElevationGroup: boolean = false, includeUserMetadata: boolean = true, // if true it includes user metadata referrer: PublicKey = PublicKey.default, currentSlot: number = 0, scopeRefresh: 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 PublicKeySet<PublicKey>([ ...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: KaminoMarket, depositAmount: string | BN, depositMint: PublicKey, borrowAmount: string | BN, borrowMint: PublicKey, payer: PublicKey, obligation: KaminoObligation | ObligationType, extraComputeBudget: number = 1_000_000, // if > 0 then adds the ixn includeAtaIxns: boolean = true, // if true it includes create and close wsol and token atas, requestElevationGroup: boolean = false, includeUserMetadata: boolean = true, // if true it includes user metadata, referrer: PublicKey = PublicKey.default, currentSlot: number = 0, scopeRefresh: 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 PublicKeySet<PublicKey>([ ...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: KaminoMarket, repayAmount: string | BN, repayMint: PublicKey, withdrawAmount: string | BN, withdrawMint: PublicKey, payer: PublicKey, currentSlot: number, obligation: KaminoObligation | ObligationType, extraComputeBudget: number = 1_000_000, // if > 0 then adds the ixn includeAtaIxns: boolean = true, // if true it includes create and close wsol and token atas, requestElevationGroup: boolean = false, includeUserMetadata: boolean = true, // if true it includes user metadata, isClosingPosition: boolean = false, referrer: PublicKey = PublicKey.default, scopeRefresh: 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 PublicKeySet<PublicKey>([ ...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: KaminoMarket, amount: string | BN, mint: PublicKey, owner: PublicKey, obligation: KaminoObligation | ObligationType, extraComputeBudget: number = 1_000_000, // if > 0 then adds the ixn includeAtaIxns: boolean = true, // if true it includes create and close wsol and token atas, requestElevationGroup: boolean = false, includeUserMetadata: boolean = true, // if true it includes user metadata referrer: PublicKey = PublicKey.default, currentSlot: number = 0, scopeRefresh: 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 PublicKeySet<PublicKey>([ ...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: KaminoMarket, amount: string | BN, mint: PublicKey, owner: PublicKey, obligation: KaminoObligation | ObligationType, currentSlot: number, payer: PublicKey | undefined = undefined, extraComputeBudget: number = 1_000_000, includeAtaIxns: boolean = true, requestElevationGroup: boolean = false, includeUserMetadata: boolean = true, referrer: PublicKey = PublicKey.default, scopeRefresh: 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 PublicKeySet<PublicKey>([ ...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: KaminoMarket, amount: string | BN, minCollateralReceiveAmount: string | BN, repayTokenMint: PublicKey, withdrawTokenMint: PublicKey, liquidator: PublicKey, obligationOwner: PublicKey, obligation: KaminoObligation | ObligationType, extraComputeBudget: number = 1_000_000, // if > 0 then adds the ixn includeAtaIxns: boolean = true, // if true it includes create and close wsol and token atas, and creates all other token atas if they don't exist requestElevationGroup: boolean = false, includeUserMetadata: boolean = true, // if true it includes user metadata referrer: PublicKey = PublicKey.default, maxAllowedLtvOverridePercent: number = 0, currentSlot: number = 0, scopeRefresh: 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 PublicKeySet<PublicKey>([ ...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: PublicKey, tokenMint: PublicKey, kaminoMarket: KaminoMarket, currentSlot: number = 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: Transaction | null; lendingTxn: Transaction | null; postLendingTxn: Transaction | null; } = { preLendingTxn: null, lendingTxn: null, postLendingTxn: null, }; if (this.preTxnIxs.length) { txns.preLendingTxn = new Transaction({ feePayer: this.owner, recentBlockhash: (await this.kaminoMarket.getConnection().getLatestBlockhash()).blockhash, }).add(...this.preTxnIxs); } if (this.lendingIxs.length === 2) { txns.lendingTxn = new 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 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 Transaction({ feePayer: this.owner, recentBlockhash: (await this.kaminoMarket.getConnection().getLatestBlockhash()).blockhash, }).add(...this.postTxnIxs); } return txns; } async sendTransactions(sendTransaction: (txn: Transaction, connection: Connection) => Promise<TransactionSignature>) { 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; } private async sendSingleTransaction( txn: Transaction | null, sendTransaction: (txn: Transaction, connection: Connection) => Promise<TransactionSignature> ) { if (!txn) return ''; const signature = await sendTransaction(txn, this.kaminoMarket.getConnection()); await this.kaminoMarket.getConnection().confirmTransaction(signature); return signature; } async simulateTransactions( sendTransaction: ( txn: Transaction, connection: Connection ) => Promise<RpcResponseAndContext<SimulatedTransactionResponse>> ) { 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; } private async simulateSingleTransaction( txn: Transaction | null, sendTransaction: ( txn: Transaction, connection: Connection ) => Promise<RpcResponseAndContext<SimulatedTransactionResponse>> ) { if (!txn) return ''; return await sendTransaction(txn, this.kaminoMarket.getConnection()); } addDepositIx() { this.lendingIxsLabels.push(`depositReserveLiquidityAndObligationCollateral`); this.lendingIxs.push( 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: TOKEN_PROGRAM_ID, liquidityTokenProgram: this.reserve.getLiquidityTokenProgram(), instructionSysvarAccount: SYSVAR_INSTRUCTIONS_PUBKEY, }, this.kaminoMarket.programId ) ); } addDepositReserveLiquidityIx() { this.lendingIxsLabels.push(`depositReserveLiquidity`); this.lendingIxs.push( 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: TOKEN_PROGRAM_ID, liquidityTokenProgram: this.reserve.getLiquidityTokenProgram(), instructionSysvarAccount: SYSVAR_INSTRUCTIONS_PUBKEY, }, this.kaminoMarket.programId ) ); } addRedeemReserveCollateralIx() { this.lendingIxsLabels.push(`redeemReserveCollateral`); this.lendingIxs.push( 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: TOKEN_PROGRAM_ID, liquidityTokenProgram: this.reserve.getLiquidityTokenProgram(), instructionSysvarAccount: SYSVAR_INSTRUCTIONS_PUBKEY, }, this.kaminoMarket.programId ) ); } addDepositObligationCollateralIx() { this.lendingIxsLabels.push(`depositObligationCollateral`); this.lendingIxs.push( 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: TOKEN_PROGRAM_ID, instructionSysvarAccount: 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 = 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: referrerTokenStatePda(this.referrer, this.reserve.address, this.kaminoMarket.programId)[0], tokenProgram: this.reserve.getLiquidityTokenProgram(), instructionSysvarAccount: 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( 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: TOKEN_PROGRAM_ID, liquidityTokenProgram: this.reserve.getLiquidityTokenProgram(), instructionSysvarAccount: 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 = 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: referrerTokenStatePda( this.referrer, this.outflowReserve.address, this.kaminoMarket.programId )[0], tokenProgram: this.outflowReserve.getLiquidityTokenProgram(), instructionSysvarAccount: 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 = 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: 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( withdrawObligationCollateralAndRedeemReserveCollateral( { collateralAmount: this.outflowAmount.eq(new BN(U64_MAX)) ? this.outflowAmount : new BN(new Decimal(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: TOKEN_PROGRAM_ID, liquidityTokenProgram: this.outflowReserve.getLiquidityTokenProgram(), instructionSysvarAccount: 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(U64_MAX)) ? this.amount : new BN(new Decimal(this.amount.toString()).mul(collateralExchangeRate).ceil().toString()); this.lendingIxsLabels.push(`withdrawObligationCollateralAndRedeemReserveCollateral`); this.lendingIxs.push( 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: TOKEN_PROGRAM_ID, liquidityTokenProgram: this.reserve.getLiquidityTokenProgram(), instructionSysvarAccount: 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 = 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: 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: number = 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)