如何在 Solana 上转移 SPL 代币

  • 想样
  • 发布于 2025-03-21 19:13
  • 阅读 83

SPL代币账户概览在开始之前,了解Solana代币计划账户的几个组件的工作原理会很有帮助:MintID和关联代币账户。MintID每个SPL代币都有一个唯一的铸币ID,可以将其与任何其他类型的代币区分开来。例如,USDCSPL代币铸币ID为[EPjFWdd5AufqSSqe

<!--StartFragment-->

SPL 代币账户概览

在开始之前,了解 Solana 代币计划账户的几个组件的工作原理会很有帮助:Mint ID 和关联代币账户。

Mint ID

每个 SPL 代币都有一个唯一的铸币 ID,可以将其与任何其他类型的代币区分开来。例如,$USDC SPL 代币铸币 ID 为[EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v],$SAMO 铸币 ID 为[7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU]。

值得注意的是,每个 NFT 都有一个唯一的铸币地址(这在一定程度上使其不可替代)。例如,Famous Fox #4679 的铸币 ID 为GS2AtoEoL9DgaYg9xC7cUumftB7vb2CPMrczZCKgJv1Y],Famous Fox #6562 的铸币 ID 为7FTdQdMqkk5Xc2oFsYR88BuJt2yyCPReTpqr3viH6b6C。正如您将在稍后看到的,这是这两个代币之间的显著区别。

关联代币账户

Solana 代币计划从用户的主要系统帐户地址和代币铸币地址派生出“代币帐户密钥,允许用户为他们拥有的每个代币创建一个主代币帐户”。该帐户称为关联代币帐户或“ATA”。实际上,ATA 是与用户和特定代币铸币厂相关联的唯一帐户。例如,假设我是帐户“DEMO...1234”的所有者。我可以拥有多个 ATA——我持有的每个代币铸币厂都有一个 ATA(例如,每个 $USDC、$SAMO 等都有一个唯一的 ATA,如下所示)。

代币转移必须始终在与同一铸币地址关联的两个 ATA 之间进行。这意味着我们无法将 $USDC 从我的 ATA 发送到您的 $SAMO ATA(见下面的示例)。

正如您稍后将在我们的代码中看到的那样,每个帐户并非都拥有每个铸币的 ATA。如果用户之前没有与代币交互,则发送者必须“创建”它并存入必要的租金以使帐户保持活跃。

这个概念一开始可能有点难懂,但它很重要,所以花点时间确保你理解了这一点!我发现在 Solana Explorer 的代币详细信息页面上浏览我的钱包很有帮助 ?display=detail 。你可以看到你的每个 ATA、它们的帐户 ID 和关联的 Mint ID:

如果您想进一步了解 Solana SPL 代币程序库,请访问spl.solana.com。

准备好开始编码了吗?我也是!

设置你的项目

使用以下命令在终端中创建一个新的项目目录:

mkdir spl-transfer
cd spl-transfer

为你的应用创建一个文件app.ts

echo > app.ts

使用“是”标志初始化您的项目以使用新包的默认值:

yarn init --yes
#or
npm init --yes

创建一个启用了 .json 导入的tsconfig.json :

tsc -init --resolveJsonModule true

安装 Solana Web3依赖项

我们需要为本次练习添加 Solana Web3 和 SPL Token 库。在您的终端中输入:

yarn add @solana/web3.js@1 @solana/spl-token
#or
npm install @solana/web3.js@1 @solana/spl-token

创建钱包并空投SOL

我们需要完成的第一个任务是创建一个带有钱包的帐户并为其注资。我们将使用下面的便捷工具自动生成一个新钱包并向其中空投 1 SOL。(如果您更喜欢手动方式,也可以使用Keypair.generate()和功能来实现这一点)。requestAirdrop()

🔑使用 Devnet SOL 生成新钱包

创建新钱包

成功生成密钥对后,您会注意到两个新常量:secretFROM_KEYPAIR,密钥对。secret是一个 32 字节数组,用于生成公钥和私钥。FROM_KEYPAIR是一个密钥对实例,用于签署交易(我们空投了一些 devnet SOL 来支付 gas 费用)。如果还没有,请确保将其添加到代码中其他常量下方。

在导入下方,粘贴您的新机密,并添加:

import { Keypair } from "@solana/web3.js";

const secret = [0,...,0]; // 👈 Replace with your secret
const FROM_KEYPAIR = Keypair.fromSecretKey(new Uint8Array(secret));
console.log(`My public key is: ${FROM_KEYPAIR.publicKey.toString()}.`);

用于简化调试的日志

您现在可以访问 RPC 端点的日志,从而帮助您更有效地解决问题。如果您在 RPC 调用中遇到问题,只需检查 QuickNode 仪表板中的日志即可快速识别和解决问题。

铸造或空投一些 SPL代币

要完成本指南,您需要一些 Devnet 上的 SPL 代币。有多种方法可以获取 Devnet SPL 代币 - 以下列出几种。

  1. 铸造您自己的可替代代币指南:如何使用新的 Metaplex 代币标准创建可替代的 SPL 代币。
  2. 使用 Candy Machine 铸造一个或多个 NFT指南:如何使用 Sugar(Candy Machine)在 Solana 上部署 NFT 集合。
  3. 从SPL 代币水龙头请求 $DUMMY 代币空投。*
  4. 如果您在另一个钱包中已经拥有 Devnet SPL 代币,则可以使用 Phantom 或其他钱包接口发送它们。*

在继续之前,您应该能够进入 Solana Explorer 并查看您的钱包在 devnet 上有 SOL 余额和至少一个 SPL 代币。它看起来应该像这样:

让我们开始吧。

设置您的应用程序

导入必要的依赖项

打开app.ts,并将以下导入粘贴到第 1 行

import { getOrCreateAssociatedTokenAccount, createTransferInstruction } from "@solana/spl-token";
import { Connection, Keypair, ParsedAccountData, PublicKey, sendAndConfirmTransaction, Transaction } from "@solana/web3.js";

除了我们在上一步中创建的钱包之外,我们还从 Solana Web3 库中导入了一些基本的方法和类。

设置您的 QuickNode端点

要在 Solana 上构建,您需要一个 API 端点来连接网络。欢迎您使用公共节点或部署和管理自己的基础设施;但是,如果您希望响应时间缩短 8 倍,您可以将繁重的工作交给我们。了解为什么 Solana 上超过 50% 的项目选择 QuickNode 并在此处]注册免费帐户。我们将使用 Solana Devnet 节点。

