@kamino-finance/klend-sdk
Version:
Typescript SDK for interacting with the Kamino Lending (klend) protocol
321 lines • 19.5 kB
JavaScript
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.getSwapCollIxs = getSwapCollIxs;
const classes_1 = require("../classes");
const leverage_1 = require("../leverage");
const utils_1 = require("../utils");
const web3_js_1 = require("@solana/web3.js");
const decimal_js_1 = __importDefault(require("decimal.js"));
const spl_token_1 = require("@solana/spl-token");
/**
* Constructs instructions needed to partially/fully swap the given source collateral for some other collateral type.
*/
async function getSwapCollIxs(inputs) {
const [args, context] = extractArgsAndContext(inputs);
// Conceptually, we need to construct the following ixs:
// 0. any set-up, like budgeting and ATAs
// 1. `flash-borrowed target coll = targetCollReserve.flashBorrow()`
// 2. `targetCollReserve.deposit(flash-borrowed target coll)`
// 3. `sourceCollReserve.withdraw(requested amount to be coll-swapped)`
// 4. `externally-swapped target coll = externalDex.swap(withdrawn current coll)`
// 5. `flashRepay(externally-swapped target coll)`
// However, there is a cyclic dependency:
// - To construct 4. (specifically, to query the external swap quote), we need to know all accounts used by Kamino's
// own ixs.
// - To construct 1. (i.e. flash-borrow), we need to know the target collateral swap-out from 4.
// Construct the Klend's own ixs with a fake swap-out (only to learn the klend accounts used):
const fakeKlendIxs = await getKlendIxs(args, FAKE_TARGET_COLL_SWAP_OUT_AMOUNT, context);
const klendAccounts = (0, utils_1.uniqueAccountsWithProgramIds)(listIxs(fakeKlendIxs));
// Construct the external swap ixs (and learn the actual swap-out amount):
const externalSwapIxsArray = await getExternalSwapIxs(args, klendAccounts, context);
return Promise.all(externalSwapIxsArray.map(async (externalSwapIxs) => {
// We now have the full information needed to simulate the end-state, so let's check that the operation is legal:
context.logger(`Expected to swap ${args.sourceCollSwapAmount} ${context.sourceCollReserve.symbol} collateral into ${externalSwapIxs.swapOutAmount} ${context.targetCollReserve.symbol} collateral`);
checkResultingObligationValid(args, externalSwapIxs.swapOutAmount, context);
// Construct the Klend's own ixs with an actual swap-out amount:
const klendIxs = await getKlendIxs(args, externalSwapIxs.swapOutAmount, context);
return {
ixs: listIxs(klendIxs, externalSwapIxs.ixs),
lookupTables: externalSwapIxs.luts,
useV2Ixs: context.useV2Ixs,
simulationDetails: {
flashLoan: {
targetCollFlashBorrowedAmount: klendIxs.simulationDetails.targetCollFlashBorrowedAmount,
targetCollFlashRepaidAmount: externalSwapIxs.swapOutAmount,
},
externalSwap: {
sourceCollSwapInAmount: args.sourceCollSwapAmount, // repeated `/inputs.sourceCollSwapAmount`, only for clarity
targetCollSwapOutAmount: externalSwapIxs.swapOutAmount, // repeated `../flashLoan.targetCollFlashRepaidAmount`, only for clarity
quoteResponse: externalSwapIxs.simulationDetails.quoteResponse,
},
},
};
}));
}
function extractArgsAndContext(inputs) {
if (inputs.sourceCollTokenMint.equals(inputs.targetCollTokenMint)) {
throw new Error(`Cannot swap from/to the same collateral`);
}
if (inputs.sourceCollSwapAmount.lte(0)) {
throw new Error(`Cannot swap a negative amount`);
}
return [
{
sourceCollSwapAmount: inputs.sourceCollSwapAmount,
isClosingSourceColl: inputs.isClosingSourceColl,
newElevationGroup: inputs.market.getExistingElevationGroup(inputs.newElevationGroup, 'Newly-requested'),
},
{
budgetAndPriorityFeeIxs: inputs.budgetAndPriorityFeeIxs || (0, utils_1.getComputeBudgetAndPriorityFeeIxs)(utils_1.DEFAULT_MAX_COMPUTE_UNITS),
sourceCollReserve: inputs.market.getExistingReserveByMint(inputs.sourceCollTokenMint, 'Current collateral'),
targetCollReserve: inputs.market.getExistingReserveByMint(inputs.targetCollTokenMint, 'Target collateral'),
logger: console.log,
market: inputs.market,
obligation: inputs.obligation,
quoter: inputs.quoter,
swapper: inputs.swapper,
referrer: inputs.referrer,
scopeRefreshConfig: inputs.scopeRefreshConfig,
currentSlot: inputs.currentSlot,
useV2Ixs: inputs.useV2Ixs,
},
];
}
const FAKE_TARGET_COLL_SWAP_OUT_AMOUNT = new decimal_js_1.default(1); // see the lengthy `getSwapCollIxs()` impl comment
async function getKlendIxs(args, targetCollSwapOutAmount, context) {
const { ataCreationIxs, targetCollAta } = getAtaCreationIxs(context);
const setupIxs = [...context.budgetAndPriorityFeeIxs, ...ataCreationIxs];
const scopeRefreshIxn = await (0, leverage_1.getScopeRefreshIx)(context.market, context.sourceCollReserve, context.targetCollReserve, context.obligation, context.scopeRefreshConfig);
if (scopeRefreshIxn) {
setupIxs.unshift(...scopeRefreshIxn);
}
const targetCollFlashBorrowedAmount = calculateTargetCollFlashBorrowedAmount(targetCollSwapOutAmount, context);
const { targetCollFlashBorrowIx, targetCollFlashRepayIx } = getTargetCollFlashLoanIxs(targetCollFlashBorrowedAmount, setupIxs.length, targetCollAta, context);
const depositTargetCollIxs = await getDepositTargetCollIxs(targetCollFlashBorrowedAmount, context);
const withdrawSourceCollIxs = await getWithdrawSourceCollIxs(args, depositTargetCollIxs.removesElevationGroup, context);
const cleanupIxs = getAtaCloseIxs(context);
return {
setupIxs,
flashLoanInfo: {
flashBorrowReserve: context.targetCollReserve.address,
flashLoanFee: context.targetCollReserve.getFlashLoanFee(),
},
targetCollFlashBorrowIx,
depositTargetCollIxs: depositTargetCollIxs.ixs,
withdrawSourceCollIxs,
targetCollFlashRepayIx,
cleanupIxs,
simulationDetails: {
targetCollFlashBorrowedAmount,
},
};
}
function calculateTargetCollFlashBorrowedAmount(targetCollFlashRepaidAmount, context) {
const { protocolFees, referrerFees } = context.targetCollReserve.calculateFees(targetCollFlashRepaidAmount.mul(context.targetCollReserve.getMintFactor()), context.targetCollReserve.getFlashLoanFee(), classes_1.FeeCalculation.Inclusive, // denotes that the amount parameter above means "to be repaid" (not "borrowed")
context.market.state.referralFeeBps, !context.referrer.equals(web3_js_1.PublicKey.default));
const targetCollFlashLoanFee = protocolFees.add(referrerFees).div(context.targetCollReserve.getMintFactor());
return targetCollFlashRepaidAmount.sub(targetCollFlashLoanFee);
}
function getAtaCreationIxs(context) {
const atasAndAtaCreationIxs = (0, utils_1.createAtasIdempotent)(context.obligation.state.owner, [
{
mint: context.sourceCollReserve.getLiquidityMint(),
tokenProgram: context.sourceCollReserve.getLiquidityTokenProgram(),
},
{
mint: context.targetCollReserve.getLiquidityMint(),
tokenProgram: context.targetCollReserve.getLiquidityTokenProgram(),
},
]);
return {
ataCreationIxs: atasAndAtaCreationIxs.map((tuple) => tuple.createAtaIx),
targetCollAta: atasAndAtaCreationIxs[1].ata,
};
}
function getAtaCloseIxs(context) {
const ataCloseIxs = [];
if (context.sourceCollReserve.getLiquidityMint().equals(spl_token_1.NATIVE_MINT) ||
context.targetCollReserve.getLiquidityMint().equals(spl_token_1.NATIVE_MINT)) {
const owner = context.obligation.state.owner;
const wsolAta = (0, utils_1.getAssociatedTokenAddress)(spl_token_1.NATIVE_MINT, owner, false);
ataCloseIxs.push((0, spl_token_1.createCloseAccountInstruction)(wsolAta, owner, owner, [], spl_token_1.TOKEN_PROGRAM_ID));
}
return ataCloseIxs;
}
function getTargetCollFlashLoanIxs(targetCollAmount, flashBorrowIxIndex, destinationAta, context) {
const { flashBorrowIx: targetCollFlashBorrowIx, flashRepayIx: targetCollFlashRepayIx } = (0, leverage_1.getFlashLoanInstructions)({
borrowIxIndex: flashBorrowIxIndex,
walletPublicKey: context.obligation.state.owner,
lendingMarketAuthority: context.market.getLendingMarketAuthority(),
lendingMarketAddress: context.market.getAddress(),
reserve: context.targetCollReserve,
amountLamports: targetCollAmount.mul(context.targetCollReserve.getMintFactor()),
destinationAta,
// TODO(referrals): once we support referrals, we will have to replace the placeholder args below:
referrerAccount: context.market.programId,
referrerTokenState: context.market.programId,
programId: context.market.programId,
});
return { targetCollFlashBorrowIx, targetCollFlashRepayIx };
}
async function getDepositTargetCollIxs(targetCollAmount, context) {
const removesElevationGroup = mustRemoveElevationGroupBeforeDeposit(context);
const depositCollAction = await classes_1.KaminoAction.buildDepositTxns(context.market, targetCollAmount.mul(context.targetCollReserve.getMintFactor()).toString(), // in lamports
context.targetCollReserve.getLiquidityMint(), context.obligation.state.owner, context.obligation, context.useV2Ixs, undefined, // we create the scope refresh ix outside of KaminoAction
0, // no extra compute budget
false, // we do not need ATA ixs here (we construct and close them ourselves)
removesElevationGroup, // we may need to (temporarily) remove the elevation group; the same or a different one will be set on withdraw, if requested
{ skipInitialization: true, skipLutCreation: true }, // we are dealing with an existing obligation, no need to create user metadata
context.referrer, context.currentSlot, removesElevationGroup ? 0 : undefined // only applicable when removing the group
);
return {
ixs: classes_1.KaminoAction.actionToIxs(depositCollAction),
removesElevationGroup,
};
}
function mustRemoveElevationGroupBeforeDeposit(context) {
if (context.obligation.deposits.has(context.targetCollReserve.address)) {
return false; // the target collateral already was a reserve in the obligation, so we do not affect any potential elevation group
}
const currentElevationGroupId = context.obligation.state.elevationGroup;
if (currentElevationGroupId == 0) {
return false; // simply nothing to remove
}
if (!context.targetCollReserve.state.config.elevationGroups.includes(currentElevationGroupId)) {
return true; // the target collateral reserve is NOT in the obligation's group - must remove the group
}
const currentElevationGroup = context.market.getElevationGroup(currentElevationGroupId);
if (context.obligation.deposits.size >= currentElevationGroup.maxReservesAsCollateral) {
return true; // the obligation is already at its elevation group's deposits count limit - must remove the group
}
return false; // the obligation has some elevation group and the new collateral can be added to it
}
async function getWithdrawSourceCollIxs(args, depositRemovedElevationGroup, context) {
const withdrawnSourceCollLamports = args.isClosingSourceColl
? utils_1.U64_MAX
: args.sourceCollSwapAmount.mul(context.sourceCollReserve.getMintFactor()).toString();
const requestedElevationGroup = elevationGroupIdToRequestAfterWithdraw(args, depositRemovedElevationGroup, context);
const withdrawCollAction = await classes_1.KaminoAction.buildWithdrawTxns(context.market, withdrawnSourceCollLamports, context.sourceCollReserve.getLiquidityMint(), context.obligation.state.owner, context.obligation, context.useV2Ixs, undefined, // we create the scope refresh ix outside of KaminoAction
0, // no extra compute budget
false, // we do not need ATA ixs here (we construct and close them ourselves)
requestedElevationGroup !== undefined, // the `elevationGroupIdToRequestAfterWithdraw()` has already decided on this
{ skipInitialization: true, skipLutCreation: true }, // we are dealing with an existing obligation, no need to create user metadata
context.referrer, context.currentSlot, requestedElevationGroup, context.obligation.deposits.has(context.targetCollReserve.address) // if our obligation already had the target coll...
? undefined // ... then we need no customizations here, but otherwise...
: {
addedDepositReserves: [context.targetCollReserve.address], // ... we need to inform our infra that the obligation now has one more reserve that needs refreshing.
});
return classes_1.KaminoAction.actionToIxs(withdrawCollAction);
}
function elevationGroupIdToRequestAfterWithdraw(args, depositRemovedElevationGroup, context) {
const obligationInitialElevationGroup = context.obligation.state.elevationGroup;
const requestedElevationGroupId = args.newElevationGroup?.elevationGroup ?? 0;
if (requestedElevationGroupId === 0) {
// the user doesn't want any elevation group, and...
if (obligationInitialElevationGroup === 0) {
return undefined; // ... he already didn't have it - fine!
}
if (depositRemovedElevationGroup) {
return undefined; // ... our deposit already forced us to remove it - fine!
}
return 0; // ... but he *did have one*, and our deposit didn't need to remove it - so we remove it now, just to satisfy him
}
else {
// the user wants some elevation group, and...
if (depositRemovedElevationGroup) {
return requestedElevationGroupId; // ...our deposit forced us to remove it - so we now request the new one, whatever it is
}
if (obligationInitialElevationGroup === requestedElevationGroupId) {
return undefined; // ...and he already had exactly this one - fine!
}
return requestedElevationGroupId; // ...and he had some different one - so we request the new one
}
}
async function getExternalSwapIxs(args, klendAccounts, context) {
const externalSwapInputs = {
inputAmountLamports: args.sourceCollSwapAmount.mul(context.sourceCollReserve.getMintFactor()),
inputMint: context.sourceCollReserve.getLiquidityMint(),
outputMint: context.targetCollReserve.getLiquidityMint(),
amountDebtAtaBalance: undefined, // only used for kTokens
};
const externalSwapQuote = await context.quoter(externalSwapInputs, klendAccounts);
const externalSwapIxsAndLuts = await context.swapper(externalSwapInputs, klendAccounts, externalSwapQuote);
// Note: we can ignore the returned `preActionIxs` field - we do not request any of them from the swapper.
return externalSwapIxsAndLuts.map((externalSwapIxsAndLuts) => {
const swapOutAmount = externalSwapIxsAndLuts.quote.priceAInB.mul(args.sourceCollSwapAmount);
return {
swapOutAmount,
ixs: externalSwapIxsAndLuts.swapIxs,
luts: externalSwapIxsAndLuts.lookupTables,
simulationDetails: {
quoteResponse: externalSwapIxsAndLuts.quote.quoteResponse,
},
};
});
}
function checkResultingObligationValid(args, targetCollAmount, context) {
// The newly-requested elevation group must have its conditions satisfied:
if (args.newElevationGroup !== null) {
// Note: we cannot use the existing `isLoanEligibleForElevationGroup()`, since it operates on a `KaminoObligation`,
// and our instance is stale (we want to assert on the state *after* potential changes).
// Let's start with the (simpler) debt reserve - it cannot change during a coll-swap:
const debtReserveAddresses = [...context.obligation.borrows.keys()];
if (debtReserveAddresses.length > 1) {
throw new Error(`The obligation with ${debtReserveAddresses.length} debt reserves cannot request any elevation group`);
}
if (debtReserveAddresses.length == 1) {
const debtReserveAddress = debtReserveAddresses[0];
if (!args.newElevationGroup.debtReserve.equals(debtReserveAddress)) {
throw new Error(`The obligation with debt reserve ${debtReserveAddress.toBase58()} cannot request elevation group ${args.newElevationGroup.elevationGroup}`);
}
}
// Now the coll reserves: this requires first finding out the resulting set of deposits:
const collReserveAddresses = new utils_1.PublicKeySet([
...context.obligation.deposits.keys(),
context.targetCollReserve.address,
]);
if (args.isClosingSourceColl) {
collReserveAddresses.remove(context.sourceCollReserve.address);
}
if (collReserveAddresses.size() > args.newElevationGroup.maxReservesAsCollateral) {
throw new Error(`The obligation with ${collReserveAddresses.size()} collateral reserves cannot request elevation group ${args.newElevationGroup.elevationGroup}`);
}
for (const collReserveAddress of collReserveAddresses.toArray()) {
if (!args.newElevationGroup.collateralReserves.contains(collReserveAddress)) {
throw new Error(`The obligation with collateral reserve ${collReserveAddress.toBase58()} cannot request elevation group ${args.newElevationGroup.elevationGroup}`);
}
}
}
// The LTV cannot be exceeded:
const effectiveWithdrawAmount = args.isClosingSourceColl
? context.obligation.getDepositAmountByReserve(context.sourceCollReserve)
: args.sourceCollSwapAmount;
const resultingStats = context.obligation.getPostSwapCollObligationStats({
withdrawAmountLamports: effectiveWithdrawAmount.mul(context.sourceCollReserve.getMintFactor()),
withdrawReserveAddress: context.sourceCollReserve.address,
depositAmountLamports: targetCollAmount.mul(context.targetCollReserve.getMintFactor()),
depositReserveAddress: context.targetCollReserve.address,
market: context.market,
newElevationGroup: args.newElevationGroup?.elevationGroup ?? 0,
slot: context.currentSlot,
});
const maxLtv = resultingStats.borrowLimit.div(resultingStats.userTotalCollateralDeposit);
if (resultingStats.loanToValue > maxLtv) {
throw new Error(`Swapping collateral ${effectiveWithdrawAmount} ${context.sourceCollReserve.symbol} into ${targetCollAmount} ${context.targetCollReserve.symbol} would result in the obligation's LTV ${resultingStats.loanToValue} exceeding its max LTV ${maxLtv}`);
}
}
function listIxs(klendIxs, externalSwapIxs) {
return [
...klendIxs.setupIxs,
klendIxs.targetCollFlashBorrowIx,
...klendIxs.depositTargetCollIxs,
...klendIxs.withdrawSourceCollIxs,
...(externalSwapIxs || []),
klendIxs.targetCollFlashRepayIx,
...klendIxs.cleanupIxs,
];
}
//# sourceMappingURL=swap_collateral_operations.js.map
;