本文详细介绍了如何使用Anchor在Solana上创建和铸造可互换的SPL代币,包括项目初始化、代币创建、铸造代币以及测试的步骤。
你是否曾考虑过在用户完成任务时奖励他们一个代币?智能合约允许你创建管理代币分发和使用规则的规范。本指南将教你如何使用 Anchor 在 Solana 上创建和铸造可替代的 SPL 代币。我们将涵盖程序和测试所需的代码,以确保代币在账户之间无缝转移。
依赖 | 版本 |
---|---|
anchor-lang | 0.29.0 |
anchor-spl | 0.29.0 |
solana-program | 1.16.24 |
spl-token | 4.0.0 |
通过访问 https://beta.solpg.io/ 在 Solana Playground 中创建一个新项目。Solana Playground 是一个基于浏览器的 Solana 代码编辑器,将使我们能够快速启动此项目。欢迎在自己的代码编辑器中进行操作,但本指南将针对 Solana Playground 的步骤进行调整。首先,点击“创建新项目”:
输入项目名称“token-minter”,并选择“Anchor (Rust)”:
要在 Solana 上进行构建,你需要一个 API 端点以连接到网络。你可以使用公共节点或部署和管理自己的基础设施;不过,如果你希望获得更快的响应时间,采用我们的服务会省事很多。
你现在可以 使用 USDC 在 Solana 上支付 QuickNode 计划。作为首个接受 Solana 付款的多链服务提供商,我们正在简化开发者的流程——无论你是创建新账户还是管理现有账户。在这里了解有关使用 Solana 付款的更多信息。
看看为什么超过 50% 的 Solana 项目选择 QuickNode,并在 这里 注册一个免费账户。我们将使用一个 Solana Devnet 端点。
拷贝 HTTP Provider 链接:
现在你有了一个端点,返回到 Solana Playground,点击浏览器窗口左下角的设置齿轮 (⚙️) 按钮。你将看到一个“端点”的下拉菜单。打开下拉菜单并选择“自定义”:
将你的 QuickNode 端点粘贴到文本框中并点击“添加”:
由于这个项目仅用于演示目的,我们可以使用一个“临时”钱包。Solana Playground 使创建一个钱包变得容易。你应该在浏览器窗口左下角看到一个红点“未连接”。点击它:
Solana Playground 将为你生成一个钱包(或者你也可以导入自己的)。可以随意保存以备后用,在准备好时点击继续。一个新钱包将被初始化并连接到 Solana devnet。Solana Playground 会自动向你的新钱包空投一些 SOL,但我们将请求额外一些以确保我们有足够的资金来部署我们的程序。在浏览器终端,你可以使用 Solana CLI 命令。输入 solana airdrop 1
向你的钱包空投 1 SOL。注意:由于 SOL 空投的限制,可能需要在不同的时间多次运行此命令。你也可以从 QuickNode Faucet 领取额外的 SOL。
你的钱包现在应该连接到 devnet,余额大约为 8 SOL(你可能需要从另一个地址发送一些额外的 devnet SOL,以确保你有足够的 SOL 部署到 devnet):
你准备好了!让我们开始构建吧!
让我们开始打开 lib.rs
并删除初始代码。一旦你有了一个空白界面,我们就可以开始构建我们的程序。首先,让我们导入一些依赖并构建我们的程序框架。将以下内容添加到文件顶部:
// 1. 导入依赖
use anchor_lang::prelude::*;
use anchor_spl::{
associated_token::AssociatedToken,
token::{mint_to, Mint, MintTo, Token, TokenAccount},
metadata::{
create_metadata_accounts_v3,
mpl_token_metadata::types::DataV2,
CreateMetadataAccountsV3,
Metadata as Metaplex,
},
};
// 2. 声明程序 ID(SolPG 将在你部署时自动更新)
declare_id!("11111111111111111111111111111111");
// 3. 定义程序和指令
#[program]
mod token_minter {
use super::*;
pub fn init_token(ctx: Context<InitToken>, metadata: InitTokenParams) -> Result<()> {
// TODO 添加初始化铸币逻辑
Ok(())
}
pub fn mint_tokens(ctx: Context<MintTokens>, quantity: u64) -> Result<()> {
// TODO 添加铸造代币逻辑
Ok(())
}
}
// 4. 定义每个指令的上下文
#[derive(Accounts)]
#[instruction(params: InitTokenParams)]
pub struct InitToken<'info> {
//TODO: 添加初始化账户上下文
}
#[derive(Accounts)]
pub struct MintTokens<'info> {
//TODO: 添加铸造代币的账户上下文
}
// 5. 定义初始化代币的参数
#[derive(AnchorSerialize, AnchorDeserialize, Debug, Clone)]
pub struct InitTokenParams {
pub name: String,
pub symbol: String,
pub uri: String,
pub decimals: u8,
}
让我们解析一下这里发生的事情:
anchor-spl
)。init_token
和 mint_tokens
。init_token
指令将使用元数据初始化一个新的 SPL 代币,而 mint_tokens
指令将向指定账户铸造代币。init_token
指令的参数创建一个结构体。这个结构体包含在 Solana 上创建代币所需的元数据(代币的名称、符号、URI 和小数)。我们使用 AnchorSerialize
和 AnchorDeserialize
属性允许这个结构体被 Anchor 序列化和反序列化。在我们的 init_token
指令中,你会注意到我们将这个结构体作为参数传递。现在我们有了程序的轮廓,我们可以开始填充细节。让我们先从 init_token
指令开始。我们的指令需要一些账户才能执行:
让我们将这些添加到 InitToken
结构体中:
#[derive(Accounts)]
#[instruction(\
params: InitTokenParams\
)]
pub struct InitToken<'info> {
/// CHECK: 正在创建的新 Metaplex 账户
#[account(mut)]
pub metadata: UncheckedAccount<'info>,
#[account(\
init,\
seeds = [b"mint"],\
bump,\
payer = payer,\
mint::decimals = params.decimals,\
mint::authority = mint,\
)]
pub mint: Account<'info, Mint>,
#[account(mut)]
pub payer: Signer<'info>,
pub rent: Sysvar<'info, Rent>,
pub system_program: Program<'info, System>,
pub token_program: Program<'info, Token>,
pub token_metadata_program: Program<'info, Metaplex>,
}
这里有几点需要注意:
metadata
账户。我们将使用 UncheckedAccount 类型以表示另一个程序将创建此账户。mint
账户将由我们的程序创建,使用 init
。我们将通过 "mint" 这个词为账户提供种子,这意味着每个程序只能创建一个铸币账户(可以根据你的需求使用不同的种子)。我们还将指定支付者、铸币的权限(我们的程序)和铸币的小数(来自我们的指令参数)。你会注意到我们将权限设置为 mint
账户本身。这实际上是将权限授予我们的程序,而不必创建另一个 PDA。现在,让我们在指令中使用这些账户。将你的 init_token
指令更新为:
pub fn init_token(ctx: Context<InitToken>, metadata: InitTokenParams) -> Result<()> {
let seeds = &["mint".as_bytes(), &[ctx.bumps.mint]];
let signer = [&seeds[..]];
let token_data: DataV2 = DataV2 {
name: metadata.name,
symbol: metadata.symbol,
uri: metadata.uri,
seller_fee_basis_points: 0,
creators: None,
collection: None,
uses: None,
};
let metadata_ctx = CpiContext::new_with_signer(
ctx.accounts.token_metadata_program.to_account_info(),
CreateMetadataAccountsV3 {
payer: ctx.accounts.payer.to_account_info(),
update_authority: ctx.accounts.mint.to_account_info(),
mint: ctx.accounts.mint.to_account_info(),
metadata: ctx.accounts.metadata.to_account_info(),
mint_authority: ctx.accounts.mint.to_account_info(),
system_program: ctx.accounts.system_program.to_account_info(),
rent: ctx.accounts.rent.to_account_info(),
},
&signer
);
create_metadata_accounts_v3(
metadata_ctx,
token_data,
false,
true,
None,
)?;
msg!("代币铸造成功。");
Ok(())
}
我们的指令在做以下事情:
InitToken
结构体中定义的 seeds
,并通过 ctx.bumps.mint
获取我们的 bump 来创建签名者。token_data
定义为 DataV2 对象。create_metadata_accounts_v3
指令。这个指令将为我们的代币创建元数据账户。我们将必要的数据和我们的 CPI 上下文作为参数传递。
_注意:由于我们在 mint
账户中使用了 init
,我们不需要从 SPL 代币程序中调用 create_mint
指令。这将在后台由 Anchor 自动处理。_要铸造代币,我们需要一组类似但略有不同的账户:
init_token
指令中创建的同一铸币账户匹配)将以下内容添加到你的 MintTokens
结构体:
#[derive(Accounts)]
pub struct MintTokens<'info> {
#[account(\
mut,\
seeds = [b"mint"],\
bump,\
mint::authority = mint,\
)]
pub mint: Account<'info, Mint>,
#[account(\
init_if_needed,\
payer = payer,\
associated_token::mint = mint,\
associated_token::authority = payer,\
)]
pub destination: Account<'info, TokenAccount>,
#[account(mut)]
pub payer: Signer<'info>,
pub rent: Sysvar<'info, Rent>,
pub system_program: Program<'info, System>,
pub token_program: Program<'info, Token>,
pub associated_token_program: Program<'info, AssociatedToken>,
}
有几点重要的地方需要注意:
mint
设置为 mut
以指示我们将修改账户(在这种情况下是铸造新代币/增加供应)。destination
账户使用 init_if_needed
属性。这将在账户不存在时创建该账户。我们确保代币的铸金和权限分别设置为 mint
账户和 payer
账户。
注意:我们在这个指令中不需要元数据账户或代币元数据程序,因为我们不创建或使用代币的元数据。最后,你将需要定义 mint_tokens
指令。添加以下代码:
pub fn mint_tokens(ctx: Context<MintTokens>, quantity: u64) -> Result<()> {
let seeds = &["mint".as_bytes(), &[ctx.bumps.mint]];
let signer = [&seeds[..]];
mint_to(
CpiContext::new_with_signer(
ctx.accounts.token_program.to_account_info(),
MintTo {
authority: ctx.accounts.mint.to_account_info(),
to: ctx.accounts.destination.to_account_info(),
mint: ctx.accounts.mint.to_account_info(),
},
&signer,
),
quantity,
)?;
Ok(())
}
在这里,我们使用来自上一步的相同签名者 PDA,并将其传递给 SPL 代币程序中的 mint_to
指令。这个指令将指定数量的代币铸造到目标账户。如果你还记得我们之前的 SPL 代币工作,这个指令期望 quantity
是扩展的小数值。例如,如果代币有两个小数,而我们想要铸造 100 个代币,我们将传递 10,000 作为 quantity
值。
继续构建你的程序,以确保一切正常,通过点击“构建”或在 Solana Playground 终端中输入 anchor build
。
如果程序成功构建,你可以将其部署到 Solana devnet。如果有任何问题,仔细检查你在上面的指南中的代码或查看我们提供的工作示例。有问题或需要帮助吗?随时在 Discord 上与我们联系。
点击页面左侧的工具图标 🛠,然后点击“部署”:
这可能需要一两分钟,但完成后,你应该在浏览器终端看到类似这样的信息:
干得好!让我们测试一下。
返回到你修改 lib.rs
文件的主窗口,点击页面左上角的 📑 图标。打开 anchor.test.ts
并用以下内容替换:
describe("测试铸造器", () => {
// Metaplex 常量
const METADATA_SEED = "metadata";
const TOKEN_METADATA_PROGRAM_ID = new web3.PublicKey("metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s");
// 我们程序的常量
const MINT_SEED = "mint";
// 我们测试的数据
const payer = pg.wallet.publicKey;
const metadata = {
name: "一个测试代币",
symbol: "TEST",
uri: "https://5vfxc4tr6xoy23qefqbj4qx2adzkzapneebanhcalf7myvn5gzja.arweave.net/7UtxcnH13Y1uBCwCnkL6APKsge0hAgacQFl-zFW9NlI",
decimals: 9,
};
const mintAmount = 10;
const [mint] = web3.PublicKey.findProgramAddressSync(
[Buffer.from(MINT_SEED)],
pg.PROGRAM_ID
);
const [metadataAddress] = web3.PublicKey.findProgramAddressSync(
[\
Buffer.from(METADATA_SEED),\
TOKEN_METADATA_PROGRAM_ID.toBuffer(),\
mint.toBuffer(),\
],
TOKEN_METADATA_PROGRAM_ID
);
// 测试初始化代币
it("初始化", async () => {
});
// 测试铸造代币
it("铸造代币", async () => {
});
});
简化调试的日志
你现在可以访问你的 RPC 端点的日志,帮助你更有效地排查问题。如果你遇到 RPC 调用问题,只需在你的 QuickNode 控制面板中查看日志以快速识别和解决问题。了解更多关于日志历史限制的内容,查看我们的定价页面。
在这里,我们创建了测试套件并定义了一些我们将在测试中使用的常量。我们将使用几个种子进行 PDA 挂址推导,Metaplex 代币元数据程序 ID,支付者的公钥,代币的元数据(可以自由使用你自己的),以及我们想要铸造的令牌数量。我们还将定义一个辅助函数来确认交易。
最后,我们推导出两个我们将在测试中使用的 PDA:
mint
PDA。我们使用 MINT_SEED
和程序 ID 在 findProgramAddressSync
方法中推导出它。metadataAddress
。我们以相同的方式推导它,但这次我们将 METADATA_SEED
、Metaplex 代币元数据程序 ID 和 mint
PDA 传递给该方法。(来源:Metaplex 文档)让我们编写一个初始化测试以创建一个铸币账户(如果尚不存在)。在 it("initialize"...)
块中添加以下测试:
it("初始化", async () => {
const info = await pg.connection.getAccountInfo(mint);
if (info) {
return; // 如果已初始化,则不尝试再次初始化
}
console.log(" 找不到铸币账户。尝试进行初始化。");
const context = {
metadata: metadataAddress,
mint,
payer,
rent: web3.SYSVAR_RENT_PUBKEY,
systemProgram: web3.SystemProgram.programId,
tokenProgram: anchor.utils.token.TOKEN_PROGRAM_ID,
tokenMetadataProgram: TOKEN_METADATA_PROGRAM_ID,
};
const tx = await pg.program.methods
.initToken(metadata)
.accounts(context)
.transaction();
const txHash = await web3.sendAndConfirmTransaction(pg.connection, tx, [pg.wallet.keypair], {skipPreflight: true});
console.log(` https://explorer.solana.com/tx/${txHash}?cluster=devnet`);
const newInfo = await pg.connection.getAccountInfo(mint);
assert(newInfo, " 应该初始化铸币账户。");
});
在这个 initialize
测试中发生了以下事情:
init_token
(在 TypeScript 中为 initToken
)指令,传递元数据和上下文作为参数。rpc()
方法将我们的交易发送到集群,并返回交易哈希。让我们编写第二个测试以确保 mint_tokens
指令按预期工作。将你的 it("mint tokens"...)
测试块替换为以下代码:
it("铸造代币", async () => {
const destination = await anchor.utils.token.associatedAddress({
mint: mint,
owner: payer,
});
let initialBalance: number;
try {
const balance = (await pg.connection.getTokenAccountBalance(destination))
initialBalance = balance.value.uiAmount;
} catch {
// 代币账户尚未初始化,余额为0
initialBalance = 0;
}
const context = {
mint,
destination,
payer,
rent: web3.SYSVAR_RENT_PUBKEY,
systemProgram: web3.SystemProgram.programId,
tokenProgram: anchor.utils.token.TOKEN_PROGRAM_ID,
associatedTokenProgram: anchor.utils.token.ASSOCIATED_PROGRAM_ID,
};
const tx = await pg.program.methods
.mintTokens(new BN(mintAmount * 10 ** metadata.decimals))
.accounts(context)
.transction();
const txHash = await web3.sendAndConfirmTransaction(pg.connection, tx, [pg.wallet.keypair], {skipPreflight: true});
console.log(` https://explorer.solana.com/tx/${txHash}?cluster=devnet`);
const postBalance = (
await pg.connection.getTokenAccountBalance(destination)
).value.uiAmount;
assert.equal(
initialBalance + mintAmount,
postBalance,
"最终余额应等于初始余额加铸造数量"
);
});
在这个 mint tokens
测试中发生的事情:
mint
和 payer
公钥传递给 associatedAddress
方法推导出支付者的目标代币账户。mint_tokens
指令(在 TypeScript 中为 mintTokens
),将 mintAmount
作为参数传递。rpc()
方法将我们的交易发送到集群,并返回交易哈希。完成测试后,你可以通过点击“🧪 测试”按钮或在 Solana Playground 终端中输入 anchor test
来运行它们。你应该会看到类似如下的信息:
正在运行测试...
anchor.test.ts:
测试铸造器
找不到铸币账户。尝试进行初始化。
https://explorer.solana.com/tx/4YEzstg3UxWBqfFBcTwjWvg4bdkXw47zLCieBGcr1WNPoZDNZrQxD5H6YL6fSxix3SumYGTYBPFY2vnYxYRAG8J6?cluster=devnet
✔ 初始化 (797ms)
https://explorer.solana.com/tx/2Ws647Z4q5Lsdcm6zSqHqTRpqQZY8nuQaS5Z7fqbw925jjXoztqqydQZsyMA21yF3PecLzVLt936NMo1qfXHoQe1?cluster=devnet
✔ 铸造代币 (722ms)
2 通过 (2s)
干得好!你现在有一个可以创建和铸造代币的工作程序。
你可以重新运行测试函数以铸造额外的代币——由于我们添加了检查铸币账户是否已存在的机制,它将不会尝试再次初始化。或者,你可以点击左侧的“🧪” (测试) 图标,使用 Solana Playground 的 UI 测试铸造指令。
该 UI 可以推导出测试程序所需的所有 PDA 和账户地址。确保在铸造代币时考虑小数(根据代币的小数位数在数量末尾添加额外的零)。
你现在有了一个可以创建和铸造代币的工作程序——这可以应用于任何类型的 Solana 项目:例如,DeFi 用户的奖励、NFT 或游戏物品。继续构建!
如果你遇到了困难,或者有问题,或者只想讨论你正在构建的内容,请在 Discord 或 Twitter 与我们联系!
请告诉我们 如果你有任何反馈或新的主题要求。我们很乐意听取你的意见。
- 原文链接: quicknode.com/guides/sol...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!