import { Connection, Keypair, PublicKey, Signer, SystemProgram, TransactionInstruction } from "@solana/web3.js";
import BN from "bn.js";
import { bool, s32, struct, u128, u64, u8 } from "@/marshmallow";
import { ASSOCIATED_TOKEN_PROGRAM_ID, getAssociatedTokenAddressSync, TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID } from "@solana/spl-token";
import { getATAAddress, InstructionType, MEMO_PROGRAM_ID, RENT_PROGRAM_ID } from "@/common";
import { ReturnTypeMakeInstructions } from "../type";
import { getPdaExBitmapAccount, getPdaPersonalPositionAddress, getPdaProtocolPositionAddress, getPdaTickArrayAddress, getPdaUserState } from "./utils/pda";
import { ApiV3PoolInfoConcentratedItem, ApiV3Token, ClmmKeys } from "@/api";
import { ClosePositionExtInfo, ManipulateLiquidityExtInfo, OpenPositionFromBaseCustodyExtInfo, OpenPositionFromBaseExtInfo } from "./type";
import { TickUtils } from "./utils/tick";
import { PoolUtils } from "./utils/pool";
import { ClmmPositionLayout } from "./layout";

const anchorDataBuf = {
  initializeUser: [111, 17, 185, 250, 60, 122, 38, 254],
  deposit: [242, 35, 198, 137, 82, 225, 242, 182],
  withdraw: [183, 18, 70, 156, 148, 109, 161, 34],
  openPosition: [135, 128, 47, 77, 15, 152, 240, 49],
  closePosition: [123, 134, 81, 0, 49, 68, 98, 98],
  increaseLiquidity: [46, 156, 243, 118, 13, 205, 251, 178],
  decreaseLiquidity: [160, 38, 208, 111, 104, 91, 44, 1],
  swap: [248, 198, 158, 145, 225, 117, 135, 200],
}

interface InitializeUserInstruction {
  connection: Connection,
  programId: PublicKey,
  payer: PublicKey,
  authority: PublicKey,
}

