本文是关于如何在Raydium上启动流动性池的系列文章的第二部分,重点介绍了使用Jito bundles来最大化代币的可见性和交易量,并提供了一个分步指南,包括构建完整的池密钥对象、创建和优化swap交易、以及如何将这些交易添加到Jito bundle中以确保原子执行。文章还提供了创建查找表以优化链上交易的步骤, 以及项目代码的GitHub链接。
在上一篇文章中,我们获得了关于 OpenBook 市场和 token 账户信息的重要数据。现在,我们将利用这些信息来构建完整的池密钥对象,这对于后续与流动性池的交互至关重要。
由于我们已经获得了钱包分配的基础 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 交易,
// 在 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
);
在计算出优化的 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 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!