在Raydium上启动流动性池,采用保护策略以对抗机器人操纵…

  • Shyft_to
  • 发布于 2024-07-16 16:43
  • 阅读 19

本文是关于如何在Raydium上启动流动性池的系列文章的第二部分,重点介绍了使用Jito bundles来最大化代币的可见性和交易量,并提供了一个分步指南,包括构建完整的池密钥对象、创建和优化swap交易、以及如何将这些交易添加到Jito bundle中以确保原子执行。文章还提供了创建查找表以优化链上交易的步骤, 以及项目代码的GitHub链接。

在 Raydium 上启动流动性池,并采取保护策略来对抗机器人操纵 (第 2 部分)

在 Raydium 上使用 Jito bundles 启动 token,最大化 token 的可见性和交易量的分步指南

流动性池封面

在上一篇文章中,我们获得了关于 OpenBook 市场和 token 账户信息的重要数据。现在,我们将利用这些信息来构建完整的池密钥对象,这对于后续与流动性池的交互至关重要。

如果你错过了本博客的第一部分,请点击此处查看。整个项目代码都在GitHub 上供你跟进。

由于我们已经获得了钱包分配的基础 token 账户,现在我们继续检索报价 mint 信息。

  //lpCreate.ts

  const accountInfo_quote = await connection.getAccountInfo(quoteMint);
  if (!accountInfo_quote) throw Error("no accountInfo_quote");
  const quoteTokenProgramId = accountInfo_quote.owner;
  const quoteDecimals = unpackMint(
    quoteMint,
    accountInfo_quote,
    quoteTokenProgramId
  ).decimals; //报价的小数位数

  const associatedPoolKeys = await Liquidity.getAssociatedPoolKeys({
    version: 4,
    marketVersion: 3,
    baseMint,
    quoteMint,
    baseDecimals,
    quoteDecimals,
    marketId: new PublicKey(marketId),
    programId: MAINNET_PROGRAM_ID.AmmV4,
    marketProgramId: MAINNET_PROGRAM_ID.OPENBOOK_MARKET,
  });
  const { id: ammId, lpMint } = associatedPoolKeys;
  console.log("AMM ID: ", ammId.toString());
  console.log("lpMint: ", lpMint.toString());

接下来,我们确定基础 token 和报价 token 的小数精度。 随后,我们使用 getAssociatedPoolKeys 函数生成一个 LiquidityAssociatedPoolKeysV4 对象。这个对象本质上是一个标准的池密钥结构,其中省略了一些 OpenBook Market 特定的字段,这些字段我们在上一篇文章中已经获得。通过合并这些组件,我们构建了一个完整的池密钥对象,为后续操作铺平了道路。

  console.log("Quote Decimals: ", quoteDecimals);
  const targetPoolInfo = {
    id: associatedPoolKeys.id.toString(),
    baseMint: associatedPoolKeys.baseMint.toString(),
    quoteMint: associatedPoolKeys.quoteMint.toString(),
    lpMint: associatedPoolKeys.lpMint.toString(),
    baseDecimals: associatedPoolKeys.baseDecimals,
    quoteDecimals: associatedPoolKeys.quoteDecimals,
    lpDecimals: associatedPoolKeys.lpDecimals,
    version: 4,
    programId: associatedPoolKeys.programId.toString(),
    authority: associatedPoolKeys.authority.toString(),
    openOrders: associatedPoolKeys.openOrders.toString(),
    targetOrders: associatedPoolKeys.targetOrders.toString(),
    baseVault: associatedPoolKeys.baseVault.toString(),
    quoteVault: associatedPoolKeys.quoteVault.toString(),
    withdrawQueue: associatedPoolKeys.withdrawQueue.toString(),
    lpVault: associatedPoolKeys.lpVault.toString(),
    marketVersion: 3,
    marketProgramId: associatedPoolKeys.marketProgramId.toString(),
    marketId: associatedPoolKeys.marketId.toString(),
    marketAuthority: associatedPoolKeys.marketAuthority.toString(),
    marketBaseVault: marketBaseVault.toString(),
    marketQuoteVault: marketQuoteVault.toString(),
    marketBids: marketBids.toString(),
    marketAsks: marketAsks.toString(),
    marketEventQueue: marketEventQueue.toString(),
    lookupTableAccount: PublicKey.default.toString(),
  };
  console.log(targetPoolInfo);

  const poolKeys = jsonInfo2PoolKeys(targetPoolInfo) as LiquidityPoolKeys;

  // 创建流动性池并获取池密钥 + 池创建指令

  const { innerTransactions } =
    await Liquidity.makeCreatePoolV4InstructionV2Simple({
      connection,
      programId: MAINNET_PROGRAM_ID.AmmV4,
      marketInfo: {
        programId: MAINNET_PROGRAM_ID.OPENBOOK_MARKET,
        marketId: marketId,
      },
      associatedOnly: false,
      ownerInfo: {
        feePayer: wallet.publicKey,
        wallet: wallet.publicKey,
        tokenAccounts: tokenAccountInfo,
        useSOLBalance: true,
      },
      baseMintInfo: {
        mint: baseMint,
        decimals: baseDecimals,
      },
      quoteMintInfo: {
        mint: quoteMint,
        decimals: quoteDecimals,
      },

      startTime: new BN(Math.floor(Date.now() / 1000)),
      baseAmount: new BN(baseAmount.toString()),
      quoteAmount: new BN(quoteAmount.toString()),

      computeBudgetConfig: await getComputeBudgetConfig(),
      checkCreateATAOwner: true,
      makeTxVersion: TxVersion.V0,
      lookupTableCache: LOOKUP_TABLE_CACHE,
      feeDestinationId: new PublicKey(
        "7YttLkHDoNj9wyDur5pM1ejNaAvT9X4eqaYcHQqtj2G5"
      ),
    });

  const message = new TransactionMessage({
    instructions: innerTransactions.flatMap((it) => it.instructions),
    payerKey: wallet.publicKey,
    recentBlockhash: (await connection.getLatestBlockhash()).blockhash,
  }).compileToV0Message();

  const transaction = new VersionedTransaction(message);
  await transaction.sign([wallet]);

  return { poolKeys, createPoolTx: transaction };
}

