Solana - 如何使用Solidity和Solang创建和铸造SPL代币 - Quicknode

  • QuickNode
  • 发布于 2025-01-30 20:39
  • 阅读 20

这篇指南详细介绍了如何使用Solidity和Solang在Solana上创建和铸造SPL代币,涵盖了从基本知识到实际操作的完整过程,适合想要在Solana网络上利用其Solidity知识进行开发的程序员。内容包含了所需的工具、术语解释、创建与部署的详细步骤,并提供了代码示例和测试流程。

概述

准备好根据你的 Solidity 知识在 Solana 上发布代币吗?本指南介绍了使用 Solidity 和 Solang 在 Solana 上创建 SPL 代币的革命性方法。

你将要做什么

  • 学习一些关于使用 Solang 的基础知识
  • 使用 Solidity 和 Solang 创建一个可替代的 SPL 代币程序(相当于以太坊的 ERC20 和 ERC721 代币标准)
  • 运行测试以确保程序按预期工作
  • 将程序部署到 Solana 并铸造你的代币

你需要的准备

依赖项 版本
node.js 18.16.1
anchor cli 0.29
solana cli 1.16.5
tsc 5.0.2

为了确保你准备好开始,请验证你是否已安装 Solana 1.16+ 和 Anchor 0.28+。你可以通过运行以下命令来确认:

solana --version
anchor --version

现在,让我们开始吧!

Solang 基础知识

Solang 是一个针对 Solana 区块链的 Solidity 编译器。它允许熟悉 Solidity 的开发人员使用他们熟悉的语言编写智能合约,然后将这些合约编译为可在 Solana 网络上运行的代码。

Solang 旨在与 Solidity 兼容,尽管由于 Solana 和以太坊之间的差异,并不是所有特性都受到支持。它与 Anchor 和 Solana CLI 等工具协作,为转向 Solana 的以太坊开发人员提供了顺畅的工作流程。

如果你对 Solana 和 Solidity 感兴趣并想了解更多关于 Solang 的信息,请查看 我们的综合指南

以太坊与 Solana 之间的术语差异

在使用 Solidity 和 Solang 为 Solana 开发时,了解关键术语差异是很重要的,概述如下。

此表仅仅是一个快速概览;如果你想深入了解 Solana,请查看我们的 Solana 基础知识参考指南

以太坊 Solana 描述
地址 账户 在以太坊中,“地址”是一个存储位置。在 Solana 中,“账户”可以持有数据,并且更加动态。值得一提的是,Solana 账户不会以 0x 前缀开头。
智能合约 程序 在以太坊中,“智能合约”在 Solana 中称为“程序”。
主网,测试网 主网,开发网,测试网 在 Solana 中,开发人员使用开发网进行测试,而测试网则是为 Solana 核心开发人员准备的。
代币(ERC20、ERC721 等) 代币(SPL 代币) Solana 支持 SPL 代币,而以太坊支持 ERC20、ERC721 和其他代币标准。
开发工具(Hardhat、Foundry 等) 开发工具(Solang、Anchor 等) 以太坊和 Solana 的开发工具不同。
钱包(Metamask 等) 钱包(Phantom 等) 以太坊使用不同的钱包(如 Metamask)而 Solana 使用不同的钱包(如 Phantom)。
有状态合约 无状态程序 以太坊合约是有状态的,在合约内部存储状态。Solana 程序是无状态的,状态存储在账户中。
20字节公钥地址 32字节公钥地址 Solana 账户地址使用 32 字节公钥,而以太坊使用 20 字节公钥。

设置你的 QuickNode 端点

在 Solana 上构建时,你需要一个 API 端点来连接网络。你可以使用公共节点或部署和管理自己的基础设施;但是,如果你希望响应时间更快 8 倍,可以让我们来处理繁重的任务。

查看超过 50% 的 Solana 项目为何选择 QuickNode,并在 这里 注册一个免费账户。我们将使用 Solana 开发网端点。

复制 HTTP 提供程序链接:

QuickNode Solana Node Endpoint

将Token图标和元数据上传到 IPFS

在我们追求去中心化的过程中,使我们的代币图标和元数据可供公众访问至关重要,以确保在区块浏览器、钱包和交易所上的最佳展示。我们提倡使用 IPFS 来固定和提供数据。为提供全面的指南,我们将涵盖两种方法—— 利用 QuickNode 的托管 IPFS 服务运行本地 IPFS 节点

  • QuickNode IPFS
  • 本地 IPFS 节点

tip

要了解更多关于 QuickNode IPFS 的价格信息,请查看我们的定价计划 这里

访问 QuickNode 控制面板 并访问左侧边栏中的 IPFS 选项卡。

导航到 Files 选项卡,使用 New 按钮上传要上传的文件,或通过拖放你想要的文件来上传。首先上传代币图标图像,然后上传元数据 JSON 文件。

Token Logo

上传后,单击 Files 选项卡中的文件名称,然后单击 copy IPFS URL 按钮。IPFS URL 应具有类似如下格式。

https://qn-shared.quicknode-ipfs.com/ipfs/QmS4UopJD2P843YWHJxxoqHdgK1YQfw2AF48fFJbSoCSr7

现在,让我们撰写我们的元数据。Solana 采用 Metaplex 的可替代代币标准,需要 namesymboldescriptionimage。创建一个新的元数据 JSON 文件(例如,token.json),将 IPFS_URL_OF_IMAGE 替换为已上传的图像的 IPFS URL,然后保存它。保存后,将你的 JSON 文件上传到 IPFS,类似于之前的步骤。

token.json

{
    "name": "My Awesome Token",
    "symbol": "MAT",
    "description": "This token is awesome!",
    "image": "IPFS_URL_OF_IMAGE"
}

上传后,单击 Files 选项卡中的文件名称,复制 IPFS URL 以在铸造代币时使用。它的格式应与下面的类似。

https://qn-shared.quicknode-ipfs.com/ipfs/QmYCwFK1KJWCXBE1aiRzUP1BZruaPW2g3PyPF25QJzDpP5

现在,我们的文件通过 QuickNode 固定在 IPFS 上,让我们继续铸造我们的代币!

对于选择运行本地 IPFS 节点的用户,第一步是安装 IPFS 并发布文件。根据你的操作系统下载并安装 IPFS CLI,按照 IPFS 文档中的 安装指南,创建一个项目目录并导航到它。

在终端中初始化 IPFS 仓库:

ipfs init

在另一个终端窗口中,启动 IPFS 守护进程,作为你的本地 IPFS 节点:

ipfs daemon

可以使用你的图像或元数据,也可以使用提供的示例图像。

Token Logo

将你选择的图像移动到项目目录(例如,token-logo.png)。返回先前的终端窗口并将图像发布到 IPFS:

ipfs add token-logo.png

成功上传后,你将收到类似于以下的输出:

Local IPFS Output

保存生成的哈希,添加后缀 https://ipfs.io/ipfs/。IPFS URL 应具有类似如下格式。

https://ipfs.io/ipfs/QmS4UopJD2P843YWHJxxoqHdgK1YQfw2AF48fFJbSoCSr7

现在,让我们撰写我们的元数据。Solana 遵循 Metaplex 的可替代代币标准,需要 namesymboldescriptionimage。创建一个新的元数据 JSON 文件(例如,token.json)。

token.json

{
    "name": "My Awesome Token",
    "symbol": "MAT",
    "description": "This token is awesome!",
    "image": "IPFS_URL_OF_IMAGE"
}

将你的元数据 JSON 移到项目目录中(例如,token.json)。发布图像到 IPFS:

ipfs add token.json

更新 IPFS_URL_OF_IMAGE 为已上传图像的完整 URL。保存并上传文件。

Local IPFS JSON Output

https://ipfs.io/ipfs/ 后缀添加到 token.json 的哈希。完整的 URL 看起来像下面的样子。

https://ipfs.io/ipfs/QmTFyKYDUgftB9Tu17zfmoAKLDX1HeZctq3MEyHhxDvaHm

此 URL 对于后续铸造代币非常重要。

现在我们的文件已在 IPFS 上,让我们继续铸造我们的代币!

设置开发环境