export class CustodyInstrument {
  static initializeUserInstruction(
    programId: PublicKey,
    payer: PublicKey,
    authority: PublicKey,
    userState: PublicKey,
  ): TransactionInstruction {
    const dataLayout = struct([])

    const keys = [
      { pubkey: payer, isSigner: true, isWritable: true },
      { pubkey: authority, isSigner: false, isWritable: false },
      { pubkey: userState, isSigner: false, isWritable: true },
      { pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
    ]

    const data = Buffer.alloc(dataLayout.span)
    dataLayout.encode({}, data)

    const aData = Buffer.from([...anchorDataBuf.initializeUser, ...data])
    return new TransactionInstruction({
      keys,
      programId,
      data: aData,
    })
  }

  static initializeUserInstructions(props: InitializeUserInstruction): 
    ReturnTypeMakeInstructions<{
      userState: PublicKey,
    }> {
    const { programId, payer, authority } = props
    const { publicKey: userState } = getPdaUserState(programId, payer)

    const ins = this.initializeUserInstruction(programId, payer, authority, userState)

    return {
      signers: [],
      instructions: [ins],
      instructionTypes: [InstructionType.CreateAccount, InstructionType.CustodyInitializeUser],
      address: { userState },
      lookupTableAddress: [],
    }
  }

  static depositInstruction(
    programId: PublicKey,
    payer: PublicKey,
    userState: PublicKey,
    userTokenAccount: PublicKey,
    userTokenCustody: PublicKey,
    mint: PublicKey,
    amount: BN,
  ): TransactionInstruction {
    const dataLayout = struct([u64("amount")])

    const keys = [
      { pubkey: payer, isSigner: true, isWritable: true },
      { pubkey: userState, isSigner: false, isWritable: false },
      { pubkey: userTokenAccount, isSigner: false, isWritable: true },
      { pubkey: userTokenCustody, isSigner: false, isWritable: true },
      { pubkey: mint, isSigner: false, isWritable: false },
      { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
      { pubkey: TOKEN_2022_PROGRAM_ID, isSigner: false, isWritable: false },
      { pubkey: ASSOCIATED_TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
      { pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
    ]

    const data = Buffer.alloc(dataLayout.span)
    dataLayout.encode({ amount }, data)
    const aData = Buffer.from([...anchorDataBuf.deposit, ...data])

    return new TransactionInstruction({
      keys,
      programId,
      data: aData,
    })
  }

  static async depositInstructions({
    programId,
    ownerInfo,
    mint,
    amount,
  }: {
    programId: PublicKey,
    ownerInfo: {
      wallet: PublicKey,
      tokenAccount: PublicKey,
    },
    mint: ApiV3Token,
    amount: BN,
  }): Promise<ReturnTypeMakeInstructions> {
    const { publicKey: userState } = getPdaUserState(programId, ownerInfo.wallet)

    const userTokenCustody = getAssociatedTokenAddressSync(new PublicKey(mint.address), userState, true, new PublicKey(mint.programId))

    const ins = this.depositInstruction(
      programId,
      ownerInfo.wallet,
      userState,
      ownerInfo.tokenAccount,
      userTokenCustody,
      new PublicKey(mint.address),
      amount,
    )
    
    return {
      signers: [],
      instructions: [ins],
      instructionTypes: [InstructionType.CustodyDeposit],
      address: { userState, userTokenCustody },
      lookupTableAddress: [],
    }
  }

  static withdrawInstruction(
    programId: PublicKey,
    payer: PublicKey,
    userState: PublicKey,
    userTokenAccount: PublicKey,
    userTokenCustody: PublicKey,
    mint: PublicKey,
    amount: BN,
  ): TransactionInstruction {
    const dataLayout = struct([u64("amount")])

    const keys = [
      { pubkey: payer, isSigner: true, isWritable: true },
      { pubkey: userState, isSigner: false, isWritable: false },
      { pubkey: userTokenAccount, isSigner: false, isWritable: true },
      { pubkey: userTokenCustody, isSigner: false, isWritable: true },
      { pubkey: mint, isSigner: false, isWritable: false },
      { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
      { pubkey: TOKEN_2022_PROGRAM_ID, isSigner: false, isWritable: false },
      { pubkey: ASSOCIATED_TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
      { pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
    ]

    const data = Buffer.alloc(dataLayout.span)
    dataLayout.encode({ amount }, data)
    const aData = Buffer.from([...anchorDataBuf.withdraw, ...data])

    return new TransactionInstruction({
      keys,
      programId,
      data: aData,
    })
  }

  static async withdrawInstructions({
    programId,
    ownerInfo,
    mint,
    amount,
  }: {
    programId: PublicKey,
    ownerInfo: {
      wallet: PublicKey,
      tokenAccount: PublicKey,
    },
    mint: ApiV3Token,
    amount: BN,
  }): Promise<ReturnTypeMakeInstructions> {
    const { publicKey: userState } = getPdaUserState(programId, ownerInfo.wallet)
  
    const userTokenCustody = getAssociatedTokenAddressSync(new PublicKey(mint.address), userState, true, new PublicKey(mint.programId))
  
    const ins = this.withdrawInstruction(
      programId,
      ownerInfo.wallet,
      userState,
      ownerInfo.tokenAccount,
      userTokenCustody,
      new PublicKey(mint.address),
      amount,
    )

    return {
      signers: [],
      instructions: [ins],
      instructionTypes: [InstructionType.CustodyWithdraw],
      address: { userState, userTokenCustody },
      lookupTableAddress: [],
    }
  }

  static openPositionFromLiquidityInstruction(
    custodyProgramId: PublicKey,
    remoteProgramId: PublicKey,
    payer: PublicKey,
    userState: PublicKey,
    positionNftMint: PublicKey,
    positionNftAccount: PublicKey,
    poolId: PublicKey,
    protocolPosition: PublicKey,
    tickArrayLower: PublicKey,
    tickArrayUpper: PublicKey,
    personalPosition: PublicKey,
    userTokenAccountA: PublicKey,
    userTokenAccountB: PublicKey,
    tokenCustodyA: PublicKey,
    tokenCustodyB: PublicKey,
    tokenVaultA: PublicKey,
    tokenVaultB: PublicKey,
    tokenMintA: PublicKey,
    tokenMintB: PublicKey,

    tickLowerIndex: number,
    tickUpperIndex: number,
    tickArrayLowerStartIndex: number,
    tickArrayUpperStartIndex: number,
    liquidity: BN,
    amountMaxA: BN,
    amountMaxB: BN,
    withMetadata: "create" | "no-create",

    exTickArrayBitmap?: PublicKey,
  ): TransactionInstruction {
    const dataLayout = struct([
      s32("tickLowerIndex"),
      s32("tickUpperIndex"),
      s32("tickArrayLowerStartIndex"),
      s32("tickArrayUpperStartIndex"),
      u128("liquidity"),
      u64("amountMaxA"),
      u64("amountMaxB"),
      bool("withMetadata"),
      u8("optionBaseFlag"),
      bool("baseFlag"),
    ])

    const remainingAccounts = [
      ...(exTickArrayBitmap ? [{ pubkey: exTickArrayBitmap, isSigner: false, isWritable: true }] : []),
    ]

    const keys = [
      { pubkey: payer, isSigner: true, isWritable: true },
      { pubkey: userState, isSigner: false, isWritable: true },
      { pubkey: positionNftMint, isSigner: true, isWritable: true },
      { pubkey: positionNftAccount, isSigner: false, isWritable: true },
      { pubkey: poolId, isSigner: false, isWritable: true },
      { pubkey: protocolPosition, isSigner: false, isWritable: true },
      { pubkey: tickArrayLower, isSigner: false, isWritable: true },
      { pubkey: tickArrayUpper, isSigner: false, isWritable: true },
      { pubkey: personalPosition, isSigner: false, isWritable: true },
      { pubkey: userTokenAccountA, isSigner: false, isWritable: true },
      { pubkey: userTokenAccountB, isSigner: false, isWritable: true },
      { pubkey: tokenCustodyA, isSigner: false, isWritable: true },
      { pubkey: tokenCustodyB, isSigner: false, isWritable: true },
      { pubkey: tokenVaultA, isSigner: false, isWritable: true },
      { pubkey: tokenVaultB, isSigner: false, isWritable: true },

      { pubkey: RENT_PROGRAM_ID, isSigner: false, isWritable: false },
      { pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
      { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
      { pubkey: ASSOCIATED_TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
      { pubkey: TOKEN_2022_PROGRAM_ID, isSigner: false, isWritable: false },

      { pubkey: tokenMintA, isSigner: false, isWritable: false },
      { pubkey: tokenMintB, isSigner: false, isWritable: false },

      { pubkey: remoteProgramId, isSigner: false, isWritable: false },

      ...remainingAccounts,
    ]

    const data = Buffer.alloc(dataLayout.span);
    dataLayout.encode(
      {
        tickLowerIndex,
        tickUpperIndex,
        tickArrayLowerStartIndex,
        tickArrayUpperStartIndex,
        liquidity,
        amountMaxA,
        amountMaxB,
        withMetadata: withMetadata === "create",
        baseFlag: false,
        optionBaseFlag: 0,
      },
      data,
    )

    const aData = Buffer.from([...anchorDataBuf.openPosition, ...data])

    return new TransactionInstruction({
      keys,
      programId: custodyProgramId,
      data: aData,
    })
  }

  static async openPositionFromLiquidityInstructions({
    custodyProgramId,
    poolInfo,
    poolKeys,
    ownerInfo,
    tickLower,
    tickUpper,
    liquidity,
    amountMaxA,
    amountMaxB,
    withMetadata,
    getEphemeralSigners,
  }: {
    custodyProgramId: PublicKey,
    poolInfo: ApiV3PoolInfoConcentratedItem;
    poolKeys: ClmmKeys;
    ownerInfo: {
      feePayer: PublicKey;
      wallet: PublicKey;
      tokenAccountA: PublicKey;
      tokenAccountB: PublicKey;
    };

    tickLower: number;
    tickUpper: number;
    liquidity: BN;
    amountMaxA: BN;
    amountMaxB: BN;
    withMetadata: "create" | "no-create";
    getEphemeralSigners?: (k: number) => any;
  }): Promise<ReturnTypeMakeInstructions> {
    const signers: Signer[] = [];
    const [programId, id] = [new PublicKey(poolInfo.programId), new PublicKey(poolInfo.id)];

    let nftMintAccount;
    if (getEphemeralSigners) {
      nftMintAccount = new PublicKey((await getEphemeralSigners(1))[0]);
    } else {
      const _k = Keypair.generate();
      signers.push(_k);
      nftMintAccount = _k.publicKey;
    }

    const tickArrayLowerStartIndex = TickUtils.getTickArrayStartIndexByTick(tickLower, poolInfo.config.tickSpacing);
    const tickArrayUpperStartIndex = TickUtils.getTickArrayStartIndexByTick(tickUpper, poolInfo.config.tickSpacing);

    const { publicKey: tickArrayLower } = getPdaTickArrayAddress(programId, id, tickArrayLowerStartIndex);
    const { publicKey: tickArrayUpper } = getPdaTickArrayAddress(programId, id, tickArrayUpperStartIndex);

    const { publicKey: userState } = getPdaUserState(custodyProgramId, ownerInfo.wallet)
    const { publicKey: tokenCustodyA } = getATAAddress(userState, new PublicKey(poolInfo.mintA.address), new PublicKey(poolInfo.mintA.programId))
    const { publicKey: tokenCustodyB } = getATAAddress(userState, new PublicKey(poolInfo.mintB.address), new PublicKey(poolInfo.mintB.programId))

    const { publicKey: positionNftAccount } = getATAAddress(userState, nftMintAccount, TOKEN_2022_PROGRAM_ID)
    const { publicKey: personalPosition } = getPdaPersonalPositionAddress(programId, nftMintAccount);
    const { publicKey: protocolPosition } = getPdaProtocolPositionAddress(programId, id, tickLower, tickUpper);

    const ins = this.openPositionFromLiquidityInstruction(
      custodyProgramId,
      programId,
      ownerInfo.feePayer,
      userState,
      nftMintAccount,
      positionNftAccount,
      id,
      protocolPosition,
      tickArrayLower,
      tickArrayUpper,
      personalPosition,
      ownerInfo.tokenAccountA,
      ownerInfo.tokenAccountB,
      tokenCustodyA,
      tokenCustodyB,
      new PublicKey(poolKeys.vault.A),
      new PublicKey(poolKeys.vault.B),
      new PublicKey(poolInfo.mintA.address),
      new PublicKey(poolInfo.mintB.address),

      tickLower,
      tickUpper,
      tickArrayLowerStartIndex,
      tickArrayUpperStartIndex,
      liquidity,
      amountMaxA,
      amountMaxB,
      withMetadata,
      PoolUtils.isOverflowDefaultTickarrayBitmap(poolInfo.config.tickSpacing, [
        tickArrayLowerStartIndex,
        tickArrayUpperStartIndex,
      ])
        ? getPdaExBitmapAccount(programId, id).publicKey
        : undefined,
    )

    return {
      address: {
        nftMint: nftMintAccount,
        tickArrayLower,
        tickArrayUpper,
        positionNftAccount,
        personalPosition,
        protocolPosition,
      },
      instructions: [ins],
      signers,
      instructionTypes: [InstructionType.CustodyOpenPosition],
      lookupTableAddress: poolKeys.lookupTableAccount ? [poolKeys.lookupTableAccount] : [],
    }
  }

  static openPositionFromBaseInstruction(
    custodyProgramId: PublicKey,
    remoteProgramId: PublicKey,
    payer: PublicKey,
    userState: PublicKey,
    positionNftMint: PublicKey,
    positionNftAccount: PublicKey,
    poolId: PublicKey,
    protocolPosition: PublicKey,
    tickArrayLower: PublicKey,
    tickArrayUpper: PublicKey,
    personalPosition: PublicKey,
    userTokenAccountA: PublicKey,
    userTokenAccountB: PublicKey,
    tokenCustodyA: PublicKey,
    tokenCustodyB: PublicKey,
    tokenVaultA: PublicKey,
    tokenVaultB: PublicKey,
    tokenMintA: PublicKey,
    tokenMintB: PublicKey,

    tickLowerIndex: number,
    tickUpperIndex: number,
    tickArrayLowerStartIndex: number,
    tickArrayUpperStartIndex: number,

    withMetadata: "create" | "no-create",
    base: "MintA" | "MintB",
    baseAmount: BN,
    otherAmountMax: BN,

    exTickArrayBitmap?: PublicKey,
  ): TransactionInstruction {
    const dataLayout = struct([
      s32("tickLowerIndex"),
      s32("tickUpperIndex"),
      s32("tickArrayLowerStartIndex"),
      s32("tickArrayUpperStartIndex"),
      u128("liquidity"),
      u64("amountMaxA"),
      u64("amountMaxB"),
      bool("withMetadata"),
      u8("optionBaseFlag"),
      bool("baseFlag"),
    ])

    const remainingAccounts = [
      ...(exTickArrayBitmap ? [{ pubkey: exTickArrayBitmap, isSigner: false, isWritable: true }] : []),
    ]

    const keys = [
      { pubkey: payer, isSigner: true, isWritable: true },
      { pubkey: userState, isSigner: false, isWritable: true },
      { pubkey: positionNftMint, isSigner: true, isWritable: true },
      { pubkey: positionNftAccount, isSigner: false, isWritable: true },
      { pubkey: poolId, isSigner: false, isWritable: true },
      { pubkey: protocolPosition, isSigner: false, isWritable: true },
      { pubkey: tickArrayLower, isSigner: false, isWritable: true },
      { pubkey: tickArrayUpper, isSigner: false, isWritable: true },
      { pubkey: personalPosition, isSigner: false, isWritable: true },
      { pubkey: userTokenAccountA, isSigner: false, isWritable: true },
      { pubkey: userTokenAccountB, isSigner: false, isWritable: true },
      { pubkey: tokenCustodyA, isSigner: false, isWritable: true },
      { pubkey: tokenCustodyB, isSigner: false, isWritable: true },
      { pubkey: tokenVaultA, isSigner: false, isWritable: true },
      { pubkey: tokenVaultB, isSigner: false, isWritable: true },

      { pubkey: RENT_PROGRAM_ID, isSigner: false, isWritable: false },
      { pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
      { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
      { pubkey: ASSOCIATED_TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
      { pubkey: TOKEN_2022_PROGRAM_ID, isSigner: false, isWritable: false },

      { pubkey: tokenMintA, isSigner: false, isWritable: false },
      { pubkey: tokenMintB, isSigner: false, isWritable: false },

      { pubkey: remoteProgramId, isSigner: false, isWritable: false },

      ...remainingAccounts,
    ]

    const data = Buffer.alloc(dataLayout.span)
    dataLayout.encode(
      {
        tickLowerIndex,
        tickUpperIndex,
        tickArrayLowerStartIndex,
        tickArrayUpperStartIndex,
        liquidity: new BN(0),
        amountMaxA: base === "MintA" ? baseAmount : otherAmountMax,
        amountMaxB: base === "MintA" ? otherAmountMax : baseAmount,
        withMetadata: withMetadata === "create",
        baseFlag: base === "MintA",
        optionBaseFlag: 1,
      },
      data,
    )

    const aData = Buffer.from([...anchorDataBuf.openPosition, ...data])
    return new TransactionInstruction({
      keys,
      programId: custodyProgramId,
      data: aData,
    })
  }

  static async openPositionFromBaseInstructions({
    custodyProgramId,
    poolInfo,
    poolKeys,
    ownerInfo,
    tickLower,
    tickUpper,
    base,
    baseAmount,
    otherAmountMax,
    withMetadata,
    getEphemeralSigners,
  }: {
    custodyProgramId: PublicKey,
    poolInfo: ApiV3PoolInfoConcentratedItem;
    poolKeys: ClmmKeys;
    ownerInfo: {
      feePayer: PublicKey;
      wallet: PublicKey;
      tokenAccountA: PublicKey;
      tokenAccountB: PublicKey;
    };

    tickLower: number;
    tickUpper: number;

    base: "MintA" | "MintB";
    baseAmount: BN;

    otherAmountMax: BN;
    withMetadata: "create" | "no-create";
    getEphemeralSigners?: (k: number) => any;
  }): Promise<ReturnTypeMakeInstructions> {
    const signers: Signer[] = []
    const [programId, id] = [new PublicKey(poolInfo.programId), new PublicKey(poolInfo.id)]

    let nftMintAccount: PublicKey;
    if (getEphemeralSigners) {
      nftMintAccount = new PublicKey((await getEphemeralSigners(1))[0]);
    } else {
      const _k = Keypair.generate();
      signers.push(_k);
      nftMintAccount = _k.publicKey;
    }

    const tickArrayLowerStartIndex = TickUtils.getTickArrayStartIndexByTick(tickLower, poolInfo.config.tickSpacing);
    const tickArrayUpperStartIndex = TickUtils.getTickArrayStartIndexByTick(tickUpper, poolInfo.config.tickSpacing);

    const { publicKey: tickArrayLower } = getPdaTickArrayAddress(programId, id, tickArrayLowerStartIndex);
    const { publicKey: tickArrayUpper } = getPdaTickArrayAddress(programId, id, tickArrayUpperStartIndex);

    const { publicKey: userState } = getPdaUserState(custodyProgramId, ownerInfo.wallet)
    const { publicKey: tokenCustodyA } = getATAAddress(userState, new PublicKey(poolInfo.mintA.address), new PublicKey(poolInfo.mintA.programId))
    const { publicKey: tokenCustodyB } = getATAAddress(userState, new PublicKey(poolInfo.mintB.address), new PublicKey(poolInfo.mintB.programId))

    const { publicKey: positionNftAccount } = getATAAddress(userState, nftMintAccount, TOKEN_2022_PROGRAM_ID)
    const { publicKey: personalPosition } = getPdaPersonalPositionAddress(programId, nftMintAccount);
    const { publicKey: protocolPosition } = getPdaProtocolPositionAddress(programId, id, tickLower, tickUpper);    

    const ins = this.openPositionFromBaseInstruction(
      custodyProgramId,
      programId,
      ownerInfo.feePayer,
      userState,
      nftMintAccount,
      positionNftAccount,
      id,
      protocolPosition,
      tickArrayLower,
      tickArrayUpper,
      personalPosition,
      ownerInfo.tokenAccountA,
      ownerInfo.tokenAccountB,
      tokenCustodyA,
      tokenCustodyB,
      new PublicKey(poolKeys.vault.A),
      new PublicKey(poolKeys.vault.B),
      new PublicKey(poolInfo.mintA.address),
      new PublicKey(poolInfo.mintB.address),

      tickLower,
      tickUpper,
      tickArrayLowerStartIndex,
      tickArrayUpperStartIndex,
      withMetadata,
      base,
      baseAmount,
      otherAmountMax,
      PoolUtils.isOverflowDefaultTickarrayBitmap(poolInfo.config.tickSpacing, [
        tickArrayLowerStartIndex,
        tickArrayUpperStartIndex,
      ])
        ? getPdaExBitmapAccount(programId, id).publicKey
        : undefined,
    )

    return {
      address: {
        nftMint: nftMintAccount,
        tickArrayLower,
        tickArrayUpper,
        positionNftAccount,
        personalPosition,
        protocolPosition,
      },
      instructions: [ins],
      signers,
      instructionTypes: [InstructionType.CustodyOpenPosition],
      lookupTableAddress: poolKeys.lookupTableAccount ? [poolKeys.lookupTableAccount] : [],
    }
  }

  static closePositionInstruction(
    custodyProgramId: PublicKey,
    remoteProgramId: PublicKey,
    payer: PublicKey,
    userState: PublicKey,
    positionNftMint: PublicKey,
    positionNftAccount: PublicKey,
    personalPosition: PublicKey,
  ): TransactionInstruction {
    const dataLayout = struct([])

    const keys = [
      { pubkey: payer, isSigner: true, isWritable: true },
      { pubkey: userState, isSigner: false, isWritable: true },
      { pubkey: positionNftMint, isSigner: false, isWritable: true },
      { pubkey: positionNftAccount, isSigner: false, isWritable: true },
      { pubkey: personalPosition, isSigner: false, isWritable: true },

      { pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
      { pubkey: TOKEN_2022_PROGRAM_ID, isSigner: false, isWritable: false },

      { pubkey: remoteProgramId, isSigner: false, isWritable: false },
    ]

    const data = Buffer.alloc(dataLayout.span)
    const aData = Buffer.from([...anchorDataBuf.closePosition, ...data])

    return new TransactionInstruction({
      keys,
      programId: custodyProgramId,
      data: aData,
    })
  }

  static closePositionInstructions({
    custodyProgramId,
    poolInfo,
    poolKeys,
    ownerInfo,
    ownerPosition,
  }: {
    custodyProgramId: PublicKey,
    poolInfo: ApiV3PoolInfoConcentratedItem
    poolKeys: ClmmKeys
    ownerInfo: {
      feePayer: PublicKey;
      wallet: PublicKey;
    }
    ownerPosition: ClmmPositionLayout
  }): ReturnTypeMakeInstructions<ClosePositionExtInfo["address"]> {
    const programId = new PublicKey(poolInfo.programId);

    const { publicKey: userState } = getPdaUserState(custodyProgramId, ownerInfo.wallet)
    const positionNftAccount = getATAAddress(userState, ownerPosition.nftMint, TOKEN_2022_PROGRAM_ID).publicKey
    const { publicKey: personalPosition } = getPdaPersonalPositionAddress(programId, ownerPosition.nftMint);

    const ins = this.closePositionInstruction(
      custodyProgramId,
      programId,
      ownerInfo.feePayer,
      userState,
      ownerPosition.nftMint,
      positionNftAccount,
      personalPosition,
    )

    return {
      address: {
        positionNftAccount,
        personalPosition,
      },
      signers: [],
      instructions: [ins],
      instructionTypes: [InstructionType.CustodyClosePosition],
      lookupTableAddress: poolKeys.lookupTableAccount ? [poolKeys.lookupTableAccount] : [],
    }
  }

  static increasePositionFromLiquidityInstruction(
    custodyProgramId: PublicKey,
    remoteProgramId: PublicKey,
    payer: PublicKey,
    userState: PublicKey,
    positionNftAccount: PublicKey,
    personalPosition: PublicKey,

    poolId: PublicKey,
    protocolPosition: PublicKey,
    tickArrayLower: PublicKey,
    tickArrayUpper: PublicKey,
    tokenCustodyA: PublicKey,
    tokenCustodyB: PublicKey,
    tokenVaultA: PublicKey,
    tokenVaultB: PublicKey,
    tokenMintA: PublicKey,
    tokenMintB: PublicKey,

    liquidity: BN,
    amountMaxA: BN,
    amountMaxB: BN,

    exTickArrayBitmap?: PublicKey,    
  ): TransactionInstruction {
    const dataLayout = struct([
      u128("liquidity"),
      u64("amountMaxA"),
      u64("amountMaxB"),
      u8("optionBaseFlag"),
      bool("baseFlag"),
    ])

    const remainingAccounts = [
      ...(exTickArrayBitmap ? [{ pubkey: exTickArrayBitmap, isSigner: false, isWritable: true }] : []),
    ]

    const keys = [
      { pubkey: payer, isSigner: true, isWritable: true },
      { pubkey: userState, isSigner: false, isWritable: false },
      { pubkey: positionNftAccount, isSigner: false, isWritable: false },
      { pubkey: poolId, isSigner: false, isWritable: true },
      { pubkey: protocolPosition, isSigner: false, isWritable: true },
      { pubkey: personalPosition, isSigner: false, isWritable: true },
      { pubkey: tickArrayLower, isSigner: false, isWritable: true },
      { pubkey: tickArrayUpper, isSigner: false, isWritable: true },
      { pubkey: tokenCustodyA, isSigner: false, isWritable: true },
      { pubkey: tokenCustodyB, isSigner: false, isWritable: true },
      { pubkey: tokenVaultA, isSigner: false, isWritable: true },
      { pubkey: tokenVaultB, isSigner: false, isWritable: true },
      { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
      { pubkey: TOKEN_2022_PROGRAM_ID, isSigner: false, isWritable: false },
      { pubkey: tokenMintA, isSigner: false, isWritable: false },
      { pubkey: tokenMintB, isSigner: false, isWritable: false },

      { pubkey: remoteProgramId, isSigner: false, isWritable: false },

      ...remainingAccounts,
    ]

    const data = Buffer.alloc(dataLayout.span);
    dataLayout.encode(
      {
        liquidity,
        amountMaxA,
        amountMaxB,
        optionBaseFlag: 0,
        baseFlag: false,
      },
      data,
    );

    const aData = Buffer.from([...anchorDataBuf.increaseLiquidity, ...data]);

    return new TransactionInstruction({
      keys,
      programId: custodyProgramId,
      data: aData,
    })
  }

  static increasePositionFromLiquidityInstructions({
    custodyProgramId,
    poolInfo,
    poolKeys,
    ownerPosition,
    ownerInfo,
    liquidity,
    amountMaxA,
    amountMaxB,
  }: {
    custodyProgramId: PublicKey,
    poolInfo: ApiV3PoolInfoConcentratedItem,
    poolKeys: ClmmKeys,
    ownerPosition: ClmmPositionLayout;
    ownerInfo: {
      wallet: PublicKey;
      feePayer: PublicKey;
    };

    liquidity: BN;
    amountMaxA: BN;
    amountMaxB: BN;
  }): ReturnTypeMakeInstructions<ManipulateLiquidityExtInfo["address"]> {
    const [programId, id] = [new PublicKey(poolInfo.programId), new PublicKey(poolInfo.id)];
    const tickArrayLowerStartIndex = TickUtils.getTickArrayStartIndexByTick(
      ownerPosition.tickLower,
      poolInfo.config.tickSpacing,
    );
    const tickArrayUpperStartIndex = TickUtils.getTickArrayStartIndexByTick(
      ownerPosition.tickUpper,
      poolInfo.config.tickSpacing,
    );

    const { publicKey: tickArrayLower } = getPdaTickArrayAddress(programId, id, tickArrayLowerStartIndex);
    const { publicKey: tickArrayUpper } = getPdaTickArrayAddress(programId, id, tickArrayUpperStartIndex);

    const { publicKey: userState } = getPdaUserState(custodyProgramId, ownerInfo.wallet)
    const { publicKey: tokenCustodyA } = getATAAddress(userState, new PublicKey(poolInfo.mintA.address), new PublicKey(poolInfo.mintA.programId))
    const { publicKey: tokenCustodyB } = getATAAddress(userState, new PublicKey(poolInfo.mintB.address), new PublicKey(poolInfo.mintB.programId))

    const { publicKey: positionNftAccount } = getATAAddress(userState, ownerPosition.nftMint, TOKEN_2022_PROGRAM_ID)
    const { publicKey: personalPosition } = getPdaPersonalPositionAddress(programId, ownerPosition.nftMint);
    const { publicKey: protocolPosition } = getPdaProtocolPositionAddress(
      programId,
      id,
      ownerPosition.tickLower,
      ownerPosition.tickUpper,
    )
    

    const ins = this.increasePositionFromLiquidityInstruction(
      custodyProgramId,
      programId,
      ownerInfo.wallet,
      userState,
      positionNftAccount,
      personalPosition,
      id,
      protocolPosition,
      tickArrayLower,
      tickArrayUpper,
      tokenCustodyA,
      tokenCustodyB,
      new PublicKey(poolKeys.vault.A),
      new PublicKey(poolKeys.vault.B),
      new PublicKey(poolInfo.mintA.address),
      new PublicKey(poolInfo.mintB.address),

      liquidity,
      amountMaxA,
      amountMaxB,
      PoolUtils.isOverflowDefaultTickarrayBitmap(poolInfo.config.tickSpacing, [
        tickArrayLowerStartIndex,
        tickArrayUpperStartIndex,
      ])
        ? getPdaExBitmapAccount(programId, id).publicKey
        : undefined,
    )

    return {
      address: {
        tickArrayLower,
        tickArrayUpper,
        positionNftAccount,
        personalPosition,
        protocolPosition,
      },
      instructions: [ins],
      signers: [],
      instructionTypes: [InstructionType.CustodyIncreasePosition],
      lookupTableAddress: poolKeys.lookupTableAccount ? [poolKeys.lookupTableAccount] : [],
    }
  }

  static increasePositionFromBaseInstruction(
    custodyProgramId: PublicKey,
    remoteProgramId: PublicKey,
    payer: PublicKey,
    userState: PublicKey,
    positionNftAccount: PublicKey,
    personalPosition: PublicKey,

    poolId: PublicKey,
    protocolPosition: PublicKey,
    tickArrayLower: PublicKey,
    tickArrayUpper: PublicKey,
    tokenCustodyA: PublicKey,
    tokenCustodyB: PublicKey,
    tokenVaultA: PublicKey,
    tokenVaultB: PublicKey,
    tokenMintA: PublicKey,
    tokenMintB: PublicKey,

    base: "MintA" | "MintB",
    baseAmount: BN,

    otherAmountMax: BN,

    exTickArrayBitmap?: PublicKey,
  ): TransactionInstruction {
    const dataLayout = struct([
      u128("liquidity"),
      u64("amountMaxA"),
      u64("amountMaxB"),
      u8("optionBaseFlag"),
      bool("baseFlag"),
    ])

    const remainingAccounts = [
      ...(exTickArrayBitmap ? [{ pubkey: exTickArrayBitmap, isSigner: false, isWritable: true }] : []),
    ];

    const keys = [
      { pubkey: payer, isSigner: true, isWritable: true },
      { pubkey: userState, isSigner: false, isWritable: false },
      { pubkey: positionNftAccount, isSigner: false, isWritable: false },
      { pubkey: poolId, isSigner: false, isWritable: true },
      { pubkey: protocolPosition, isSigner: false, isWritable: true },
      { pubkey: personalPosition, isSigner: false, isWritable: true },
      { pubkey: tickArrayLower, isSigner: false, isWritable: true },
      { pubkey: tickArrayUpper, isSigner: false, isWritable: true },
      { pubkey: tokenCustodyA, isSigner: false, isWritable: true },
      { pubkey: tokenCustodyB, isSigner: false, isWritable: true },
      { pubkey: tokenVaultA, isSigner: false, isWritable: true },
      { pubkey: tokenVaultB, isSigner: false, isWritable: true },
      { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
      { pubkey: TOKEN_2022_PROGRAM_ID, isSigner: false, isWritable: false },
      { pubkey: tokenMintA, isSigner: false, isWritable: false },
      { pubkey: tokenMintB, isSigner: false, isWritable: false },

      { pubkey: remoteProgramId, isSigner: false, isWritable: false },

      ...remainingAccounts,
    ];

    const data = Buffer.alloc(dataLayout.span)
    dataLayout.encode(
      {
        liquidity: new BN(0),
        amountMaxA: base === "MintA" ? baseAmount : otherAmountMax,
        amountMaxB: base === "MintA" ? otherAmountMax : baseAmount,
        baseFlag: base === "MintA",
        optionBaseFlag: 1,
      },
      data,
    )

    const aData = Buffer.from([...anchorDataBuf.increaseLiquidity, ...data])

    return new TransactionInstruction({
      keys,
      programId: custodyProgramId,
      data: aData,
    })
  }

  static increasePositionFromBaseInstructions({
    custodyProgramId,
    poolInfo,
    poolKeys,
    ownerPosition,
    ownerInfo,
    base,
    baseAmount,
    otherAmountMax,
  }: {
    custodyProgramId: PublicKey,
    poolInfo: ApiV3PoolInfoConcentratedItem;
    poolKeys: ClmmKeys;
    ownerPosition: ClmmPositionLayout;

    ownerInfo: {
      feePayer: PublicKey;
      wallet: PublicKey;
    };

    base: "MintA" | "MintB";
    baseAmount: BN;

    otherAmountMax: BN;
  }): ReturnTypeMakeInstructions<ManipulateLiquidityExtInfo["address"]> {
    const [programId, id] = [new PublicKey(poolInfo.programId), new PublicKey(poolInfo.id)];
    const tickArrayLowerStartIndex = TickUtils.getTickArrayStartIndexByTick(
      ownerPosition.tickLower,
      poolInfo.config.tickSpacing,
    );
    const tickArrayUpperStartIndex = TickUtils.getTickArrayStartIndexByTick(
      ownerPosition.tickUpper,
      poolInfo.config.tickSpacing,
    );

    const { publicKey: tickArrayLower } = getPdaTickArrayAddress(programId, id, tickArrayLowerStartIndex);
    const { publicKey: tickArrayUpper } = getPdaTickArrayAddress(programId, id, tickArrayUpperStartIndex);

    const { publicKey: userState } = getPdaUserState(custodyProgramId, ownerInfo.wallet)
    const { publicKey: tokenCustodyA } = getATAAddress(userState, new PublicKey(poolInfo.mintA.address), new PublicKey(poolInfo.mintA.programId))
    const { publicKey: tokenCustodyB } = getATAAddress(userState, new PublicKey(poolInfo.mintB.address), new PublicKey(poolInfo.mintB.programId))

    const { publicKey: positionNftAccount } = getATAAddress(userState, ownerPosition.nftMint, TOKEN_2022_PROGRAM_ID)
    const { publicKey: personalPosition } = getPdaPersonalPositionAddress(programId, ownerPosition.nftMint);
    const { publicKey: protocolPosition } = getPdaProtocolPositionAddress(
      programId,
      id,
      ownerPosition.tickLower,
      ownerPosition.tickUpper,
    )

    return {
      address: {
        tickArrayLower,
        tickArrayUpper,
        positionNftAccount,
        personalPosition,
        protocolPosition,
      },
      instructions: [
        this.increasePositionFromBaseInstruction(
          custodyProgramId,
          programId,
          ownerInfo.feePayer,
          userState,
          positionNftAccount,
          personalPosition,
          id,
          protocolPosition,
          tickArrayLower,
          tickArrayUpper,
          tokenCustodyA,
          tokenCustodyB,
          new PublicKey(poolKeys.vault.A),
          new PublicKey(poolKeys.vault.B),
          new PublicKey(poolInfo.mintA.address),
          new PublicKey(poolInfo.mintB.address),

          base,
          baseAmount,

          otherAmountMax,
          PoolUtils.isOverflowDefaultTickarrayBitmap(poolInfo.config.tickSpacing, [
            tickArrayLowerStartIndex,
            tickArrayUpperStartIndex,
          ])
            ? getPdaExBitmapAccount(programId, id).publicKey
            : undefined,
        ),
      ],
      signers: [],
      instructionTypes: [InstructionType.CustodyIncreasePosition],
      lookupTableAddress: poolKeys.lookupTableAccount ? [poolKeys.lookupTableAccount] : [],
    }
  }

  static decreaseLiquidityInstruction(
    custodyProgramId: PublicKey,
    remoteProgramId: PublicKey,
    payer: PublicKey,
    userState: PublicKey,
    positionNftAccount: PublicKey,
    personalPosition: PublicKey,

    poolId: PublicKey,
    protocolPosition: PublicKey,
    tickArrayLower: PublicKey,
    tickArrayUpper: PublicKey,
    tokenCustodyA: PublicKey,
    tokenCustodyB: PublicKey,
    tokenVaultA: PublicKey,
    tokenVaultB: PublicKey,
    tokenMintA: PublicKey,
    tokenMintB: PublicKey,
    rewardAccounts: {
      poolRewardVault: PublicKey;
      ownerRewardVault: PublicKey;
      rewardMint: PublicKey;
    }[],

    liquidity: BN,
    amountMinA: BN,
    amountMinB: BN,

    exTickArrayBitmap?: PublicKey,
  ): TransactionInstruction {
    const dataLayout = struct([u128("liquidity"), u64("amountMinA"), u64("amountMinB")]);

    const remainingAccounts = [
      ...(exTickArrayBitmap ? [{ pubkey: exTickArrayBitmap, isSigner: false, isWritable: true }] : []),
      ...rewardAccounts
        .map((i) => [
          { pubkey: i.poolRewardVault, isSigner: false, isWritable: true },
          { pubkey: i.ownerRewardVault, isSigner: false, isWritable: true },
          { pubkey: i.rewardMint, isSigner: false, isWritable: false },
        ])
        .flat(),
    ];

    const keys = [
      { pubkey: payer, isSigner: true, isWritable: true },
      { pubkey: userState, isSigner: false, isWritable: false },
      { pubkey: positionNftAccount, isSigner: false, isWritable: false },
      { pubkey: personalPosition, isSigner: false, isWritable: true },
      { pubkey: poolId, isSigner: false, isWritable: true },
      { pubkey: protocolPosition, isSigner: false, isWritable: true },
      { pubkey: tokenVaultA, isSigner: false, isWritable: true },
      { pubkey: tokenVaultB, isSigner: false, isWritable: true },
      { pubkey: tickArrayLower, isSigner: false, isWritable: true },
      { pubkey: tickArrayUpper, isSigner: false, isWritable: true },
      { pubkey: tokenCustodyA, isSigner: false, isWritable: true },
      { pubkey: tokenCustodyB, isSigner: false, isWritable: true },
      { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
      { pubkey: TOKEN_2022_PROGRAM_ID, isSigner: false, isWritable: false },
      { pubkey: MEMO_PROGRAM_ID, isSigner: false, isWritable: false },
      { pubkey: tokenMintA, isSigner: false, isWritable: false },
      { pubkey: tokenMintB, isSigner: false, isWritable: false },

      { pubkey: remoteProgramId, isSigner: false, isWritable: false },

      ...remainingAccounts,
    ];

    const data = Buffer.alloc(dataLayout.span);
    dataLayout.encode(
      {
        liquidity,
        amountMinA,
        amountMinB,
      },
      data,
    );

    const aData = Buffer.from([...anchorDataBuf.decreaseLiquidity, ...data]);

    return new TransactionInstruction({
      keys,
      programId: custodyProgramId,
      data: aData,
    });
  }

  static decreaseLiquidityInstructions({
    custodyProgramId,
    poolInfo,
    poolKeys,
    ownerPosition,
    ownerInfo,
    liquidity,
    amountMinA,
    amountMinB,
  }: {
    custodyProgramId: PublicKey,
    poolInfo: ApiV3PoolInfoConcentratedItem;
    poolKeys: ClmmKeys;
    ownerPosition: ClmmPositionLayout;
    ownerInfo: {
      feePayer: PublicKey;
      wallet: PublicKey;
      rewardAccounts: PublicKey[];
    };

    liquidity: BN;
    amountMinA: BN;
    amountMinB: BN;
  }): ReturnTypeMakeInstructions<ManipulateLiquidityExtInfo["address"]> {
    const [poolProgramId, id] = [new PublicKey(poolInfo.programId), new PublicKey(poolInfo.id)];
    const tickArrayLowerStartIndex = TickUtils.getTickArrayStartIndexByTick(
      ownerPosition.tickLower,
      poolInfo.config.tickSpacing,
    );
    const tickArrayUpperStartIndex = TickUtils.getTickArrayStartIndexByTick(
      ownerPosition.tickUpper,
      poolInfo.config.tickSpacing,
    );

    const { publicKey: tickArrayLower } = getPdaTickArrayAddress(poolProgramId, id, tickArrayLowerStartIndex);
    const { publicKey: tickArrayUpper } = getPdaTickArrayAddress(poolProgramId, id, tickArrayUpperStartIndex);

    const { publicKey: userState } = getPdaUserState(custodyProgramId, ownerInfo.wallet)
    const { publicKey: tokenCustodyA } = getATAAddress(userState, new PublicKey(poolInfo.mintA.address), new PublicKey(poolInfo.mintA.programId))
    const { publicKey: tokenCustodyB } = getATAAddress(userState, new PublicKey(poolInfo.mintB.address), new PublicKey(poolInfo.mintB.programId))

    const { publicKey: positionNftAccount } = getATAAddress(userState, ownerPosition.nftMint, TOKEN_2022_PROGRAM_ID)
    const { publicKey: personalPosition } = getPdaPersonalPositionAddress(poolProgramId, ownerPosition.nftMint);
    const { publicKey: protocolPosition } = getPdaProtocolPositionAddress(
      poolProgramId,
      id,
      ownerPosition.tickLower,
      ownerPosition.tickUpper,
    );

    const rewardAccounts: {
      poolRewardVault: PublicKey;
      ownerRewardVault: PublicKey;
      rewardMint: PublicKey;
    }[] = [];
    for (let i = 0; i < poolKeys.rewardInfos.length; i++) {
      rewardAccounts.push({
        poolRewardVault: new PublicKey(poolKeys.rewardInfos[i].vault),
        ownerRewardVault: ownerInfo.rewardAccounts[i],
        rewardMint: new PublicKey(poolKeys.rewardInfos[i].mint.address),
      })
    }

    const ins = this.decreaseLiquidityInstruction(
      custodyProgramId,
      poolProgramId,
      ownerInfo.feePayer,
      userState,
      positionNftAccount,
      personalPosition,
      id,
      protocolPosition,
      tickArrayLower,
      tickArrayUpper,
      tokenCustodyA,
      tokenCustodyB,
      new PublicKey(poolKeys.vault.A),
      new PublicKey(poolKeys.vault.B),
      new PublicKey(poolInfo.mintA.address),
      new PublicKey(poolInfo.mintB.address),
      rewardAccounts,

      liquidity,
      amountMinA,
      amountMinB,
      PoolUtils.isOverflowDefaultTickarrayBitmap(poolInfo.config.tickSpacing, [
        tickArrayLowerStartIndex,
        tickArrayUpperStartIndex,
      ])
        ? getPdaExBitmapAccount(poolProgramId, id).publicKey
        : undefined,
    )

    return {
      address: {
        tickArrayLower,
        tickArrayUpper,
        positionNftAccount,
        personalPosition,
        protocolPosition,
      },
      signers: [],
      instructions: [ins],
      instructionTypes: [InstructionType.CustodyDecreasePosition],
      lookupTableAddress: poolKeys.lookupTableAccount ? [poolKeys.lookupTableAccount] : [],
    }
  }
  
  static swapInstruction(
    custodyProgramId: PublicKey,
    remoteProgramId: PublicKey,
    payer: PublicKey,
    owner: PublicKey,
    poolId: PublicKey,
    ammConfigId: PublicKey,
    inputTokenAccount: PublicKey,
    outputTokenAccount: PublicKey,
    inputVault: PublicKey,
    outputVault: PublicKey,
    inputMint: PublicKey,
    outputMint: PublicKey,
    tickArray: PublicKey[],
    observationId: PublicKey,

    amount: BN,
    otherAmountThreshold: BN,
    sqrtPriceLimitX64: BN,
    isBaseInput: boolean,

    exTickArrayBitmap?: PublicKey,
  ): TransactionInstruction {
    const dataLayout = struct([
      u64("amount"),
      u64("otherAmountThreshold"),
      u128("sqrtPriceLimitX64"),
      bool("isBaseInput"),
    ]);

    const remainingAccounts = [
      ...(exTickArrayBitmap ? [{ pubkey: exTickArrayBitmap, isSigner: false, isWritable: true }] : []),
      ...tickArray.map((i) => ({ pubkey: i, isSigner: false, isWritable: true })),
    ]

    const { publicKey: userState } = getPdaUserState(custodyProgramId, owner)

    const keys = [
      { pubkey: payer, isSigner: true, isWritable: true },
      { pubkey: userState, isSigner: false, isWritable: true },
      { pubkey: ammConfigId, isSigner: false, isWritable: false },
      { pubkey: poolId, isSigner: false, isWritable: true },
      { pubkey: inputTokenAccount, isSigner: false, isWritable: true },
      { pubkey: outputTokenAccount, isSigner: false, isWritable: true },
      { pubkey: inputVault, isSigner: false, isWritable: true },
      { pubkey: outputVault, isSigner: false, isWritable: true },

      { pubkey: observationId, isSigner: false, isWritable: true },

      { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
      { pubkey: TOKEN_2022_PROGRAM_ID, isSigner: false, isWritable: false },
      { pubkey: MEMO_PROGRAM_ID, isSigner: false, isWritable: false },

      { pubkey: inputMint, isSigner: false, isWritable: false },
      { pubkey: outputMint, isSigner: false, isWritable: false },

      { pubkey: remoteProgramId, isSigner: false, isWritable: false },

      ...remainingAccounts,
    ];

    const data = Buffer.alloc(dataLayout.span)
    dataLayout.encode(
      {
        amount,
        otherAmountThreshold,
        sqrtPriceLimitX64,
        isBaseInput,
      },
      data
    )

    const aData = Buffer.from([...anchorDataBuf.swap, ...data])

    return new TransactionInstruction({
      keys,
      programId: custodyProgramId,
      data: aData,
    })
  }

  static makeSwapBaseInInstructions({
    custodyProgramId,
    poolInfo,
    poolKeys,
    observationId,
    ownerInfo,
    inputMint,
    amountIn,
    amountOutMin,
    sqrtPriceLimitX64,
    remainingAccounts,
  }: {
    custodyProgramId: PublicKey,
    poolInfo: Pick<ApiV3PoolInfoConcentratedItem, "id" | "programId" | "mintA" | "mintB" | "config">;
    poolKeys: ClmmKeys;
    observationId: PublicKey;
    ownerInfo: {
      feePayer: PublicKey;
      wallet: PublicKey;
    };

    inputMint: PublicKey;

    amountIn: BN;
    amountOutMin: BN;
    sqrtPriceLimitX64: BN;

    remainingAccounts: PublicKey[];
  }): ReturnTypeMakeInstructions {
    const [programId, id] = [new PublicKey(poolInfo.programId), new PublicKey(poolInfo.id)];
    const [mintAVault, mintBVault] = [new PublicKey(poolKeys.vault.A), new PublicKey(poolKeys.vault.B)];
    const [mintA, mintB] = [new PublicKey(poolInfo.mintA.address), new PublicKey(poolInfo.mintB.address)];

    const isInputMintA = poolInfo.mintA.address === inputMint.toString();

    const { publicKey: userState } = getPdaUserState(custodyProgramId, ownerInfo.wallet)
    const { publicKey: tokenCustodyA } = getATAAddress(userState, new PublicKey(poolInfo.mintA.address), new PublicKey(poolInfo.mintA.programId))
    const { publicKey: tokenCustodyB } = getATAAddress(userState, new PublicKey(poolInfo.mintB.address), new PublicKey(poolInfo.mintB.programId))

    const ins = this.swapInstruction(
      custodyProgramId,
      programId,
      ownerInfo.feePayer,
      ownerInfo.wallet,

      id,
      new PublicKey(poolInfo.config.id),

      isInputMintA ? tokenCustodyA : tokenCustodyB,
      isInputMintA ? tokenCustodyB : tokenCustodyA,

      isInputMintA ? mintAVault : mintBVault,
      isInputMintA ? mintBVault : mintAVault,

      isInputMintA ? mintA : mintB,
      isInputMintA ? mintB : mintA,

      remainingAccounts,
      observationId,
      amountIn,
      amountOutMin,
      sqrtPriceLimitX64,
      true,
      getPdaExBitmapAccount(programId, id).publicKey,
    )

    return {
      signers: [],
      instructions: [ins],
      instructionTypes: [InstructionType.CustodySwapBaseIn],
      lookupTableAddress: poolKeys.lookupTableAccount ? [poolKeys.lookupTableAccount] : [],
      address: {},
    }
  }

  static makeSwapBaseOutInstructions({
    custodyProgramId,
    poolInfo,
    poolKeys,
    observationId,
    ownerInfo,
    outputMint,
    amountOut,
    amountInMax,
    sqrtPriceLimitX64,
    remainingAccounts,
  }: {
    custodyProgramId: PublicKey,
    poolInfo: Pick<ApiV3PoolInfoConcentratedItem, "id" | "programId" | "mintA" | "mintB" | "config">;
    poolKeys: ClmmKeys;
    observationId: PublicKey;

    ownerInfo: {
      feePayer: PublicKey;
      wallet: PublicKey;
    };

    outputMint: PublicKey;

    amountOut: BN;
    amountInMax: BN;
    sqrtPriceLimitX64: BN;

    remainingAccounts: PublicKey[];
  }): ReturnTypeMakeInstructions {
    const [programId, id] = [new PublicKey(poolInfo.programId), new PublicKey(poolInfo.id)];
    const [mintAVault, mintBVault] = [new PublicKey(poolKeys.vault.A), new PublicKey(poolKeys.vault.B)];
    const [mintA, mintB] = [new PublicKey(poolInfo.mintA.address), new PublicKey(poolInfo.mintB.address)];
    const isInputMintA = poolInfo.mintA.address === outputMint.toBase58();

    const { publicKey: userState } = getPdaUserState(custodyProgramId, ownerInfo.wallet)
    const { publicKey: tokenCustodyA } = getATAAddress(userState, new PublicKey(poolInfo.mintA.address), new PublicKey(poolInfo.mintA.programId))
    const { publicKey: tokenCustodyB } = getATAAddress(userState, new PublicKey(poolInfo.mintB.address), new PublicKey(poolInfo.mintB.programId))

    const ins = this.swapInstruction(
      custodyProgramId,
      programId,
      ownerInfo.feePayer,
      ownerInfo.wallet,

      id,
      new PublicKey(poolInfo.config.id),

      isInputMintA ? tokenCustodyB : tokenCustodyA,
      isInputMintA ? tokenCustodyA : tokenCustodyB,

      isInputMintA ? mintBVault : mintAVault,
      isInputMintA ? mintAVault : mintBVault,

      isInputMintA ? mintB : mintA,
      isInputMintA ? mintA : mintB,

      remainingAccounts,
      observationId,
      amountOut,
      amountInMax,
      sqrtPriceLimitX64,
      false,
      getPdaExBitmapAccount(programId, id).publicKey,
    )

    return {
      signers: [],
      instructions: [ins],
      instructionTypes: [InstructionType.CustodySwapBaseOut],
      lookupTableAddress: poolKeys.lookupTableAccount ? [poolKeys.lookupTableAccount] : [],
      address: {},
    }
  }
}