然后,我们将所有导出的池密钥信息合并到一个完整的结构中。然后,使用 jsonInfo2PoolKeys 函数将这些数据处理成与 SDK 兼容的格式。随后,使用 makeCreatePoolV4InstructionV2Simple 函数生成一个初步的交易。从这个交易中,我们提取必要的指令,将它们打包到 VersionedTransaction 中,并将它们与池密钥集成,以便后续创建 Jito bundle。

我们可以通过将 startTime 字段设置为当前时间来立即启动流动性池,或者我们也可以将其设置为稍后的时间戳,以便在稍后的时间启动池。要自动将 SOL 字段转换为封装的 SOL,我们可以将 useSolBalance 字段设置为 true。

准备好这些对象后,让我们回到 index.ts 文件:

  const createPoolIxResponse = await createPoolIx(
    new PublicKey(marketId),
    wallet,
    walletTokenAccounts,
    inputToken.mint,
    outputToken.mint,
    createLpBaseAmount,
    createLpQuoteAmount
   );
  if (createPoolIxResponse) {
    const { poolKeys, createPoolTx } = createPoolIxResponse;
    //从 createPoolIxResponse 中提取指令

    console.log(poolKeys);
    // 我们有了创建池的指令,现在我们添加 swap 交易
    // 将池密钥传递给查找表
    const onlyPublicKeys = Object.values(poolKeys).filter(
      (poolKey) => poolKey instanceof PublicKey
    );
    const lookupTableAddress = await createLookupTable(
      wallet,
      onlyPublicKeys as PublicKey[]
    );

由于我们已经有了导出的池密钥,我们构建一个地址查找表来优化后续的 swap 交易。可以通过从整套池密钥中选择所有 pubkey 对象来轻松创建此查找表。以下函数用于创建查找表:

import {
  ComputeBudgetProgram,
  AddressLookupTableProgram,
  TransactionMessage,
  VersionedTransaction,
  PublicKey,
  Keypair,
} from "@solana/web3.js";
import { connection } from "./config";
export default async function createLookupTable(
  wallet: Keypair,
  addresses: PublicKey[]
) {
  let latestBH = await connection.getLatestBlockhash("finalized");
  const recentSlot = await connection.getSlot("finalized");

  const bribe = ComputeBudgetProgram.setComputeUnitPrice({
    microLamports: 25000,
  });
  const [lookupTableInst, lookupTableAddress] =
    await AddressLookupTableProgram.createLookupTable({
      authority: wallet.publicKey,
      recentSlot,
      payer: wallet.publicKey,
    });

  const LUTmessage = new TransactionMessage({
    instructions: [bribe, lookupTableInst],
    payerKey: wallet.publicKey,
    recentBlockhash: latestBH.blockhash,
  }).compileToV0Message();
  const tx = new VersionedTransaction(LUTmessage);
  tx.sign([wallet]);
  const lutSignature = await connection.sendRawTransaction(tx.serialize(), {
    maxRetries: 20,
  });
  console.log("luttxid:", lutSignature);
  await connection.confirmTransaction({
    blockhash: latestBH.blockhash,
    signature: lutSignature,
    lastValidBlockHeight: latestBH.lastValidBlockHeight,
  });
  await new Promise((resolve) => setTimeout(resolve, 5000));

  const extendInst = AddressLookupTableProgram.extendLookupTable({
    addresses: [wallet.publicKey, ...addresses],
    authority: wallet.publicKey,
    payer: wallet.publicKey,
    lookupTable: lookupTableAddress,
  });

  // -------- step 1.7: 扩展查找表 --------
  const ExtendMessage = new TransactionMessage({
    instructions: [bribe, extendInst],
    payerKey: wallet.publicKey,
    recentBlockhash: latestBH.blockhash,
  }).compileToV0Message();
  const extendTx = new VersionedTransaction(ExtendMessage);
  extendTx.sign([wallet]);
  const extendSignature = await connection.sendRawTransaction(
    extendTx.serialize(),
    { maxRetries: 20 }
  );
  console.log("extendtxxid:", extendSignature);
  await connection.confirmTransaction({
    blockhash: (await connection.getLatestBlockhash()).blockhash,
    signature: extendSignature,
    lastValidBlockHeight: (
      await connection.getLatestBlockhash()
    ).lastValidBlockHeight,
  });
  // 等待交易完成
  await new Promise((resolve) => setTimeout(resolve, 10000));
  // 返回地址查找表
  return lookupTableAddress;
}

总而言之,这些是创建查找表所需的步骤:

  • 获取最近的区块哈希。
  • 支付计算预算费用以加快交易处理。(向验证者支付的额外费用)
  • 初始化查找表,并使用必要的地址填充查找表,包括钱包和池相关的帐户。
  • 创建并提交一个包含查找表创建和填充指令的版本化交易。

请确保这些交易之间有足够的基于 promise 的延迟,以便按顺序完成,而不会失败。这将确保创建查找表并提交版本化交易。

创建 swap 交易

由于初始流动性较低以及我们将要执行的捆绑交易结构,我们将执行一个具有更宽滑点容忍度的固定输入 swap,以适应潜在的价格波动。

现在我们继续创建 swap 交易,

// 在 src/swapCreate.ts 中创建 swap 指令

import {
  InnerSimpleV0Transaction,
  Liquidity,
  Percent,
  TxVersion,
} from "@raydium-io/raydium-sdk";
import {
  TransactionInstruction,
  TransactionMessage,
  VersionedTransaction,
  PublicKey,
} from "@solana/web3.js";
import { connection } from "./config";

export default async function createSwapIx(
  poolKeys: any,
  inputTokenAmount: any,
  outputToken: any,
  walletTokenAccounts: any,
  wallet: any,
  times: number = 1,
  LUT: PublicKey
) {
  // -- 获取查找表
  const lookupTableAcc = await connection.getAddressLookupTable(LUT);
  // -------- 步骤 1:计算输出金额 --------
  const { minAmountOut } = Liquidity.computeAmountOut({
    poolKeys: poolKeys,
    poolInfo: await Liquidity.fetchInfo({ connection, poolKeys }),
    amountIn: inputTokenAmount,
    currencyOut: outputToken,
    slippage: new Percent(30, 100), // 对于新的 LP 来说,具有高滑点是很常见的
  });
  const ix = await Liquidity.makeSwapFixedInInstruction(
    {
      poolKeys,
      userKeys: {
        owner: wallet.publicKey,
        tokenAccountIn: walletTokenAccounts[0],
        tokenAccountOut: walletTokenAccounts[1],
      },
      amountIn: inputTokenAmount,
      minAmountOut: minAmountOut.raw,
    },
    4
  );
  // 在数组中重复 ix times 次
  let ixs = [];
  for (let i = 0; i < times; i++) {
    ixs.push(...ix.innerTransaction.instructions);
  }
  // 创建一个在同一交易中复制 swap ix 的交易
  const message = new TransactionMessage({
    instructions: ixs,
    payerKey: wallet.publicKey,
    recentBlockhash: (await connection.getLatestBlockhash()).blockhash,
  }).compileToV0Message([lookupTableAcc.value!]);

  const transaction = new VersionedTransaction(message);
  await transaction.sign([wallet]);

  return transaction;
}

此函数基本上获取在上一步中创建的查找表,并计算出一旦我们的 swap 交易通过,我们将获得的 token 的最坏情况数量。我们利用 Raydium SDK 生成一个固定输入 swap 指令。为了优化交易大小并容纳多个 swap,我们提取这些指令并创建一个新的交易,该交易使用先前创建的查找表引用地址。这会将每个地址(32 字节)替换为一个 1 字节的引用,有效地节省了我们 31 *(地址总数)字节的空间(从 1232 字节的限制中),以便我们可以在 1 个交易中堆叠多个 swap,这样我们就可以进一步将更多的 swap 堆叠到一个 bundle 中。

然后我们可以在我们的主函数中调用这个新创建的函数,

//main.js

const swapIx = await createSwapIx(
   poolKeys,
   inputTokenAmount,
   outputToken,
   walletTokenAccounts,
   wallet,
   3,
   lookupTableAddress
);

将交易添加到 Jito Bundle

在计算出优化的 swap 交易,并使用查找表将它们堆叠在同一个 bundle 中后,我们继续使用 Jito Bundle 执行它们。

Jito bundles 是一种将多个交易分组到单个原子操作中的机制。这对于创建流动性池和执行后续交易等复杂交互特别有用。通过使用 Jito Bundle,我们可以确保 Bundle 中的所有交易要么完全执行,要么在任何部分失败时回滚。

单个 Jito Bundle 最多可以容纳 5 个交易。通过使用地址查找表,我们可以在单个交易中放入大约 5 个 swap 指令。为了确保及时处理并避免潜在的抢先交易,至关重要的是在 Bundle 中包含一个小的 Jito 验证器提示,作为最终交易。

下面的函数说明了使用 Jito 提交交易,

import { InnerSimpleV0Transaction } from "@raydium-io/raydium-sdk";
import { Bundle } from "jito-ts/dist/sdk/block-engine/types";
import { PublicKey, VersionedTransaction, Signer } from "@solana/web3.js";
import { connection, sc, wallet } from "./config";
// 创建一个 Jito 捆绑对象,添加交易,监控它
export default async function submitJitoBundle(
  txs: VersionedTransaction[],
  payer: PublicKey,
  signer: Signer,
  LUT: PublicKey
) {
  // 同一个 LUT 可以用于创建交易和 LUT
  const recentBlockHash = (await connection.getLatestBlockhash()).blockhash;

  let bundle = new Bundle(txs, 5);

  // 获取一个提示帐户
  const tipAcc = await sc.getTipAccounts();
  const maybebundle = bundle.addTipTx(
    wallet,
    25000,
    new PublicKey(tipAcc[0]),
    recentBlockHash
  );
  if (maybebundle instanceof Error) {
    throw new Error("bundle error");
  } else {
    bundle = maybebundle;
  }

  const bundleId = await sc.sendBundle(bundle);
  // 在 Jito 网站上搜索 ID
  console.log(`bundleId: ${bundleId}`);
  return bundleId;
}

addTipTx 函数根据链上模拟的结果生成更新后的 Bundle 或标记错误。成功的 Bundle 执行需要所有内部交易都成功完成,并且提示帐户有效。我们的 getTipAccounts 助手简化了识别合适提示接收者的过程。

一旦定义了此函数,我们只需在主函数中调用它,并将版本交易对象作为提示放在最后。

// 将池创建 ix、swap ix、查找表地址传递给 Jito Bundle

    const submitBundleRes = await submitJitoBundle(
      // 创建 LP 交易、swap 交易、swap 交易、swap 交易(每个 5 个 swap 指令)
      // 在 submitJitoBundle 函数中添加 1 个提示交易
      [createPoolTx, swapIx, swapIx, swapIx],
      wallet.publicKey,
      wallet,
      lookupTableAddress
    );
    console.log("submitBundleRes:", submitBundleRes);
  } else {
    console.log("createPoolIx failed");
    return;
  }
}

main()
  .then((value) => console.log(value))
  .catch((err) => console.log(err));

成功执行后,它将返回一个 Jito Bundle ID,该 ID 可用于监控目的。你还可以使用生成的 Bundle ID,使用他们的块引擎浏览器在 Jito 网站上检查 Bundle。

这就是本系列文章的全部内容,它展示了如何在 Raydium 上成功启动你的 token。如果你错过了上一部分,请点击此链接:第一部分的链接

整个项目代码都可在 GitHub 上的此处获得,请随意克隆它并试用一下!

资源

  • 原文链接: blogs.shyft.to/launching...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Shyft_to
Shyft_to
在 Solana上更快更智能地构建,使用Shyft的SuperIndexer、gRPC、RPC、API和SDK