复制 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'; //You must change this value!
const TRANSFER_AMOUNT = 1;

将其替换MINT_ADDRESS为您计划发送的代币的铸币地址。如果您不知道代币的铸币地址,可以前往并单击复制图标以获取您的铸币地址。将其粘贴到MINT_ADDRESS变量中。

注意:如果愿意,您还可以更新您的 DESTINATION_WALLET 地址和 TRANSFER_AMOUNT ,但这两个值都适用于此示例。

获取小数位数

如果您之前创建过可替代的 SPL 代币,那么您就会知道小数很重要。由于链将代币供应表示为整数值,因此我们必须根据代币元数据中分配的小数位数来转换代币数量(例如,如果铸币厂有三个小数,我们必须发送 1,000 个单位才能有效传输一个代币 [1,000 / 10**3])。

要获取小数,我们将在SOLANA_CONNECTION上调用**getParsedAccountInfo并传递令牌MINT_ADDRESS。由于该方法可以返回多种类型,因此我们需要将类型声明为ParsedAccountData才能有效地解析数据。在声明下方添加此函数:**

async function getNumberDecimals(mintAddress: string):Promise&lt;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(如果不存在则创建)。由于我们可能需要为这些交易创建关联账户,我们的付款人可能必须向新 ATA 注入少量 SOL 以支付租金。在sendTokens中,将步骤 1 添加到您的代码中以获取我们的源账户(从技术上讲,因为我们知道我们已经有 SPL 代币——我们已经有一个 ATA,所以我们也可以使用getAssociatedTokenAddress):

    console.log(`Sending ${TRANSFER_AMOUNT} ${(MINT_ADDRESS)} from ${(FROM_KEYPAIR.publicKey.toString())} to ${(DESTINATION_WALLET)}.`)
    //Step 1
    console.log(`1 - Getting Source Token Account`);
    let sourceAccount = await getOrCreateAssociatedTokenAccount(
        SOLANA_CONNECTION, 
        FROM_KEYPAIR,
        new PublicKey(MINT_ADDRESS),
        FROM_KEYPAIR.publicKey
    );
    console.log(`    Source Account: ${sourceAccount.address.toString()}`);

