UNPKG

@kamino-finance/klend-sdk

Version:

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

321 lines 19.5 kB
"use strict"; 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