在深入之前,请确保你已安装所有列出的先决条件 这里

第 1 步:启动你的项目

在你的项目目录中运行以下命令,为你的 SPL 代币启动一个新的 Solang 项目。

anchor init my-spl-token --solidity

该命令会设置一个名为 my-spl-token 的新文件夹,其中包含基本文件。 --solidity 标志表示使用 Solang 进行编译。导航到你的新项目并在你喜欢的代码编辑器中打开它。

cd my-spl-token

使用 yarn 或 npm 安装软件包。

  • yarn
  • npm
yarn add @coral-xyz/anchor @solana/spl-token
npm install @coral-xyz/anchor @solana/spl-token

第 2 步:创建钱包

要开始与 Solana 交互,你需要一个钱包。如果你还没有 Solana 钱包,请使用 Solana CLI 创建一个。此命令会生成一个新钱包,并将密钥对文件保存为 id.json 在你当前的目录中:

solana-keygen new --no-bip39-passphrase -o ./id.json

请安全保存你的助记词。同时保留你的公钥,因为你将在后面的部分中需要它。

注意: 如果你已经有一个 Solana 钱包并且知道你的 JSON 密钥对文件,可以跳过此步骤。只需确保将项目的配置文件配置为指向现有钱包的 JSON 文件即可。此步骤确保你拥有与 Solana 网络交互的必要凭证,以便部署和测试你的 SPL 代币。

第 3 步:配置钱包

确保你的项目与你的钱包连接。为此,请在项目根目录下的 Anchor.toml 文件中修改 [provider] 部分,如下所示。

如果在上一步中未创建新钱包,因为你已经有一个 Solana 钱包,请相应地用你的 JSON 密钥对文件的路径替换 ./id.json

Anchor.toml

[toolchain]

[features]
seeds = false
skip-lint = false

[programs.localnet]
my_spl_token = "7HtSCtT6cKH4iZQEWuTFrUqm8nLd4Nxb8foXKdBhEpzU"

[registry]
url = "https://api.apr.dev"

[provider]
cluster = "devnet"
wallet = "./id.json"

[scripts]
test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"

通过运行以下代码确认你的钱包是否已正确配置。它应返回你钱包的公钥。请记住,因为你将需要它在开发网获取一些免费的 SOL。

solana address -k ./id.json

第 4 步:链接你的钱包

将 Solana CLI 链接到此钱包和开发网。

要使用你自己的 QuickNode Solana 开发网端点,只需将 devnet 替换为你端点的 URL。

solana config set -u devnet -k ./id.json

仔细检查你的配置,确保与 Anchor.toml 对齐。

solana config get

第 5 步:空投 SOL

要在开发网上获取一些免费的 SOL,请使用 QuickNode 多链水龙头。只需输入你的 Solana 钱包公钥即可获取你的 SOL。你也可以使用以下命令。

solana airdrop 1

获取免费的 SOL 后,检查你的余额。此命令返回开发网上的 SOL 余额,因为我们在第 4 步中配置了开发网。

solana balance

使用 Solidity 创建你的 SPL 代币

目前,你已设置开发环境并创建了代币的元数据。现在,让我们专注于编码。

由于 SPL 代币程序是用 Rust 编写的,而不是 Solidity,因此需要一些库文件来建立 Solidity 代码与 SPL 代币程序之间的通信。因此,在跳入 SPL 代币代码之前,让我们将一些与 SPL 代币程序相关的库文件添加到项目中。

第 1 步:创建库文件

在项目目录中创建一个文件夹 libraries。然后,通过运行以下命令在 libraries 文件夹中创建三个库文件( _mplmetadata、_spltoken 和 _systeminstruction)。

mkdir libraries
echo > libraries/mpl_metadata.sol
echo > libraries/spl_token.sol
echo > libraries/system_instruction.sol

现在,你的项目文件夹结构应类似于以下内容。

├── Anchor.toml
├── app
├── id.json
├── libraries
├── migrations
├── node_modules
├── package.json
├── solidity
├── target
├── tests
├── tsconfig.json
└── yarn.lock

MPL Metadata

打开 mpl_metadata.sol 文件并按如下方式修改。

libraries/mpl_metadata.sol

import 'solana';

// 参考: https://github.com/metaplex-foundation/metaplex-program-library/blob/master/token-metadata/program/src/instruction/metadata.rs#L449
// Solidity 不支持 Rust 的 Option<> 类型,因此我们需要手动处理
// 需要为 Option<> 类型的每种组合创建一个结构体
// 如果 Option<> 类型的 bool 为 false,则注释掉对应的结构字段,否则指令会因“无效的账户数据”而失败
// TODO:找出更好的方法来处理 Option<> 类型

library MplMetadata {
    address constant systemAddress = address"11111111111111111111111111111111";

    // 参考: https://github.com/metaplex-foundation/metaplex-program-library/blob/master/token-metadata/program/src/instruction/metadata.rs#L31
    struct CreateMetadataAccountArgsV3 {
        DataV2 data;
        bool isMutable;
        bool collectionDetailsPresent; // 在 Solidity 中处理 Rust Option<> 类型
        // CollectionDetails collectionDetails;
    }

    // 参考: https://github.com/metaplex-foundation/metaplex-program-library/blob/master/token-metadata/program/src/state/data.rs#L22
    struct DataV2 {
        string name;
        string symbol;
        string uri;
        uint16 sellerFeeBasisPoints;
        bool creatorsPresent; // 在 Solidity 中处理 Rust Option<> 类型
        // Creator[] creators;
        bool collectionPresent; // 在 Solidity 中处理 Rust Option<> 类型
        // Collection collection;
        bool usesPresent; // 在 Solidity 中处理 Rust Option<> 类型
        // Uses uses;
    }

    // 参考: https://github.com/metaplex-foundation/metaplex-program-library/blob/master/bubblegum/program/src/state/metaplex_adapter.rs#L10
    struct Creator {
        address creatorAddress;
        bool verified;
        uint8 share;
    }

    // 参考: https://github.com/metaplex-foundation/metaplex-program-library/blob/master/bubblegum/program/src/state/metaplex_adapter.rs#L66
    struct Collection {
        bool verified;
        address key;
    }

    // 参考: https://github.com/metaplex-foundation/metaplex-program-library/blob/master/token-metadata/program/src/state/collection.rs#L57
    struct CollectionDetails {
        CollectionDetailsType detailType;
        uint64 size;
    }
    enum CollectionDetailsType {
        V1
    }

    // 参考: https://github.com/metaplex-foundation/metaplex-program-library/blob/master/bubblegum/program/src/state/metaplex_adapter.rs#L43
    struct Uses {
        UseMethod useMethod;
        uint64 remaining;
        uint64 total;
    }

    // 参考: https://github.com/metaplex-foundation/metaplex-program-library/blob/master/bubblegum/program/src/state/metaplex_adapter.rs#L35
    enum UseMethod {
        Burn,
        Multiple,
        Single
    }

    function create_metadata_account(
        address metadata,
        address mint,
        address mintAuthority,
        address payer,
        address updateAuthority,
        string name,
        string symbol,
        string uri,
        address rentAddress,
        address metadataProgramId
    ) public {
        // // 如何将 Creator[] 数组添加到 DataV2 结构的示例
        // Creator[] memory creators = new Creator[](1);
        // creators[0] = Creator({
        //     creatorAddress: payer,
        //     verified: false,
        //     share: 100
        // });

        DataV2 data = DataV2({
            name: name,
            symbol: symbol,
            uri: uri,
            sellerFeeBasisPoints: 0,
            creatorsPresent: false,
             // creators: creators,
            collectionPresent: false,
            // collection: Collection({
            //     verified: false,
            //     key: address(0)
            // }),
            usesPresent: false
            // uses: Uses({
            //     useMethod: UseMethod.Burn,
            //     remaining: 0,
            //     total: 0
            // })
        });

        CreateMetadataAccountArgsV3 args = CreateMetadataAccountArgsV3({
            data: data,
            isMutable: true,
            collectionDetailsPresent: false
            // collectionDetails: CollectionDetails({
            //     detailType: CollectionDetailsType.V1,
            //     size: 0
            // })
        });

        AccountMeta[7] metas = [\
            AccountMeta({pubkey: metadata, is_writable: true, is_signer: false}),\
            AccountMeta({pubkey: mint, is_writable: false, is_signer: false}),\
            AccountMeta({pubkey: mintAuthority, is_writable: false, is_signer: true}),\
            AccountMeta({pubkey: payer, is_writable: true, is_signer: true}),\
            AccountMeta({pubkey: updateAuthority, is_writable: false, is_signer: false}),\
            AccountMeta({pubkey: systemAddress, is_writable: false, is_signer: false}),\
            AccountMeta({pubkey: rentAddress, is_writable: false, is_signer: false})\
        ];

        bytes1 discriminator = 33;
        bytes instructionData = abi.encode(discriminator, args);

        metadataProgramId.call{accounts: metas}(instructionData);
    }
}

