Solana[part16]_Solana质押-编写应用级别的质押合约-质押部分

Solana质押-编写应用级别的质押合约-质押部分

核心代码见:

https://github.com/SoniaChan33/anchor_social/commit/48b428c2a33213275cea8cc42be199f8bfc7a332 nft mint 编写

https://github.com/SoniaChan33/anchor_social/commit/a27180fc342c3be81e670794f0e0ddf22817881c stake nft

1. NFT 铸造(Mint)功能

1.1 功能说明

通过 nft_mint_v1 方法实现 NFT 铸造,遵循 MPL Token Metadata 标准,创建包含元数据的唯一 NFT,主要流程包括:

  • 创建 NFT 元数据账户
  • 铸造 1 个代币到关联账户
  • 创建主版本账户(Master Edition)确保唯一性

1.2 核心实现(nft_mint.rs

1.2.1 铸造方法

pub fn nft_mint_v1(ctx: Context<NFTMint>, nft_id: String) -> Result<()> {
    // 定义签名种子(PDA)
    let signer_seeds: &[&[&[u8]]] = &[&[
        MyNFT::SEED_PREFIX.as_bytes(),
        nft_id.as_bytes(),
        &[ctx.bumps.nft_mint_account],
    ]];

    // 创建元数据账户
    create_metadata_accounts_v3(
        CpiContext::new_with_signer(
            ctx.accounts.token_metadata_program.to_account_info(),
            CreateMetadataAccountsV3 {
                metadata: ctx.accounts.meta_account.to_account_info(),
                mint: ctx.accounts.nft_mint_account.to_account_info(),
                mint_authority: ctx.accounts.nft_mint_account.to_account_info(),
                update_authority: ctx.accounts.nft_mint_account.to_account_info(),
                payer: ctx.accounts.authority.to_account_info(),
                system_program: ctx.accounts.system_program.to_account_info(),
                rent: ctx.accounts.rent.to_account_info(),
            },
            signer_seeds,
        ),
        DataV2 {
            name: format!("{} #{}", MyNFT::TOKEN_NAME.to_string(), nft_id),
            symbol: MyNFT::TOKEN_SYMBOL.to_string(),
            uri: MyNFT::TOKEN_URL.to_string(),
            seller_fee_basis_points: 0,
            creators: None,
            collection: None,
            uses: None,
        },
        false,
        true,
        None,
    )?;

    // 铸造 1 个 NFT 到关联账户
    mint_to(
        CpiContext::new_with_signer(
            ctx.accounts.token_program.to_account_info(),
            MintTo {
                mint: ctx.accounts.nft_mint_account.to_account_info(),
                to: ctx.accounts.nft_associated_token_account.to_account_info(),
                authority: ctx.accounts.nft_mint_account.to_account_info(),
            },
            signer_seeds,
        ),
        1,
    )?;

    // 创建主版本账户(限制最大发行量为 1)
    create_master_edition_v3(
        CpiContext::new_with_signer(
            ctx.accounts.token_metadata_program.to_account_info(),
            CreateMasterEditionV3 {
                edition: ctx.accounts.master_edition_account.to_account_info(),
                payer: ctx.accounts.authority.to_account_info(),
                mint: ctx.accounts.nft_mint_account.to_account_info(),
                metadata: ctx.accounts.meta_account.to_account_info(),
                mint_authority: ctx.accounts.nft_mint_account.to_account_info(),
                update_authority: ctx.accounts.nft_mint_account.to_account_info(),
                system_program: ctx.accounts.system_program.to_account_info(),
                token_program: ctx.accounts.token_program.to_account_info(),
                rent: ctx.accounts.rent.to_account_info(),
            },
            signer_seeds,
        ),
        Some(1), // 最大发行量,确保 NFT 唯一性
    )?;

    Ok(())
}

1.2.2 账户结构(NFTMint

#[derive(Accounts)]
#[instruction(nft_id: String)]
pub struct NFTMint<'info> {
    /// 主版本账户(MPL 标准)
    #[account(
        mut,
        seeds = [
            b"metadata",
            token_metadata_program.key().as_ref(),
            nft_mint_account.key().as_ref(),
            b"edition".as_ref(),
        ],
        bump,
        seeds::program = token_metadata_program.key(),
    )]
    pub master_edition_account: UncheckedAccount<'info>,

    /// 元数据账户(MPL 标准)
    #[account(
        mut,
        seeds = [
            b"metadata",
            token_metadata_program.key().as_ref(),
            nft_mint_account.key().as_ref(),
        ],
        bump,
        seeds::program = token_metadata_program.key(),
    )]
    pub meta_account: UncheckedAccount<'info>,

    /// NFT Mint 账户(PDA 派生)
    #[account(
        init_if_needed,
        payer = authority,
        seeds = [MyNFT::SEED_PREFIX.as_bytes(), &nft_id.to_string().as_bytes()],
        bump,
        mint::decimals = MyNFT::TOKEN_DECIMALS,
        mint::authority = nft_mint_account.key(),
        mint::freeze_authority = nft_mint_account.key(),
    )]
    pub nft_mint_account: Account<'info, Mint>,

    /// 用户关联的 NFT 代币账户
    #[account(
        init_if_needed,
        payer = authority,
        associated_token::mint = nft_mint_account,
        associated_token::authority = authority,
    )]
    pub nft_associated_token_account: Account<'info, TokenAccount>,

    #[account(mut)]
    pub authority: Signer<'info>, // 发起者签名账户

    // 依赖程序
    pub token_program: Program<'info, Token>,
    pub system_program: Program<'info, System>,
    pub token_metadata_program: Program<'info, Metadata>,
    pub associated_token_program: Program<'info, AssociatedToken>,
    pub rent: Sysvar<'info, Rent>,
}

