该文章详细介绍了如何在Solana区块链上使用TypeScript脚本进行SPL代币的转账,涵盖了从项目设置到执行转账的完整流程,并提供了相关资源链接。
发送 Solana 程序库 (SPL) 代币是 Solana 开发的一个关键机制。无论你是在向你的社区空投白名单代币,批量发送 NFT 到另一个钱包,还是管理托管账户之间的代币流动,你都不可避免地需要能够转移 SPL 代币。转移 SPL 代币与发送 SOL 略有不同,因此了解其工作原理在你推进 Solana 开发旅程时至关重要。
在本指南中,你将使用 TypeScript 编写一个脚本,能够在 Solana 的开发网(devnet)中将一个 SPL 代币从一个账户转移到另一个账户。
如果你想了解如何使用 Anchor 框架在程序中转移 SPL 代币和 SOL,请查看我们的指南:如何使用 Anchor 转移 SOL 和 SPL 代币。
更喜欢视频演示吗?跟随 Sahil 学习如何在 Solana 开发网中转移 SPL 代币。
在开始之前,了解 Solana 代币程序账户的几个组件是有帮助的:Mint ID 和关联代币账户。
每个 SPL 代币都有一个唯一的 Mint ID,可以将其与任何其他类型的代币区分开来。例如,$USDC 的 SPL 代币 Mint ID 是 EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v,而 $SAMO 的 Mint ID 是 7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU。
值得注意的是,每个 NFT 都有一个唯一的铸造地址(这部分也是其不可替代性的原因之一)。例如,Famous Fox #4679 的 Mint ID 是 GS2AtoEoL9DgaYg9xC7cUumftB7vb2CPMrczZCKgJv1Y,Famous Fox #6562 的 Mint ID 是 7FTdQdMqkk5Xc2oFsYR88BuJt2yyCPReTpqr3viH6b6C。正如你稍后将看到的,这是这两种代币之间的重要区别。
如果你想了解更多关于铸造 SPL 代币的信息,请查看我们的指南 如何使用新的 Metaplex 代币标准创建可替代的 SPL 代币 和 如何在 Solana 上铸造 NFT 。
Solana 代币程序通过"从用户的主系统账户地址和代币铸造地址派生一个代币账户密钥,从而使用户能够为其持有的每种代币创建一个主代币账户"(来源:spl.solana.com)。该账户被称为关联代币账户或 "ATA"。有效地说,ATA 是一个唯一的账户,与用户和特定的代币铸造关联。例如,假设我拥有账户 "DEMO...1234"。我可以拥有多个 ATA——每个我持有的代币铸造一个独特的 ATA(例如,每个 $USDC、$SAMO 等的独特 ATA,如下所示)。
代币转移 必须始终 在两个与相同关联铸造地址相关的 ATA 之间进行。这意味着我们不能从我的 ATA 转送 $USDC 到你的 $SAMO ATA(见示例)。
正如你稍后在我们的代码中看到的,并不是每个账户都有每种铸造的 ATA。如果一个用户之前没有与某个代币互动,发送者必须"创建"它并为账户存入必要的租金以保持其活跃。
这个概念起初可能会有些棘手,但它很重要,所以花时间确保你理解这一点!我发现浏览我的钱包在 Solana Explorer 上的代币详细页面是有帮助的:https://explorer.solana.com/address/YOUR_WALLET_ADDRESS/tokens?display=detail。你可以看到每个你的 ATA、它们的账户 ID 和关联的 Mint ID:
如果你想更深入了解 Solana SPL 代币程序库,请访问 spl.solana.com。
准备好开始编码了吗?我也是!
在终端中创建一个新的项目目录:
mkdir spl-transfer
cd spl-transfer
为你的应用程序创建一个文件,app.ts:
echo > app.ts
使用"yes"标志来初始化你的项目以使用新包的默认值:
yarn init --yes
# 或者
npm init --yes
创建一个 tsconfig.json,并启用 .json 导入:
tsc -init --resolveJsonModule true
我们需要添加 Solana Web3 和 SPL 代币库以供本练习使用。在终端中输入:
yarn add @solana/web3.js@1 @solana/spl-token
# 或者
npm install @solana/web3.js@1 @solana/spl-token
我们需要完成的第一项任务是创建一个带有钱包的账户并为其提供资金。我们将使用下面的快捷工具自动生成一个新钱包,并向其空投 1 SOL。(如果你更喜欢手动的方法,也可以使用 Keypair.generate()
和 requestAirdrop()
函数来实现)
🔑 使用开发网 SOL 生成一个新钱包
创建新钱包
一旦你成功生成了密钥对,你会注意到两个新常量:secret
和 FROM_KEYPAIR
,一个 Keypair。secret
是一个 32 字节数组,用于生成公钥和私钥。FROM_KEYPAIR
是一个 Keypair 实例,用于签署交易(我们已经空投了一些开发网 SOL 以覆盖 gas 费用)。如果你还没有添加它,请确保在你其他常量的下面加入它。
在你的导入语句下面,粘贴你的新密钥,并添加:
import { Keypair } from "@solana/web3.js";
const secret = [0,...,0]; // 👈 以你的密钥替换
const FROM_KEYPAIR = Keypair.fromSecretKey(new Uint8Array(secret));
console.log(`我的公钥是: ${FROM_KEYPAIR.publicKey.toString()}。`);
简化调试的日志
现在你可以访问你的 RPC 端点的日志,帮助你更有效地解决问题。如果你在 RPC 调用中遇到问题,只需检查你 QuickNode 控制面板中的日志,快速识别并解决问题。了解关于日志历史限制的更多信息,请访问 [我们的定价页面]。(https://www.quicknode.com/pricing#features)
要完成本指南,你需要在开发网中拥有一些 SPL 代币。有几种方式获取开发网 SPL 代币——以下是几个。
铸造你自己的可替代代币(指南:如何使用新的 Metaplex 代币标准创建可替代的 SPL 代币)。
使用 Candy Machine 铸造一个或多个 NFT(指南:如何使用 Sugar(Candy Machine)在 Solana 上部署 NFT 收藏)。
从 SPL 代币水龙头 请求 $DUMMY 代币空投。*
如果你在另一个钱包中已经拥有开发网 SPL 代币,你可以使用 Phantom 或其他钱包界面将其发送。*
在继续之前,你应该能够访问 Solana Explorer,并看到你的钱包中有 SOL 余额和至少一种 SPL 代币在开发网中(https://explorer.solana.com/address/YOUR_WALLET_ADDRESS/tokens?cluster=devnet)。它应该看起来像这样:
让我们开始吧。
打开 app.ts,并在 第 1 行 粘贴以下导入:
import { getOrCreateAssociatedTokenAccount, createTransferInstruction } from "@solana/spl-token";
import { Connection, Keypair, ParsedAccountData, PublicKey, sendAndConfirmTransaction, Transaction } from "@solana/web3.js";
除了我们在上一步创建的钱包外,我们还导入了一些来自 Solana Web3 库的重要方法和类。
要在 Solana 上构建,你需要一个 API 端点以连接到网络。你可以使用公共节点或部署和管理自己的基础设施;但是,如果你想要访问 8 倍更快的响应时间,可以将繁重的工作交给我们。看看为什么超过 50% 的 Solana 项目选择 QuickNode,并在这里注册一个免费账户。我们将使用 Solana 开发网节点。
复制 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 DESTINATION_WALLET = 'DemoKMZWkk483hX4mUrcJoo3zVvsKhm8XXs28TuwZw9H';
const MINT_ADDRESS = 'DoJuta7joTSuuoozqQtjtnASRYiVsT435gh4srh5LLGK'; // 你必须更改这个值!
const TRANSFER_AMOUNT = 1;
将 MINT_ADDRESS
替换为你计划发送的代币的铸造地址。如果你不知道你的代币的铸造地址,你可以访问 https://explorer.solana.com/address/YOUR_WALLET_ADDRESS/tokens?cluster=devnet 并点击 复制 图标以捕获你的铸造地址。将其粘贴到 MINT_ADDRESS 变量中。
备注: 如果你愿意,也可以更新你的 DESTINATION_WALLET 地址和 TRANSFER_AMOUNT,但这两个值在本示例中都将有效。
如果你之前创建过可替代的 SPL 代币,你就知道小数位很重要。由于链将代币的供应以整数值表示,因此我们必须根据代币元数据中分配的小数位数转换代币数量(例如,如果铸造有三位小数,我们必须发送 1,000 个单位以有效转移一个代币[1,000 / 10**3])。
要获取小数位数,我们将调用 getParsedAccountInfo 在 SOLANA_CONNECTION 上,并传入代币 MINT_ADDRESS。因为该方法可以返回多种类型,我们需要将我们的类型声明为 ParsedAccountData 以有效解析数据。将以下函数添加到你的声明下面:
async function getNumberDecimals(mintAddress: string):Promise<number> {
const info = await SOLANA_CONNECTION.getParsedAccountInfo(new PublicKey(MINT_ADDRESS));
const result = (info.value?.data as ParsedAccountData).parsed.info.decimals as number;
return result;
}
从技术上讲,如果你知道你的代币铸造的小数位数(你可以在 Solana Explorer 的代币页面找到它),你不需要这个函数。尽管如此,我们还是喜欢包括它,因为它允许你在不更改其他变量的情况下用不同的铸造修改代码。
让我们开始有趣的部分吧!
创建一个新的异步函数,sendTokens:
async function sendTokens() {
}
首先,我们需要获取源账户和目标账户的关联代币账户("ATA")。请记住,每个账户必须有一个 ATA,将钱包与铸造关联;ATA 只能为单一铸造持有代币;发送者和接收者关联代币账户的铸造必须相同。
我们将使用 getOrCreateAssociatedTokenAccount 来获取 ATA(如果不存在则创建),适用于此次交易的每一方。由于我们可能需要为这些交易创建关联账户,付款方可能需要使用少量 SOL 来为新 ATA 提供种子以覆盖租金。在 sendTokens 内部,将步骤 1 添加到你的代码中,以获取我们的源账户(从技术上讲,由于我们知道我们已经拥有 SPL 代币——我们已经有了 ATA,所以我们也可以使用 getAssociatedTokenAddress):
console.log(`从 ${(FROM_KEYPAIR.publicKey.toString())} 向 ${(DESTINATION_WALLET)} 发送 ${TRANSFER_AMOUNT} ${(MINT_ADDRESS)}。`)
// 步骤 1
console.log(`1 - 获取源代币账户`);
let sourceAccount = await getOrCreateAssociatedTokenAccount(
SOLANA_CONNECTION,
FROM_KEYPAIR,
new PublicKey(MINT_ADDRESS),
FROM_KEYPAIR.publicKey
);
console.log(` 源账户: ${sourceAccount.address.toString()}`);
我们将对目标账户做同样的事情。请在下面添加步骤 2,以获取或创建我们的目标账户(接收者的 ATA,将接收代币发送到):
// 步骤 2
console.log(`2 - 获取目标代币账户`);
let destinationAccount = await getOrCreateAssociatedTokenAccount(
SOLANA_CONNECTION,
FROM_KEYPAIR,
new PublicKey(MINT_ADDRESS),
new PublicKey(DESTINATION_WALLET)
);
console.log(` 目标账户: ${destinationAccount.address.toString()}`);
我们还需要一条信息来组装我们的交易。记得我们创建的 getNumberDecimals 函数吗?让我们调用它并将返回值存储在常量 numberDecimals 中:
// 步骤 3
console.log(`3 - 获取铸造的数字小数位: ${MINT_ADDRESS}`);
const numberDecimals = await getNumberDecimals(MINT_ADDRESS);
console.log(` 小数位数: ${numberDecimals}`);
最后,我们需要组装并发送我们的交易。在步骤 3 下面添加以下指令组装:
// 步骤 4
console.log(`4 - 创建并发送交易`);
const tx = new Transaction();
tx.add(createTransferInstruction(
sourceAccount.address,
destinationAccount.address,
FROM_KEYPAIR.publicKey,
TRANSFER_AMOUNT * Math.pow(10, numberDecimals)
))
这里我们告诉我们的应用程序从我们获取的 sourceAccount 转移 TRANSFER_AMOUNT 代币(请注意我们必须乘以 10 的 numberDecimals 次方)到我们获取的 destinationAccount。
最佳实践是,在发送事务之前获取一个最近的区块哈希。在步骤 4 的末尾在你的 sendTokens 函数中添加以下代码片段:
const latestBlockHash = await SOLANA_CONNECTION.getLatestBlockhash('confirmed');
tx.recentBlockhash = await latestBlockHash.blockhash;
const signature = await sendAndConfirmTransaction(SOLANA_CONNECTION,tx,[FROM_KEYPAIR]);
console.log(
'\x1b[32m', // 绿色文本
` 交易成功!🎉`,
`\n https://explorer.solana.com/tx/${signature}?cluster=devnet`
);
不用担心那个 console.log——那只是一些技巧使我们的成功以绿色文本显示。
最后,在你的代码末尾调用 sendTokens:
sendTokens();
在你的终端中,通过输入以下命令运行你的代码:
ts-node app.ts
恭喜!如果你遵循了上述步骤,你应该会看到类似这样的内容:
太棒了!你现在知道如何将 SPL 代币从一个用户转移到另一个用户了!想要进一步提升吗?以下是几个想法,看看你可以进行哪些扩展:
你能写一个脚本,将你所有的 NFT 转移到另一个钱包吗?(提示:你可能想首先研究 如何获取用户钱包中持有的所有代币)。
你能写一个脚本,将 SPL 代币空投给可能或不可能与其铸造关联的多个钱包吗?(提示:查看我们关于发送批量 SOL 分配的指南以获取灵感)。
如果你尝试了其中一项或有任何问题,请告诉我们!在 Discord 和 Twitter 找到我们!
- 原文链接: quicknode.com/guides/sol...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!