SPL Token

打开 spl_token.sol 文件并按如下方式修改。

libraries/spl_token.sol

import 'solana';
import './system_instruction.sol';

library SplToken {
    address constant tokenProgramId = address"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
    address constant associatedTokenProgramId = address"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL";
    address constant rentAddress = address"SysvarRent111111111111111111111111111111111";
    enum TokenInstruction {
        InitializeMint, // 0
        InitializeAccount, // 1
        InitializeMultisig, // 2
        Transfer, // 3
        Approve, // 4
        Revoke, // 5
        SetAuthority, // 6
        MintTo, // 7
        Burn, // 8
        CloseAccount, // 9
        FreezeAccount, // 10
        ThawAccount, // 11
        TransferChecked, // 12
        ApproveChecked, // 13
        MintToChecked, // 14
        BurnChecked, // 15
        InitializeAccount2, // 16
        SyncNative, // 17
        InitializeAccount3, // 18
        InitializeMultisig2, // 19
        InitializeMint2, // 20
        GetAccountDataSize, // 21
        InitializeImmutableOwner, // 22
        AmountToUiAmount, // 23
        UiAmountToAmount, // 24
        InitializeMintCloseAuthority, // 25
        TransferFeeExtension, // 26
        ConfidentialTransferExtension, // 27
        DefaultAccountStateExtension, // 28
        Reallocate, // 29
        MemoTransferExtension, // 30
        CreateNativeMint // 31
    }

    /// 初始化一个新的代币账户。
    ///
    /// @param tokenAccount 要初始化的代币账户的公钥
    /// @param mint 此新代币账户的铸币账户的公钥
    /// @param owner 此新代币账户的所有者的公钥
    function initialize_account(address tokenAccount, address mint, address owner) internal{
        bytes instr = new bytes(1);

        instr[0] = uint8(TokenInstruction.InitializeAccount);
        AccountMeta[4] metas = [\
            AccountMeta({pubkey: tokenAccount, is_writable: true, is_signer: false}),\
            AccountMeta({pubkey: mint, is_writable: false, is_signer: false}),\
            AccountMeta({pubkey: owner, is_writable: false, is_signer: false}),\
            AccountMeta({pubkey: rentAddress, is_writable: false, is_signer: false})\
        ];

        tokenProgramId.call{accounts: metas}(instr);
    }

    /// 初始化一个新的关联代币账户。
    ///
    /// @param payer 创建关联代币账户的付款公钥
    /// @param tokenAccount 要初始化的代币账户的公钥
    /// @param mint 此新代币账户的铸币账户的公钥
    /// @param owner 此新代币账户的所有者的公钥
    function create_associated_token_account(address payer, address tokenAccount, address mint, address owner) internal {
        AccountMeta[6] metas = [\
            AccountMeta({pubkey: payer, is_writable: true, is_signer: true}),\
            AccountMeta({pubkey: tokenAccount, is_writable: true, is_signer: false}),\
            AccountMeta({pubkey: owner, is_writable: false, is_signer: false}),\
            AccountMeta({pubkey: mint, is_writable: false, is_signer: false}),\
            AccountMeta({pubkey: SystemInstruction.systemAddress, is_writable: false, is_signer: false}),\
            AccountMeta({pubkey: SplToken.tokenProgramId, is_writable: false, is_signer: false})\
        ];

        bytes instructionData = abi.encode((0));
        associatedTokenProgramId.call{accounts: metas}(instructionData);
    }

    /// 初始化铸币指令数据
    struct InitializeMintInstruction {
        uint8 instruction;
        uint8 decimals;
        address mintAuthority;
        uint8 freezeAuthorityOption;
        address freezeAuthority;
    }

    /// 初始化新的铸币账户。
    ///
    /// @param mint 要初始化的铸币账户的公钥
    /// @param mintAuthority 铸币权限的公钥
    /// @param freezeAuthority 冻结权限的公钥
    /// @param decimals 铸币的小数位数
    function initialize_mint(address mint, address mintAuthority, address freezeAuthority, uint8 decimals) internal {
        InitializeMintInstruction instr = InitializeMintInstruction({
            instruction: 20,
            decimals: decimals,
            mintAuthority: mintAuthority,
            freezeAuthorityOption: 1,
            freezeAuthority: freezeAuthority
        });

        AccountMeta[1] metas = [\
            AccountMeta({pubkey: mint, is_writable: true, is_signer: false})\
        ];

        tokenProgramId.call{accounts: metas}(instr);
    }

    /// 以一个指令创建并初始化一个新的铸币账户
    ///
    /// @param payer 用于创建铸币账户的账户公钥
    /// @param mint 要初始化的铸币账户的公钥
    /// @param mintAuthority 铸币权限的公钥
    /// @param freezeAuthority 冻结权限的公钥
    /// @param decimals 铸币的小数位数
    function create_mint(address payer, address mint, address mintAuthority, address freezeAuthority, uint8 decimals) internal {
        // 调用系统程序为铸币账户创建一个新账户
        // 程序拥有者设置为代币程序
        SystemInstruction.create_account(
            payer,   // 从此账户发送的 lamports (付款者)
            mint,    // 收到新账户的 lamports (待创建的账户)
            1461600, // lamport 数量(铸币账户的最小 lamports)
            82,      // 帐户所需空间(铸币账户)
            SplToken.tokenProgramId // 新程序拥有者
        );

        InitializeMintInstruction instr = InitializeMintInstruction({
            instruction: 20,
            decimals: decimals,
            mintAuthority: mintAuthority,
            freezeAuthorityOption: 1,
            freezeAuthority: freezeAuthority
        });

        AccountMeta[1] metas = [\
            AccountMeta({pubkey: mint, is_writable: true, is_signer: false})\
        ];

        tokenProgramId.call{accounts: metas}(instr);
    }

    /// 铸造新代币。事务应由铸币权限密钥对签名
    ///
    /// @param mint 铸币的账户
    /// @param account 要去往的代币账户
    /// @param authority 铸币权限的公钥
    /// @param amount 要铸造的代币数量
    function mint_to(address mint, address account, address authority, uint64 amount) internal {
        bytes instr = new bytes(9);

        instr[0] = uint8(TokenInstruction.MintTo);
        instr.writeUint64LE(amount, 1);

        AccountMeta[3] metas = [\
            AccountMeta({pubkey: mint, is_writable: true, is_signer: false}),\
            AccountMeta({pubkey: account, is_writable: true, is_signer: false}),\
            AccountMeta({pubkey: authority, is_writable: true, is_signer: true})\
        ];

        tokenProgramId.call{accounts: metas}(instr);
    }

    /// 从 @from 转移 @amount 的代币到 @to。交易应由 @from 账户的所有者密钥对签名。
    ///
    /// @param from 要转移 tokens 的账户
    /// @param to 要转移 tokens 的账户
    /// @param owner @from 账户所有者密钥对的公钥
    /// @param amount 转移的数量
    function transfer(address from, address to, address owner, uint64 amount) internal {
        bytes instr = new bytes(9);

        instr[0] = uint8(TokenInstruction.Transfer);
        instr.writeUint64LE(amount, 1);

        AccountMeta[3] metas = [\
            AccountMeta({pubkey: from, is_writable: true, is_signer: false}),\
            AccountMeta({pubkey: to, is_writable: true, is_signer: false}),\
            AccountMeta({pubkey: owner, is_writable: true, is_signer: true})\
        ];

        tokenProgramId.call{accounts: metas}(instr);
    }

    /// 在账户中燃烧 @amount 的代币。该交易应由所有者签名。
    ///
    /// @param account 需要燃烧代币的账户
    /// @param mint 此代币的铸币账户
    /// @param owner 账户所有者的公钥
    /// @param amount 转移的数量
    function burn(address account, address mint, address owner, uint64 amount) internal {
        bytes instr = new bytes(9);

        instr[0] = uint8(TokenInstruction.Burn);
        instr.writeUint64LE(amount, 1);

        AccountMeta[3] metas = [\
            AccountMeta({pubkey: account, is_writable: true, is_signer: false}),\
            AccountMeta({pubkey: mint, is_writable: true, is_signer: false}),\
            AccountMeta({pubkey: owner, is_writable: true, is_signer: true})\
        ];

        tokenProgramId.call{accounts: metas}(instr);
    }

    /// 批准将某个数量代币授权给某个委托。该交易应由所有者签名。
    ///
    /// @param account 应该批准的账户
    /// @param delegate 被批准的委托公钥
    /// @param owner 账户所有者的公钥
    /// @param amount 被批准的数量
    function approve(address account, address delegate, address owner, uint64 amount) internal {
        bytes instr = new bytes(9);

        instr[0] = uint8(TokenInstruction.Approve);
        instr.writeUint64LE(amount, 1);

        AccountMeta[3] metas = [\
            AccountMeta({pubkey: account, is_writable: true, is_signer: false}),\
            AccountMeta({pubkey: delegate, is_writable: false, is_signer: false}),\
            AccountMeta({pubkey: owner, is_writable: false, is_signer: true})\
        ];

        tokenProgramId.call{accounts: metas}(instr);
    }

    /// 撤销之前批准的委托。这笔交易应该由所有者签名。此后,不会为任何数量批准任何委托。
    ///
    /// @param account 应该批准的账户
    /// @param owner 账户所有者的公钥
    function revoke(address account, address owner) internal {
        bytes instr = new bytes(1);

        instr[0] = uint8(TokenInstruction.Revoke);

        AccountMeta[2] metas = [\
            AccountMeta({pubkey: account, is_writable: true, is_signer: false}),\
            AccountMeta({pubkey: owner, is_writable: false, is_signer: true})\
        ];

        tokenProgramId.call{accounts: metas}(instr);
    }

    /// 获取铸币的总供给,即流通中的总量
    /// @param mint 此代币的铸币账户
    function total_supply(address mint) internal view returns (uint64) {
        AccountInfo account = get_account_info(mint);

        return account.data.readUint64LE(36);
    }

    /// 获取账户余额。
    ///
    /// @param account 我们想知道余额的账户
    function get_balance(address account) internal view returns (uint64) {
        AccountInfo ai = get_account_info(account);

        return ai.data.readUint64LE(64);
    }

    /// 获取账户的账户信息。此函数遍历事务账户信息并查找账户信息,否则事务将失败。
    ///
    /// @param account 我们想要获取账户信息的账户。
    function get_account_info(address account) internal view returns (AccountInfo) {
        for (uint64 i = 0; i < tx.accounts.length; i++) {
            AccountInfo ai = tx.accounts[i];
            if (ai.key == account) {
                return ai;
            }
        }

        revert("账户丢失");
    }

    /// 此枚举表示代币账户的状态
    enum AccountState {
        Uninitialized,
        Initialized,
        Frozen
    }

    /// 此结构是 'get_token_account_data' 的返回值
    struct TokenAccountData {
        address mintAccount;
        address owner;
        uint64 balance;
        bool delegate_present;
        address delegate;
        AccountState state;
        bool is_native_present;
        uint64 is_native;
        uint64 delegated_amount;
        bool close_authority_present;
        address close_authority;
    }

    /// 获取关联代币账户的所有者、铸币账户和余额。
    ///
    /// @param tokenAccount 代币账户
    /// @return struct TokenAccountData
    function get_token_account_data(address tokenAccount) public view returns (TokenAccountData) {
        AccountInfo ai = get_account_info(tokenAccount);

        TokenAccountData data = TokenAccountData(
            {
                mintAccount: ai.data.readAddress(0),
                owner: ai.data.readAddress(32),
                balance: ai.data.readUint64LE(64),
                delegate_present: ai.data.readUint32LE(72) > 0,
                delegate: ai.data.readAddress(76),
                state: AccountState(ai.data[108]),
                is_native_present: ai.data.readUint32LE(109) > 0,
                is_native: ai.data.readUint64LE(113),
                delegated_amount: ai.data.readUint64LE(121),
                close_authority_present: ai.data.readUint32LE(129) > 10,
                close_authority: ai.data.readAddress(133)
            }
        );

        return data;
    }

    // 此结构是 'get_mint_account_data' 的返回值
    struct MintAccountData {
        bool authority_present;
        address mint_authority;
        uint64 supply;
        uint8 decimals;
        bool is_initialized;
        bool freeze_authority_present;
        address freeze_authority;
    }

    /// 检索保存在铸币账户中的信息
    ///
    /// @param mintAccount 我们想要检索信息的账户
    /// @return MintAccountData 结构体
    function get_mint_account_data(address mintAccount) public view returns (MintAccountData) {
        AccountInfo ai = get_account_info(mintAccount);
``````solidity
uint32 authority_present = ai.data.readUint32LE(0);
uint32 freeze_authority_present = ai.data.readUint32LE(46);
MintAccountData data = MintAccountData({
    authority_present: authority_present > 0,
    mint_authority: ai.data.readAddress(4),
    supply: ai.data.readUint64LE(36),
    decimals: uint8(ai.data[44]),
    is_initialized: ai.data[45] > 0,
    freeze_authority_present: freeze_authority_present > 0,
    freeze_authority: ai.data.readAddress(50)
});

return data;
}

// 一个铸币账户有一个权限,该权限的类型是此结构的成员之一。
enum AuthorityType {
    MintTokens,
    FreezeAccount,
    AccountOwner,
    CloseAccount
}

/// 从铸币账户中移除铸币权限
///
/// @param mintAccount 铸币账户的公钥
/// @param mintAuthority 铸币权限的公钥
function remove_mint_authority(address mintAccount, address mintAuthority) public {
    AccountMeta[2] metas = [\
        AccountMeta({pubkey: mintAccount, is_signer: false, is_writable: true}),\
        AccountMeta({pubkey: mintAuthority, is_signer: true, is_writable: false})\
    ];

    bytes data = new bytes(9);
    data[0] = uint8(TokenInstruction.SetAuthority);
    data[1] = uint8(AuthorityType.MintTokens);
    data[3] = 0;

    tokenProgramId.call{accounts: metas}(data);
}
}

