Solana 60 天课程

2025年02月27日更新 76 人订阅
原价: ¥ 28 限时优惠
专栏简介 开始 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实施代币元数据

Solana 教程 - 如何实现 Token 出售

本文介绍了如何使用 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 条评论

请先 登录 后评论