我们将对我们的目标账户执行相同的操作。添加下面的步骤 2 以获取或创建我们的目标账户(将向其发送令牌的收件人的 ATA):

    //Step 2
    console.log(`2 - Getting Destination Token Account`);
    let destinationAccount = await getOrCreateAssociatedTokenAccount(
        SOLANA_CONNECTION, 
        FROM_KEYPAIR,
        new PublicKey(MINT_ADDRESS),
        new PublicKey(DESTINATION_WALLET)
    );
    console.log(`    Destination Account: ${destinationAccount.address.toString()}`);

只需要再提供一条信息即可完成我们的交易。还记得我们创建的getNumberDecimals函数吗?让我们调用它并将返回值存储在常量numberDecimals中:

    //Step 3
    console.log(`3 - Fetching Number of Decimals for Mint: ${MINT_ADDRESS}`);
    const numberDecimals = await getNumberDecimals(MINT_ADDRESS);
    console.log(`    Number of Decimals: ${numberDecimals}`);

最后,我们需要组装并发送我们的交易。在步骤 3 下方添加以下指令组装:

    //Step 4
    console.log(`4 - Creating and Sending Transaction`);
    const tx = new Transaction();
    tx.add(createTransferInstruction(
        sourceAccount.address,
        destinationAccount.address,
        FROM_KEYPAIR.publicKey,
        TRANSFER_AMOUNT * Math.pow(10, numberDecimals)
    ))

我们在这里所做的是告诉我们的应用程序将TRANSFER_AMOUNT代币(请注意,我们必须乘以 10 的**numberDecimals次方)从我们获取的sourceAccount转移到我们获取的目标帐户**。

为了达到最佳实践,我们应该在发送交易之前获取最近的区块哈希。在sendTokens函数的第 4 步末尾添加此代码片段:

    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', //Green Text
        `   Transaction Success!🎉`,
        `\n    https://explorer.solana.com/tx/${signature}?cluster=devnet`
    );

不要介意那个 console.log——那只是一些让我们的成功以绿色文本显示的技巧。

🏃‍♂️ 运行你的代码

最后,在代码末尾调用sendTokens

sendTokens();

在你的终端中,输入以下命令运行代码:

ts-node app.ts

恭喜!如果你按照上述步骤操作,你应该会看到类似这样的内容:

下一步及总结

太棒了!现在您知道如何将 SPL 代币从一个用户转移到另一个用户了!想更上一层楼吗?以下是一些可以进一步深入的方法:

  1. 你能编写一个脚本,将你所有的 NFT 转移到另一个钱包吗?(提示:你可能需要先研究如何获取用户钱包中的所有代币。
  2. 您可以编写一个脚本,将 SPL 代币空投到一堆可能有或可能没有与您的铸币厂关联的 ATA 的钱包中吗?(提示:查看我们关于发送批量 SOL 分发的指南以获取灵感)。 <!--EndFragment-->
  • 原创
  • 学分: 0
  • 分类: Solana
  • 标签:
点赞 0
收藏 0
分享

0 条评论

请先 登录 后评论
想样
想样
江湖只有他的大名,没有他的介绍。