系统指令

打开 system_instruction.sol 文件在 libraries 文件夹中,并根据下面的内容进行修改。

libraries/system_instruction.sol

// SPDX-License-Identifier: Apache-2.0

// 声明:这个库提供了一个桥梁,以便 Solidity 与 Solana 的系统指令进行交互。虽然它已经准备好用于生产,
// 但尚未经过安全审计,因此使用时需自担风险。

import 'solana';

library SystemInstruction {
    address constant systemAddress = address"11111111111111111111111111111111";
    address constant recentBlockHashes = address"SysvarRecentB1ockHashes11111111111111111111";
    address constant rentAddress = address"SysvarRent111111111111111111111111111111111111";
    uint64 constant state_size = 80;

    enum Instruction {
        CreateAccount,
        Assign,
        Transfer,
        CreateAccountWithSeed,
        AdvanceNounceAccount,
        WithdrawNonceAccount,
        InitializeNonceAccount,
        AuthorizeNonceAccount,
        Allocate,
        AllocateWithSeed,
        AssignWithSeed,
        TransferWithSeed,
        UpgradeNonceAccount // 此功能在 Solana v1.9.15 中不可用
    }

    /// 在 Solana 上创建一个新账户
    ///
    /// @param from 从转移 lamports 到新账户的账户公钥
    /// @param to 要创建的账户的公钥
    /// @param lamports 转移到新账户的 lamports 数量
    /// @param space 将为账户提供的字节大小
    /// @param owner 将拥有被创建账户的程序公钥
    function create_account(address from, address to, uint64 lamports, uint64 space, address owner) internal {
        AccountMeta[2] metas = [\
            AccountMeta({pubkey: from, is_signer: true, is_writable: true}),\
            AccountMeta({pubkey: to, is_signer: true, is_writable: true})\
        ];

        bytes bincode = abi.encode(uint32(Instruction.CreateAccount), lamports, space, owner);

        systemAddress.call{accounts: metas}(bincode);
    }

    /// 使用从种子派生的公钥在 Solana 上创建一个新账户
    ///
    /// @param from 从转移 lamports 到新账户的账户公钥
    /// @param to 要创建的账户的公钥。公钥必须匹配 create_with_seed(base, seed, owner)
    /// @param base 通过种子派生 'to' 地址的基础地址
    /// @param seed 用于创建 'to' 公钥的字符串
    /// @param lamports 转移到新账户的 lamports 数量
    /// @param space 将为账户提供的字节大小
    /// @param owner 将拥有被创建账户的程序公钥
    function create_account_with_seed(address from, address to, address base, string seed, uint64 lamports, uint64 space, address owner) internal {
        AccountMeta[3] metas = [\
            AccountMeta({pubkey: from, is_signer: true, is_writable: true}),\
            AccountMeta({pubkey: to, is_signer: false, is_writable: true}),\
            AccountMeta({pubkey: base, is_signer: true, is_writable: false})\
        ];

        uint32 buffer_size = 92 + seed.length;
        bytes bincode = new bytes(buffer_size);
        bincode.writeUint32LE(uint32(Instruction.CreateAccountWithSeed), 0);
        bincode.writeAddress(base, 4);
        bincode.writeUint64LE(uint64(seed.length), 36);
        bincode.writeString(seed, 44);
        uint32 offset = seed.length + 44;
        bincode.writeUint64LE(lamports, offset);
        offset += 8;
        bincode.writeUint64LE(space, offset);
        offset += 8;
        bincode.writeAddress(owner, offset);

        systemAddress.call{accounts: metas}(bincode);
    }

    /// 将账户分配给程序(所有者)
    ///
    /// @param pubkey 将要重新分配所有者的账户的公钥
    /// @param owner 新账户所有者的公钥
    function assign(address pubkey, address owner) internal {
        AccountMeta[1] meta = [\
            AccountMeta({pubkey: pubkey, is_signer: true, is_writable: true})\
        ];
        bytes bincode = abi.encode(uint32(Instruction.Assign), owner);

        systemAddress.call{accounts: meta}(bincode);
    }

    /// 根据种子将账户分配给程序(所有者)
    ///
    /// @param addr 将要重新分配所有者的账户公钥。公钥必须匹配 create_with_seed(base, seed, owner)
    /// @param base 通过种子派生 'addr' 键的基础地址
    /// @param seed 用于创建 'addr' 公钥的字符串
    /// @param owner 新程序所有者的公钥
    function assign_with_seed(address addr, address base, string seed, address owner) internal {
        AccountMeta[2] metas = [\
            AccountMeta({pubkey: addr, is_signer: false, is_writable: true}),\
            AccountMeta({pubkey: base, is_signer: true, is_writable: false})\
        ];

        uint32 buffer_size = 76 + seed.length;
        bytes bincode = new bytes(buffer_size);
        bincode.writeUint32LE(uint32(Instruction.AssignWithSeed), 0);
        bincode.writeAddress(base, 4);
        bincode.writeUint64LE(uint64(seed.length), 36);
        bincode.writeString(seed, 44);
        bincode.writeAddress(owner, 44 + seed.length);

        systemAddress.call{accounts: metas}(bincode);
    }

    /// 在账户之间转移 lamports
    ///
    /// @param from 资金账户的公钥
    /// @param to 收件账户的公钥
    /// @param lamports 转移的 lamports 数量
    function transfer(address from, address to, uint64 lamports) internal {
        AccountMeta[2] metas = [\
            AccountMeta({pubkey: from, is_signer: true, is_writable: true}),\
            AccountMeta({pubkey: to, is_signer: false, is_writable: true})\
        ];

        bytes bincode = abi.encode(uint32(Instruction.Transfer), lamports);

        systemAddress.call{accounts: metas}(bincode);
    }

    /// 从派生地址转移 lamports
    ///
    /// @param from_pubkey 资金账户公钥。它应该匹配 create_with_seed(from_base, seed, from_owner)
    /// @param from_base 通过种子派生 'from_pubkey' 键的基础地址
    /// @param seed 用于创建 'from_pubkey' 公钥的字符串
    /// @param from_owner 用于派生资金账户地址的所有者
    /// @param to_pubkey 收件账户的公钥
    /// @param lamports 转移的 lamports 数量
    function transfer_with_seed(address from_pubkey, address from_base, string seed, address from_owner, address to_pubkey, uint64 lamports) internal {
        AccountMeta[3] metas = [\
            AccountMeta({pubkey: from_pubkey, is_signer: false, is_writable: true}),\
            AccountMeta({pubkey: from_base, is_signer: true, is_writable: false}),\
            AccountMeta({pubkey: to_pubkey, is_signer: false, is_writable: true})\
        ];

        uint32 buffer_size = seed.length + 52;
        bytes bincode = new bytes(buffer_size);
        bincode.writeUint32LE(uint32(Instruction.TransferWithSeed), 0);
        bincode.writeUint64LE(lamports, 4);
        bincode.writeUint64LE(seed.length, 12);
        bincode.writeString(seed, 20);
        bincode.writeAddress(from_owner, 20 + seed.length);

        systemAddress.call{accounts: metas}(bincode);
    }

    /// 在(可能是新)账户中分配空间而不提供资金
    ///
    /// @param pub_key 要分配空间的账户
    /// @param space 要分配的字节数
    function allocate(address pub_key, uint64 space) internal {
        AccountMeta[1] meta = [\
            AccountMeta({pubkey: pub_key, is_signer: true, is_writable: true})\
        ];

        bytes bincode = abi.encode(uint32(Instruction.Allocate), space);

        systemAddress.call{accounts: meta}(bincode);
    }

    /// 为根据基础公钥和种子派生的地址分配空间
    ///
    /// @param addr 要分配空间的账户。它应该匹配 create_with_seed(base, seed, owner)
    /// @param base 通过种子派生的 'addr' 键的基础地址
    /// @param seed 用于创建 'addr' 公钥的字符串
    /// @param space 要分配的字节数
    /// @param owner 用于派生 'addr' 账户地址的所有者
    function allocate_with_seed(address addr, address base, string seed, uint64 space, address owner) internal {
        AccountMeta[2] metas = [\
            AccountMeta({pubkey: addr, is_signer: false, is_writable: true}),\
            AccountMeta({pubkey: base, is_signer: true, is_writable: false})\
        ];

        bytes bincode = new bytes(seed.length + 84);
        bincode.writeUint32LE(uint32(Instruction.AllocateWithSeed), 0);
        bincode.writeAddress(base, 4);
        bincode.writeUint64LE(seed.length, 36);
        bincode.writeString(seed, 44);
        uint32 offset = 44 + seed.length;
        bincode.writeUint64LE(space, offset);
        offset += 8;
        bincode.writeAddress(owner, offset);

        systemAddress.call{accounts: metas}(bincode);
    }

    /// 使用从种子派生的公钥在 Solana 上创建一个新非账户
    ///
    /// @param from 从转移 lamports 到新账户的账户公钥
    /// @param nonce 要创建的账户的公钥。公钥必须匹配 create_with_seed(base, seed, systemAddress)
    /// @param base 通过种子派生 'nonce' 键的基础地址
    /// @param seed 用于创建 'addr' 公钥的字符串
    /// @param authority 被授权执行 nonce 指令的实体
    /// @param lamports 转移到新账户的 lamports 数量
    function create_nonce_account_with_seed(address from, address nonce, address base, string seed, address authority, uint64 lamports) internal {
        create_account_with_seed(from, nonce, base, seed, lamports, state_size, systemAddress);

        AccountMeta[3] metas = [\
            AccountMeta({pubkey: nonce, is_signer: false, is_writable: true}),\
            AccountMeta({pubkey: recentBlockHashes, is_signer: false, is_writable: false}),\
            AccountMeta({pubkey: rentAddress, is_signer: false, is_writable: false})\
        ];

        bytes bincode = abi.encode(uint32(Instruction.InitializeNonceAccount), authority);
        systemAddress.call{accounts: metas}(bincode);
    }

    /// 在 Solana 上创建一个新账户
    ///
    /// @param from 从转移 lamports 到新账户的账户公钥
    /// @param nonce 要创建的 nonce 账户的公钥
    /// @param authority 被授权执行 nonce 指令的实体
    /// @param lamports 转移到新账户的 lamports 数量
    function create_nonce_account(address from, address nonce, address authority, uint64 lamports) internal {
        create_account(from, nonce, lamports, state_size, systemAddress);

        AccountMeta[3] metas = [\
            AccountMeta({pubkey: nonce, is_signer: false, is_writable: true}),\
            AccountMeta({pubkey: recentBlockHashes, is_signer: false, is_writable: false}),\
            AccountMeta({pubkey: rentAddress, is_signer: false, is_writable: false})\
        ];

        bytes bincode = abi.encode(uint32(Instruction.InitializeNonceAccount), authority);
        systemAddress.call{accounts: metas}(bincode);
    }

    /// 消耗存储的 nonce,用一个后继者替换它
    ///
    /// @param nonce_pubkey nonce 账户的公钥
    /// @param authorized_pubkey 被授权在账户上执行指令的实体的公钥
    function advance_nonce_account(address nonce_pubkey, address authorized_pubkey) internal {
        AccountMeta[3] metas = [\
            AccountMeta({pubkey: nonce_pubkey, is_signer: false, is_writable: true}),\
            AccountMeta({pubkey: recentBlockHashes, is_signer: false, is_writable: false}),\
            AccountMeta({pubkey: authorized_pubkey, is_signer: true, is_writable: false})\
        ];

        bytes binc  vecode = abi.encode(uint32(Instruction.AdvanceNounceAccount));
        systemAddress.call{accounts: metas}(bincode);
    }

    /// 从 nonce 账户提取资金
    ///
    /// @param nonce_pubkey nonce 账户的公钥
    /// @param authorized_pubkey 被授权在账户上执行指令的实体的公钥
    /// @param to_pubkey 收件账户
    /// @param lamports 要提取的 lamports 数量
    function withdraw_nonce_account(address nonce_pubkey, address authorized_pubkey, address to_pubkey, uint64 lamports) internal {
        AccountMeta[5] metas = [\
            AccountMeta({pubkey: nonce_pubkey, is_signer: false, is_writable: true}),\
            AccountMeta({pubkey: to_pubkey, is_signer: false, is_writable: true}),\
            AccountMeta({pubkey: recentBlockHashes, is_signer: false, is_writable: false}),\
            AccountMeta({pubkey: rentAddress, is_signer: false, is_writable: false}),\
            AccountMeta({pubkey: authorized_pubkey, is_signer: true, is_writable: false})\
        ];

        bytes bincode = abi.encode(uint32(Instruction.WithdrawNonceAccount), lamports);
        systemAddress.call{accounts: metas}(bincode);
    }

    /// 更改被授权在账户上执行 nonce 指令的实体
    ///
    /// @param nonce_pubkey nonce 账户的公钥
    /// @param authorized_pubkey 被授权在账户上执行指令的实体的公钥
    /// @param new_authority
    function authorize_nonce_account(address nonce_pubkey, address authorized_pubkey, address new_authority) internal {
        AccountMeta[2] metas = [\
            AccountMeta({pubkey: nonce_pubkey, is_signer: false, is_writable: true}),\
            AccountMeta({pubkey: authorized_pubkey, is_signer: true, is_writable: false})\
        ];

        bytes bincode = abi.encode(uint32(Instruction.AuthorizeNonceAccount), new_authority);
        systemAddress.call{accounts: metas}(bincode);
    }

    /// 一次性幂等升级遗留 nonce 版本,以将它们从链域中移除。
    ///
    /// @param nonce nonce 账户的公钥
    // 此功能在 Solana v1.9.15 中不可用
    function upgrade_nonce_account(address nonce) internal {
        AccountMeta[1] meta = [\
            AccountMeta({pubkey: nonce, is_signer: false, is_writable: true})\
        ];

        bytes bincode = abi.encode(uint32(Instruction.UpgradeNonceAccount));
        systemAddress.call{accounts: meta}(bincode);
    }
}

