如何使用Solana代币扩展要求来电转账附带便签

  • QuickNode
  • 发布于 2025-01-30 17:24
  • 阅读 15

本文详细介绍了如何使用Solana的Token-2022程序中的新功能——必需的备注来实现更复杂的代币经济结构。通过逐步教程,读者可以学习到如何设置环境、创建和管理带备注要求的代币账户及其使用案例,涵盖了从令牌发行到交易发送的整个流程,最终包括对既存账户添加备注要求的扩展功能。

概述

Token 扩展(又称为 Token-2022 程序)是 Solana Labs 提供的一种新原语,提供了开发者更强的灵活性和控制力,可以实现复杂的经济结构和 web3 代币的实现。在本指南中,我们将深入探讨与 Token-2022 程序相关的新账户特性,即对入账交易的必需备忘录(类似于银行转账)。

你将做什么

在本指南中,你将测试使用 Token-2022 的必需备忘录扩展发送代币:

  1. 使用 Token-2022 程序铸造一个代币
  2. 测试有备忘录和无备忘录指令的各种代币发送场景
  3. 开启和关闭账户的备忘录要求

你需要什么

依赖 版本
node.js 18.12.1
tsc 5.0.2
ts-node 10.9.1
solana-cli 1.14.16

第一步 - 设置你的环境

让我们创建一个新的 Node.js 项目并安装 Solana-Web3.js 库。在终端中依次输入以下命令:

mkdir token-2022-memo && cd token-2022-memo
npm init -y # 或 yarn init -y
npm install @solana/web3.js@1 @solana/spl-token @solana/spl-memo # 或 yarn add @solana/web3.js@1 @solana/spl-token @solana/spl-memo
echo > app.ts

在你喜欢的编辑器中打开 app.ts 文件并添加以下导入:

// 从 Solana web3.js 和 SPL Token 包导入必要的函数和常量
import {
    sendAndConfirmTransaction,
    Connection,
    Keypair,
    SystemProgram,
    Transaction,
    LAMPORTS_PER_SOL,
    PublicKey,
    SendTransactionError,
    TransactionSignature,
    SignatureStatus,
    TransactionConfirmationStatus
} from '@solana/web3.js';
import {
    createMint,
    createEnableRequiredMemoTransfersInstruction,
    createInitializeAccountInstruction,
    disableRequiredMemoTransfers,
    enableRequiredMemoTransfers,
    getAccountLen,
    ExtensionType,
    TOKEN_2022_PROGRAM_ID,
    mintTo,
    createAssociatedTokenAccountIdempotent,
    createTransferCheckedInstruction,
    unpackAccount,
    getMemoTransfer
} from '@solana/spl-token';
import { createMemoInstruction } from '@solana/spl-memo';

我们从 @solana/web3.js@solana/spl-token@solana/spl-memo 导入必要的依赖。我们将使用这些来创建、铸造和转移 Token-2022 代币。这里有一些特定于备忘录转移的内容:createEnableRequiredMemoTransfersInstructiondisableRequiredMemoTransfersenableRequiredMemoTransfersgetAccountLengetMemoTransfer。我们将在指南的后面部分详细介绍这些内容。

让我们定义一个帮助函数来确认交易。在 app.ts 文件中添加以下代码:

    async function confirmTransaction(
        connection: Connection,
        signature: TransactionSignature,
        desiredConfirmationStatus: TransactionConfirmationStatus = 'confirmed',
        timeout: number = 30000,
        pollInterval: number = 1000,
        searchTransactionHistory: boolean = false
    ): Promise<SignatureStatus> {
        const start = Date.now();

        while (Date.now() - start < timeout) {
            const { value: statuses } = await connection.getSignatureStatuses([signature], { searchTransactionHistory });

            if (!statuses || statuses.length === 0) {
                throw new Error('获取签名状态失败');
            }

            const status = statuses[0];

            if (status === null) {
                await new Promise(resolve => setTimeout(resolve, pollInterval));
                continue;
            }

            if (status.err) {
                throw new Error(`交易失败: ${JSON.stringify(status.err)}`);
            }

            if (status.confirmationStatus && status.confirmationStatus === desiredConfirmationStatus) {
                return status;
            }

            if (status.confirmationStatus === 'finalized') {
                return status;
            }

            await new Promise(resolve => setTimeout(resolve, pollInterval));
        }

        throw new Error(`交易确认超时,超过 ${timeout}ms`);
    }

最后,创建一个名为 main 的异步函数,并添加以下代码:

