import {
  getLiquidateIx,
  getAllLiquidations,
  getVaultsProgram,
} from "@jup-ag/lend/borrow";
import { getFlashBorrowIx, getFlashPaybackIx } from "@jup-ag/lend/flashloan";
import {
  Connection,
  PublicKey,
  ComputeBudgetProgram,
  TransactionMessage,
  VersionedTransaction,
  TransactionInstruction,
  AddressLookupTableAccount,
} from "@solana/web3.js";
import BN from "bn.js";
import axios from "axios";
const RPC_URL = "<RPC_URL>";
const SLIPPAGE_BPS = 100;
const signer = new PublicKey("1234567890");
const connection = new Connection(RPC_URL);
const program = getVaultsProgram({ connection, signer });
const configs = await program.account.vaultConfig.all();
async function fetchLiquidations() {
  try {
    const allAvailableLiquidations = await getAllLiquidations({
      connection,
      signer,
    });
    const validLiquidations: any = [];
    for (const vaultLiquidation of allAvailableLiquidations) {
      const { liquidations, vaultId } = vaultLiquidation;
      if (liquidations.length === 0) continue;
      // prettier-ignore
      for (const liquidation of liquidations) {
          const supplyToken = configs.find((config) => config.account.vaultId === vaultId)?.account.supplyToken;
          const borrowToken = configs.find((config) => config.account.vaultId === vaultId)?.account.borrowToken;
          validLiquidations.push({
            vaultId,
            liquidation,
            debtAmount: new BN(liquidation.amtIn),
            collateralAmount: new BN(liquidation.amtOut),
            supplyToken,
            borrowToken,
          });
        }
    }
    return validLiquidations;
  } catch (error) {
    console.error("Error fetching liquidations:", error);
    throw error;
  }
}
async function getJupiterQuote({
  inputMint,
  outputMint,
  amount,
  slippageBps = SLIPPAGE_BPS,
}) {
  try {
    const response = await axios.get("https://lite-api.jup.ag/swap/v1/quote", {
      params: {
        inputMint,
        outputMint,
        amount,
        slippageBps,
        restrictIntermediateTokens: true,
        maxAccounts: 32,
      },
    });
    return response.data;
  } catch (error) {
    console.error(
      "Error fetching Jupiter quote:",
      error.response?.data || error.message
    );
    throw error;
  }
}
async function getJupiterSwapInstructions({ quoteResponse, userPublicKey }) {
  try {
    const response = await axios.post(
      "https://lite-api.jup.ag/swap/v1/swap-instructions",
      {
        quoteResponse,
        userPublicKey,
      },
      {
        headers: { "Content-Type": "application/json" },
      }
    );
    if (response.data.error) {
      throw new Error(
        "Failed to get swap instructions: " + response.data.error
      );
    }
    return response.data;
  } catch (error) {
    console.error(
      "Error getting swap instructions:",
      error.response?.data || error.message
    );
    throw error;
  }
}
function deserializeInstruction(instruction) {
  return new TransactionInstruction({
    programId: new PublicKey(instruction.programId),
    keys: instruction.accounts.map((key) => ({
      pubkey: new PublicKey(key.pubkey),
      isSigner: key.isSigner,
      isWritable: key.isWritable,
    })),
    data: Buffer.from(instruction.data, "base64"),
  });
}
async function getAddressLookupTableAccounts(connection, keys) {
  if (!keys || keys.length === 0) return [];
  const addressLookupTableAccountInfos =
    await connection.getMultipleAccountsInfo(
      keys.map((key) => new PublicKey(key))
    );
  return addressLookupTableAccountInfos.reduce((acc, accountInfo, index) => {
    const addressLookupTableAddress = keys[index];
    if (accountInfo) {
      const addressLookupTableAccount = new AddressLookupTableAccount({
        key: new PublicKey(addressLookupTableAddress),
        state: AddressLookupTableAccount.deserialize(accountInfo.data),
      });
      acc.push(addressLookupTableAccount);
    }
    return acc;
  }, []);
}
async function executeLiquidation({
  vaultId,
  debtAmount,
  collateralAmount,
  supplyToken,
  borrowToken,
}) {
  try {
    console.log(`Executing liquidation for vault ${vaultId}...`);
    const instructions: TransactionInstruction[] = [];
    let allAddressLookupTableAccounts: AddressLookupTableAccount[] = [];
    // Step 1: Flash borrow the debt amount
    const flashBorrowIx = await getFlashBorrowIx({
      amount: debtAmount,
      asset: borrowToken,
      signer,
      connection,
    });
    instructions.push(flashBorrowIx);
    // Step 2: Get liquidation instructions
    const {
      ixs: liquidateIxs,
      addressLookupTableAccounts: liquidateLookupTables,
    } = await getLiquidateIx({
      vaultId,
      debtAmount,
      signer,
      connection,
    });
    instructions.push(...liquidateIxs);
    if (liquidateLookupTables && liquidateLookupTables.length > 0) {
      allAddressLookupTableAccounts.push(...liquidateLookupTables);
    }
    // Step 3: Get Jupiter swap quote (collateral token -> debt token)
    const quoteResponse = await getJupiterQuote({
      inputMint: supplyToken.toString(), // Collateral token
      outputMint: borrowToken.toString(), // Debt token
      amount: collateralAmount.toString(),
      slippageBps: SLIPPAGE_BPS,
    });
    // Step 4: Get Jupiter swap instructions
    const swapInstructions = await getJupiterSwapInstructions({
      quoteResponse,
      userPublicKey: signer.toString(),
    });
    const {
      setupInstructions,
      swapInstruction: swapInstructionPayload,
      cleanupInstruction,
      addressLookupTableAddresses,
    } = swapInstructions;
    if (setupInstructions && setupInstructions.length > 0) {
      instructions.push(...setupInstructions.map(deserializeInstruction));
    }
    instructions.push(deserializeInstruction(swapInstructionPayload));
    if (cleanupInstruction) {
      instructions.push(deserializeInstruction(cleanupInstruction));
    }
    // Step 5: Flash payback
    const flashPaybackIx = await getFlashPaybackIx({
      amount: debtAmount,
      asset: borrowToken,
      signer,
      connection,
    });
    instructions.push(flashPaybackIx);
    // Step 6: Get Jupiter address lookup tables
    if (addressLookupTableAddresses && addressLookupTableAddresses.length > 0) {
      const jupiterLookupTables = await getAddressLookupTableAccounts(
        connection,
        addressLookupTableAddresses
      );
      allAddressLookupTableAccounts.push(...jupiterLookupTables);
    }
    // Step 7: Build and send transaction
    const latestBlockhash = await connection.getLatestBlockhash();
    const messageV0 = new TransactionMessage({
      payerKey: signer,
      recentBlockhash: latestBlockhash.blockhash,
      instructions: [
        ComputeBudgetProgram.setComputeUnitLimit({
          units: 1_000_000,
        }),
        ...instructions,
      ],
    }).compileToV0Message(allAddressLookupTableAccounts);
    const transaction = new VersionedTransaction(messageV0);
    return transaction;
  } catch (error) {
    console.error(`Error executing liquidation for vault ${vaultId}:`, error);
    throw error;
  }
}
async function runLiquidationBot() {
  try {
    const liquidations = await fetchLiquidations();
    if (liquidations.length === 0) {
      console.log("No liquidations available at this time.");
      return;
    }
    for (const liquidationData of liquidations) {
      const {
        vaultId,
        debtAmount,
        collateralAmount,
        borrowToken,
        supplyToken,
      } = liquidationData;
      try {
        const signature = await executeLiquidation({
          vaultId,
          debtAmount,
          collateralAmount,
          borrowToken,
          supplyToken,
        });
        console.log(`Successfully liquidated vault ${vaultId}: ${signature}`);
        await new Promise((resolve) => setTimeout(resolve, 2000));
      } catch (error) {
        console.error(`Failed to liquidate vault ${vaultId}:`, error.message);
        continue;
      }
    }
  } catch (error) {
    console.error("Error in liquidation bot:", error);
  }
}