步骤 2:创建 SPL Token 文件

由于 solidity 文件夹在项目初始化时自动创建,并且包含一个文件 my-spl-token.sol,因此在此步骤中你无需创建任何文件夹或文件。

SPL Token Minter 合约充当 Solidity 智能合约代码和 Solana 上的 SPL Token程序之间的桥梁。合约包含创建新Token铸币的功能,指定冻结权限、小数、名称、符号和用于元数据的 URI。除此之外,它还提供了向所创建铸币内指定的Token账户铸造指定数量的Token的功能。

打开 my-spl-token.sol 文件在 solidity 文件夹中,并根据以下代码进行修改。

如果在初始化项目时选择了不同的项目名称,则文件名可能会有所不同。

solidity/my-spl-token.sol

// 导入处理 SPL Token和元数据所需的库。
import "../libraries/spl_token.sol";
import "../libraries/mpl_metadata.sol";

// 定义程序合约,并在 Solana 区块链上指定程序 ID。
@program_id("F1ipperKF9EfD821ZbbYjS319LXYiBmjhzkkf5a26rC")
contract spl_token_minter {
    @payer(payer) // "数据账户"的付款人
    constructor() {}

    // 创建新Token铸币及相关元数据的函数。
    @mutableSigner(payer) // 支付者账户
    @mutableSigner(mint) // 要创建的铸币账户
    @mutableAccount(metadata) // 要创建的元数据账户
    @signer(mintAuthority) // 铸币账户的铸币权限
    @account(rentAddress)
    @account(metadataProgramId)
    function createTokenMint(
        address freezeAuthority, // 铸币账户的冻结权限
        uint8 decimals, // 铸币账户的小数
        string name, // 元数据账户的名称
        string symbol, // 元数据账户的符号
        string uri // 元数据账户的 URI
    ) external {
        // 调用系统程序创建铸币账户的新账户,并且
        // 调用Token程序对铸币账户进行初始化
        // 设置铸币权限、冻结权限和小数用于铸币账户
        SplToken.create_mint(
            tx.accounts.payer.key,            // 付款人账户
            tx.accounts.mint.key,            // 铸币账户
            tx.accounts.mintAuthority.key,   // 铸币权限
            freezeAuthority, // 冻结权限
            decimals         // 小数
        );

        // 调用元数据程序创建新的元数据账户
        MplMetadata.create_metadata_account(
            tx.accounts.metadata.key, // 元数据账户
            tx.accounts.mint.key,  // 铸币账户
            tx.accounts.mintAuthority.key, // 铸币权限
            tx.accounts.payer.key, // 付款人
            tx.accounts.payer.key, // 元数据账户的更新权限
            name, // 名称
            symbol, // 符号
            uri, // URI(链外元数据 JSON)
            tx.accounts.rentAddress.key,
            tx.accounts.metadataProgramId.key
        );
    }

    // 向指定的Token账户铸造Token的函数。
    @mutableAccount(mint)
    @mutableAccount(tokenAccount)
    @mutableSigner(mintAuthority)
    function mintTo(uint64 amount) external {
        // 向Token账户铸造Token
        SplToken.mint_to(
            tx.accounts.mint.key, // 铸币账户
            tx.accounts.tokenAccount.key, // Token账户
            tx.accounts.mintAuthority.key, // 铸币权限
            amount // 数量
        );
    }
}