async function main() {
    // 初始化到本地 Solana 节点的连接
    const connection = new Connection('http://127.0.0.1:8899', 'confirmed');

    // 转账代币的数量
    const decimals = 9;
    const transferAmount = BigInt(1_000 * Math.pow(10, decimals)); // 转账 1,000 个代币

    // 定义 Keypair - 付款人和源账户的所有者
    const payer = Keypair.generate();

    // 定义 Keypair - 铸币权限
    const mintAuthority = Keypair.generate();

    // 定义 Keypair - 目标账户(目标账户的所有者)
    const owner = Keypair.generate();

    // 定义目标账户(需要备忘录要求的代币账户)
    const destinationKeypair = Keypair.generate();
    const destination = destinationKeypair.publicKey;

    // 1 - 请求 airdrop 给付款人
    const airdropSignature = await connection.requestAirdrop(payer.publicKey, 2 * LAMPORTS_PER_SOL);
    await confirmTransaction(connection, airdropSignature);

    // 2 - 创建一个铸造

    // 3 - 创建一个目标账户并启用备忘录要求

    // 4 - 将代币铸造到源账户(由付款人拥有)

    // 5 - 创建一个转账指令

    // 6 - 尝试发送一个没有备忘录的交易(应该失败)

    // 7 - 尝试发送一个带有备忘录的交易(应该成功)

    // 8 - 取消启用必需的备忘录转账

    // 9 - 尝试发送一个没有备忘录的交易(应该成功)

    // 10 - 验证备忘录要求的切换
}

// 调用 main 函数
main().then(() => {
    console.log("🎉 - 演示完成。");
}).catch((err) => {
    console.error("⚠️ - 演示失败: ", err);
});

