Solana 60 天课程

2025年02月27日更新 80 人订阅
原价: ¥ 36 限时优惠
专栏简介 开始 Solana - 安装与故障排除 Solana 和 Rust 中的算术与基本类型 Solana Anchor 程序 IDL Solana中的Require、Revert和自定义错误 Solana程序是可升级的,并且没有构造函数 Solidity开发者的Rust基础 Rust不寻常的语法 Rust 函数式过程宏 Rust 结构体与属性式和自定义派生宏 Rust 和 Solana 中的可见性与“继承” Solana时钟及其他“区块”变量 Solana 系统变量详解 Solana 日志、“事件”与交易历史 Tx.origin、msg.sender 和 onlyOwner 在 Solana 中:识别调用者 Solana 计算单元与交易费用介绍 在 Solana 和 Anchor 中初始化账户 Solana 计数器教程:在账户中读写数据 使用 Solana web3 js 和 Anchor 读取账户数据 在Solana中创建“映射”和“嵌套映射” Solana中的存储成本、最大存储容量和账户调整 在 Solana 中读取账户余额的 Anchor 方法:address(account).balance 功能修饰符(view、pure、payable)和回退函数在 Solana 中不存在的原因 在 Solana 上实现 SOL 转账及构建支付分配器 使用不同签名者修改账户 PDA(程序派生地址)与 Solana 中的密钥对账户 理解 Solana 中的账户所有权:从PDA中转移SOL Anchor 中的 Init if needed 与重初始化攻击 Solana 中的多重调用:批量交易与交易大小限制 Solana 中的所有者与权限 在Solana中删除和关闭账户与程序 在 Anchor 中:不同类型的账户 在链上读取另一个锚点程序账户数据 在 Anchor 中的跨程序调用(CPI) SPL Token 的运作方式 使用 Anchor 和 Web3.js 转移 SPL Token Solana 教程 - 如何实现 Token 出售 基础银行教程 Metaplex Token 元数据工作原理 使用Metaplex实施代币元数据 使用 LiteSVM 进行时间旅行测试 Solana Token-2022 标准规范 生息代币第一部分 计息代币第二部分 Solana 中的 Ed25519 签名验证 Solana 指令自省

Solana 教程 - 如何实现 Token 出售

  • RareSkills
  • 发布于 2025-10-14 20:31
  • 阅读 1171

本文介绍了如何使用 Anchor 框架在 Solana 上创建一个 Token Sale 程序。该程序允许用户通过支付 SOL 来购买指定 token,并使用 PDA(Program Derived Address)来管理 token 的铸造和 SOL 的存储,同时实现了管理员提款和防止非管理员提款的功能。

一个 token sale program 是一个智能合约,它以固定价格出售特定的 token,通常以 SOL 等原生 token 作为交换。销售会一直持续到预定义的供应量售完,或者所有者采取行动结束销售。

我们的实现遵循以下流程:

  1. 用户根据我们的汇率存入 SOL,例如,1 SOL 兑换 100 个 token。
  2. 程序将 SOL 存储在 treasury Program Derived Address (PDA) 中,这是一个程序控制的账户。
  3. 一旦收到 SOL,token 就会被铸造给用户。
  4. 销售会持续到达到预定义的供应上限。
  5. 管理员可以从 treasury 中提取已收集的 SOL。

创建 Token Sale 程序

我们将构建的 Solana 程序直接将 SPL token 铸造给买家,而不需要我们作为铸币机构签署每笔交易。这是标准方法——否则,管理员需要手动批准每次购买,这不切实际。

创建 token sale 所需的账户

首先,使用 Anchor 创建一个新的 token_sale 程序,并将 programs/token_sale/src/lib.rs 中的样板代码替换为以下代码。 以下代码导入了我们的程序依赖项,并定义了一个 initialize 函数。该函数执行以下操作:

  1. 设置管理员帐户以控制 treasury 提款
  2. 为我们正在销售的新 token 创建一个 mint 账户
  3. 创建一个 treasury 账户来收集 token 购买的 SOL
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 = 100SUPPLY_CAP = 1000(带 9 位小数)。

接下来,为我们的函数添加 Initialize 账户结构体。它包含以下账户:

  • admin:支付交易费用并担任程序管理员的帐户
  • admin_config:这是一个程序拥有的帐户,用于存储管理员的公钥,这样稍后在提款期间,我们可以验证签名者是否是同一管理员(就像在 Solidity 中检查 msg.sender == admin,其中 admin 是一个存储管理员公钥的状态变量)。
  • mint:一个自引用 mint PDA,它既是 token 的 mint,又是它自己的授权机构(我们稍后会解释这个概念)
  • treasury:一个 PDA,用于保存从 token 购买中收集的 SOL
  • 最后,我们传递与我们交互的 Token Program 和 System Program。
#[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 struct 账户

让我们分解 Initialize 账户结构体中的每个账户,并了解它们的目的:

admin config

admin_config:此账户持有管理员的公钥(由 AdminConfig 结构体定义),用于确保只有管理员可以从 treasury 中提取 SOL。

显示 admin_config 账户初始化约束的屏幕截图

AdminConfig 的账户定义

mint 账户

这是我们的 SPL token(正在出售的 token)的 mint 账户。我们将其创建为 PDA,以便程序稍后可以为其签名(我们将在本文后面解释这一点)。

该账户在链上不存在,直到我们调用 initialize。在该调用中,Anchor 将:

  1. 使用种子 "token_mint" 和程序 ID 计算 mint PDA 地址
  2. 使用 mint::decimals = 9 创建账户(这是我们设置的,如下所示)
  3. 将 mint 的授权机构设置为自身(mint::authority = mint.key())。这部分很重要,因为通过使 PDA 成为其自身的授权机构,只有我们的程序可以使用相同的种子和 bump 签署 mint_to 指令(同样,我们将在本文后面解释这是如何工作的)。

treasury

此 PDA 专门用于保存用户在销售期间发送的 SOL(lamports)。

保存 SOL 的 PDA 的初始化参数

现在使用以下内容更新 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 以更新依赖项。

运行测试,它会通过。

image.png

用 1 SOL 购买 100 个 token

我们已经设置了 Token Sale 程序。我们现在将添加一个函数来 mint 新的 token 单位进行销售,以便用户可以购买我们的 token。

该代码执行以下操作:

  1. 根据 lamport 输入计算要 mint 的 token 数量。
  2. 检查我们是否超过了总供应量。
  3. 将 SOL 从买家转移到 treasury。
  4. 准备签名者种子,以便程序可以代表 mint PDA 进行签名(更多内容请参见下面的代码块之后)。
  5. 使用 mint 账户作为其自身的授权机构来设置 mint 指令。
  6. 使用签名者种子创建一个 CPI 上下文。
  7. 将 token mint 到买家的 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 设置为其自身的授权机构

在上面的 mint() 函数中,你可以看到我们如何使用 CpiContext::new_with_signer 来 mint token。这是因为我们之前设置 mint 账户的方式。回想一下,在初始化期间,我们设置了 mint::authority = mint.key(),使 mint PDA 成为其自身的授权机构。

以下是此模式至关重要的原因。

**token mint...

剩余50%的内容订阅专栏后可查看

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论