Skip to main content

Documentation Index

Fetch the complete documentation index at: https://kamino.com/docs/llms.txt

Use this file to discover all available pages before exploring further.

Repay with collateral reduces debt in a single atomic transaction by converting collateral into the debt token without requiring additional funds. The protocol flash borrows the debt token, repays the debt, withdraws the freed collateral, swaps it to the debt token via KSwap, and repays the flash loan—all in one transaction.

Repay with Collateral using KSwap

Reduce debt in a single transaction by converting collateral using a flash loan and KSwap.
1

Import Dependencies

Import the required packages for Solana RPC communication, Kamino SDK operations, KSwap routing, and transaction building.
import {
  createSolanaRpc,
  createSolanaRpcSubscriptions,
  address,
  pipe,
  createTransactionMessage,
  setTransactionMessageFeePayerSigner,
  setTransactionMessageLifetimeUsingBlockhash,
  appendTransactionMessageInstructions,
  signTransactionMessageWithSigners,
  sendAndConfirmTransactionFactory,
  getSignatureFromTransaction,
  none,
  compressTransactionMessageUsingAddressLookupTables,
} from '@solana/kit';
import type { Address } from '@solana/kit';
import {
  KaminoMarket,
  MultiplyObligation,
  PROGRAM_ID,
  parseKeypairFile,
  getRepayWithCollIxs,
  getUserLutAddressAndSetupIxs,
  getScopeRefreshIxForObligationAndReserves,
  getComputeBudgetAndPriorityFeeIxs,
  lamportsToNumberDecimal,
  simulateTx,
  DEFAULT_RECENT_SLOT_DURATION_MS,
} from '@kamino-finance/klend-sdk';
import { KswapSdk } from '@kamino-finance/kswap-sdk';
import { Scope } from '@kamino-finance/scope-sdk';
import Decimal from 'decimal.js';
import { getKswapQuoter, getKswapSwapper } from './kswap_utils.js';
import { fetchAllAddressLookupTable } from '@solana-program/address-lookup-table';
2

Load Configuration and Initialize SDKs

Load the keypair, initialize RPC connections, and set up the market and SDKs.
const KEYPAIR_FILE = '/path/to/your/keypair.json';
const CDN_ENDPOINT = 'https://cdn.kamino.finance';

const signer = await parseKeypairFile(KEYPAIR_FILE);

const rpc = createSolanaRpc('https://api.mainnet-beta.solana.com');
const rpcSubscriptions = createSolanaRpcSubscriptions('wss://api.mainnet-beta.solana.com');

const marketPubkey = address('5wJeMrUYECGq41fxRESKALVcHnNX26TAWy4W98yULsua'); // xStocks Market
const debtTokenMint = address('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'); // USDC
const XSTOCKS_MARKET_LUT = address('8ofreL6hKfEet1DnhHVGvCTnSdz4pg85PpbuCUHnEcKm');

const market = await KaminoMarket.load(rpc, marketPubkey, DEFAULT_RECENT_SLOT_DURATION_MS);
const scope = new Scope('mainnet-beta', rpc);
const kswapSdk = new KswapSdk('https://api.kamino.finance/kswap', rpc, rpcSubscriptions);
3

Find Collateral Reserve

Dynamically discover the collateral token mint by searching the market reserves by symbol.
const tslaReserve = Array.from(market!.reserves.values()).find((reserve) => reserve.symbol === 'TSLAx');
if (!tslaReserve) {
  console.log('TSLAx reserve not found in xStocks market');
}
const collTokenMint = tslaReserve.getLiquidityMint();
4

Fetch Multiply Lookup Tables

Retrieve the multiply-specific lookup tables from Kamino’s CDN.
const kaminoResourcesResponse = await fetch(`${CDN_ENDPOINT}/resources.json`);
const kaminoResourcesData = await kaminoResourcesResponse.json();
const kaminoResources = kaminoResourcesData['mainnet-beta'];
const multiplyColPairs = kaminoResources.multiplyLUTsPairs[collTokenMint] || {};
const multiplyLut = multiplyColPairs[debtTokenMint] || [];
const multiplyLutKeys = multiplyLut.map((lut: string) => address(lut));
5

Configure Repay Parameters

Set the amount of debt to repay using collateral and the slippage tolerance.
const repayAmount = new Decimal(5); // $5 worth of debt to repay using collateral
const slippageBps = 100; // 1% slippage
Some assets may require higher slippage than others.
6

Setup Lookup Tables

Initialize the user’s lookup table and execute any required setup transactions.
const multiplyMints: { coll: Address; debt: Address }[] = [{ coll: collTokenMint, debt: debtTokenMint }];
const leverageMints: { coll: Address; debt: Address }[] = [];