1.2.3 NFT 配置常量(state/nft.rs

pub struct MyNFT;

impl MyNFT {
    pub const SEED_PREFIX: &'static str = "MyNFT_v1"; // PDA 种子前缀
    pub const TOKEN_DECIMALS: u8 = 0; // NFT 通常使用 0 位小数
    pub const TOKEN_NAME: &'static str = "My NFT"; // 名称前缀
    pub const TOKEN_SYMBOL: &'static str = "MTK"; // 符号
    pub const TOKEN_URL: &'static str = "https://img.soniachen.com/IMG_0151.JPG"; // 元数据 URI
}

1.3 客户端调用示例(app/api/nft.ts

import * as anchor from "@coral-xyz/anchor";
import { program } from "./wallet";

export async function nftMint(wallet: anchor.Wallet, nftId: string) {
    const tx = await program.methods.nftMint(nftId).accounts({
        authority: wallet.publicKey,
    }).rpc();
    return tx;
}

2. NFT 质押(Stake)功能

2.1 功能说明

通过 nft_stake 方法实现 NFT 质押,主要流程包括:

  • 创建质押信息记录(StakeInfo
  • 将 NFT 从用户账户转移到质押池(程序托管账户)
  • 铸造流动性代币作为质押奖励

2.2 核心实现(nft_stake.rs

2.2.1 质押方法

pub fn nft_stake(ctx: Context<NFTStake>) -> Result<()> {
    // 记录质押关系
    let stake_info = StakeInfo::new(
        ctx.accounts.authority.key(),
        ctx.accounts.nft_mint_account.key(),
    );
    ctx.accounts.stake_info.set_inner(stake_info.clone());

    // 将 NFT 从用户账户转移到质押池
    transfer(
        CpiContext::new(
            ctx.accounts.token_program.to_account_info(),
            Transfer {
                from: ctx.accounts.nft_associated_token_account.to_account_info(),
                to: ctx.accounts.program_receipt_ata.to_account_info(),
                authority: ctx.accounts.authority.to_account_info(),
            },
        ),
        1, // NFT 数量为 1
    )?;

    // 铸造流动性代币作为奖励
    let signer_seeds: &[&[&[u8]]] = &[&[
        MyToken::SEED_PREFIX.as_bytes(),
        &[ctx.bumps.token_mint_account],
    ]];

    mint_to(
        CpiContext::new(
            ctx.accounts.token_program.to_account_info(),
            MintTo {
                mint: ctx.accounts.token_mint_account.to_account_info(),
                to: ctx.accounts.associated_token_account.to_account_info(),
                authority: ctx.accounts.token_mint_account.to_account_info(),
            },
        )
        .with_signer(signer_seeds),
        10000, // 奖励代币数量
    )?;

    Ok(())
}

2.2.2 账户结构(NFTStake

#[derive(Accounts)]
pub struct NFTStake<'info> {
    /// 质押信息记录账户
    #[account(
        init_if_needed,
        payer = authority,
        space = 8 + StakeInfo::INIT_SPACE,
        seeds = [
            StakeInfo::SEED_PREFIX.as_bytes(),
            nft_mint_account.key().as_ref(),
        ],
        bump,
    )]
    pub stake_info: Box<Account<'info, StakeInfo>>,

    /// 质押池接收 NFT 的账户
    #[account(
        init_if_needed,
        payer = authority,
        associated_token::mint = nft_mint_account,
        associated_token::authority = stake_info,
    )]
    pub program_receipt_ata: Box<Account<'info, TokenAccount>>,

    /// 流动性代币 Mint 账户
    #[account(
        mut,
        seeds = [MyToken::SEED_PREFIX.as_bytes(),],
        bump,
    )]
    pub token_mint_account: Box<Account<'info, Mint>>,

    /// 用户的流动性代币关联账户
    #[account(
        init_if_needed,
        payer = authority,
        associated_token::mint = token_mint_account,
        associated_token::authority = authority,
    )]
    pub associated_token_account: Box<Account<'info, TokenAccount>>,

    /// 被质押的 NFT Mint 账户
    #[account(mut)]
    pub nft_mint_account: Box<Account<'info, Mint>>,

    /// 用户的 NFT 关联账户
    #[account(mut,
        associated_token::mint = nft_mint_account,
        associated_token::authority = authority,
    )]
    pub nft_associated_token_account: Box<Account<'info, TokenAccount>>,

    #[account(mut)]
    pub authority: Signer<'info>, // 发起者签名账户

    // 依赖程序
    pub token_program: Program<'info, Token>,
    pub system_program: Program<'info, System>,
    pub associated_token_program: Program<'info, AssociatedToken>,
}

