本文介绍了如何使用 Anchor 框架在 Solana 上创建一个 Token Sale 程序。该程序允许用户通过支付 SOL 来购买指定 token,并使用 PDA(Program Derived Address)来管理 token 的铸造和 SOL 的存储,同时实现了管理员提款和防止非管理员提款的功能。
一个 token sale program 是一个智能合约,它以固定价格出售特定的 token,通常以 SOL 等原生 token 作为交换。销售会一直持续到预定义的供应量售完,或者所有者采取行动结束销售。
我们的实现遵循以下流程:
我们将构建的 Solana 程序直接将 SPL token 铸造给买家,而不需要我们作为铸币机构签署每笔交易。这是标准方法——否则,管理员需要手动批准每次购买,这不切实际。
首先,使用 Anchor 创建一个新的 token_sale
程序,并将 programs/token_sale/src/lib.rs
中的样板代码替换为以下代码。
以下代码导入了我们的程序依赖项,并定义了一个 initialize
函数。该函数执行以下操作:
use anchor_lang::prelude::*;
use anchor_lang::system_program::{transfer, Transfer};
use anchor_spl::token::{mint_to, Mint, MintTo, Token, TokenAccount};
declare_id!("Gm8bFHtX3TapZDqA2tjviP1Qn1f8bLjTf8tbhFcgzcFs"); // 用你的程序 ID 替换此项或运行 `anchor sync`
// 每个 SOL 的 Token 数量,例如,1 SOL == 100 个我们的 token
const TOKENS_PER_SOL: u64 = 100;
// 最大供应量:1000 个 token(带 9 位小数)
const SUPPLY_CAP: u64 = 1000e9 as u64;
#[program]
pub mod token_sale {
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
// 设置管理员密钥
ctx.accounts.admin_config.admin = ctx.accounts.admin.key();
Ok(())
}
}
在上面的代码中,我们为 token sale 程序定义了常量:TOKENS_PER_SOL = 100
和 SUPPLY_CAP = 1000
(带 9 位小数)。
接下来,为我们的函数添加 Initialize
账户结构体。它包含以下账户:
admin
:支付交易费用并担任程序管理员的帐户admin_config
:这是一个程序拥有的帐户,用于存储管理员的公钥,这样稍后在提款期间,我们可以验证签名者是否是同一管理员(就像在 Solidity 中检查 msg.sender == admin
,其中 admin
是一个存储管理员公钥的状态变量)。mint
:一个自引用 mint PDA,它既是 token 的 mint,又是它自己的授权机构(我们稍后会解释这个概念)treasury
:一个 PDA,用于保存从 token 购买中收集的 SOL#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub admin: Signer<'info>, // 交易签名者
#[account(\
init,\
payer = admin,\
space = 8+AdminConfig::INIT_SPACE, // 8 用于鉴别器\
)]
pub admin_config: Account<'info, AdminConfig>,
#[account(\
init,\
payer = admin,\
seeds = [b"token_mint"],\
bump,\
mint::decimals = 9,\
mint::authority = mint.key(),\
)]
pub mint: Account<'info, Mint>,
/// CHECK: treasury 的 PDA
#[account(\
seeds = [b"treasury"],\
bump\
)]
pub treasury: AccountInfo<'info>,
pub token_program: Program<'info, Token>,
pub system_program: Program<'info, System>,
}
// 存储管理员公钥
#[account]
#[derive(InitSpace)] // 这是一个由 anchor 提供的 derive attribute macro,它计算账户所需的空间,并让我们访问 AdminConfig::INIT_SPACE,如上所示
pub struct AdminConfig {
pub admin: Pubkey,
}
#[error_code]
pub enum Errors {
#[msg("达到最大的 token 供应量限制")]
SupplyLimit,
#[msg("数学溢出")]
Overflow,
#[msg("只有管理员才能提款")]
UnauthorizedAccess,
#[msg("treasury 中没有足够的 SOL")]
InsufficientFunds,
}
让我们分解 Initialize
账户结构体中的每个账户,并了解它们的目的:
admin config
admin_config
:此账户持有管理员的公钥(由 AdminConfig
结构体定义),用于确保只有管理员可以从 treasury 中提取 SOL。
mint 账户
这是我们的 SPL token(正在出售的 token)的 mint 账户。我们将其创建为 PDA,以便程序稍后可以为其签名(我们将在本文后面解释这一点)。
该账户在链上不存在,直到我们调用 initialize
。在该调用中,Anchor 将:
"token_mint"
和程序 ID 计算 mint PDA 地址mint::decimals = 9
创建账户(这是我们设置的,如下所示)mint::authority = mint.key()
)。这部分很重要,因为通过使 PDA 成为其自身的授权机构,只有我们的程序可以使用相同的种子和 bump 签署 mint_to
指令(同样,我们将在本文后面解释这是如何工作的)。treasury
此 PDA 专门用于保存用户在销售期间发送的 SOL(lamports)。
现在使用以下内容更新 programs/token_sale/Cargo.toml
文件。
[package]
name = "token_sale"
version = "0.1.0"
description = "Created with Anchor"
edition = "2021"
[lib]
crate-type = ["cdylib", "lib"]
name = "token_sale"
[features]
default = []
cpi = ["no-entrypoint"]
no-entrypoint = []
no-idl = []
no-log-ix-name = []
idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] # 添加了 "anchor-spl/idl-build"
[dependencies]
anchor-lang = "0.31.0"
anchor-spl = "0.31.0" # 添加了此项
现在更新我们程序的测试。
此测试与我们在之前的教程中看到的非常相似。它只是使用所需的账户调用我们程序的 initialize
指令,并断言新创建的 mint 账户(token)的属性。
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import {
createAssociatedTokenAccount,
getAccount,
getMint,
TOKEN_PROGRAM_ID
} from "@solana/spl-token";
import * as web3 from "@solana/web3.js";
import { assert } from "chai";
import { TokenSale } from "../target/types/token_sale";
describe("token_sale", async () => {
const provider = anchor.AnchorProvider.env();
anchor.setProvider(provider);
const program = anchor.workspace.TokenSale as Program<TokenSale>;
const connection = provider.connection;
const adminKp = provider.wallet.payer;
const buyer = adminKp; // 使用相同的密钥对作为管理员和买家进行测试
const TOKENS_PER_SOL = 100;
// 为 admin config 账户生成密钥对(将作为签名者传递以授权 adminConfig 账户创建)
const adminConfigKp = web3.Keypair.generate();
let mint: anchor.web3.PublicKey;
let treasuryPda: anchor.web3.PublicKey;
let buyerAta: anchor.web3.PublicKey;
it("creates mint", async () => {
[mint] = web3.PublicKey.findProgramAddressSync(
[Buffer.from("token_mint")],
program.programId
);
[treasuryPda] = web3.PublicKey.findProgramAddressSync(
[Buffer.from("treasury")],
program.programId
);
const tx = await program.methods
.initialize()
.accounts({
admin: adminKp.publicKey,
adminConfig: adminConfigKp.publicKey,
mint: mint,
treasury: treasuryPda,
tokenProgram: TOKEN_PROGRAM_ID,
systemProgram: anchor.web3.SystemProgram.programId,
})
.signers([adminKp, adminConfigKp])
.rpc();
console.log("initialize tx:", tx);
const mintInfo = await getMint(connection, mint);
assert.equal(mintInfo.mintAuthority.toBase58(), mint.toBase58());
assert.equal(Number(mintInfo.supply), 0);
assert.equal(mintInfo.decimals, 9);
});
});
运行 npm install @solana/spl-token
以更新依赖项。
运行测试,它会通过。
我们已经设置了 Token Sale 程序。我们现在将添加一个函数来 mint 新的 token 单位进行销售,以便用户可以购买我们的 token。
该代码执行以下操作:
pub fn mint(ctx: Context<MintTokens>, lamports: u64) -> Result<()> {
// 计算要 mint 的 token 数量(lamports * TOKENS_PER_SOL)
let amount = lamports
.checked_mul(TOKENS_PER_SOL)
.ok_or(Errors::Overflow)?; // 如果溢出,则返回错误
// 确保我们不超过最大供应量
let current_supply = ctx.accounts.mint.supply;
let new_supply = current_supply.checked_add(amount).ok_or(Errors::Overflow)?; // 如果溢出,则返回错误
require!(new_supply <= SUPPLY_CAP, Errors::SupplyLimit);
// 将 SOL 发送到 treasury
let transfer_instruction = Transfer {
from: ctx.accounts.buyer.to_account_info(),
to: ctx.accounts.treasury.to_account_info(),
};
let cpi_context = CpiContext::new(
ctx.accounts.system_program.to_account_info(),
transfer_instruction,
);
transfer(cpi_context, lamports)?;
// 为 mint PDA 创建签名者种子
let bump = ctx.bumps.mint;
let signer_seeds: &[&[&[u8]]] = &[&[b"token mint".as_ref(), &[bump]]];
// 使用 mint 作为其自身的授权机构来设置 mint 指令
let mint_to_instruction = MintTo {
mint: ctx.accounts.mint.to_account_info(),
to: ctx.accounts.buyer_token_account.to_account_info(),
authority: ctx.accounts.mint.to_account_info(),
};
// 使用 `new_with_signer` 创建 CPI 上下文 - 允许我们的 token sale 程序为 mint PDA 签名。这是因为 Solana 运行时验证了我们的程序使用这些种子和 bump 派生了 mint PDA
// 有关更多信息,请参见此处:<https://github.com/solana-foundation/developer-content/blob/main/content/guides/getstarted/how-to-cpi-with-signer.md>
let cpi_ctx = CpiContext::new_with_signer(
ctx.accounts.token_program.to_account_info(),
mint_to_instruction,
signer_seeds,
);
mint_to(cpi_ctx, amount)?;
Ok(())
}
在上面的 mint()
函数中,你可以看到我们如何使用 CpiContext::new_with_signer
来 mint token。这是因为我们之前设置 mint 账户的方式。回想一下,在初始化期间,我们设置了 mint::authority = mint.key()
,使 mint PDA 成为其自身的授权机构。
以下是此模式至关重要的原因。
**token mint...
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!