const [userLookupTable, setupTxsIxs] = await getUserLutAddressAndSetupIxs(
  market!,
  signer,
  none(),
  true,
  multiplyMints,
  leverageMints
);

for (const setupIxs of setupTxsIxs) {
  const { value: setupBlockhash } = await rpc.getLatestBlockhash({ commitment: 'finalized' }).send();

  const setupTx = pipe(
    createTransactionMessage({ version: 0 }),
    (tx) => setTransactionMessageFeePayerSigner(signer, tx),
    (tx) => setTransactionMessageLifetimeUsingBlockhash(setupBlockhash, tx),
    (tx) => appendTransactionMessageInstructions(setupIxs, tx)
  );

  const signedSetupTx = await signTransactionMessageWithSigners(setupTx);

  await sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions })(signedSetupTx, {
    commitment: 'confirmed',
    skipPreflight: true,
  });

  await new Promise((resolve) => setTimeout(resolve, 2000));
}
Setup transactions extend the lookup table with addresses specific to the position. This is a one-time operation per collateral/debt pair.
7

Fetch Existing Obligation

Retrieve the current obligation and display position details.
const currentSlot = await rpc.getSlot().send();
const collTokenReserve = market!.getReserveByMint(collTokenMint)!;
const debtTokenReserve = market!.getReserveByMint(debtTokenMint)!;

const obligationType = new MultiplyObligation(collTokenMint, debtTokenMint, PROGRAM_ID);
const obligationAddress = await obligationType.toPda(market!.getAddress(), signer.address);
const obligation = await market!.getObligationByAddress(obligationAddress);

if (!obligation) {
  console.log('No obligation found. You must have an active position to repay with collateral.');
}

const deposited = lamportsToNumberDecimal(
  Array.from(obligation.deposits.values())[0]?.amount.toString() || '0',
  collTokenReserve.state.liquidity.mintDecimals.toNumber()
);
const borrowed = lamportsToNumberDecimal(
  Array.from(obligation.borrows.values())[0]?.amount.toString() || '0',
  debtTokenReserve.state.liquidity.mintDecimals.toNumber()
);

console.log(`Current position: ${deposited.toString()} TSLAx deposited, ${borrowed.toString()} USDC borrowed`);
This example uses MultiplyObligation. For standard borrow positions, use VanillaObligation instead.
8

Build Repay Instructions

Generate repay with collateral instructions with scope price refresh and compute budget.
const scopeConfiguration = { scope, scopeConfigurations: await scope.getAllConfigurations() };
const scopeRefreshIx = await getScopeRefreshIxForObligationAndReserves(
  market!,
  collTokenReserve,
  debtTokenReserve,
  obligation,
  scopeConfiguration
);

const computeIxs = getComputeBudgetAndPriorityFeeIxs(1_400_000, new Decimal(500000));

const repayWithCollRoutes = await getRepayWithCollIxs({
  owner: signer,
  kaminoMarket: market!,
  debtTokenMint: debtTokenMint,
  collTokenMint: collTokenMint,
  obligation: obligation,
  referrer: none(),
  currentSlot,
  repayAmount,
  isClosingPosition: false,
  budgetAndPriorityFeeIxs: computeIxs,
  scopeRefreshIx,
  quoter: getKswapQuoter(kswapSdk, signer.address, slippageBps, collTokenReserve, debtTokenReserve),
  swapper: getKswapSwapper(kswapSdk, signer.address, slippageBps),
  useV2Ixs: true,
});
9

Simulate Routes

Prepare lookup tables, simulate all routes to filter out failures.
const klendLookupTableKeys: Address[] = [];
klendLookupTableKeys.push(userLookupTable);
klendLookupTableKeys.push(...multiplyLutKeys);
klendLookupTableKeys.push(XSTOCKS_MARKET_LUT);

const klendLutAccounts = await fetchAllAddressLookupTable(rpc, klendLookupTableKeys);

const simulationResults = await Promise.all(
  repayWithCollRoutes.map(async (route) => {
    const lookupTables = route.lookupTables;
    lookupTables.push(...klendLutAccounts);

    const simulation = await simulateTx(rpc, signer.address, route.ixs, lookupTables).catch(() => undefined);

    if (!simulation || simulation.value.err) {
      return undefined;
    }

    return {
      ixs: route.ixs,
      luts: lookupTables.map((l) => l.address),
      routeOutput: route.quote!,
      swapInputs: route.swapInputs,
    };
  })
);

const passingSimulations = simulationResults.filter((tx) => tx !== undefined);
10

Select Best Route