2.2.3 质押信息结构(state/stake.rs

#[account]
#[derive(InitSpace)]
pub struct StakeInfo {
    pub staker: Pubkey,           // 质押人地址
    pub nft_mint_account: Pubkey, // 质押的 NFT Mint 地址
    pub staked_at: u64,           // 质押时间(纪元)
}

impl StakeInfo {
    pub const SEED_PREFIX: &'static str = "stake_v1"; // PDA 种子前缀

    pub fn new(staker: Pubkey, nft_mint_account: Pubkey) -> Self {
        Self {
            staker,
            nft_mint_account,
            staked_at: Clock::get().unwrap().epoch,
        }
    }
}

2.3 客户端调用示例(app/api/stake.ts

import * as anchor from "@coral-xyz/anchor";
import { program } from "./wallet";
import { getNftMintAccount } from "./account";

export async function stakeNFT(wallet: anchor.Wallet, nft_id: string) {
    return await program.methods.nftStake().accounts({
        nftMintAccount: getNftMintAccount(nft_id),
    }).signers([wallet.payer]).rpc();
}

3. 关键技术点

  1. PDA 账户管理:通过种子(Seed)派生程序派生地址(PDA)管理 NFT Mint 账户、质押信息账户等,确保账户安全性和可预测性。

  2. MPL 元数据标准:遵循 Metaplex Token Metadata 标准创建 NFT 元数据和主版本账户,确保 NFT 兼容性和唯一性。

  3. 代币转移与铸造权限:使用 SPL Token 程序进行代币转移,通过 PDA 签名授予铸造权限,避免私钥直接暴露。

  4. 关联代币账户(ATA):使用关联代币账户规范管理用户与代币的关联关系,简化账户创建和管理流程。

  5. 质押奖励机制:质押 NFT 时铸造流动性代币作为奖励,实现激励机制闭环。

验证

查看账号,发现我的nft已经转移走了,取而代之的是token发放到我的账户中

image-20250822162010948