步骤 3:构建你的程序

由于所有合约相关文件都已准备好,请继续并确保通过运行以下命令进行编译:

anchor build

你应该会收到有关你的 LLVM IR 和锚定元数据文件已生成的通知。现在,让我们编写一些测试,以确保我们的程序按预期工作。

测试和部署

在这一部分,我们将创建一个测试文件,然后将 SPL Token 程序部署到 devnet。但是,所有过程同样可以应用于 mainnet

打开 my-spl-token.ts 文件在 tests 目录中。

该测试文件测试 spl_token_minter Solidity 智能合约的功能,该合约旨在创建 SPL Token并向你在 Solana 上的钱包铸造一些 SPL Token。

根据下面的代码修改该文件。

注意

在突出行中更新Token标题、Token符号和Token URI,以使用你自己Token的信息。你应该将 IPFS_URL_OF_JSON_FILE 替换为你在 Upload Token Icon and Metadata to IPFS 部分获取的个 JSON 文件的 IPFS URL。

在此文件中,铸币数量被指定为 100 个我的精彩代币。如果要更改,请修改下面代码的第 99 行。

点击查看代码说明

  1. 配置客户端:
  • 测试文件配置客户端以使用 Anchor.toml 中指定的 Solana 集群。
  • 它使用 anchor.AnchorProvider.env() 方法设置提供者。
  1. 生成密钥对:
  • 为数据账户 ( dataAccount) 和铸币 ( mintKeypair) 生成密钥对。
  • 从提供者检索钱包和连接。
  1. 初始化程序数据账户:
  • it("Is initialized!") 测试初始化 Solang 所需的数据账户。
  • 它调用程序的新方法以初始化数据账户。
  1. 创建 SPL Token:
  • it("Create an SPL Token!") 测试通过调用 createTokenMint 方法来创建 SPL Token。
  • 它提供诸如冻结权限、小数、Token名称、符号和 URI 等必要参数。
  • 此外,它使用 Metaplex 库检索元数据地址。
  • 指定测试账户和签名者,然后执行交易。
  1. 铸造Token到钱包:
  • it("Mint some tokens to your wallet!") 测试铸造Token到用户的钱包。
  • 它首先确保钱包的相关代币账户存在。
  • 然后调用 mintTo 方法来铸造指定数量的Token到钱包的相关代币账户。
  • 指定必要的账户并执行交易。
  1. 记录交易签名:
  • 在测试过程中,交易签名被记录到控制台以供参考。