Compare passing routes by price and select the best one.
const bestRoute =
  passingSimulations.length > 0
    ? passingSimulations.reduce((best, current) => {
        const inputMintReserve = market!.getReserveByMint(best.swapInputs.inputMint)!;
        const outputMintReserve = market!.getReserveByMint(best.swapInputs.outputMint)!;

        const bestPrice = new Decimal(best.routeOutput.amountsExactIn.amountOutGuaranteed.toString())
          .div(outputMintReserve.getMintFactor())
          .div(new Decimal(best.routeOutput.amountsExactIn.amountIn.toString()).div(inputMintReserve.getMintFactor()));

        const currentPrice = new Decimal(current.routeOutput.amountsExactIn.amountOutGuaranteed.toString())
          .div(outputMintReserve.getMintFactor())
          .div(
            new Decimal(current.routeOutput.amountsExactIn.amountIn.toString()).div(inputMintReserve.getMintFactor())
          );

        return bestPrice.greaterThan(currentPrice) ? best : current;
      })
    : (() => {
        const lookupTables = repayWithCollRoutes[0].lookupTables;
        lookupTables.push(...klendLutAccounts);
        return { ixs: repayWithCollRoutes[0].ixs, luts: lookupTables.map((l) => l.address) };
      })();

if (!bestRoute) {
  console.log('No route found');
}
11

Build, Sign, and Send Transaction

Wait briefly to avoid rate limiting, get a fresh blockhash, compress the transaction with LUTs, and send it.
await new Promise((resolve) => setTimeout(resolve, 2000));

const { value: latestBlockhash } = await rpc.getLatestBlockhash({ commitment: 'finalized' }).send();

const lutsByAddress: Record<Address, Address[]> = {};
const bestRouteLutAccounts = await fetchAllAddressLookupTable(rpc, bestRoute.luts || []);
for (const acc of bestRouteLutAccounts) {
  lutsByAddress[acc.address] = acc.data.addresses;
}

const transactionMessage = pipe(
  createTransactionMessage({ version: 0 }),
  (tx) => appendTransactionMessageInstructions(bestRoute.ixs, tx),
  (tx) => setTransactionMessageFeePayerSigner(signer, tx),
  (tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx),
  (tx) => compressTransactionMessageUsingAddressLookupTables(tx, lutsByAddress)
);

const signedTransaction = await signTransactionMessageWithSigners(transactionMessage);
const signature = getSignatureFromTransaction(signedTransaction);

await sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions })(signedTransaction, {
  commitment: 'processed',
  preflightCommitment: 'processed',
  skipPreflight: true,
});

console.log(`Repay with collateral successful! Sold TSLAx collateral to repay ${repayAmount.toString()} USDC debt.`);
console.log(`Transaction signature: ${signature}`);
The repay with collateral transaction is complete. Collateral was sold to repay debt without requiring external funds.

Helper Functions

The tutorial requires KSwap helper functions for quote retrieval and swap execution.
import BN from 'bn.js';
import { KaminoReserve } from '@kamino-finance/klend-sdk';
import type { SwapInputs, SwapQuote, SwapIxs, SwapIxsProvider, SwapQuoteProvider } from '@kamino-finance/klend-sdk';
import { KswapSdk } from '@kamino-finance/kswap-sdk/dist';
import type { RouteOutput, RouteParams, RouterType } from '@kamino-finance/kswap-sdk/dist';
import Decimal from 'decimal.js';
import type { Address } from '@solana/kit';
import { PublicKey } from '@solana/web3.js';

function toLegacyPublicKey(address: Address) {
  return new PublicKey(address.toString());
}

export const KSWAP_API = 'https://api.kamino.finance/kswap';
const ALLOWED_ROUTERS: RouterType[] = ['dflow', 'jupiter', 'jupiterU', 'okx', 'jupiterLite'];

export async function getTokenPriceFromJupWithFallback(
  kswapSdk: KswapSdk,
  inputMint: Address,
  outputMint: Address
): Promise<number> {
  const params = {
    ids: inputMint.toString(),
    vsToken: outputMint.toString(),
  };
  const res = await kswapSdk.getJupiterPriceWithFallback(params);

  return Number(res[inputMint.toString()]?.usdPrice || 0);
}

export async function getTokenPriceFromBirdeye(
  kswapSdk: KswapSdk,
  inputMint: Address,
  outputMint: Address
): Promise<number> {
  const prices = await kswapSdk.getBatchTokenPrices([toLegacyPublicKey(inputMint), toLegacyPublicKey(outputMint)]);

  return prices[inputMint.toString()] / prices[outputMint.toString()];
}

