这篇文章详细介绍了如何在Solana网络上批量处理交易,特别是如何通过编写脚本向多个钱包发送SOL。文章涵盖了环境设置、代码实现和最佳实践,包括如何使用并发方法执行交易,以提高处理效率和降低成本。
你是否正在运行一个有很多交易的批处理过程?也许是向你社区的 NFT 持有者空投或向你的 dApp 的早期用户分发代币。Solana 的交易组合和并发方法调用可以提高你的脚本的速度和有效性!
你将编写一个脚本,向多个钱包发送 $SOL。
你将组装包含多个 Solana 转账指令的批量交易。
你将创建一个阶段函数,允许你的交易并发处理,而不会使网络验证者不堪重负。
通过这些,你将能够运行批量作业,最小化交易成本并减少处理时间!
在终端中创建一个新的项目目录:
mkdir bulk-send-sol
cd bulk-send-sol
使用 "yes" 标志初始化你的项目,以使用新包的默认值:
yarn init --yes
#或
npm init --yes
使用 .json 导入启用的 tsconfig 进行初始化:
tsc -init --resolveJsonModule true
我们需要为本练习添加 Solana Web3 库。在终端中键入:
yarn add @solana/web3.js@1
#或
npm install @solana/web3.js@1
你需要创建一个 Solana 文件系统钱包(密钥对写入 guideSecret.json 文件)并向其申请一些 SOL。你可以通过 Solana CLI 完成此操作,或者使用 我们为你创建的脚本。如果你已经拥有一个钱包并且只需要一些 devnet SOL,可以在这里申请:
🪂申请 Devnet SOL
申请 1 SOL(Devnet)
请确保将钱包保存到项目目录中,命名为 guideSecret.json。
创建两个文件,app.ts 和 dropList.ts。我们将使用 app.ts 作为组装和执行交易的主要代码。我们将使用 dropList.ts 存储我们希望空投的地址和代币数量。
echo > app.ts && echo > dropList.ts
你的环境应该如下所示:
好的!我们准备好了。
让我们开始创建一个我们希望向其发送 SOL 的钱包列表。我们将使用 TypeScript 来让我们的生活变得更加轻松。在 dropList.ts 中,创建一个新的 接口 叫做 Drop,包括我们要发送到的钱包和要发送的 lamports 数量。还要创建一个新的空的 Drop 数组 叫做 DropList:
export interface Drop {
walletAddress: string,
numLamports: number
}
export const dropList:Drop[] = [];
这将是我们将用于示例的钱包和 lamports 列表。随意生成你自己的 dropList,但我们在 这个 GitHub 文件 中也提供了一个可供你使用。我们建议你的 dropList 数组至少有 30 个 Drop 元素,以便你可以测试程序的一些批量功能。在我们的示例文件中,我们包括了 50 个空投,将在整个指南中引用。
打开 app.ts,并在 第 1 行 粘贴以下导入:
import { Connection, Keypair, PublicKey, sendAndConfirmTransaction, SystemProgram, Transaction, TransactionInstruction } from "@solana/web3.js";
import { Drop, dropList } from "./dropList";
import secret from './guideSecret.json';
除了我们在前一个步骤中创建的钱包和 DropList,我们还从 Solana Web3 库导入了一些基本方法和类。
要在 Solana 上构建,你需要一个 API 端点来连接网络。你可以选择使用公共节点或部署和管理自己的基础设施;但是,如果你希望获得 8 倍更快的响应时间,可以将重任交给我们。
查看为什么超过 50% 的 Solana 项目选择 QuickNode,并在 这里 注册一个免费账户。我们将使用 Solana Devnet 节点。由于此练习使用了 SOL 的转移,使用主网节点将导致真正的 SOL 的转移。
复制 HTTP 提供程序链接:
在 app.ts 中,在你的导入语句下,声明你的 RPC 并建立与 Solana 的 连接:
const QUICKNODE_RPC = 'https://example.solana-devnet.quiknode.pro/0123456/';
const SOLANA_CONNECTION = new Connection(QUICKNODE_RPC);
让我们定义三个对我们交易组装很重要的关键变量。在 SOLANA_CONNECTION 下添加:
const FROM_KEY_PAIR = Keypair.fromSecretKey(new Uint8Array(secret));
const NUM_DROPS_PER_TX = 10;
const TX_INTERVAL = 1000;
FROM_KEY_PAIR 将从我们的 guideSecret.json 中生成一个密钥对,将用作我们交易的资金来源(“付款人”)。我们添加了两个额外的常量:
我们将更详细地讨论这两个部分。
我们的应用程序将需要两个主要功能:用于生成交易的函数和用于执行交易的函数。我们将使用前者从我们的 dropList 生成 Solana 交易,并将使用后者创建和调用 Promises 来将交易发送到 Solana 网络。在较高层面上,过程将看起来如下所示:
请注意,有许多方法可以实现此结果——我们只提供了一种你可以使用和修改以满足自身需要的解决方案。发挥创意,让其独特!
让我们开始构建吧!
如果你是 Solana 或 web3 的新手,你可能会很高兴地发现 Solana 交易实际上是一个或多个指令的组合——这意味着我们可以通过一次调用 Solana 网络来完成多个任务!对于我们的目的,这意味着我们可以在单个交易中包括多个“转移 SOL”指令。不过请小心!Solana 限制了我们的交易大小;交易的最大大小为 1,232 字节。要点是?我们需要将我们的交易批量化,以包含有限数量的指令;对于这个练习,少于 20 个指令的单个交易应该足够——我们将 NUM_DROPS_PER_TX 设置为 10,因此应该没问题。有关计算交易大小的更多信息,请查看本指南末尾的奖励部分。在我们进行本指南时,请注意不要将交易与交易指令混淆。它们可能看起来相似,但实际上是非常不同的!交易指令具体告诉程序该做什么。交易将包含一个或多个交易指令以及在链上处理该交易所需的额外上下文(例如,签名、费用支付者和最近的区块哈希)。
有关交易大小的更多信息:发送给 Solana 验证者的消息不得超过 Internet Protocol v6 最大传输单元大小,以确保快速可靠的集群信息网络传输。Solana 的网络堆栈使用保守的 MTU 大小为 1,280 字节,考虑到头部后,为数据包数据(如序列化交易)的大小留下 1,232 字节。源: Solana Github Labs ( 代码 ,_ 参考 )_
让我们开始创建一个名为 generateTransactions 的函数,该函数接受三个参数:
让我们让函数返回一个交易数组:
function generateTransactions(batchSize:number, dropList: Drop[], fromWallet: PublicKey):Transaction[] {
let result: Transaction[] = [];
// 在这里添加你的代码
return result;
}
我们在这里要做的第一件事是将我们的 dropList 转换为交易指令列表( TransactionInstruction[])。你可以通过使用 .map 完成此操作。使用以下代码将 dropList 中的每个 drop 转换为 SystemProgram.transfer:
let txInstructions: TransactionInstruction[] = dropList.map(drop => {return SystemProgram.transfer({
fromPubkey: fromWallet,
toPubkey: new PublicKey(drop.walletAddress),
lamports: drop.numLamports
})})
每个新的 TransactionInstruction 都将告诉 Solana 系统程序从 fromPubkey 转移 lamports 到 toPubkey。太棒了!你现在已经将我们的枯燥 dropList 转换为一个有用的 Solana 交易指令数组。现在你需要将这些指令添加到一些交易中。
现在,你可以使用 batchSize 参数将我们的交易指令分块成多个交易。在 txInstructions 后添加此循环代码:
const numTransactions = Math.ceil(txInstructions.length / batchSize);
for (let i = 0; i < numTransactions; i++){
let bulkTransaction = new Transaction();
let lowerIndex = i * batchSize;
let upperIndex = (i+1) * batchSize;
for (let j = lowerIndex; j < upperIndex; j++){
if (txInstructions[j]) bulkTransaction.add(txInstructions[j]);
}
result.push(bulkTransaction);
}
让我们来了解一下这里发生的事情。
你的最终函数应该看起来像这样:
function generateTransactions(batchSize:number, dropList: Drop[], fromWallet: PublicKey):Transaction[] {
let result: Transaction[] = [];
let txInstructions: TransactionInstruction[] = dropList.map(drop => {return SystemProgram.transfer({
fromPubkey: fromWallet,
toPubkey: new PublicKey(drop.walletAddress),
lamports: drop.numLamports
})})
const numTransactions = Math.ceil(txInstructions.length / batchSize);
for (let i = 0; i < numTransactions; i++){
let bulkTransaction = new Transaction();
let lowerIndex = i * batchSize;
let upperIndex = (i+1) * batchSize;
for (let j = lowerIndex; j < upperIndex; j++){
if (txInstructions[j]) bulkTransaction.add(txInstructions[j]);
}
result.push(bulkTransaction);
}
return result;
}
这是一个很好的例子,展示了 TypeScript 是如何成为你学习与 Solana 交互时的得力工具。你可以快速看到不同对象在各种方法中是如何工作的,并在出现错误时迅速修复代码!
从根本上说,我们的函数正在操作输入数据,以生成 Solana 网络能够理解的有用交易数据。在未来,你可以修改此函数,以适应自己的输入数据或不同类型的交易指令。
好的!你有了一组 Solana 交易。现在,我们需要对其做什么?创建一个新的函数 executeTransactions,接受 solanaConnection(Solana 连接)、Solana 交易数组( transactionList)和我们的 payer Keypair。我们将使用一个叫做 Promise.allSettled 的有趣 JS 方法,因此我们需要一些 TypeScript 魔法来正确捕获我们的返回值。我们期待一个返回 PromiseSettledResult 数组的 Promise(一个对象看起来像这样:{status: 'fulfilled', value: string }
或 {status: 'rejected', reason: Error}
)。
从未听说过 Promise.allSettled? 我们还可以使用 Promise.all 或逐个评估,但这样做有一些缺点。使用 Promise.all,如果我们的任何 Promise 返回错误,我们会收到单个错误响应,即使我们的其他 Promise 成功。我们会失去关于成功完成的交易 ID 的所有信息。不!我们也可以逐个评估我们的交易,但等待每个交易从网络返回响应可能会为我们的查询增加大量不必要的运行时间。Promise.allSettled 将允许我们同时发起多个交易,然后在所有 Promise 完成(或失败)后返回每个 Promise 的结果或错误。
async function executeTransactions(solanaConnection: Connection, transactionList: Transaction[], payer: Keypair):Promise<PromiseSettledResult<string>[]> {
let result:PromiseSettledResult<string>[] = [];
return result;
}
现在,我们需要将每个交易映射到一个 Promise,该 Promise 将返回我们的交易 ID。有时,当 Solana 网络拥堵时,交易传播可能会变得有些棘手(我们在之前的 指南:Solana 交易传播:处理丢失的交易 中探讨过)。Solana 的 web3 SDK 有一个便捷的函数,会为我们处理很多事务 sendAndConfirmTransaction。该方法会将交易发送到网络,订阅交易的状态变化,并在交易成功或失败时向我们报告。最后,为了减少交易失败的可能性,我们将在调用每个交易之前获取最新的区块哈希,通过 .getLatestBlockhash() 实现。
我们还需要在这里做一件事:使用 index 和 setTimeout 构建“错开”的超时。这将使我们可以控制向网络发送请求的频率。为什么这很重要?对于像这样的间歇性批量处理,突发的意外大请求可能会使网络验证者不堪重负。为每个交易提供一个小延迟可以确保你的请求被无误接收,并减少接收 HTTP429/请求过多错误 的可能性。
如果你有计划或系统的高容量批量处理,请联系我们的 解决方案团队,以确保你的操作尽可能顺利进行。
创建一个变量 staggeredTransactions,将我们的 transactionList 映射到 setTimeouts,并在每个交易调用 sendAndConfirmTransaction:
let staggeredTransactions:Promise<string>[] = transactionList.map((transaction, i, allTx) => {
return (new Promise((resolve) => {
setTimeout(() => {
console.log(`请求交易 ${i+1}/${allTx.length}`);
solanaConnection.getLatestBlockhash()
.then(recentHash=>transaction.recentBlockhash = recentHash.blockhash)
.then(()=>sendAndConfirmTransaction(solanaConnection,transaction,[payer])).then(resolve);
}, i * TX_INTERVAL);
})
)})
挺不错吧?我们现在有一个 Promise 数组,可以同时调用,但将以错开的方式发送到网络!我们在回调中添加了一个控制台日志,以便查看我们的代码何时执行(这应该按 TX_INTERVAL 顺序发生)。
现在你只需调用 await Promise.allSettled(staggeredTransactions)!这将等待我们所有的交易执行,然后返回 Solana 网络对每笔交易的结果或错误。我们整个函数看起来像这样:
async function executeTransactions(solanaConnection: Connection, transactionList: Transaction[], payer: Keypair):Promise<PromiseSettledResult<string>[]> {
let result:PromiseSettledResult<string>[] = [];
let staggeredTransactions:Promise<string>[] = transactionList.map((transaction, i, allTx) => {
return (new Promise((resolve) => {
setTimeout(() => {
console.log(`请求交易 ${i+1}/${allTx.length}`);
solanaConnection.getLatestBlockhash()
.then(recentHash=>transaction.recentBlockhash = recentHash.blockhash)
.then(()=>sendAndConfirmTransaction(solanaConnection,transaction,[payer])).then(resolve);
}, i * TX_INTERVAL);
})
)})
result = await Promise.allSettled(staggeredTransactions);
return result;
}
让我们调用我们的函数并发送我们的 SOL(确保你在 guideSecret.json 钱包中有足够的资金)!在 app.ts 底部添加此 async 代码块,以构建你的交易列表,执行你的交易,并返回所有交易的结果!
(async () => {
console.log(`从 ${FROM_KEY_PAIR.publicKey.toString()} 初始化 SOL 空投`);
const transactionList = generateTransactions(NUM_DROPS_PER_TX,dropList,FROM_KEY_PAIR.publicKey);
const txResults = await executeTransactions(SOLANA_CONNECTION,transactionList,FROM_KEY_PAIR);
console.log(await txResults);
})()
你完成了!如果你想检查你的代码,我们的源代码可以在 GitHub 上找到,在这里。
你准备好了吗...? 在你的控制台中,启动吧!
ts-node app.ts
轰!你看到这个了吗?
你应该能够在 Solana Explorer 上查看任何一笔交易,并查看你添加的所有转账到该单一交易中:
那么,我们上面的简单示例使用了一堆大小相同的交易指令。如果你想创建一个运行不同类型和/或动态数量交易指令的程序,你需要在发送交易之前计算交易大小。你可以通过在 Transaction 类的实例上运行 serialize().length 来完成:
transaction.serialize().length;
这将返回你交易的大小,一个字节数!这是一个方便的工具。想尝试一下吗?你如何修改你的 executeTransactions 函数以估算交易大小而不是发送交易?你能计算出在不超过 1,232 字节限制的情况下可以在一笔交易中包含的最大 transfer SOL 指令数量吗?
恭喜!你刚刚在不到一分钟的时间内向 50 个钱包发送了 SOL!如果我们不将我们的交易指令打包并按顺序执行交易,我们可能还在等待。从我们上面提到的,考虑这个练习作为一个工具或框架,你可以在考虑组装和发送其他大型交易组合时使用。
我们很想听听你的批处理过程以及你如何使用像这样的工具——请在 Discord 加入讨论或在 Twitter 联系我们!要了解更多信息,请查看我们的其他 Solana 教程 在这里。
如果你对本指南有任何反馈或问题,请 告诉我们。我们很想听到你的声音!
- 原文链接: quicknode.com/guides/sol...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!