你可能希望深入了解 Anchor 如何将这些函数映射到 SplTokenMinter 的 IDL(接口描述语言)文件中的方法。IDL 文件位于 ./target/types 目录中,提供智能合约方法和类型的结构化表示,是一种有价值的参考。

tests/my-spl-token.ts

import * as anchor from '@coral-xyz/anchor'
import { Program } from '@coral-xyz/anchor'
import { SplTokenMinter } from '../target/types/spl_token_minter'
import { PublicKey, SystemProgram, SYSVAR_RENT_PUBKEY } from '@solana/web3.js'

import {
    ASSOCIATED_TOKEN_PROGRAM_ID,
    getOrCreateAssociatedTokenAccount,
    TOKEN_PROGRAM_ID,
} from '@solana/spl-token'

describe('spl-token-minter', () => {
    // 配置客户端以使用本地集群。
    const provider = anchor.AnchorProvider.env()
    anchor.setProvider(provider)

    // Metaplex 常量
    const METADATA_SEED = 'metadata'
    const TOKEN_METADATA_PROGRAM_ID = new PublicKey(
        'metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s'
    )

    // 为程序生成新的数据账户密钥对
    const dataAccount = anchor.web3.Keypair.generate()
    // 生成一个铸币密钥对
    const mintKeypair = anchor.web3.Keypair.generate()
    const wallet = provider.wallet as anchor.Wallet
    const connection = provider.connection

    console.log('你的钱包地址', wallet.publicKey.toString())

    const program = anchor.workspace.SplTokenMinter as Program<SplTokenMinter>

    // Token元数据
    const tokenTitle = '我的精彩代币'
    const tokenSymbol = 'MAT'
    const tokenUri = 'IPFS_URL_OF_JSON_FILE'

    const tokenDecimals = 9

    const mint = mintKeypair.publicKey

    it('已初始化!', async () => {
        // 初始化 Solang 所需的程序的数据账户
        const tx = await program.methods
            .new()
            .accounts({ dataAccount: dataAccount.publicKey })
            .signers([dataAccount])
            .rpc()
        console.log('你的交易签名', tx)
    })

    it('创建 SPL Token!', async () => {
        const [metadataAddress] = PublicKey.findProgramAddressSync(
            [\
                Buffer.from(METADATA_SEED),\
                TOKEN_METADATA_PROGRAM_ID.toBuffer(),\
                mint.toBuffer(),\
            ],
            TOKEN_METADATA_PROGRAM_ID
        )

        // 创建Token铸币
        const tx = await program.methods
            .createTokenMint(
                wallet.publicKey, // 冻结权限
                tokenDecimals, // 小数
                tokenTitle, // Token名称
                tokenSymbol, // Token符号
                tokenUri // Token URI
            )
            .accounts({
                payer: wallet.publicKey,
                mint: mintKeypair.publicKey,
                metadata: metadataAddress,
                mintAuthority: wallet.publicKey,
                rentAddress: SYSVAR_RENT_PUBKEY,
                metadataProgramId: TOKEN_METADATA_PROGRAM_ID,
            })
            .signers([mintKeypair]) // 使用密钥对签署交易,你实际上证明了你有权将账户分配给Token程序
            .rpc({ skipPreflight: true })
        console.log('你的交易签名', tx)
    })

    it('铸造一些Token到你的钱包!', async () => {
        // 钱包的关联Token账户地址
        // 要了解更多有关Token账户的信息,请查看本指南 https://www.quicknode.com/guides/solana-development/spl-tokens/how-to-look-up-the-address-of-a-token-account#spl-token-accounts
        const tokenAccount = await getOrCreateAssociatedTokenAccount(
            connection,
            wallet.payer, // 付款人
            mintKeypair.publicKey, // 铸币
            wallet.publicKey // 所有者
        )
        const numTokensToMint = new anchor.BN(100)
        const decimalTokens = numTokensToMint.mul(
            new anchor.BN(10).pow(new anchor.BN(tokenDecimals))
        )

        const tx = await program.methods
            .mintTo(
                new anchor.BN(decimalTokens) // 以 Lamports 单位铸造的数量
            )
            .accounts({
                mintAuthority: wallet.publicKey,
                tokenAccount: tokenAccount.address,
                mint: mintKeypair.publicKey,
            })
            .rpc({ skipPreflight: true })
        console.log('你的交易签名', tx)
    })
})

如前所述,智能合约在 Solana 中称为“程序”,并使用 @program_id 注解来指定程序的链上地址。现在,我们需要更新智能合约中的 @program_id

  1. 通过运行以下命令获取 program_id
anchor keys sync
anchor keys list

尽管项目名称为 my-spl-token,但智能合约名称为 spl_token_minter,如 solidity/my-spl-token.sol 文件所述。

  1. 从终端复制地址,打开智能合约文件 solidity/my-spl-token.sol,并使用你的程序 ID 更新 program_id 行。

solidity/my-spl-token.sol

@program_id("YOUR_PROGRAM_ID") // 链上程序地址
  1. 使用第一步中获取的程序 ID 更新 Anchor.toml 文件中的 program_id 。此外,由于我们将智能合约部署到 devnet,还需将 " [programs.localnet]" 更改为 " [programs.devnet]"。

Anchor.toml

[toolchain]

[features]
seeds = false
skip-lint = false

[programs.devnet]
my_spl_token = "YOUR_PROGRAM_ID"

[registry]
url = "https://api.apr.dev"

[provider]
cluster = "devnet"
wallet = "./id.json"

[scripts]
test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"
  1. 虽然也可以使用公共节点,但我们强烈建议使用自定义端点。如果你已创建 QuickNode 帐户并获取了 HTTP 提供程序链接,请在 Set Up Your QuickNode Endpoint 部分更新此部分。

Anchor.toml

[toolchain]

[features]
seeds = false
skip-lint = false