export function getKswapQuoter(
  kswapSdk: KswapSdk,
  executor: Address,
  slippageBps: number,
  inputMintReserve: KaminoReserve,
  outputMintReserve: KaminoReserve
): SwapQuoteProvider<RouteOutput> {
  const quoter: SwapQuoteProvider<RouteOutput> = async (
    inputs: SwapInputs,
    klendAccounts: Array<Address>
  ): Promise<SwapQuote<RouteOutput>> => {
    const routeParams: RouteParams = {
      executor: toLegacyPublicKey(executor),
      tokenIn: toLegacyPublicKey(inputs.inputMint),
      tokenOut: toLegacyPublicKey(inputs.outputMint),
      amount: new BN(inputs.inputAmountLamports.toDP(0).toString()),
      maxSlippageBps: slippageBps,
      wrapAndUnwrapSol: false,
      swapType: 'exactIn',
      routerTypes: ALLOWED_ROUTERS,
      includeRfq: false,
      includeLimoLogs: false,
    };

    const routeOutputs = await kswapSdk.getAllRoutes(routeParams);

    const bestRoute = routeOutputs.routes.reduce((best, current) => {
      const inAmountBest = new Decimal(best.amountsExactIn.amountIn.toString()).div(inputMintReserve.getMintFactor());
      const minAmountOutBest = new Decimal(best.amountsExactIn.amountOutGuaranteed.toString()).div(
        outputMintReserve.getMintFactor()
      );
      const priceAInBBest = minAmountOutBest.div(inAmountBest);
      const inAmountCurrent = new Decimal(current.amountsExactIn.amountIn.toString()).div(
        inputMintReserve.getMintFactor()
      );
      const minAmountOutCurrent = new Decimal(current.amountsExactIn.amountOutGuaranteed.toString()).div(
        outputMintReserve.getMintFactor()
      );
      const priceAInBCurrent = minAmountOutCurrent.div(inAmountCurrent);
      return priceAInBBest.greaterThan(priceAInBCurrent) ? best : current;
    });

    const inAmountBest = new Decimal(bestRoute.amountsExactIn.amountIn.toString()).div(
      inputMintReserve.getMintFactor()
    );
    const minAmountOutBest = new Decimal(bestRoute.amountsExactIn.amountOutGuaranteed.toString()).div(
      outputMintReserve.getMintFactor()
    );
    const priceAInBBest = minAmountOutBest.div(inAmountBest);

    return {
      priceAInB: priceAInBBest,
      quoteResponse: bestRoute,
    };
  };

  return quoter;
}

export function getKswapSwapper(
  kswapSdk: KswapSdk,
  executor: Address,
  slippageBps: number
): SwapIxsProvider<RouteOutput> {
  const swapper: SwapIxsProvider<RouteOutput> = async (
    inputs: SwapInputs,
    klendAccounts: Array<Address>,
    quote: SwapQuote<RouteOutput>
  ): Promise<Array<SwapIxs<RouteOutput>>> => {
    const routeParams: RouteParams = {
      executor: toLegacyPublicKey(executor),
      tokenIn: toLegacyPublicKey(inputs.inputMint),
      tokenOut: toLegacyPublicKey(inputs.outputMint),
      amount: new BN(inputs.inputAmountLamports.toString()),
      maxSlippageBps: slippageBps,
      wrapAndUnwrapSol: false,
      swapType: 'exactIn',
      routerTypes: ALLOWED_ROUTERS,
      includeRfq: false,
      includeLimoLogs: false,
    };

    const routeOutputs = await kswapSdk.getAllRoutes(routeParams);

    return routeOutputs.routes.map((routeOutput) => {
      const inAmount = new Decimal(routeOutput.amountsExactIn.amountIn.toString()).div(
        new Decimal(10).pow(routeOutput.inputTokenDecimals!)
      );
      const minAmountOut = new Decimal(routeOutput.amountsExactIn.amountOutGuaranteed.toString()).div(
        new Decimal(10).pow(routeOutput.outputTokenDecimals!)
      );
      const priceAInB = minAmountOut.div(inAmount);

      const allSwapIxs = [
        ...(routeOutput.instructions?.createInAtaIxs || []),
        ...(routeOutput.instructions?.wrapSolIxs || []),
        ...(routeOutput.instructions?.swapIxs || []),
        ...(routeOutput.instructions?.unwrapSolIxs || []),
        ...(routeOutput.instructions?.createOutAtaIxs || []),
      ];

      return {
        preActionIxs: [],
        swapIxs: allSwapIxs,
        lookupTables: routeOutput.lookupTableAccounts || [],
        quote: {
          priceAInB: new Decimal(priceAInB),
          quoteResponse: routeOutput,
        },
      };
    });
  };

  return swapper;
}