@kamino-finance/klend-sdk
Version:
Typescript SDK for interacting with the Kamino Lending (klend) protocol
1,451 lines (1,331 loc) • 42.8 kB
text/typescript
import { Address, Instruction, Slot, Option, none, TransactionSigner, lamports } from '@solana/kit';
import Decimal from 'decimal.js';
import {
KaminoAction,
KaminoMarket,
KaminoObligation,
KaminoReserve,
lamportsToNumberDecimal as fromLamports,
getTokenIdsForScopeRefresh,
isKaminoObligation,
toJson,
} from '../classes';
import { getFlashLoanInstructions } from './instructions';
import { numberToLamportsDecimal as toLamports } from '../classes';
import {
LeverageObligation,
MultiplyObligation,
ObligationType,
ObligationTypeTag,
SOL_DECIMALS,
ScopePriceRefreshConfig,
U64_MAX,
createAtasIdempotent,
getAssociatedTokenAddress,
getComputeBudgetAndPriorityFeeIxs,
getTransferWsolIxs,
removeBudgetIxs,
uniqueAccountsWithProgramIds,
WRAPPED_SOL_MINT,
} from '../utils';
import {
adjustDepositLeverageCalcs,
adjustWithdrawLeverageCalcs,
calcAdjustAmounts,
depositLeverageCalcs,
withdrawLeverageCalcs,
} from './calcs';
import { FullBPS } from '@kamino-finance/kliquidity-sdk/dist/utils/CreationParameters';
import {
AdjustLeverageCalcsResult,
AdjustLeverageInitialInputs,
AdjustLeverageIxsResponse,
AdjustLeverageProps,
AdjustLeverageSwapInputsProps,
DepositLeverageCalcsResult,
DepositLeverageInitialInputs,
DepositWithLeverageProps,
DepositWithLeverageSwapInputsProps,
DepositLeverageIxsResponse,
SwapInputs,
SwapIxs,
SwapIxsProvider,
WithdrawLeverageCalcsResult,
WithdrawLeverageInitialInputs,
WithdrawLeverageIxsResponse,
WithdrawWithLeverageProps,
WithdrawWithLeverageSwapInputsProps,
LeverageIxsOutput,
FlashLoanInfo,
} from './types';
import { TOKEN_PROGRAM_ADDRESS } from '@solana-program/token';
import { findAssociatedTokenPda, getCloseAccountInstruction } from '@solana-program/token-2022';
import { LAMPORTS_PER_SOL } from '../utils/consts';
export const WITHDRAW_SLOT_OFFSET = 150; // Offset for the withdraw slot to underestimate the exchange rate
export async function getDepositWithLeverageSwapInputs<QuoteResponse>({
owner,
kaminoMarket,
debtTokenMint,
collTokenMint,
depositAmount,
priceDebtToColl,
slippagePct,
obligation,
referrer,
currentSlot,
targetLeverage,
selectedTokenMint,
obligationTypeTagOverride,
scopeRefreshIx,
budgetAndPriorityFeeIxs,
quoteBufferBps,
quoter,
useV2Ixs,
elevationGroupOverride,
}: DepositWithLeverageSwapInputsProps<QuoteResponse>): Promise<{
flashLoanInfo: FlashLoanInfo;
swapInputs: SwapInputs;
initialInputs: DepositLeverageInitialInputs<QuoteResponse>;
}> {
const collReserve = kaminoMarket.getExistingReserveByMint(collTokenMint);
const debtReserve = kaminoMarket.getExistingReserveByMint(debtTokenMint);
const solTokenReserve = kaminoMarket.getReserveByMint(WRAPPED_SOL_MINT);
const flashLoanFee = collReserve.getFlashLoanFee() || new Decimal(0);
const selectedTokenIsCollToken = selectedTokenMint === collTokenMint;
const depositTokenIsSol = !solTokenReserve ? false : selectedTokenMint === solTokenReserve!.getLiquidityMint();
const calcs = depositLeverageCalcs({
depositAmount: depositAmount,
depositTokenIsCollToken: selectedTokenIsCollToken,
depositTokenIsSol,
priceDebtToColl,
targetLeverage,
slippagePct,
flashLoanFee,
});
console.log('Ops Calcs', toJson(calcs));
const obligationType = checkObligationType(obligationTypeTagOverride, collTokenMint, debtTokenMint, kaminoMarket);
// Build the repay & withdraw collateral tx to get the number of accounts
const klendIxs: LeverageIxsOutput = (
await buildDepositWithLeverageIxs(
kaminoMarket,
debtReserve,
collReserve,
owner,
obligation ? obligation : obligationType,
referrer,
currentSlot,
depositTokenIsSol,
scopeRefreshIx,
calcs,
budgetAndPriorityFeeIxs,
[
{
preActionIxs: [],
swapIxs: [],
lookupTables: [],
quote: {
priceAInB: new Decimal(0), // not used
quoteResponse: undefined,
},
},
],
useV2Ixs,
elevationGroupOverride
)
)[0];
const uniqueKlendAccounts = uniqueAccountsWithProgramIds(klendIxs.instructions);
const swapInputAmount = toLamports(calcs.swapDebtTokenIn, debtReserve.stats.decimals).ceil();
const swapInputsForQuote: SwapInputs = {
inputAmountLamports: swapInputAmount.mul(new Decimal(1).add(quoteBufferBps.div(FullBPS))),
inputMint: debtTokenMint,
outputMint: collTokenMint,
};
const swapQuote = await quoter(swapInputsForQuote, uniqueKlendAccounts);
const quotePriceCalcs = depositLeverageCalcs({
depositAmount: depositAmount,
depositTokenIsCollToken: selectedTokenIsCollToken,
depositTokenIsSol,
priceDebtToColl: swapQuote.priceAInB,
targetLeverage,
slippagePct,
flashLoanFee,
});
const swapInputAmountQuotePrice = toLamports(quotePriceCalcs.swapDebtTokenIn, debtReserve.stats.decimals).ceil();
return {
swapInputs: {
inputAmountLamports: swapInputAmountQuotePrice,
minOutAmountLamports: toLamports(quotePriceCalcs.flashBorrowInCollToken, collReserve.stats.decimals),
inputMint: debtTokenMint,
outputMint: collTokenMint,
},
flashLoanInfo: klendIxs.flashLoanInfo,
initialInputs: {
calcs: quotePriceCalcs,
swapQuote,
currentSlot,
obligation: obligation ? obligation : obligationType,
klendAccounts: uniqueKlendAccounts,
},
};
}
export async function getDepositWithLeverageIxs<QuoteResponse>({
owner,
kaminoMarket,
debtTokenMint,
collTokenMint,
depositAmount,
priceDebtToColl,
slippagePct,
obligation,
referrer,
currentSlot,
targetLeverage,
selectedTokenMint,
obligationTypeTagOverride,
scopeRefreshIx,
budgetAndPriorityFeeIxs,
quoteBufferBps,
quoter,
swapper,
elevationGroupOverride,
useV2Ixs,
}: DepositWithLeverageProps<QuoteResponse>): Promise<Array<DepositLeverageIxsResponse<QuoteResponse>>> {
const { swapInputs, initialInputs } = await getDepositWithLeverageSwapInputs({
owner,
kaminoMarket,
debtTokenMint,
collTokenMint,
depositAmount,
priceDebtToColl,
slippagePct,
obligation,
referrer,
currentSlot,
targetLeverage,
selectedTokenMint,
obligationTypeTagOverride,
scopeRefreshIx,
budgetAndPriorityFeeIxs,
quoteBufferBps,
quoter,
useV2Ixs,
});
const depositSwapper: SwapIxsProvider<QuoteResponse> = swapper;
const swapsArray = await depositSwapper(swapInputs, initialInputs.klendAccounts, initialInputs.swapQuote);
// Strategy lookup table logic removed
const collReserve = kaminoMarket.getReserveByMint(collTokenMint);
const debtReserve = kaminoMarket.getReserveByMint(debtTokenMint);
const solTokenReserve = kaminoMarket.getReserveByMint(WRAPPED_SOL_MINT);
const depositTokenIsSol = !solTokenReserve ? false : selectedTokenMint === solTokenReserve!.getLiquidityMint();
const depositWithLeverageIxs = await buildDepositWithLeverageIxs(
kaminoMarket,
debtReserve!,
collReserve!,
owner,
initialInputs.obligation,
referrer,
currentSlot,
depositTokenIsSol,
scopeRefreshIx,
initialInputs.calcs,
budgetAndPriorityFeeIxs,
swapsArray.map((swap) => {
return {
preActionIxs: [],
swapIxs: swap.swapIxs,
lookupTables: swap.lookupTables,
quote: swap.quote,
};
}),
useV2Ixs,
elevationGroupOverride
);
return depositWithLeverageIxs.map((depositWithLeverageIxs, index) => {
return {
ixs: depositWithLeverageIxs.instructions,
flashLoanInfo: depositWithLeverageIxs.flashLoanInfo,
lookupTables: swapsArray[index].lookupTables,
swapInputs,
initialInputs,
quote: swapsArray[index].quote.quoteResponse,
};
});
}
async function buildDepositWithLeverageIxs<QuoteResponse>(
market: KaminoMarket,
debtReserve: KaminoReserve,
collReserve: KaminoReserve,
owner: TransactionSigner,
obligation: KaminoObligation | ObligationType | undefined,
referrer: Option<Address>,
currentSlot: Slot,
depositTokenIsSol: boolean,
scopeRefreshIx: Instruction[],
calcs: DepositLeverageCalcsResult,
budgetAndPriorityFeeIxs: Instruction[] | undefined,
swapQuoteIxsArray: SwapIxs<QuoteResponse>[],
useV2Ixs: boolean,
elevationGroupOverride?: number
): Promise<LeverageIxsOutput[]> {
const collTokenMint = collReserve.getLiquidityMint();
const debtTokenMint = debtReserve.getLiquidityMint();
const [[collTokenAta]] = await Promise.all([
findAssociatedTokenPda({
owner: owner.address,
mint: collTokenMint,
tokenProgram: collReserve.getLiquidityTokenProgram(),
}),
]);
// 1. Create atas & budget ixs
const { budgetIxs, createAtasIxs } = await getSetupIxs(
owner,
collTokenMint,
collReserve,
debtTokenMint,
debtReserve,
budgetAndPriorityFeeIxs
);
const fillWsolAtaIxs: Instruction[] = [];
if (depositTokenIsSol) {
fillWsolAtaIxs.push(
...getTransferWsolIxs(
owner,
await getAssociatedTokenAddress(WRAPPED_SOL_MINT, owner.address),
lamports(BigInt(toLamports(calcs.initDepositInSol, SOL_DECIMALS).ceil().toString()))
)
);
}
// 2. Flash borrow & repay the collateral amount needed for given leverage
// if user deposits coll, then we borrow the diff, else we borrow the entire amount
const { flashBorrowIx, flashRepayIx } = getFlashLoanInstructions({
borrowIxIndex: createAtasIxs.length + fillWsolAtaIxs.length + (scopeRefreshIx.length > 0 ? 1 : 0),
userTransferAuthority: owner,
lendingMarketAuthority: await market.getLendingMarketAuthority(),
lendingMarketAddress: market.getAddress(),
reserve: collReserve,
amountLamports: toLamports(calcs.flashBorrowInCollToken, collReserve.stats.decimals),
destinationAta: collTokenAta,
// TODO(referrals): once we support referrals, we will have to replace the placeholder args below:
referrerAccount: none(),
referrerTokenState: none(),
programId: market.programId,
});
// 3. Deposit initial tokens + borrowed tokens into reserve
const kaminoDepositAndBorrowAction = await KaminoAction.buildDepositAndBorrowTxns(
market,
toLamports(calcs.collTokenToDeposit, collReserve.stats.decimals).floor().toString(),
collTokenMint,
toLamports(calcs.debtTokenToBorrow, debtReserve.stats.decimals).ceil().toString(),
debtTokenMint,
owner,
obligation!,
useV2Ixs,
undefined,
0,
false,
elevationGroupOverride === 0 ? false : true, // emode
{ skipInitialization: true, skipLutCreation: true }, // to be checked and created in a setup tx in the UI
referrer,
currentSlot
);
return swapQuoteIxsArray.map((swapQuoteIxs) => {
// 4. Swap
const { swapIxs } = swapQuoteIxs;
const swapInstructions = removeBudgetIxs(swapIxs);
const flashBorrowReserve = collReserve;
const flashLoanInfo = {
flashBorrowReserve: flashBorrowReserve.address,
flashLoanFee: flashBorrowReserve.getFlashLoanFee(),
};
return {
flashLoanInfo,
instructions: [
...scopeRefreshIx,
...createAtasIxs,
...fillWsolAtaIxs,
...[flashBorrowIx],
...KaminoAction.actionToIxs(kaminoDepositAndBorrowAction),
...swapInstructions,
...[flashRepayIx],
...budgetIxs,
],
};
});
}
export async function getWithdrawWithLeverageSwapInputs<QuoteResponse>({
owner,
kaminoMarket,
debtTokenMint,
collTokenMint,
deposited,
borrowed,
obligation,
referrer,
currentSlot,
withdrawAmount,
priceCollToDebt,
slippagePct,
isClosingPosition,
selectedTokenMint,
budgetAndPriorityFeeIxs,
scopeRefreshIx,
quoteBufferBps,
quoter,
useV2Ixs,
userSolBalanceLamports,
}: WithdrawWithLeverageSwapInputsProps<QuoteResponse>): Promise<{
swapInputs: SwapInputs;
flashLoanInfo: FlashLoanInfo;
initialInputs: WithdrawLeverageInitialInputs<QuoteResponse>;
}> {
const collReserve = kaminoMarket.getReserveByMint(collTokenMint);
const debtReserve = kaminoMarket.getReserveByMint(debtTokenMint);
const flashLoanFee = debtReserve!.getFlashLoanFee() || new Decimal(0);
const selectedTokenIsCollToken = selectedTokenMint === collTokenMint;
const inputTokenIsSol = selectedTokenMint === WRAPPED_SOL_MINT;
const calcs = withdrawLeverageCalcs(
kaminoMarket,
collReserve!,
debtReserve!,
priceCollToDebt,
withdrawAmount,
deposited,
borrowed,
currentSlot,
isClosingPosition,
selectedTokenIsCollToken,
selectedTokenMint,
obligation,
flashLoanFee,
slippagePct
);
const klendIxs = (
await buildWithdrawWithLeverageIxs(
kaminoMarket,
debtReserve!,
collReserve!,
owner,
obligation,
referrer,
currentSlot,
isClosingPosition,
inputTokenIsSol,
scopeRefreshIx,
calcs,
budgetAndPriorityFeeIxs,
[
{
preActionIxs: [],
swapIxs: [],
lookupTables: [],
quote: {
priceAInB: new Decimal(0), // not used
quoteResponse: undefined,
},
},
],
useV2Ixs,
userSolBalanceLamports
)
)[0];
const uniqueKlendAccounts = uniqueAccountsWithProgramIds(klendIxs.instructions);
const swapInputAmount = toLamports(calcs.collTokenSwapIn, collReserve!.getMintDecimals()).ceil();
const swapInputsForQuote: SwapInputs = {
inputAmountLamports: swapInputAmount.mul(new Decimal(1).add(quoteBufferBps.div(FullBPS))),
inputMint: collTokenMint,
outputMint: debtTokenMint,
};
const swapQuote = await quoter(swapInputsForQuote, uniqueKlendAccounts);
const calcsQuotePrice = withdrawLeverageCalcs(
kaminoMarket,
collReserve!,
debtReserve!,
swapQuote.priceAInB,
withdrawAmount,
deposited,
borrowed,
currentSlot,
isClosingPosition,
selectedTokenIsCollToken,
selectedTokenMint,
obligation,
flashLoanFee,
slippagePct
);
const swapInputAmountQuotePrice = toLamports(calcsQuotePrice.collTokenSwapIn, collReserve!.getMintDecimals()).ceil();
return {
swapInputs: {
inputAmountLamports: swapInputAmountQuotePrice,
minOutAmountLamports: calcsQuotePrice.repayAmount,
inputMint: collTokenMint,
outputMint: debtTokenMint,
},
flashLoanInfo: klendIxs.flashLoanInfo,
initialInputs: {
calcs: calcsQuotePrice,
swapQuote,
currentSlot,
obligation,
klendAccounts: uniqueKlendAccounts,
},
};
}
export async function getWithdrawWithLeverageIxs<QuoteResponse>({
owner,
kaminoMarket,
debtTokenMint,
collTokenMint,
obligation,
deposited,
borrowed,
referrer,
currentSlot,
withdrawAmount,
priceCollToDebt,
slippagePct,
isClosingPosition,
selectedTokenMint,
budgetAndPriorityFeeIxs,
scopeRefreshIx,
quoteBufferBps,
quoter,
swapper,
useV2Ixs,
userSolBalanceLamports,
}: WithdrawWithLeverageProps<QuoteResponse>): Promise<Array<WithdrawLeverageIxsResponse<QuoteResponse>>> {
const collReserve = kaminoMarket.getReserveByMint(collTokenMint);
const debtReserve = kaminoMarket.getReserveByMint(debtTokenMint);
const inputTokenIsSol = selectedTokenMint === WRAPPED_SOL_MINT;
const { swapInputs, initialInputs } = await getWithdrawWithLeverageSwapInputs({
owner,
kaminoMarket,
debtTokenMint,
collTokenMint,
deposited,
borrowed,
obligation,
referrer,
currentSlot,
withdrawAmount,
priceCollToDebt,
slippagePct,
isClosingPosition,
selectedTokenMint,
budgetAndPriorityFeeIxs,
scopeRefreshIx,
quoteBufferBps,
quoter,
useV2Ixs,
userSolBalanceLamports,
});
const withdrawSwapper: SwapIxsProvider<QuoteResponse> = swapper;
const swapsArray = await withdrawSwapper(swapInputs, initialInputs.klendAccounts, initialInputs.swapQuote);
// Strategy lookup table logic removed
const withdrawWithLeverageIxs = await buildWithdrawWithLeverageIxs<QuoteResponse>(
kaminoMarket,
debtReserve!,
collReserve!,
owner,
obligation,
referrer,
currentSlot,
isClosingPosition,
inputTokenIsSol,
scopeRefreshIx,
initialInputs.calcs,
budgetAndPriorityFeeIxs,
swapsArray.map((swap) => {
return {
preActionIxs: [],
swapIxs: swap.swapIxs,
lookupTables: swap.lookupTables,
quote: swap.quote,
};
}),
useV2Ixs,
userSolBalanceLamports
);
// Send ixs and lookup tables
return withdrawWithLeverageIxs.map((ixs, index) => {
return {
ixs: ixs.instructions,
flashLoanInfo: ixs.flashLoanInfo,
lookupTables: swapsArray[index].lookupTables,
swapInputs,
initialInputs: initialInputs,
quote: swapsArray[index].quote.quoteResponse,
};
});
}
export async function buildWithdrawWithLeverageIxs<QuoteResponse>(
market: KaminoMarket,
debtReserve: KaminoReserve,
collReserve: KaminoReserve,
owner: TransactionSigner,
obligation: KaminoObligation,
referrer: Option<Address>,
currentSlot: Slot,
isClosingPosition: boolean,
depositTokenIsSol: boolean,
scopeRefreshIx: Instruction[],
calcs: WithdrawLeverageCalcsResult,
budgetAndPriorityFeeIxs: Instruction[] | undefined,
swapQuoteIxsArray: SwapIxs<QuoteResponse>[],
useV2Ixs: boolean,
userSolBalanceLamports: number
): Promise<LeverageIxsOutput[]> {
const collTokenMint = collReserve.getLiquidityMint();
const debtTokenMint = debtReserve.getLiquidityMint();
const debtTokenAta = await getAssociatedTokenAddress(
debtTokenMint,
owner.address,
debtReserve.getLiquidityTokenProgram()
);
// 1. Create atas & budget txns & user metadata
const { budgetIxs, createAtasIxs } = await getSetupIxs(
owner,
collTokenMint,
collReserve,
debtTokenMint,
debtReserve,
budgetAndPriorityFeeIxs
);
const closeWsolAtaIxs: Instruction[] = [];
if (depositTokenIsSol || debtTokenMint === WRAPPED_SOL_MINT) {
const wsolAta = await getAssociatedTokenAddress(WRAPPED_SOL_MINT, owner.address);
closeWsolAtaIxs.push(
getCloseAccountInstruction(
{
owner,
destination: owner.address,
account: wsolAta,
},
{ programAddress: TOKEN_PROGRAM_ADDRESS }
)
);
}
// TODO: Mihai/Marius check if we can improve this logic and not convert any SOL
// This is here so that we have enough wsol to repay in case the kAB swapped to sol after estimates is not enough
const fillWsolAtaIxs: Instruction[] = [];
if (debtTokenMint === WRAPPED_SOL_MINT) {
const halfSolBalance = userSolBalanceLamports / LAMPORTS_PER_SOL / 2;
const balanceToWrap = halfSolBalance < 0.1 ? halfSolBalance : 0.1;
fillWsolAtaIxs.push(
...getTransferWsolIxs(
owner,
await getAssociatedTokenAddress(WRAPPED_SOL_MINT, owner.address),
lamports(BigInt(toLamports(balanceToWrap, SOL_DECIMALS).ceil().toString()))
)
);
}
// 2. Prepare the flash borrow and flash repay amounts and ixs
// We borrow exactly how much we need to repay
// and repay that + flash amount fee
const { flashBorrowIx, flashRepayIx } = getFlashLoanInstructions({
borrowIxIndex: createAtasIxs.length + fillWsolAtaIxs.length + (scopeRefreshIx.length > 0 ? 1 : 0),
userTransferAuthority: owner,
lendingMarketAuthority: await market.getLendingMarketAuthority(),
lendingMarketAddress: market.getAddress(),
reserve: debtReserve!,
amountLamports: toLamports(calcs.repayAmount, debtReserve!.stats.decimals),
destinationAta: debtTokenAta,
referrerAccount: none(),
referrerTokenState: none(),
programId: market.programId,
});
// 3. Repay borrowed tokens and Withdraw tokens from reserve that will be swapped to repay flash loan
const repayAndWithdrawAction = await KaminoAction.buildRepayAndWithdrawTxns(
market,
isClosingPosition ? U64_MAX : toLamports(calcs.repayAmount, debtReserve!.stats.decimals).floor().toString(),
debtTokenMint,
isClosingPosition
? U64_MAX
: toLamports(calcs.depositTokenWithdrawAmount, collReserve!.stats.decimals).ceil().toString(),
collTokenMint,
owner,
currentSlot,
obligation,
useV2Ixs,
undefined,
0,
false,
false,
{ skipInitialization: true, skipLutCreation: true }, // to be checked and created in a setup tx in the UI (won't be the case for withdraw anyway as this would be created in deposit)
referrer
);
return swapQuoteIxsArray.map((swapQuoteIxs) => {
const swapInstructions = removeBudgetIxs(swapQuoteIxs.swapIxs);
return {
flashLoanInfo: {
flashLoanFee: debtReserve.getFlashLoanFee(),
flashBorrowReserve: debtReserve.address,
},
instructions: [
...scopeRefreshIx,
...createAtasIxs,
...fillWsolAtaIxs,
...[flashBorrowIx],
...KaminoAction.actionToIxs(repayAndWithdrawAction),
...swapInstructions,
...[flashRepayIx],
...closeWsolAtaIxs,
...budgetIxs,
],
};
});
}
export async function getAdjustLeverageSwapInputs<QuoteResponse>({
owner,
kaminoMarket,
debtTokenMint,
collTokenMint,
obligation,
depositedLamports,
borrowedLamports,
referrer,
currentSlot,
targetLeverage,
priceCollToDebt,
priceDebtToColl,
slippagePct,
budgetAndPriorityFeeIxs,
scopeRefreshIx,
quoteBufferBps,
quoter,
useV2Ixs,
withdrawSlotOffset,
userSolBalanceLamports,
}: AdjustLeverageSwapInputsProps<QuoteResponse>): Promise<{
swapInputs: SwapInputs;
flashLoanInfo: FlashLoanInfo;
initialInputs: AdjustLeverageInitialInputs<QuoteResponse>;
}> {
const collReserve = kaminoMarket.getReserveByMint(collTokenMint)!;
const debtReserve = kaminoMarket.getReserveByMint(debtTokenMint)!;
const deposited = fromLamports(depositedLamports, collReserve.stats.decimals);
const borrowed = fromLamports(borrowedLamports, debtReserve.stats.decimals);
// Getting current flash loan fee
const currentLeverage = obligation.refreshedStats.leverage;
const isDepositViaLeverage = targetLeverage.gte(new Decimal(currentLeverage));
let flashLoanFee;
if (isDepositViaLeverage) {
flashLoanFee = collReserve.getFlashLoanFee() || new Decimal(0);
} else {
flashLoanFee = debtReserve.getFlashLoanFee() || new Decimal(0);
}
const { adjustDepositPosition, adjustBorrowPosition } = calcAdjustAmounts({
currentDepositPosition: deposited,
currentBorrowPosition: borrowed,
targetLeverage: targetLeverage,
priceCollToDebt: priceCollToDebt,
flashLoanFee: new Decimal(flashLoanFee),
});
const isDeposit = adjustDepositPosition.gte(0) && adjustBorrowPosition.gte(0);
if (isDepositViaLeverage !== isDeposit) {
throw new Error('Invalid target leverage');
}
if (isDeposit) {
const calcs = adjustDepositLeverageCalcs(
debtReserve!,
adjustDepositPosition,
adjustBorrowPosition,
priceDebtToColl,
flashLoanFee,
slippagePct
);
// Build the repay & withdraw collateral tx to get the number of accounts
const klendIxs: LeverageIxsOutput = (
await buildIncreaseLeverageIxs(
owner,
kaminoMarket,
collTokenMint,
debtTokenMint,
obligation,
referrer,
currentSlot,
calcs,
scopeRefreshIx,
[
{
preActionIxs: [],
swapIxs: [],
lookupTables: [],
quote: {
priceAInB: new Decimal(0), // not used
quoteResponse: undefined,
},
},
],
budgetAndPriorityFeeIxs,
useV2Ixs
)
)[0];
const uniqueKlendAccounts = uniqueAccountsWithProgramIds(klendIxs.instructions);
const swapInputAmount = toLamports(calcs.borrowAmount, debtReserve.stats.decimals).ceil();
const swapInputsForQuote: SwapInputs = {
inputAmountLamports: swapInputAmount.mul(new Decimal(1).add(quoteBufferBps.div(FullBPS))),
inputMint: debtTokenMint,
outputMint: collTokenMint,
};
const swapQuote = await quoter(swapInputsForQuote, uniqueKlendAccounts);
const {
adjustDepositPosition: adjustDepositPositionQuotePrice,
adjustBorrowPosition: adjustBorrowPositionQuotePrice,
} = calcAdjustAmounts({
currentDepositPosition: deposited,
currentBorrowPosition: borrowed,
targetLeverage: targetLeverage,
priceCollToDebt: new Decimal(1).div(swapQuote.priceAInB),
flashLoanFee: new Decimal(flashLoanFee),
});
const calcsQuotePrice = adjustDepositLeverageCalcs(
debtReserve,
adjustDepositPositionQuotePrice,
adjustBorrowPositionQuotePrice,
swapQuote.priceAInB,
flashLoanFee,
slippagePct
);
const swapInputAmountQuotePrice = toLamports(calcsQuotePrice.borrowAmount, debtReserve.getMintDecimals()).ceil();
return {
swapInputs: {
inputAmountLamports: swapInputAmountQuotePrice,
minOutAmountLamports: toLamports(calcsQuotePrice.adjustDepositPosition, collReserve.stats.decimals),
inputMint: debtTokenMint,
outputMint: collTokenMint,
},
flashLoanInfo: klendIxs.flashLoanInfo,
initialInputs: {
calcs: calcsQuotePrice,
swapQuote,
currentSlot,
obligation: obligation,
klendAccounts: uniqueKlendAccounts,
isDeposit: isDeposit,
},
};
} else {
const calcs = adjustWithdrawLeverageCalcs(adjustDepositPosition, adjustBorrowPosition, flashLoanFee, slippagePct);
const klendIxs: LeverageIxsOutput = (
await buildDecreaseLeverageIxs(
owner,
kaminoMarket,
collTokenMint,
debtTokenMint,
obligation,
referrer,
currentSlot,
calcs,
scopeRefreshIx,
[
{
preActionIxs: [],
swapIxs: [],
lookupTables: [],
quote: {
priceAInB: new Decimal(0), // not used
quoteResponse: undefined,
},
},
],
budgetAndPriorityFeeIxs,
useV2Ixs,
withdrawSlotOffset,
userSolBalanceLamports
)
)[0];
const uniqueKlendAccounts = uniqueAccountsWithProgramIds(klendIxs.instructions);
const swapInputAmount = toLamports(
calcs.withdrawAmountWithSlippageAndFlashLoanFee,
collReserve.state.liquidity.mintDecimals.toNumber()
).ceil();
const swapInputsForQuote: SwapInputs = {
inputAmountLamports: swapInputAmount.mul(new Decimal(1).add(quoteBufferBps.div(FullBPS))),
inputMint: collTokenMint,
outputMint: debtTokenMint,
};
const swapQuote = await quoter(swapInputsForQuote, uniqueKlendAccounts);
const {
adjustDepositPosition: adjustDepositPositionQuotePrice,
adjustBorrowPosition: adjustBorrowPositionQuotePrice,
} = calcAdjustAmounts({
currentDepositPosition: deposited,
currentBorrowPosition: borrowed,
targetLeverage: targetLeverage,
priceCollToDebt: swapQuote.priceAInB,
flashLoanFee: new Decimal(flashLoanFee),
});
const calcsQuotePrice = adjustWithdrawLeverageCalcs(
adjustDepositPositionQuotePrice,
adjustBorrowPositionQuotePrice,
flashLoanFee,
slippagePct
);
const swapInputAmountQuotePrice = toLamports(
calcsQuotePrice.withdrawAmountWithSlippageAndFlashLoanFee,
collReserve.getMintDecimals()
).ceil();
return {
swapInputs: {
inputAmountLamports: swapInputAmountQuotePrice,
minOutAmountLamports: toLamports(calcsQuotePrice.adjustBorrowPosition.abs(), debtReserve.stats.decimals),
inputMint: collTokenMint,
outputMint: debtTokenMint,
},
flashLoanInfo: klendIxs.flashLoanInfo,
initialInputs: {
calcs: calcsQuotePrice,
swapQuote,
currentSlot,
obligation,
klendAccounts: uniqueKlendAccounts,
isDeposit,
},
};
}
}
export async function getAdjustLeverageIxs<QuoteResponse>({
owner,
kaminoMarket,
debtTokenMint,
collTokenMint,
obligation,
depositedLamports,
borrowedLamports,
referrer,
currentSlot,
targetLeverage,
priceCollToDebt,
priceDebtToColl,
slippagePct,
budgetAndPriorityFeeIxs,
scopeRefreshIx,
quoteBufferBps,
quoter,
swapper,
useV2Ixs,
withdrawSlotOffset,
userSolBalanceLamports,
}: AdjustLeverageProps<QuoteResponse>): Promise<Array<AdjustLeverageIxsResponse<QuoteResponse>>> {
const { swapInputs, initialInputs } = await getAdjustLeverageSwapInputs({
owner,
kaminoMarket,
debtTokenMint,
collTokenMint,
obligation,
depositedLamports,
borrowedLamports,
referrer,
currentSlot,
targetLeverage,
priceCollToDebt,
priceDebtToColl,
slippagePct,
budgetAndPriorityFeeIxs,
scopeRefreshIx,
quoteBufferBps,
quoter,
useV2Ixs,
userSolBalanceLamports,
});
// leverage increased so we need to deposit and borrow more
if (initialInputs.isDeposit) {
const depositSwapper: SwapIxsProvider<QuoteResponse> = swapper;
const swapsArray = await depositSwapper(swapInputs, initialInputs.klendAccounts, initialInputs.swapQuote);
const increaseLeverageIxs = await buildIncreaseLeverageIxs(
owner,
kaminoMarket,
collTokenMint,
debtTokenMint,
obligation,
referrer,
currentSlot,
initialInputs.calcs,
scopeRefreshIx,
swapsArray.map((swap) => {
return {
preActionIxs: [],
swapIxs: swap.swapIxs,
lookupTables: swap.lookupTables,
quote: swap.quote,
};
}),
budgetAndPriorityFeeIxs,
useV2Ixs
);
return increaseLeverageIxs.map((ixs, index) => {
return {
ixs: ixs.instructions,
flashLoanInfo: ixs.flashLoanInfo,
lookupTables: swapsArray[index].lookupTables,
swapInputs,
initialInputs,
quote: swapsArray[index].quote.quoteResponse,
};
});
} else {
console.log('Decreasing leverage');
const withdrawSwapper: SwapIxsProvider<QuoteResponse> = swapper;
// 5. Get swap ixs
const swapsArray = await withdrawSwapper(swapInputs, initialInputs.klendAccounts, initialInputs.swapQuote);
const decreaseLeverageIxs = await buildDecreaseLeverageIxs(
owner,
kaminoMarket,
collTokenMint,
debtTokenMint,
obligation,
referrer,
currentSlot,
initialInputs.calcs,
scopeRefreshIx,
swapsArray.map((swap) => {
return {
preActionIxs: [],
swapIxs: swap.swapIxs,
lookupTables: swap.lookupTables,
quote: swap.quote,
};
}),
budgetAndPriorityFeeIxs,
useV2Ixs,
withdrawSlotOffset,
userSolBalanceLamports
);
return decreaseLeverageIxs.map((ixs, index) => {
return {
ixs: ixs.instructions,
flashLoanInfo: ixs.flashLoanInfo,
lookupTables: swapsArray[index].lookupTables,
swapInputs,
initialInputs,
quote: swapsArray[index].quote.quoteResponse,
};
});
}
}
/**
* Deposit and borrow tokens if leverage increased
*/
async function buildIncreaseLeverageIxs<QuoteResponse>(
owner: TransactionSigner,
kaminoMarket: KaminoMarket,
collTokenMint: Address,
debtTokenMint: Address,
obligation: KaminoObligation,
referrer: Option<Address>,
currentSlot: Slot,
calcs: AdjustLeverageCalcsResult,
scopeRefreshIx: Instruction[],
swapQuoteIxsArray: SwapIxs<QuoteResponse>[],
budgetAndPriorityFeeIxs: Instruction[] | undefined,
useV2Ixs: boolean
): Promise<LeverageIxsOutput[]> {
const collReserve = kaminoMarket.getExistingReserveByMint(collTokenMint);
const debtReserve = kaminoMarket.getExistingReserveByMint(debtTokenMint);
const collTokenAta = await getAssociatedTokenAddress(
collTokenMint,
owner.address,
collReserve.getLiquidityTokenProgram()
);
// 1. Create atas & budget txns
const { budgetIxs, createAtasIxs } = await getSetupIxs(
owner,
collTokenMint,
collReserve,
debtTokenMint,
debtReserve,
budgetAndPriorityFeeIxs
);
// 2. Create borrow flash loan instruction
const { flashBorrowIx, flashRepayIx } = getFlashLoanInstructions({
borrowIxIndex: createAtasIxs.length + (scopeRefreshIx.length > 0 ? 1 : 0), // TODO: how about user metadata ixs
userTransferAuthority: owner,
lendingMarketAuthority: await kaminoMarket.getLendingMarketAuthority(),
lendingMarketAddress: kaminoMarket.getAddress(),
reserve: collReserve!,
amountLamports: toLamports(calcs.adjustDepositPosition, collReserve!.stats.decimals),
destinationAta: collTokenAta,
// TODO(referrals): once we support referrals, we will have to replace the placeholder args below:
referrerAccount: none(),
referrerTokenState: none(),
programId: kaminoMarket.programId,
});
const depositAction = await KaminoAction.buildDepositTxns(
kaminoMarket,
toLamports(calcs.adjustDepositPosition, collReserve!.stats.decimals).floor().toString(),
collTokenMint,
owner,
obligation,
useV2Ixs,
undefined,
0,
false,
false,
{ skipInitialization: true, skipLutCreation: true },
referrer,
currentSlot
);
// 4. Borrow tokens in borrow token reserve that will be swapped to repay flash loan
const borrowAction = await KaminoAction.buildBorrowTxns(
kaminoMarket,
toLamports(calcs.borrowAmount, debtReserve!.stats.decimals).ceil().toString(),
debtTokenMint,
owner,
obligation,
useV2Ixs,
undefined,
0,
false,
false,
{ skipInitialization: true, skipLutCreation: true }, // to be checked and create in a setup tx in the UI (won't be the case for adjust anyway as this would be created in deposit)
referrer,
currentSlot
);
return swapQuoteIxsArray.map((swapQuoteIxs) => {
const swapInstructions = removeBudgetIxs(swapQuoteIxs.swapIxs);
const ixs = [
...scopeRefreshIx,
...createAtasIxs,
...[flashBorrowIx],
...KaminoAction.actionToIxs(depositAction),
...KaminoAction.actionToIxs(borrowAction),
...swapInstructions,
...[flashRepayIx],
...budgetIxs,
];
const flashBorrowReserve = collReserve!;
const res: LeverageIxsOutput = {
flashLoanInfo: {
flashBorrowReserve: flashBorrowReserve.address,
flashLoanFee: flashBorrowReserve.getFlashLoanFee(),
},
instructions: ixs,
};
return res;
});
}
/**
* Withdraw and repay tokens if leverage decreased
*/
async function buildDecreaseLeverageIxs<QuoteResponse>(
owner: TransactionSigner,
kaminoMarket: KaminoMarket,
collTokenMint: Address,
debtTokenMint: Address,
obligation: KaminoObligation,
referrer: Option<Address>,
currentSlot: Slot,
calcs: AdjustLeverageCalcsResult,
scopeRefreshIx: Instruction[],
swapQuoteIxsArray: SwapIxs<QuoteResponse>[],
budgetAndPriorityFeeIxs: Instruction[] | undefined,
useV2Ixs: boolean,
withdrawSlotOffset: number = WITHDRAW_SLOT_OFFSET,
userSolBalanceLamports: number
): Promise<LeverageIxsOutput[]> {
const collReserve = kaminoMarket.getExistingReserveByMint(collTokenMint);
const debtReserve = kaminoMarket.getExistingReserveByMint(debtTokenMint);
const [debtTokenAta] = await findAssociatedTokenPda({
owner: owner.address,
mint: debtTokenMint,
tokenProgram: debtReserve.getLiquidityTokenProgram(),
});
// 1. Create atas & budget txns
const { budgetIxs, createAtasIxs } = await getSetupIxs(
owner,
collTokenMint,
collReserve,
debtTokenMint,
debtReserve,
budgetAndPriorityFeeIxs
);
// TODO: Mihai/Marius check if we can improve this logic and not convert any SOL
// This is here so that we have enough wsol to repay in case the kAB swapped to sol after estimates is not enough
const closeWsolAtaIxs: Instruction[] = [];
const fillWsolAtaIxs: Instruction[] = [];
if (debtTokenMint === WRAPPED_SOL_MINT) {
const wsolAta = await getAssociatedTokenAddress(WRAPPED_SOL_MINT, owner.address);
closeWsolAtaIxs.push(
getCloseAccountInstruction(
{
owner,
account: wsolAta,
destination: owner.address,
},
{ programAddress: TOKEN_PROGRAM_ADDRESS }
)
);
const halfSolBalance = userSolBalanceLamports / LAMPORTS_PER_SOL / 2;
const balanceToWrap = halfSolBalance < 0.1 ? halfSolBalance : 0.1;
fillWsolAtaIxs.push(
...getTransferWsolIxs(
owner,
wsolAta,
lamports(BigInt(toLamports(balanceToWrap, debtReserve!.stats.decimals).ceil().toString()))
)
);
}
// 3. Flash borrow & repay amount to repay (debt)
const { flashBorrowIx, flashRepayIx } = getFlashLoanInstructions({
borrowIxIndex: createAtasIxs.length + fillWsolAtaIxs.length + (scopeRefreshIx.length > 0 ? 1 : 0),
userTransferAuthority: owner,
lendingMarketAuthority: await kaminoMarket.getLendingMarketAuthority(),
lendingMarketAddress: kaminoMarket.getAddress(),
reserve: debtReserve!,
amountLamports: toLamports(Decimal.abs(calcs.adjustBorrowPosition), debtReserve!.stats.decimals),
destinationAta: debtTokenAta,
// TODO(referrals): once we support referrals, we will have to replace the placeholder args below:
referrerAccount: none(),
referrerTokenState: none(),
programId: kaminoMarket.programId,
});
// 4. Actually do the repay of the flash borrowed amounts
const repayAction = await KaminoAction.buildRepayTxns(
kaminoMarket,
toLamports(Decimal.abs(calcs.adjustBorrowPosition), debtReserve!.stats.decimals).floor().toString(),
debtTokenMint,
owner,
obligation,
useV2Ixs,
undefined,
currentSlot,
undefined,
0,
false,
false,
{ skipInitialization: true, skipLutCreation: true }, // to be checked and create in a setup tx in the UI (won't be the case for adjust anyway as this would be created in deposit)
referrer
);
const withdrawSlot = currentSlot - BigInt(withdrawSlotOffset);
// 6. Withdraw collateral (a little bit more to be able to pay for the slippage on swap)
const withdrawAction = await KaminoAction.buildWithdrawTxns(
kaminoMarket,
toLamports(calcs.withdrawAmountWithSlippageAndFlashLoanFee, collReserve!.stats.decimals).ceil().toString(),
collTokenMint,
owner,
obligation,
useV2Ixs,
undefined,
0,
false,
false,
{ skipInitialization: true, skipLutCreation: true }, // to be checked and create in a setup tx in the UI (won't be the case for adjust anyway as this would be created in deposit)
referrer,
withdrawSlot
);
return swapQuoteIxsArray.map((swapQuoteIxs) => {
const swapInstructions = removeBudgetIxs(swapQuoteIxs.swapIxs);
const ixs = [
...scopeRefreshIx,
...createAtasIxs,
...fillWsolAtaIxs,
...[flashBorrowIx],
...KaminoAction.actionToIxs(repayAction),
...KaminoAction.actionToIxs(withdrawAction),
...swapInstructions,
...[flashRepayIx],
...closeWsolAtaIxs,
...budgetIxs,
];
const res: LeverageIxsOutput = {
flashLoanInfo: {
flashBorrowReserve: debtReserve!.address,
flashLoanFee: debtReserve!.getFlashLoanFee(),
},
instructions: ixs,
};
return res;
});
}
export const getSetupIxs = async (
owner: TransactionSigner,
collTokenMint: Address,
collReserve: KaminoReserve,
debtTokenMint: Address,
debtReserve: KaminoReserve,
budgetAndPriorityFeeIxs: Instruction[] | undefined
) => {
const budgetIxs = budgetAndPriorityFeeIxs || getComputeBudgetAndPriorityFeeIxs(3000000);
const mintsWithTokenPrograms = getTokenMintsWithTokenPrograms(collTokenMint, collReserve, debtTokenMint, debtReserve);
const createAtasIxs = (await createAtasIdempotent(owner, mintsWithTokenPrograms)).map((x) => x.createAtaIx);
return {
budgetIxs,
createAtasIxs,
};
};
export const getScopeRefreshIxForObligationAndReserves = async (
market: KaminoMarket,
collReserve: KaminoReserve,
debtReserve: KaminoReserve,
obligation: KaminoObligation | ObligationType | undefined,
scopeRefreshConfig: ScopePriceRefreshConfig | undefined
): Promise<Instruction[]> => {
const allReserves =
obligation && isKaminoObligation(obligation)
? [
...new Set<Address>([
...obligation.getDeposits().map((x) => x.reserveAddress),
...obligation.getBorrows().map((x) => x.reserveAddress),
collReserve.address,
debtReserve.address,
]),
]
: [...new Set<Address>([collReserve.address, debtReserve.address])];
const scopeRefreshIxs: Instruction[] = [];
const scopeTokensMap = getTokenIdsForScopeRefresh(market, allReserves);
if (scopeTokensMap.size > 0 && scopeRefreshConfig) {
for (const [configPubkey, config] of scopeRefreshConfig.scopeConfigurations) {
const tokenIds = scopeTokensMap.get(config.oraclePrices);
if (tokenIds && tokenIds.length > 0) {
const refreshIx = await scopeRefreshConfig.scope.refreshPriceListIx({ config: configPubkey }, tokenIds);
if (refreshIx) {
scopeRefreshIxs.push(refreshIx);
}
}
}
}
return scopeRefreshIxs;
};
const checkObligationType = (
obligationTypeTag: ObligationTypeTag,
collTokenMint: Address,
debtTokenMint: Address,
kaminoMarket: KaminoMarket
) => {
let obligationType: ObligationType;
if (obligationTypeTag === ObligationTypeTag.Multiply) {
// multiply
obligationType = new MultiplyObligation(collTokenMint, debtTokenMint, kaminoMarket.programId);
} else if (obligationTypeTag === ObligationTypeTag.Leverage) {
// leverage
obligationType = new LeverageObligation(collTokenMint, debtTokenMint, kaminoMarket.programId);
} else {
throw Error('Obligation type tag not supported for leverage, please use 1 - multiply or 3 - leverage');
}
return obligationType;
};
type MintWithTokenProgram = {
mint: Address;
tokenProgram: Address;
};
const getTokenMintsWithTokenPrograms = (
collTokenMint: Address,
collReserve: KaminoReserve,
debtTokenMint: Address,
debtReserve: KaminoReserve
): Array<MintWithTokenProgram> => {
return [
{
mint: collTokenMint,
tokenProgram: collReserve.getLiquidityTokenProgram(),
},
{
mint: debtTokenMint,
tokenProgram: debtReserve.getLiquidityTokenProgram(),
},
{
mint: collReserve.getCTokenMint(),
tokenProgram: TOKEN_PROGRAM_ADDRESS,
},
];
};