我们列出了创建、铸造和运行 Token-2022 代币测试的步骤。我们已经添加了第一步:定义多个重要的常量,并将一些 SOL 空投到我们的 payer 账户中。这是为了支付交易费用。以下是我们声明的摘要:

  • connection - 与本地 Solana 集群的连接(如果你希望使用 devnet 或 mainnet,只需将 Connection URL 更改为你的 QuickNode RPC 端点
  • decimals - 我们代币的小数位数
  • transferAmount - 我们将以 BigInt 形式转移的代币数量
  • payer - 将支付交易费用并拥有代币源账户的账户
  • mintAuthority - 将拥有铸币权限的账户
  • owner - 将拥有目标账户的账户
  • destinationKeypair - 目标账户的密钥对
  • destination - 目标代币账户的公共密钥

接下来,构建剩余步骤。

第二步 - 创建一个新的代币

让我们开始创建新的代币。

    // 2 - 创建一个铸造
    const mint = await createMint(
        connection,
        payer,
        mintAuthority.publicKey,
        mintAuthority.publicKey,
        decimals,
        undefined,
        undefined,
        TOKEN_2022_PROGRAM_ID
    );

我们使用常用的 createMint 方法,并确保传入 TOKEN_2022_PROGRAM_ID。如果不使用 TOKEN_2022_PROGRAM_ID,将无法使用备忘录要求功能(默认值为旧版代币程序)。如果你完成了我们另一篇关于 Token-2022 的指南,你可能会问,为什么我们在初始化铸造时不使用 Token-2022 扩展?原因在于,备忘录要求是一个 账户 级别的要求,而不是一个 铸造 级别的要求。这意味着备忘录要求在初始化新的代币持有者账户时设置。接下来我们来做这个。

第三步 - 将代币铸造到所有者账户

现在我们有了一个代币铸造,让我们铸造一些代币!在 main() 函数中添加以下内容:

    // 3 - 创建一个目标账户并启用备忘录要求
    const accountLen = getAccountLen([ExtensionType.MemoTransfer]);
    const lamports = await connection.getMinimumBalanceForRentExemption(accountLen);
    const transaction = new Transaction().add(
        SystemProgram.createAccount({
            fromPubkey: payer.publicKey,
            newAccountPubkey: destination,
            space: accountLen,
            lamports,
            programId: TOKEN_2022_PROGRAM_ID,
        }),
        createInitializeAccountInstruction(destination, mint, owner.publicKey, TOKEN_2022_PROGRAM_ID),
        createEnableRequiredMemoTransfersInstruction(destination, owner.publicKey)
    );
    await sendAndConfirmTransaction(connection, transaction, [payer, owner, destinationKeypair], undefined);

在本节中,我们使用 getAccountLen 函数(类似于 getMintLen,但针对使用 Token-2022 扩展的 账户)来确定我们将创建的账户大小。然后,我们使用 getMinimumBalanceForRentExemption 函数来确定保持免租状态所需的最低 Lamport 余额。

我们使用 SystemProgram.createAccountcreateInitializeAccountInstruction 来初始化新的代币账户。请确保将 TOKEN_2022_PROGRAM_ID 传递作为两个指令的程序 ID,否则将无法使用备忘录要求功能。 请注意,由于 Token 2022 扩展的使用,我们无法使用 createAssociatedTokenAccount 函数,因为我们需要使用自定义账户大小和 Lamport。

最后,我们使用一个新函数 createEnableRequiredMemoTransfersInstruction 来启用新的代币账户的备忘录要求。此函数接受以下参数:账户和权威。发送交易到集群并在继续下一步之前使用 sendAndConfirmTransaction 确认它。

第四步 - 将代币铸造到源账户

现在我们有了一个 destination 账户,我们需要将一些代币铸造到源账户。在 main() 函数中添加以下内容:

    // 4 - 将代币铸造到源账户(由付款人拥有)
    const sourceAccount = await createAssociatedTokenAccountIdempotent(connection, payer, mint, payer.publicKey, {}, TOKEN_2022_PROGRAM_ID);

    await mintTo(
        connection,
        payer,
        mint,
        sourceAccount,
        mintAuthority,
        Number(transferAmount) * 10,
        [],
        undefined,
        TOKEN_2022_PROGRAM_ID
    );

我们使用两个熟悉的函数将代币铸造到源账户:

  • createAssociatedTokenAccountIdempotent - 为 payer 账户创建一个关联代币账户。与我们的 destination 不同,我们可以使用 createAssociatedTokenAccountIdempotent 函数,因为我们不使用此账户的任何 Token-2022 扩展。
  • mintTo - 使用 mintAuthority 账户将代币铸造到 sourceAccount。请注意,我们需要将我们的 transferAmountBigInt 转换为 Number 供此函数使用。

对于这两个指令,我们都小心地将 TOKEN_2022_PROGRAM_ID 传递作为程序 ID,以确保与我们的铸造兼容。

第五步 - 创建一个代币转账指令

现在我们已经设置了我们的账户和代币,让我们创建几个场景来测试备忘录要求。我们将首先创建一个可以在测试中使用的代币转账指令。在 main() 函数中添加以下内容:

    // 5 - 创建一个转账指令
    const ix = createTransferCheckedInstruction(
        sourceAccount,
        mint,
        destination,
        payer.publicKey,
        transferAmount,
        decimals,
        undefined,
        TOKEN_2022_PROGRAM_ID
    )

这是一个简单的转账指令,它使用 createTransferCheckedInstruction 函数。此函数接受以下参数:

  • sourceAccount - 转账的源代币账户
  • mint - 代币的铸造
  • destination - 转账的目标代币账户
  • owner ( payer.publicKey) - 源代币账户的所有者
  • amount ( transferAmount) - 要转账的代币数量
  • decimals - 转账金额的小数位数(“Checked”指令需要此参数,以确保传递的金额正确计算了代币的小数位数)
  • multiSigners - 本演示不适用
  • programId ( TOKEN_2022_PROGRAM_ID) - 代币程序的程序 ID

第六步 - 发送没有备忘录的交易

现在我们有了代币转账指令,让我们尝试发送没有备忘录的交易。因为我们的 destination 账户启用了备忘录要求,此交易应当失败。在 main() 函数中添加以下内容:

    // 6 - 尝试发送没有备忘录的交易(应该失败)
    try {
        const failedTx = new Transaction().add(ix);
        const failedTxSig = await sendAndConfirmTransaction(connection, failedTx, [payer], undefined);
        console.log("❌ - 这应该失败,但没有失败。交易: ", failedTxSig);
    } catch (e) {
        if (e instanceof SendTransactionError && e.logs) {
            const errorMessage = e.logs.join('\n');
            if (errorMessage.includes("No memo in previous instruction")) {
                // https://github.com/solana-labs/solana-program-library/blob/d755eae17e0a2220f31bfc69548a78be832643af/token/program-2022/src/error.rs#L143
                console.log("✅ - 交易未附带备忘录而失败(备忘录是必需的)。");
            } else {
                console.error(`❌ - 意外错误: ${errorMessage}`);
            }
        } else {
            console.error(`❌ - 未知错误: ${e}`);
        }
    }

在这里,我们创建了一个新的 TransactionfailedTx(之所以这样命名是因为我们预计它会失败),并将我们的代币转移指令添加给它。然后我们将交易发送到集群并使用 sendAndConfirmTransaction 确认。如果交易失败,我们捕获错误并检查日志中的错误消息。如果错误消息包含预期的错误(“No memo in previous instruction”),我们打印成功消息。否则,我们打印错误消息。

第七步 - 发送带有备忘录的交易

现在我们已经创建了一个测试,用于检查我们的备忘录要求是否阻止没有备忘录的交易,让我们尝试测试一个带有备忘录的交易。在 main() 函数中添加以下内容:

    // 7 - 尝试发送带有备忘录的交易(应该成功)
    try {
        const memo = createMemoInstruction("QuickNode demo.");
        const memoTx = new Transaction().add(memo, ix);
        await sendAndConfirmTransaction(connection, memoTx, [payer], undefined);
        console.log("✅ - 带备忘录的成功交易(备忘录是必需的)。");
    } catch (e) {
        console.error("❌ - 出现问题。交易意外失败: ", e);
    }

与我们之前的步骤类似,我们创建了一个新的 TransactionmemoTx。与之前的步骤不同,我们用 createMemoInstruction 函数创建并添加了一个备忘录指令到交易中。这里需要注意的一件重要事是,备忘录指令必须在代币转账指令之前添加(.add(memo,ix) 可行,但 .add(ix,memo) 不可行)。然后我们将交易发送到集群并使用 sendAndConfirmTransaction 确认。如果交易失败,我们捕获错误并打印错误消息。否则,我们打印成功消息。

现在我们已经测试了代币账户的备忘录要求(既确保没有备忘录的交易失败,又确保有备忘录的交易成功)。如果我们想要禁用这个要求怎么办?让我们试试下一步。

第八步 - 禁用备忘录要求

Solana 添加了一个非常易于使用的函数 disableRequiredMemoTransfers 到 SPL 代币库。现在在 main() 函数中添加它:

    // 8 - 禁用所需的备忘录转账
    await disableRequiredMemoTransfers(connection, payer, destination, owner);

此函数将要求你传递一个 Connection、一个费用支付者、要更新的账户以及账户的所有者/权威。然后,它将向集群发送一个交易以禁用备忘录要求。

让我们检查它是否按预期工作。

第九步 - 验证备忘录要求已禁用

现在备忘录要求已被禁用,我们应该能够发送一个没有备忘录的交易。在 main() 函数中添加以下内容:

    // 9 - 尝试发送没有备忘录的交易(应该成功)
    try {
        const noMemoTx = new Transaction().add(ix);
        await sendAndConfirmTransaction(connection, noMemoTx, [payer], undefined);
        console.log("✅ - 成功的无备忘录交易(备忘录不是必需的)。");
    } catch (e) {
        console.error("❌ - 出现问题。交易意外失败: ", e);
    }

这非常类似于我们的第一次测试,但这次我们预期交易会成功。我们创建一个新的 TransactionnoMemoTx,将我们的代币转账指令添加给它,并将其发送到集群。如果交易失败,我们捕获错误并打印错误消息。否则,我们打印成功消息。

虽然这在简单测试中有效,但如果我们需要检查一个代币账户是否启用了备忘录要求或已禁用该要求怎么办?让我们试一试下一步。

第十步 - 检查备忘录要求是否启用

在我们之前的测试中,我们已经知道(或者有个不错的想法)账户的备忘录要求状态(我们将其初始化为必需,然后进行了禁用)。如果我们不知道呢?我们需要一种方法来查找它。让我们创建一个函数来检查一下。在 main() 函数下方,添加以下函数:

async function verifyMemoRequirement(tokenAccount: PublicKey, connection: Connection): Promise<boolean> {
    const accountInfo = await connection.getAccountInfo(tokenAccount);
    const account = unpackAccount(tokenAccount, accountInfo, TOKEN_2022_PROGRAM_ID);
    const memoDetails = getMemoTransfer(account);
    if (!memoDetails) {
        throw new Error("未找到备忘录详情。");
    }
    return memoDetails.requireIncomingTransferMemos;
}

我们的异步函数接受一个代币账户地址和一个 Connection,并返回一个映 Promise 布尔值(即是否需要一个备忘录以发送代币到账户)。它的工作原理如下:

  1. 使用 getAccountInfo() 获取代币账户的账户信息。
  2. 使用 unpackAccount() 解包账户。这将返回一个带有账户数据的 Account 对象。
  3. 使用 getMemoTransfer() 获取备忘录详情。这将返回一个对象,包含一个布尔值 requireIncomingTransferMemos,我们将返回该值。

现在让我们在 main() 函数中调用它,以验证备忘录要求是否已禁用。在 main() 函数中添加以下内容:

    // 10 - 验证备忘录要求切换
    let isMemoRequired = await verifyMemoRequirement(destination, connection);
    if (isMemoRequired) {
        console.log("❌ - 有问题。预计备忘录要求已禁用。");
    } else {
        console.log("✅ - 备忘录要求已禁用。");
    }

    await enableRequiredMemoTransfers(connection, payer, destination, owner);

    isMemoRequired = await verifyMemoRequirement(destination, connection);
    if (isMemoRequired) {
        console.log("✅ - 备忘录要求已启用。");
    } else {
        console.log("❌ - 有问题。预计需提供备忘录。");
    }

我们通过传递 destination 账户和 Solana connection 来调用我们的 verifyMemoRequirement() 函数。然后我们检查返回值并打印成功或错误信息。

接着我们运行 enableRequiredMemoTransfers(这正好与 disableRequiredMemoTransfers 相反)以重新启用备忘录要求。然后我们再次调用我们的 verifyMemoRequirement() 函数并检查它是否按预期工作。

运行代码

最后,在一个单独的终端中运行以下命令以启动本地 Solana 集群:

solana-test-validator

在你的主终端中运行你的脚本:

ts-node app.ts

你应该看到类似以下内容的输出:

% ts-node memo
✅ - 交易未附带备忘录而失败(备忘录是必需的)。
✅ - 带备忘录的成功交易(备忘录是必需的)。
✅ - 成功的无备忘录交易(备忘录不是必需的)。
✅ - 备忘录要求已禁用。
✅ - 备忘录要求已启用。
🎉 - 演示完成。

干得好!

额外 - 在现有账户上实施备忘录要求

在我们的指南中,我们创建了一个新的代币账户并初始化了备忘录要求。如果我们想要向现有账户添加备忘录要求怎么办?

我们可以使用 SPL 代币库中的 createReallocateInstruction() 函数做到这一点。此函数允许我们根据传递的扩展向账户添加额外的 Lamport。我们可以传递 MemoTransfer ExtensionType 以确保正确的重新分配。然后,我们可以使用 createEnableRequiredMemoTransfersInstruction() 函数启用备忘录要求。

如果你愿意,可以在你的 main() 函数下添加 # 11 尝试一下(确保更新你的导入)。

    // 额外的导入
    import { createAccount, createReallocateInstruction } from '@solana/spl-token';

    // ...

    // 11 - bonus - 向现有账户添加备忘录要求
    try {
        // 创建一个没有备忘录要求的新代币账户
        const newOwner = Keypair.generate();
        const bonusAccount = await createAccount(
            connection,
            payer,
            mint,
            newOwner.publicKey,
            undefined,
            undefined,
            TOKEN_2022_PROGRAM_ID
        );

        const extensions = [ExtensionType.MemoTransfer];
        const addExtensionTx = new Transaction().add(
            // 创建一个重新分配指令,以为备忘录要求添加 Lamport
            createReallocateInstruction(
                bonusAccount,
                payer.publicKey,
                extensions,
                newOwner.publicKey
            ),
            // 创建一个指令以启用备忘录要求
            createEnableRequiredMemoTransfersInstruction(bonusAccount, newOwner.publicKey)
        );
        await sendAndConfirmTransaction(connection, addExtensionTx, [payer, newOwner]);
        console.log("✅ - 备忘录要求已添加到现有账户。");
    } catch (e) {
        console.error("❌ - 出现问题。交易意外失败: ", e);
    }

总结

让我们回顾一下我们在这里所做的事情:

  • 我们创建并铸造了一个新的 Token-2022 代币。
  • 我们创建了一个具有备忘录要求的新代币账户。
  • 我们尝试向该账户发送没有备忘录的代币(失败)。
  • 我们尝试向该账户发送带有备忘录的代币(成功)。
  • 我们禁用了备忘录要求。
  • 我们尝试向该账户发送没有备忘录的代币(成功)。
  • 我们检查了备忘录要求是否已禁用,再次启用它,并检查它是否已启用。

希望本指南能帮助你理解 Token-2022 程序中的备忘录要求功能。

我们希望听到更多关于你正在构建的项目以及你计划如何在项目中使用 Token-2022 的信息。请在 Discord 给我们留言,或在 Twitter 上关注我们,以保持最新信息!

我们 ❤️ 反馈!

请告诉我们 如果你有任何反馈或新主题的请求。我们期待你的来信。

  • 原文链接: quicknode.com/guides/sol...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

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