[programs.devnet]
my_spl_token = "YOUR_PROGRAM_ID"

[registry]
url = "https://api.apr.dev"

[provider]
cluster = "YOUR_DEVNET_HTTP_PROVIDER_LINK"
wallet = "./id.json"

[scripts]
test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"

一切准备就绪,待测试和部署!

你可以通过运行以下命令来测试和部署你的智能合约。由于我们已为 devnet 配置了 Anchor.toml 文件,因此测试文件将在 devnet 上运行。

anchor build
anchor test

测试文件包括创建 SPL Token,然后为你的钱包铸造一些Token。因此,在进行测试时,SPL Token被创建并铸造也是如此。

如果一切顺利,你应该看到类似的终端输出。

  spl-token-minter
你的交易签名 4G8cnr7q8GpdurPok4JjJX6ZCej5ahdxCuyj2gvxzKq83N8cQczWSCabi4QGBxMVBhNHBb9r3hoTQET5RRMVT6mG
    ✔ 已初始化! (1157ms)
你的交易签名 65GKf6M6CiaVMa3CDebFSxvqLYtDMrfMb1QzkztoEzV7c5txuc1wWa3SpWGWXgSvpr1xCK7wStEb28H32xLH8ENx
    ✔ 创建 SPL Token! (344ms)
你的交易签名 3tYMd7vVS9m3MLnJjx9JFGqMQtmvVYrYWhazLCHGuqC3jwrUiwFvAyppYxuPT81Q4yTiwYuDk5Ja78oTSBGC32oV
    ✔ 铸造一些Token到你的钱包! (4376ms)

  3 通过 (6s)

✨  在 7.58s 内完成。

让我们在 Solana Explorer 上检查 SPL Token。

  1. Solana Devnet Explorer

  2. 搜索你钱包的公共地址。

  3. 点击 代币 标签查看代币持有情况。

  4. 点击你的代币并检查代币信息。

SPL Token Details on Explorer

自定义脚本(可选)

其实,你无需运行测试文件即可执行一些操作如铸造Token、发送一些Token等。让我们深入了解 Anchor 中的自定义脚本。

此脚本类似于测试文件,部署新Token并向你的账户铸造一些Token。因此,这是可选的。

创建脚本文件

创建 scripts 文件夹,并在其中创建一个 mint.ts 文件。

mkdir scripts
echo > scripts/mint.ts

scripts 目录中打开 mint.ts 文件。

根据以下代码修改文件。

注意

高亮的行中更新Token标题、Token符号和Token URI,以使用你自己Token的信息,就像你在前面的步骤中所做的那样。

scripts/mint.ts

import * as anchor from '@coral-xyz/anchor'
import { Program } from '@coral-xyz/anchor'
import { SplTokenMinter } from '../target/types/spl_token_minter'
import { PublicKey, SYSVAR_RENT_PUBKEY } from '@solana/web3.js'
import {
    ASSOCIATED_TOKEN_PROGRAM_ID,
    getOrCreateAssociatedTokenAccount,
    TOKEN_PROGRAM_ID,
} from '@solana/spl-token'

// 配置客户端以使用本地集群。
const provider = anchor.AnchorProvider.env()
anchor.setProvider(provider)
// Metaplex 常量
const METADATA_SEED = 'metadata'
const TOKEN_METADATA_PROGRAM_ID = new PublicKey(
    'metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s'
)

// 为程序生成新的数据账户密钥对
const dataAccount = anchor.web3.Keypair.generate()
// 生成一个铸币密钥对
const mintKeypair = anchor.web3.Keypair.generate()
const wallet = provider.wallet as anchor.Wallet
const connection = provider.connection
console.log('你的钱包地址', wallet.publicKey.toString())
const program = anchor.workspace.SplTokenMinter as Program<SplTokenMinter>

// Token的元数据
const tokenTitle = '我的精彩代币'
const tokenSymbol = 'MAT'
const tokenUri = 'IPFS_URL_OF_JSON_FILE'

const tokenDecimals = 9

const mint = mintKeypair.publicKey
async function deploy() {
    // 初始化程序的数据账户
    const initTx = await program.methods
        .new()
        .accounts({ dataAccount: dataAccount.publicKey })
        .signers([dataAccount])
        .rpc()
    console.log('初始化交易签名', initTx)

    const [metadataAddress] = PublicKey.findProgramAddressSync(
        [\
            Buffer.from(METADATA_SEED),\
            TOKEN_METADATA_PROGRAM_ID.toBuffer(),\
            mint.toBuffer(),\
        ],
        TOKEN_METADATA_PROGRAM_ID
    )
``````javascript
// 创建代币铸造
    const createTokenMintTx = await program.methods
        .createTokenMint(
            wallet.publicKey, // 冻结权限
            tokenDecimals, // 小数位
            tokenTitle, // 代币名称
            tokenSymbol, // 代币符号
            tokenUri // 代币 URI
        )
        .accounts({
            payer: wallet.publicKey,
            mint: mintKeypair.publicKey,
            metadata: metadataAddress,
            mintAuthority: wallet.publicKey,
            rentAddress: SYSVAR_RENT_PUBKEY,
            metadataProgramId: TOKEN_METADATA_PROGRAM_ID,
        })
        .signers([mintKeypair]) // 使用密钥对签名交易,实际上证明你有权限将账户分配给代币程序
        .rpc({ skipPreflight: true })
    console.log('创建代币铸造交易签名', createTokenMintTx)

    // 钱包的关联代币账户地址
    // 要了解更多关于代币账户的信息,请查看这个指南。 https://www.quicknode.com/guides/solana-development/spl-tokens/how-to-look-up-the-address-of-a-token-account#spl-token-accounts
    const tokenAccount = await getOrCreateAssociatedTokenAccount(
        connection,
        wallet.payer, // 支付者
        mintKeypair.publicKey, // 代币铸造
        wallet.publicKey // 拥有者
    )
    const numTokensToMint = new anchor.BN(100)
    const decimalTokens = numTokensToMint.mul(
        new anchor.BN(10).pow(new anchor.BN(tokenDecimals))
    )
    const mintTx = await program.methods
        .mintTo(
            new anchor.BN(decimalTokens) // 以 Lamports 单位铸造的数量
        )
        .accounts({
            mintAuthority: wallet.publicKey,
            tokenAccount: tokenAccount.address,
            mint: mintKeypair.publicKey,
        })
        .rpc({ skipPreflight: true })
    console.log('铸造代币交易签名', mintTx)
}

// 运行部署脚本
deploy().catch(err => console.error(err))

添加命令

打开 Anchor.toml 文件,并修改 [scripts] 部分,如下所示。我们基本上添加一个 mint 命令以运行 scripts/mint.ts 文件。

Anchor.toml

[scripts]
test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"
mint = "yarn ts-node scripts/mint.ts"

运行脚本

运行脚本。

anchor run mint

输出应类似于以下内容。

初始化交易签名 Xq1uwvDsBPY1TQh421r6ryBi82CqsKaHfzojXuaRgppKFFW5vvk98BD6HrcagrkyU8szJYim3Ldm9fRcM7R1F2F
创建代币铸造交易签名 2JkBZQPT6WRSEx63iLej1dC4xJFiYvNLcFNPWaN2eJKaHoEmMqWcEDw3RpPDFbufim66U1cVRYCc1zSM6b6Mhixk
铸造代币交易签名 4XBdYRpUiUvD2UEVFUdkBMyhB6pLFwkwbuME7FoeFKnFFjwJXTnKqzMJe6takM6aZsjL5AiCTTgqCKWT4SWyL18X
✨  完成于 8.65秒。

结论

大功告成!你成功创建了一个使用 Solidity 的 Solana 程序,并使用新的 Metaplex 可替代代币标准在 Solana 上铸造了自己的代币。

如果你有任何问题或需要进一步的帮助,请随时加入我们的 Discord 服务器或使用下面的表单提供反馈。通过关注我们的 Twitter (@QuickNode) 和我们的 Telegram 公告频道,保持最新。

我们 ❤️ 反馈!

告诉我们 如果你有任何反馈或新的主题请求。我们很乐意听到你的声音。

其他资源

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

0 条评论

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