本文详细介绍了如何在 Solana 区块链上使用 Jito Bundles 功能,通过捆绑多个交易确保它们在同一区块中顺序执行。文章还提供了具体的代码示例,展示了如何使用 TypeScript 和 Solana Web3.js 2.0 创建和发送交易捆绑包。
JM!随着对 Solana 区块空间需求的增加,也出现了确保你的交易包含在区块中的需求。此外,许多应用程序需要以原子且顺序的方式执行多个交易。Jito Labs 是一个为 Solana 构建高性能 MEV 基础设施的团队,运营着一个 Solana 验证器客户端,该客户端启用了一个名为 Bundles 的独特功能,使得这一切成为可能。在本指南中,我们将讨论 Jito Bundles 的定义及如何使用 Lil' JIT 市场附加组件将交易打包并发送到 Jito 验证器客户端。
让我们开始吧!
喜欢视频讲解吗?跟随 Sahil 学习关于 Jito Bundles 以及如何发送一个的内容。 什么是 Jito Bundles 及如何使用?
Jito Bundles 是 Jito Labs 提供的一个功能,允许在 Solana 区块链上顺序且原子地执行多个交易。这个功能对于复杂的操作、高效的 MEV(Miner Extractable Value)捕获和某些 DeFi 应用程序尤其有用。为了理解它们的重要性,我们首先回顾一些关键概念:
Jito Bundles 解决了所有这些问题,允许将多个交易打包在一起,并确保 Bundle 中的所有交易作为一个单元成功或失败。Jito Bundle 是最多包含五个 Solana 交易的组合,这些交易由 Jito 验证者在同一块内顺序和原子地执行,确保 Bundle 中的所有交易要么成功,要么都未被处理。Bundles 根据用户定义的小费金额进行优先处理,激励验证者优先处理 Bundles 而非其他交易。
特征 | 描述 |
---|---|
顺序执行 | Bundle 中的交易保证按列出的顺序执行 |
原子执行 | 所有交易在相同的槽内执行 |
所有或无结果 | 如果 Bundle 中的任何交易失败,则所有交易均未提交到链上 |
Bundle 大小 | 每个 Bundle 最大 5 个交易(允许复杂操作超出每个交易 1.4M 计算单位限制) |
Bundles 根据用户定义的小费金额进行优先处理,这激励验证者优先处理 Bundles。使用小费时的一些关键考虑:
T1pyyaTNZsKv2WcRAB8oVnk93mLJw2XzjtVYqCsaHqt
getTipAccounts
RPC 方法访问小费账户(稍后我们会讨论)或者通过 Jito 文档通过利用 Jito Bundles,开发者可以确保复杂的多交易操作以原子方式并顺序执行,为 DeFi 应用程序、交易策略和 Solana 上的其他高级用例开启新的可能性。
欲获取更详细的信息,请参考 Jito 官方文档。
让我们测试一下!
首先,打开你选择的代码编辑器并创建一个新项目文件夹:
mkdir lil-jit-demo && cd lil-jit-demo
然后,使用以下命令初始化一个新项目:
npm init -y
接下来,安装以下依赖:
npm install --save @solana/web3.js@2 @solana-program/memo @solana-program/system
注意:你可能需要使用
--legacy-peer-deps
标记安装@solana-program/system
和@solana-program/memo
包。
确保你安装了 Node 类型:
npm i --save-dev @types/node
并初始化一个支持 JSON 模块的新 TypeScript 配置文件:
tsc --init --resolveJsonModule
并在项目根目录创建一个名为 index.ts
的新文件:
echo > index.ts
你将需要一个包含 ~0.01 SOL 的纸钱包来测试该演示(并覆盖 Bundle 小费的费用)。如果你没有,可以使用以下命令创建一个:
solana-keygen new -o secret.json -s --no-bip39-passphrase
你可以通过运行以下命令获取新钱包的地址:
solana address -k secret.json
确保在继续之前将 ~0.01 SOL 发送到该钱包。在终端运行以下命令可以验证你的主网余额:
solana balance -um -k secret.json
很好。让我们编写我们的脚本!
在我们开始编码之前,让我们概述一下我们希望脚本执行的步骤:
secret.json
文件中导入 Solana 密钥对并建立与 Lil' Jit 启用的 Solana 端点的连接来设置脚本让我们开始构建!
首先,打开你的 index.ts
文件并添加以下导入:
import {
Rpc,
createDefaultRpcTransport,
createRpc,
createJsonRpcApi,
Address,
mainnet,
Base58EncodedBytes,
createSolanaRpc,
createKeyPairSignerFromBytes,
createTransactionMessage,
setTransactionMessageFeePayerSigner,
pipe,
setTransactionMessageLifetimeUsingBlockhash,
appendTransactionMessageInstruction,
TransactionPartialSigner,
signTransactionMessageWithSigners,
getBase64EncodedWireTransaction,
Base64EncodedWireTransaction,
getTransactionEncoder,
getBase58Decoder,
} from "@solana/web3.js";
import { getAddMemoInstruction } from "@solana-program/memo";
import { getTransferSolInstruction } from "@solana-program/system";
import secret from "./secret.json";
我们将使用 @solana/web3.js
库中的多个接口和功能。我们还将使用 @solana-program/memo
和 @solana-program/system
库来创建我们的交易。
让我们定义一些将在脚本中使用的常量。将以下常量添加到导入下面:
const MINIMUM_JITO_TIP = 1_000; // lamports
const NUMBER_TRANSACTIONS = 5;
const SIMULATE_ONLY = true;
const ENDPOINT = 'https://example.solana-mainnet.quiknode.pro/123456/'; // 👈 替换为你的端点
const POLL_INTERVAL_MS = 3000;
const POLL_TIMEOUT_MS = 30000;
const DEFAULT_WAIT_BEFORE_POLL_MS = 5000;
确保将 ENDPOINT
常量替换为你自己的 Lil' JIT 启用的端点。如果你尚未拥有端点,可以在 QuickNode.com 创建一个。如果你已经有端点并需要添加 Lil' JIT Marketplace Add-on,你可以通过访问你的端点页面并点击Add-on来按照步骤添加该附加组件。
为了最佳使用最新的 Solana Web3.js 库,我们将为我们的 Lil' JIT 端点定义一个自定义类型。在你的常量下方添加以下类型定义:
type JitoBundleSimulationResponse = {
context: {
apiVersion: string;
slot: number;
};
value: {
summary: 'succeeded' | {
failed: {
error: {
TransactionFailure: [number[], string];
};
tx_signature: string;
};
};
transactionResults: Array<{
err: null | unknown;
logs: string[];
postExecutionAccounts: null | unknown;
preExecutionAccounts: null | unknown;
returnData: null | unknown;
unitsConsumed: number;
}>;
};
};
type LilJitAddon = {
getRegions(): string[];
getTipAccounts(): Address[];
getBundleStatuses(bundleIds: string[]): {
context: { slot: number };
value: {
bundleId: string;
transactions: Base58EncodedBytes[];
slot: number;
confirmationStatus: string;
err: any;
}[]
};
getInflightBundleStatuses(bundleIds: string[]): {
context: { slot: number };
value: {
bundle_id: string;
status: "Invalid" | "Pending" | "Landed" | "Failed";
landed_slot: number | null;
}[];
};
sendTransaction(transactions: Base64EncodedWireTransaction[]): string;
simulateBundle(transactions: [Base64EncodedWireTransaction[]]): JitoBundleSimulationResponse;
sendBundle(transactions: Base58EncodedBytes[]): string;
}
这里我们定义了 LilJitAddon
类型,该类型指定了与 Lil' JIT Marketplace 附加组件交互的方法、参数和返回类型。我们还将 JitoBundleSimulationResponse
类型单独定义。
让我们创建一些助手函数来让脚本更易读。
首先,让我们创建一个 createJitoBundlesRpc
函数,创建一个与我们的 Lil' JIT 端点交互的新 RPC 客户端。在导入下面添加以下函数:
function createJitoBundlesRpc({ endpoint }: { endpoint: string }): Rpc<LilJitAddon> {
const api = createJsonRpcApi<LilJitAddon>({
responseTransformer: (response: any) => response.result,
});
const transport = createDefaultRpcTransport({
url: mainnet(endpoint),
});
return createRpc({ api, transport });
}
我们简单地使用 @solana/web3.js
库中的 createRpc
函数为我们的端点创建一个新的 RPC 客户端。我们还使用 createJsonRpcApi
和 createDefaultRpcTransport
函数来为我们的 RPC 客户端创建必要的 API 和传输对象。有几点注意:
LilJitAddon
指定为你的 api
对象的类型responseTransformer
仅返回响应的 result
属性接下来,创建一个 validateSimulation
函数,检查模拟结果以确保 Bundle 成功。将以下函数添加到导入下面:
function isFailedSummary(summary: JitoBundleSimulationResponse['value']['summary']): summary is { failed: any } {
return typeof summary === 'object' && summary !== null && 'failed' in summary;
}
function validateSimulation(simulation: JitoBundleSimulationResponse) {
if (simulation.value.summary !== 'succeeded' && isFailedSummary(simulation.value.summary)) {
throw new Error(`模拟失败:${simulation.value.summary.failed.error.TransactionFailure[1]}`);
}
}
在这里,我们添加两个函数来帮助我们检查接收到的模拟的响应。isFailedSummary
函数检查模拟摘要是否为带有 failed
属性的对象。validateSimulation
函数检查模拟摘要是否具有 succeeded
,如果没有则抛出错误。
我们将需要为演示目的创建五个不同的交易。让我们创建一个 createTransaction
函数,为每个五个交易生成一个新交易。我们希望能够指定一个可以传入备忘录消息的 index
,以确保每个交易是唯一的,并希望在我们的交易中包含小费指令的选项(这将是 Bundle 中的最后一个交易)。将以下函数添加到导入下面:
async function createTransaction(
index: number,
latestBlockhash: Parameters<
typeof setTransactionMessageLifetimeUsingBlockhash
>[0],
payerSigner: TransactionPartialSigner,
includeTip?: Address
) {
const transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
(tx) => setTransactionMessageFeePayerSigner(payerSigner, tx),
(tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx),
(tx) =>
appendTransactionMessageInstruction(
getAddMemoInstruction({
memo: `lil jit demo transaction # ${index}`,
}),
tx
),
(tx) =>
includeTip
? appendTransactionMessageInstruction(
getTransferSolInstruction({
source: payerSigner,
destination: includeTip,
amount: MINIMUM_JITO_TIP,
}),
tx
)
: tx
);
return await signTransactionMessageWithSigners(transactionMessage);
}
在这里,我们使用 @solana/web3.js
的 pipe
函数创建一个新的交易消息:
setTransactionMessageFeePayerSigner
设置费用付款人setTransactionMessageLifetimeUsingBlockhash
函数设置交易的生命周期appendTransactionMessageInstruction
函数为交易添加备忘录指令(每个备忘录将根据 index
参数分配一个唯一消息——我们将用来验证交易序列是否正确执行)includeTip
参数被传入,我们还使用 appendTransactionMessageInstruction
为交易添加小费指令Lil' JIT 附加组件包括获取小费账户列表的方法。我们将使用此方法获取所有小费账户,然后随机选择一个从列表中包含在我们的交易中。将以下函数添加到导入下面:
async function getTipAccount(rpc: Rpc<LilJitAddon>): Promise<Address> {
try {
const tipAccounts = await rpc.getTipAccounts().send();
const jitoTipAddress = tipAccounts[Math.floor(Math.random() * tipAccounts.length)];
if (!jitoTipAddress) {
throw new Error("未找到 JITO 小费账户");
}
return jitoTipAddress;
} catch {
throw new Error("获取小费账户失败");
}
}
在这里,我们简单地使用 getTipAccounts
方法来检索小费账户列表。然后从列表中随机选择一个小费账户并返回。
我们将使用 getInflightBundleStatuses
方法轮询我们的 Bundle 状态,直到它落地、失败或超时。添加以下函数到你的导入下面:
async function pollBundleStatus(
rpc: Rpc<LilJitAddon>,
bundleId: string,
timeoutMs = 30000,
pollIntervalMs = 3000,
waitBeforePollMs = DEFAULT_WAIT_BEFORE_POLL_MS
) {
await new Promise(resolve => setTimeout(resolve, waitBeforePollMs));
const startTime = Date.now();
let lastStatus = '';
while (Date.now() - startTime < timeoutMs) {
try {
const bundleStatus = await rpc.getInflightBundleStatuses([bundleId]).send();
const status = bundleStatus.value[0]?.status ?? '未知';
if (status !== lastStatus) {
lastStatus = status;
}
if (status === 'Landed') {
return true;
}
if (status === 'Failed') {
console.log(`Bundle ${status.toLowerCase()}. 退出...`);
throw new Error(`Bundle 失败,状态: ${status}`);
}
await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
} catch {
console.error('❌ - 轮询 Bundle 状态时出错。');
}
}
throw new Error("轮询超时,无确认");
}
这里没有很多内容;我们只是定义了一个轮询频率和超时,并调用 getInflightBundleStatuses
来检查我们 Bundle 的状态。如果 Bundle 已经落地,我们返回 true
,中断循环。如果 Bundle 失败或轮询超时,我们记录错误并抛出错误。
考虑事项 getInflightBundleStatuses
请注意,我们在轮询前添加了一个简短的延迟,以确保 Bundle 有时间被处理——否则可能会出现轮询错误。此外,getInflightBundleStatuses
方法仅会查找过去五分钟内处理的 Bundles。如果你在处理超过五分钟之前的 Bundle 时使用此方法,将会出现 500 错误。
现在,基于上述步骤,让我们开始构建 main
函数。将以下函数添加到代码的其余部分下面:
async function main() {
// 第 1 步 - 设置
// 第 2 步 - 获取一个 Jitotip 账户
// 第 3 步 - 获取最近的区块哈希
// 第 4 步 - 创建交易
// 第 5 步 - 模拟 Bundle
// 第 6 步 - 发送 Bundle
// 第 7 步 - 验证 Bundle 是否落地
}
main().catch((error) => {
console.error(`❌ - 错误: ${error}`);
process.exit(1);
});
太好了。现在让我们填充这些步骤。
首先,让我们导入我们的密钥对,并定义我们的 Solana RPC 和 Lil' JIT RPC。将以下内容添加到你的 main
函数:
// 第 1 步 - 设置
const signer = await createKeyPairSignerFromBytes(new Uint8Array(secret));
console.log(`初始化 Jito Bundles 演示。发送 ${NUMBER_TRANSACTIONS} 个交易来自 ${signer.address}。`);
const solanaRpc = createSolanaRpc(ENDPOINT);
const lilJitRpc = createJitoBundlesRpc({ endpoint: ENDPOINT });
console.log(`✅ - 已与 QuickNode 建立连接。`);
现在我们可以使用本地 Solana RPC 方法(使用 solanaRpc
)和我们的 Jito Bundles RPC 方法(使用 lilJitRpc
)与 Jito 验证器客户端进行交互。
由于我们已经定义了 getTipAccount
函数,我们所需要做的就是调用它并将其响应存储在一个变量 jitoTipAddress
中。将以下内容添加到你的 main
函数:
// 第 2 步 - 获取一个 Jitotip 账户
const jitoTipAddress = await getTipAccount(lilJitRpc);
console.log(`✅ - 使用以下 Jito Tip 账户:${jitoTipAddress}`);
就像 Solana 上的任何交易一样,我们必须传递一个最近的区块哈希以确保交易能够及时处理。如果你发送一个带有过时区块哈希的 Bundle,交易/Bundle 将会失败。
将以下内容添加到你的 main
函数:
// 第 3 步 - 获取最近的区块哈希
const { value: latestBlockhash } = await solanaRpc
.getLatestBlockhash({ commitment: "confirmed" })
.send();
console.log(`✅ - 最新的区块哈希:${latestBlockhash.blockhash}`);
现在,我们将创建五个备忘录交易,每个交易都有一个独特的顺序消息。将以下内容添加到你的 main
函数:
// 第 4 步 - 创建交易
const signedTransactions = await Promise.all(
Array.from({ length: NUMBER_TRANSACTIONS }, (_, i) => {
const isLastTransaction = i === NUMBER_TRANSACTIONS - 1;
return createTransaction(
i + 1,
latestBlockhash,
signer,
isLastTransaction ? jitoTipAddress : undefined
);
})
);
const base64EncodedTransactions = signedTransactions.map((transaction) => {
const base64EncodedTransaction = getBase64EncodedWireTransaction(transaction);
return base64EncodedTransaction;
}) as Base64EncodedWireTransaction[];
const transactionEncoder = getTransactionEncoder();
const base58Decoder = getBase58Decoder();
const base58EncodedTransactions = signedTransactions.map((transaction) => {
const transactionBytes = transactionEncoder.encode(transaction);
return base58Decoder.decode(transactionBytes) as Base58EncodedBytes;
});
console.log(`✅ - 交易组装并编码完毕。`);
让我们走一遍我们所做的事情:
createTransaction
函数创建 5 个 Memo 交易。jitoTipAddress
作为小费账户,使得交易中包括小费指令。接下来,我们将使用 Lil' JIT simulateBundle
方法测试我们的 Bundle。我们可以使用之前写的 validateSimulation
函数来确保模拟成功。最后,我们创建一个条件,如果我们的配置设置为 仅模拟 模式,则结束脚本。将以下内容添加到你的 main
函数:
// 第 5 步 - 模拟 Bundle
const simulation = await lilJitRpc
.simulateBundle([base64EncodedTransactions])
.send();
validateSimulation(simulation);
console.log(`✅ - 模拟成功。`);
if (SIMULATE_ONLY) {
console.log("🏁 - 仅模拟模式 - 退出脚本。");
return;
}
现在,我们将使用 sendBundle
方法将我们的 Bundle 发送到 Jito 验证器客户端。将以下内容添加到你的 main
函数:
// 第 6 步 - 发送 Bundle
let bundleId: string;
try {
bundleId = await lilJitRpc
.sendBundle(base58EncodedTransactions)
.send();
console.log(`✅ - Bundle 已发送:${bundleId}`);
} catch (error) {
console.error("❌ - 发送 Bundle 时出错:", error);
throw error;
}
最后,让我们调用我们的 pollBundleStatus
函数来验证 Bundle 是否落地。将以下内容添加到你的 main
函数:
// 第 7 步 - 验证 Bundle 是否落地
await pollBundleStatus(lilJitRpc, bundleId, POLL_TIMEOUT_MS, POLL_INTERVAL_MS);
console.log(`✅ - Bundle 已落地:${bundleId}`);
console.log(` https://explorer.jito.wtf/bundle/${bundleId}`);
console.log(` (注意:该 URL 可能需要几分钟才能变得可用。)`);
干得很好。我们现在应该有一个完整的脚本,用于将 5 个 Memo 交易发送到 Jito 验证器客户端并验证它们是否成功。让我们运行它!
在运行代码之前,请检查你的 SIMULATE_ONLY
和 MINIMUM_JITO_TIP
变量。如果你将 SIMULATE_ONLY
设置为 true
,脚本将仅模拟 Bundle 并退出。否则,它将把 Bundle 发送到验证者客户端——如果交易在主网上落地,你的交易将包含在一个区块中,意味着你的小费将被支付。这是不可逆的。
当你准备好后,请在终端运行以下命令:
ts-node index.ts
如果将 SIMULATE_ONLY
设置为 false
,你应该会看到类似以下的输出:
初始化 Jito Bundles 演示。发送 5 个交易来自 FUA7zS5PXVtW2VCmFGtfv5q6AdusbpVGy8HTjVjBAAzR。
✅ - 使用以下 Jito Tip 账户:HFqU5x63VTqvQss8hp11i4wVV8bD44PvwucfZ2bU7gRe
✅ - 最新的区块哈希:CghMwVqLLdKrdgdCKDbTtRgdRhj9BfCBZxL8n87QNB9C
✅ - 交易组装并编码完毕。
✅ - 模拟成功。
✅ - Bundle 已发送:c4fb0940406d3fead71e29b9f87d2273ab6a743f3da1bf4aeeb73db3521685b0
✅ - Bundle 已落地:c4fb0940406d3fead71e29b9f87d2273ab6a743f3da1bf4aeeb73db3521685b0
https://explorer.jito.wtf/bundle/c4fb0940406d3fead71e29b9f87d2273ab6a743f3da1bf4aeeb73db3521685b0
(注意:该 URL 可能需要几分钟才能变得可用。)
LFJitoooo!干得好!你的第一个 Bundle 已经落地在 Solana 上。正如你在终端输出中所注意到的,Jito bundle explorer 可能需要一分钟来更新你的 Bundle,请稍等一分钟,然后单击链接。你应该会看到如下内容:
该 Bundle 应显示五个按顺序排列的交易。第五个交易应包含小费指令和 SOL 余额转移。你可以在上面的示例中看到这一点。你还可以通过点击交易右上角的 ⎘ 图标单独打开每个交易(或者通过在 Solana Explorer 中浏览你的钱包地址)。你应该会看到所有五个交易具有相同的时间戳和区块:
你应该能够导航到五个交易中的第一个,并在交易详情中看到“lil jit demo transaction # 1”:
你还应该能够导航到第五个交易,并在交易详情中看到“lil jit demo transaction # 5”及我们的转小费:
这表明:
非常酷,对吧?干得不错! 🎉
Jito Bundles 是提升你 Solana 开发体验的强大工具。通过利用 Lil' JIT 市场附加组件,你可以打包交易,并确保它们在同一块中并按指定顺序执行。这是优化交易并最大化你应用性能的好方法。
我们期待看到你与 Lil' JIT 附加组件的成果。如果你有任何问题或想告诉我们你的进展,请与我们联系!你可以在 Discord 或 Twitter 上与我们联系。
让我们知道如果你有任何反馈或新的主题请求。我们很乐意听取你的声音。
- 原文链接: quicknode.com/guides/sol...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!