前言:
前面我们介绍了 solana 关于 anchor 智能合约的环境搭建 配置,简单编写 发布 部署 调用 的一些落地。等等。接下来我们借着案例。详细剖析下智能合约编写。
案例 token_lottery 介绍:
这是一个关于抽奖的 anchor 智能合约。首先初始化 智能合约的一些配置(metadata collection)。然后用户 使用 SPL 进行抽奖 得到票据(nft)通过设置随机数 进行抽奖。最后根据合约的 获奖者 发送奖励; 视频链接 ,项目地址
initialize_config:
/** 执行初始化配置 **/ pub fn initialize_config( ctx: Context<Initialize>, start: u64, end: u64, price: u64, ) -> Result<()> { //缓存推导 PAD 庄户的种子 ctx.accounts.token_lottery.bump = ctx.bumps.token_lottery; ctx.accounts.token_lottery.start_time = start; ctx.accounts.token_lottery.end_time = end; ctx.accounts.token_lottery.ticket_price = price; ctx.accounts.token_lottery.authority = ctx.accounts.payer.key(); ctx.accounts.token_lottery.lottery_pot_amount = 0; ctx.accounts.token_lottery.total_tickets = 0; // Pubkey::default() 是一个固定 全零的 32 字节公钥,固定值为 [0; 32], // 被初始化为全零公钥,表示尚未设置有效的所有者地址 ctx.accounts.token_lottery.randomness_account = Pubkey::default(); ctx.accounts.token_lottery.winner_chosen = false; Ok(()) } //操作指令 关联PAD TokenLottery 账户 #[derive(Accounts)] pub struct Initialize<'info> { #[account(mut)] pub payer: Signer<'info>, #[account( init, payer = payer, space = 8 + TokenLottery::INIT_SPACE, seeds = [b"token_lottery".as_ref()], bump, )] pub token_lottery: Account<'info, TokenLottery>, pub system_program: Program<'info, System>, } #[account] #[derive(InitSpace)] pub struct TokenLottery { //缓存 种子,便于后期推导 PAD pub bump: u8, //胜利的 NFT 票据 ID pub winner: u64, //是否已经领取奖励 pub winner_chosen: bool, //事件限制 开始时间 pub start_time: u64, //事件限制 结束事件 pub end_time: u64, //奖池 pub lottery_pot_amount: u64, //参与价格 pub ticket_price: u64, //一共参与票数 pub total_tickets: u64, //授权 可以执行提交随机数 和 选出胜利者 pub authority: Pubkey, //关联执行随机信息的账户 pub randomness_account: Pubkey, }
initialize_lottery:
/** 初始化 lottery,管理 ntf, 创建 nft 设置 nft metadata **/ pub fn initialize_lottery(ctx: Context<InitializeLottery>) -> Result<()> { let signer_seeds: &[&[&[u8]]] = &[&[b"collection_mint".as_ref(), &[ctx.bumps.collection_mint]]]; //铸造 nft msg!("Creating Mint Account"); mint_to( CpiContext::new_with_signer( ctx.accounts.token_program.to_account_info(), MintTo { mint: ctx.accounts.collection_mint.to_account_info(), to: ctx.accounts.collection_mint_account.to_account_info(), authority: ctx.accounts.collection_mint.to_account_info(), }, signer_seeds, ), 1, ) .expect("TODO: panic message"); //设置nft 元数据 msg!("Creating Metadata Account"); create_metadata_accounts_v3( CpiContext::new_with_signer( ctx.accounts.metadata.to_account_info(), CreateMetadataAccountsV3 { metadata: ctx.accounts.metadata.to_account_info(), mint: ctx.accounts.collection_mint.to_account_info(), mint_authority: ctx.accounts.collection_mint.to_account_info(), payer: ctx.accounts.payer.to_account_info(), update_authority: ctx.accounts.collection_mint.to_account_info(), system_program: ctx.accounts.system_program.to_account_info(), rent: ctx.accounts.rent.to_account_info(), }, signer_seeds, ), DataV2 { name: NAME.to_string(), symbol: SYMBOL.to_string(), uri: URI.to_string(), seller_fee_basis_points: 0, creators: Some(vec![Creator { address: ctx.accounts.collection_mint.key(), verified: false, share: 100, }]), collection: None, uses: None, }, true, true, Some(CollectionDetails::V1 { size: 0 }), )?; //设置 nft 主 edition msg!("Create Master Edition Account"); create_master_edition_v3( CpiContext::new_with_signer( ctx.accounts.token_metadata_program.to_account_info(), CreateMasterEditionV3 { edition: ctx.accounts.master_edition.to_account_info(), mint: ctx.accounts.collection_mint.to_account_info(), update_authority: ctx.accounts.collection_mint.to_account_info(), mint_authority: ctx.accounts.collection_mint.to_account_info(), payer: ctx.accounts.payer.to_account_info(), metadata: ctx.accounts.metadata.to_account_info(), token_program: ctx.accounts.token_program.to_account_info(), system_program: ctx.accounts.system_program.to_account_info(), rent: ctx.accounts.rent.to_account_info(), }, &signer_seeds, ), Some(0), )?; msg!("Verifying Collection"); sign_metadata(CpiContext::new_with_signer( ctx.accounts.token_metadata_program.to_account_info(), SignMetadata { creator: ctx.accounts.collection_mint.to_account_info(), metadata: ctx.accounts.metadata.to_account_info(), }, &signer_seeds, ))?; Ok(()) } #[derive(Accounts)] pub struct InitializeLottery<'info> { #[account(mut)] pub payer: Signer<'info>, //ntf 集合代币账户,只是个PDA账户没有存储功能 也没有 space, 并且权限设置为 自己本身 #[account( init, payer = payer, mint::decimals = 0, mint::authority = collection_mint, mint::freeze_authority = collection_mint, seeds = [b"collection_mint".as_ref()], bump, )] pub collection_mint: InterfaceAccount<'info, Mint>, //nft 账户 只是 pad 关联 账户 不需要存储, 并且权限设置为 自己本身 #[account( init, payer = payer, token::mint = collection_mint, token::authority = collection_mint_account, seeds = [b"collection_associated_token".as_ref()], bump, )] pub collection_mint_account: InterfaceAccount<'info, TokenAccount>, //nft 设置 metadata 关联 pad 账户 不需要存储, #[account( mut, seeds = [ b"metadata", token_metadata_program.key().as_ref(), collection_mint.key().as_ref(), ], bump, seeds::program = token_metadata_program.key(), )] ///CHECK: 这个账户 将由 metadata 智能合约进行 检查, "///CHECK:" 固定写法 要不然编译不通过 pub metadata: UncheckedAccount<'info>, ///单一 NFT 账号配置,区别于 Normal Editions;支持单一NFT(Master Edition)或限量复制品(Normal Editions),适合不同类型的项目需求 #[account( mut, seeds = [ b"metadata", token_metadata_program.key().as_ref(), collection_mint.key().as_ref(), b"edition" ], bump, seeds::program = token_metadata_program.key(), )] ///CHECK: 这个账户 将由 metadata 智能合约进行 检查 "///CHECK:" 固定写法 要不然编译不通过 pub master_edition: UncheckedAccount<'info>, //mint 元数据处理 系统账号 pub token_metadata_program: Program<'info, Metadata>, //ATA associate_token处理 程序账号 pub associate_token_program: Program<'info, AssociatedToken>, //ATA token处理 程序账号 pub token_program: Interface<'info, TokenInterface>, //系统账号 pub system_program: Program<'info, System>, pub rent: Sysvar<'info, Rent>, }
buy_ticket:
pub fn buy_ticket(ctx: Context<BuyTicket>) -> Result<()> { let clock = Clock::get()?; //名称 + 数量(Id) let ticket_name: String = NAME.to_owned() + ctx .accounts .token_lottery .total_tickets .to_string() .as_str(); //判断时间 是否可以购买 if clock.slot < ctx.accounts.token_lottery.start_time || clock.slot > ctx.accounts.token_lottery.end_time { return Err(ErrorCode::LotteryNotOpen.into()); } //转账 支付购买费用 system_program::transfer( CpiContext::new( ctx.accounts.system_program.to_account_info(), system_program::Transfer { from: ctx.accounts.payer.to_account_info(), to: ctx.accounts.token_lottery.to_account_info(), }, ), ctx.accounts.token_lottery.ticket_price, )?; //创建票据的种子 let signer_seeds: &[&[&[u8]]] = &[&[b"collection_mint".as_ref(), &[ctx.bumps.collection_mint]]]; //铸造票据 mint_to( CpiContext::new_with_signer( ctx.accounts.token_program.to_account_info(), MintTo { mint: ctx.accounts.ticket_mint.to_account_info(), to: ctx.accounts.destination.to_account_info(), authority: ctx.accounts.collection_mint.to_account_info(), }, &signer_seeds, ), 1, )?; // 创建票据元数据 NFT msg!("Creating Metadata Account"); create_metadata_accounts_v3( CpiContext::new_with_signer( ctx.accounts.token_metadata_program.to_account_info(), CreateMetadataAccountsV3 { metadata: ctx.accounts.collection_metadata.to_account_info(), mint: ctx.accounts.ticket_mint.to_account_info(), mint_authority: ctx.accounts.collection_mint.to_account_info(), payer: ctx.accounts.payer.to_account_info(), update_authority: ctx.accounts.collection_mint.to_account_info(), system_program: ctx.accounts.system_program.to_account_info(), rent: ctx.accounts.rent.to_account_info(), }, signer_seeds, ), DataV2 { name: ticket_name, symbol: SYMBOL.to_string(), uri: URI.to_string(), seller_fee_basis_points: 0, creators: None, collection: None, uses: None, }, true, true, None, )?; msg!("Create Master Edition Account"); create_master_edition_v3( CpiContext::new_with_signer( ctx.accounts.token_metadata_program.to_account_info(), CreateMasterEditionV3 { edition: ctx.accounts.collection_master_edition.to_account_info(), mint: ctx.accounts.ticket_mint.to_account_info(), update_authority: ctx.accounts.collection_mint.to_account_info(), mint_authority: ctx.accounts.collection_mint.to_account_info(), payer: ctx.accounts.payer.to_account_info(), metadata: ctx.accounts.collection_metadata.to_account_info(), token_program: ctx.accounts.token_program.to_account_info(), system_program: ctx.accounts.system_program.to_account_info(), rent: ctx.accounts.rent.to_account_info(), }, &signer_seeds, ), Some(0), )?; //验证集合 作为集合的一部分 set_and_verify_sized_collection_item( CpiContext::new_with_signer( ctx.accounts.token_metadata_program.to_account_info(), SetAndVerifySizedCollectionItem { metadata: ctx.accounts.collection_metadata.to_account_info(), collection_authority: ctx.accounts.collection_mint.to_account_info(), payer: ctx.accounts.payer.to_account_info(), update_authority: ctx.accounts.collection_mint.to_account_info(), collection_mint: ctx.accounts.collection_mint.to_account_info(), collection_metadata: ctx.accounts.collection_metadata.to_account_info(), collection_master_edition: ctx .accounts .collection_master_edition .to_account_info(), }, &signer_seeds, ), None, )?; ctx.accounts.token_lottery.total_tickets += 1; Ok(()) } #[derive(Accounts)] pub struct BuyTicket<'info> { #[account(mut)] pub payer: Signer<'info>, //获取 token_lottery #[account( mut, seeds = [b"token_lottery".as_ref()], bump = token_lottery.bump, )] pub token_lottery: Account<'info, TokenLottery>, //获取 token_lottery #[account( mut, seeds = [b"collection_mint".as_ref()], bump, )] pub collection_mint: InterfaceAccount<'info, Mint>, //创建NFT 票据 #[account( init, payer = payer, seeds = [token_lottery.total_tickets.to_le_bytes().as_ref()], bump, mint::decimals = 0, mint::authority = collection_mint, mint::freeze_authority = collection_mint, mint::token_program = token_program, )] pub ticket_mint: InterfaceAccount<'info, Mint>, //购买完毕接收 nft 的账号 #[account( init, payer = payer, associated_token::mint = ticket_mint, associated_token::authority = payer, associated_token::token_program = token_program, )] pub destination: InterfaceAccount<'info, TokenAccount>, #[account( mut, seeds = [ b"metadata", token_metadata_program.key().as_ref(), collection_mint.key().as_ref(), ], bump, seeds::program = token_metadata_program.key(), )] /// CHECK 这个 Account 将由智能合约 进行验证 pub collection_metadata: UncheckedAccount<'info>, #[account( mut, seeds = [ b"metadata", token_metadata_program.key().as_ref(), collection_mint.key().as_ref(), b"edition" ], bump, seeds::program = token_metadata_program.key(), )] ///CHECK: 这个账户 将由 metadata 智能合约进行 检查 pub collection_master_edition: UncheckedAccount<'info>, pub associated_token_program: Program<'info, AssociatedToken>, pub token_program: Interface<'info, TokenInterface>, pub token_metadata_program: Program<'info, Metadata>, pub system_program: Program<'info, System>, //租金 pub rent: Sysvar<'info, Rent>, }
commit_randomness:
/** 提交随机数 **/ pub fn commit_randomness(ctx: Context<CommitRandomness>) -> Result<()> { let clock = Clock::get()?; let token_lottery = &mut ctx.accounts.token_lottery; //检查权限 if ctx.accounts.payer.key() != token_lottery.authority { return Err(ErrorCode::NotAuthorized.into()); } let randomness_data = RandomnessAccountData::parse(ctx.accounts.randomness_account.data.borrow()).unwrap(); //随机数不符合要求 说明这个抽奖已经揭晓了,否则就有人提前知道谁是 赢家了 if randomness_data.seed_slot != clock.slot - 1 { return Err(ErrorCode::RandomnessAlreadyRevealed.into()); } token_lottery.randomness_account = ctx.accounts.randomness_account.key(); Ok(()) } #[derive(Accounts)] pub struct CommitRandomness<'info> { //付款人 #[account(mut)] pub payer: Signer<'info>, //获取 token_lottery #[account( mut, seeds = [b"token_lottery".as_ref()], bump = token_lottery.bump, )] pub token_lottery: Account<'info, TokenLottery>, //创建随机账号 由 switchboard 处理 //命令行:cargo add switchboard_on_demand /// CHECK 这个账号是由 Switchboard smart contract 验证 pub randomness_account: UncheckedAccount<'info>, // 用到的系统账号 pub system_program: Program<'info, System>, }
reveal_winner:
/** 抽奖结果 **/ pub fn reveal_winner(ctx: Context<RevealWinner>) -> Result<()> { let clock = Clock::get()?; let token_lottery = &mut ctx.accounts.token_lottery; if ctx.accounts.payer.key() != token_lottery.authority { return Err(ErrorCode::NotAuthorized.into()); } //无效的随机因子 if ctx.accounts.randomness_account.key() != token_lottery.randomness_account { return Err(ErrorCode::IncorrectRandomnessAccount.into()); } //还没到结束时间 if clock.slot < token_lottery.end_time { return Err(ErrorCode::LotteryNotCompleted.into()); } let randomness_data = RandomnessAccountData::parse(ctx.accounts.randomness_account.data.borrow()).unwrap(); let reveal_random_value = randomness_data .get_value(&clock) .map_err(|_| ErrorCode::RandomnessNotResolved)?; //选出获胜者 let winner = reveal_random_value[0] as u64 % token_lottery.total_tickets; token_lottery.winner = winner; token_lottery.winner_chosen = true; Ok(()) } #[derive(Accounts)] pub struct RevealWinner<'info> { #[account(mut)] pub payer: Signer<'info>, #[account( mut, seeds = [b"token_lottery".as_ref()], bump = token_lottery.bump, )] pub token_lottery: Account<'info, TokenLottery>, /// CHECK 随机数种子, 由 Switchboard 进行验证 pub randomness_account: UncheckedAccount<'info>, pub system_program: Program<'info, System>, }
clain_winnings:
/** 发放奖励 **/ pub fn clain_winnings(ctx: Context<ClainWinnings>) -> Result<()> { //当前 token_lottery 已经选出 获奖励 者 require!( ctx.accounts.token_lottery.winner_chosen, ErrorCode::WinnerNotChosen ); //检查 metadata 是否是属于改 集合,进行验证 require!( ctx.accounts .token_metadata .collection .as_ref() .unwrap() .verified, ErrorCode::NotVerified ); //无效的票据 require!( ctx.accounts.token_metadata.collection.as_ref().unwrap().key == ctx.accounts.collection_mint.key(), ErrorCode::IncorrectTicket ); let ticket_name = NAME.to_owned() + &ctx.accounts.token_lottery.winner.to_string(); let metadata_name = ctx.accounts.token_metadata.name.replace("\u{0}",""); //检查票据信息 require!(metadata_name == ticket_name, ErrorCode::IncorrectTicket); //检查一共对外卖出票数 require!(ctx.accounts.ticket_account.amount > 0,ErrorCode::NoTicket); //转账 **ctx.accounts.token_lottery.to_account_info().lamports.borrow_mut() -= ctx.accounts.token_lottery.lottery_pot_amount; **ctx.accounts.payer.to_account_info().lamports.borrow_mut() += ctx.accounts.token_lottery.lottery_pot_amount; ctx.accounts.token_lottery.lottery_pot_amount = 0; Ok(()) } #[derive(Accounts)] pub struct ClainWinnings<'info> { #[account(mut)] pub payer: Signer<'info>, //关联账号 #[account( mut, seeds = [b"token_lottery".as_ref()], bump = token_lottery.bump, )] pub token_lottery: Account<'info, TokenLottery>, //关联账号 #[account( seeds = [token_lottery.winner.to_le_bytes().as_ref()], bump, )] pub ticket_mint: InterfaceAccount<'info, Mint>, //关联账号 #[account( seeds = [b"collection_mint".as_ref()], bump, )] pub collection_mint: InterfaceAccount<'info, Mint>, //关联 票据元数据 #[account( seeds = [ b"metadata", token_metadata_program.key().as_ref(), ticket_mint.key().as_ref(), ], bump, seeds::program = token_metadata_program.key(), )] pub token_metadata: Account<'info, MetadataAccount>, //关联中奖账户 #[account( associated_token::mint = ticket_mint, associated_token::authority = payer, associated_token::token_program = token_program, )] pub ticket_account: InterfaceAccount<'info, TokenAccount>, //关联票据集合 #[account( seeds = [ b"metadata", token_metadata_program.key().as_ref(), collection_mint.key().as_ref() ], bump, seeds::program = token_metadata_program.key(), )] pub collection_metadata: Account<'info, MetadataAccount>, pub token_program: Interface<'info, TokenInterface>, pub token_metadata_program: Program<'info, Metadata>, pub system_program: Program<'info, System>, }
完整脚本:
use anchor_lang::prelude::*;
use anchor_lang::*;
use anchor_spl::associated_token::AssociatedToken;
use anchor_spl::token_interface::{mint_to, Mint, MintTo, TokenAccount, TokenInterface};
use anchor_spl::metadata::mpl_token_metadata::types::{CollectionDetails, Creator, DataV2};
use anchor_spl::metadata::{
create_master_edition_v3, create_metadata_accounts_v3, set_and_verify_sized_collection_item,
sign_metadata, CreateMasterEditionV3, CreateMetadataAccountsV3, Metadata, MetadataAccount,
SetAndVerifySizedCollectionItem, SignMetadata,
};
use switchboard_on_demand::RandomnessAccountData;
declare_id!("J1VmbciWTjAmAygKi3VbXFnd857WxuSvSUpxhxTehipG");
#[constant]
pub const NAME: &str = "Token Lottery Ticket #";
#[constant]
pub const SYMBOL: &str = "TLT";
#[constant]
pub const URI: &str = "https://token-creator-lac.vercel.app/token_metadata.json";
#[program]
pub mod token_lottery {
use super::*;
/**
执行初始化配置
**/
pub fn initialize_config(
ctx: Context<Initialize>,
start: u64,
end: u64,
price: u64,
) -> Result<()> {
//缓存推导 PAD 庄户的种子
ctx.accounts.token_lottery.bump = ctx.bumps.token_lottery;
ctx.accounts.token_lottery.start_time = start;
ctx.accounts.token_lottery.end_time = end;
ctx.accounts.token_lottery.ticket_price = price;
ctx.accounts.token_lottery.authority = ctx.accounts.payer.key();
ctx.accounts.token_lottery.lottery_pot_amount = 0;
ctx.accounts.token_lottery.total_tickets = 0;
// Pubkey::default() 是一个固定 全零的 32 字节公钥,固定值为 [0; 32],
// 被初始化为全零公钥,表示尚未设置有效的所有者地址
ctx.accounts.token_lottery.randomness_account = Pubkey::default();
ctx.accounts.token_lottery.winner_chosen = false;
Ok(())
}
/**
初始化 lottery,管理 ntf,
创建 nft
设置 nft metadata
**/
pub fn initialize_lottery(ctx: Context<InitializeLottery>) -> Result<()> {
let signer_seeds: &[&[&[u8]]] =
&[&[b"collection_mint".as_ref(), &[ctx.bumps.collection_mint]]];
//铸造 nft
msg!("Creating Mint Account");
mint_to(
CpiContext::new_with_signer(
ctx.accounts.token_program.to_account_info(),
MintTo {
mint: ctx.accounts.collection_mint.to_account_info(),
to: ctx.accounts.collection_mint_account.to_account_info(),
authority: ctx.accounts.collection_mint.to_account_info(),
},
signer_seeds,
),
1,
)
.expect("TODO: panic message");
//设置nft 元数据
msg!("Creating Metadata Account");
create_metadata_accounts_v3(
CpiContext::new_with_signer(
ctx.accounts.metadata.to_account_info(),
CreateMetadataAccountsV3 {
metadata: ctx.accounts.metadata.to_account_info(),
mint: ctx.accounts.collection_mint.to_account_info(),
mint_authority: ctx.accounts.collection_mint.to_account_info(),
payer: ctx.accounts.payer.to_account_info(),
update_authority: ctx.accounts.collection_mint.to_account_info(),
system_program: ctx.accounts.system_program.to_account_info(),
rent: ctx.accounts.rent.to_account_info(),
},
signer_seeds,
),
DataV2 {
name: NAME.to_string(),
symbol: SYMBOL.to_string(),
uri: URI.to_string(),
seller_fee_basis_points: 0,
creators: Some(vec![Creator {
address: ctx.accounts.collection_mint.key(),
verified: false,
share: 100,
}]),
collection: None,
uses: None,
},
true,
true,
Some(CollectionDetails::V1 { size: 0 }),
)?;
//设置 nft 主 edition
msg!("Create Master Edition Account");
create_master_edition_v3(
CpiContext::new_with_signer(
ctx.accounts.token_metadata_program.to_account_info(),
CreateMasterEditionV3 {
edition: ctx.accounts.master_edition.to_account_info(),
mint: ctx.accounts.collection_mint.to_account_info(),
update_authority: ctx.accounts.collection_mint.to_account_info(),
mint_authority: ctx.accounts.collection_mint.to_account_info(),
payer: ctx.accounts.payer.to_account_info(),
metadata: ctx.accounts.metadata.to_account_info(),
token_program: ctx.accounts.token_program.to_account_info(),
system_program: ctx.accounts.system_program.to_account_info(),
rent: ctx.accounts.rent.to_account_info(),
},
&signer_seeds,
),
Some(0),
)?;
msg!("Verifying Collection");
sign_metadata(CpiContext::new_with_signer(
ctx.accounts.token_metadata_program.to_account_info(),
SignMetadata {
creator: ctx.accounts.collection_mint.to_account_info(),
metadata: ctx.accounts.metadata.to_account_info(),
},
&signer_seeds,
))?;
Ok(())
}
pub fn buy_ticket(ctx: Context<BuyTicket>) -> Result<()> {
let clock = Clock::get()?;
//名称 + 数量(Id)
let ticket_name: String = NAME.to_owned()
+ ctx
.accounts
.token_lottery
.total_tickets
.to_string()
.as_str();
//判断时间 是否可以购买
if clock.slot < ctx.accounts.token_lottery.start_time
|| clock.slot > ctx.accounts.token_lottery.end_time
{
return Err(ErrorCode::LotteryNotOpen.into());
}
//转账 支付购买费用
system_program::transfer(
CpiContext::new(
ctx.accounts.system_program.to_account_info(),
system_program::Transfer {
from: ctx.accounts.payer.to_account_info(),
to: ctx.accounts.token_lottery.to_account_info(),
},
),
ctx.accounts.token_lottery.ticket_price,
)?;
//创建票据的种子
let signer_seeds: &[&[&[u8]]] =
&[&[b"collection_mint".as_ref(), &[ctx.bumps.collection_mint]]];
//铸造票据
mint_to(
CpiContext::new_with_signer(
ctx.accounts.token_program.to_account_info(),
MintTo {
mint: ctx.accounts.ticket_mint.to_account_info(),
to: ctx.accounts.destination.to_account_info(),
authority: ctx.accounts.collection_mint.to_account_info(),
},
&signer_seeds,
),
1,
)?;
// 创建票据元数据 NFT
msg!("Creating Metadata Account");
create_metadata_accounts_v3(
CpiContext::new_with_signer(
ctx.accounts.token_metadata_program.to_account_info(),
CreateMetadataAccountsV3 {
metadata: ctx.accounts.collection_metadata.to_account_info(),
mint: ctx.accounts.ticket_mint.to_account_info(),
mint_authority: ctx.accounts.collection_mint.to_account_info(),
payer: ctx.accounts.payer.to_account_info(),
update_authority: ctx.accounts.collection_mint.to_account_info(),
system_program: ctx.accounts.system_program.to_account_info(),
rent: ctx.accounts.rent.to_account_info(),
},
signer_seeds,
),
DataV2 {
name: ticket_name,
symbol: SYMBOL.to_string(),
uri: URI.to_string(),
seller_fee_basis_points: 0,
creators: None,
collection: None,
uses: None,
},
true,
true,
None,
)?;
msg!("Create Master Edition Account");
create_master_edition_v3(
CpiContext::new_with_signer(
ctx.accounts.token_metadata_program.to_account_info(),
CreateMasterEditionV3 {
edition: ctx.accounts.collection_master_edition.to_account_info(),
mint: ctx.accounts.ticket_mint.to_account_info(),
update_authority: ctx.accounts.collection_mint.to_account_info(),
mint_authority: ctx.accounts.collection_mint.to_account_info(),
payer: ctx.accounts.payer.to_account_info(),
metadata: ctx.accounts.collection_metadata.to_account_info(),
token_program: ctx.accounts.token_program.to_account_info(),
system_program: ctx.accounts.system_program.to_account_info(),
rent: ctx.accounts.rent.to_account_info(),
},
&signer_seeds,
),
Some(0),
)?;
//验证集合 作为集合的一部分
set_and_verify_sized_collection_item(
CpiContext::new_with_signer(
ctx.accounts.token_metadata_program.to_account_info(),
SetAndVerifySizedCollectionItem {
metadata: ctx.accounts.collection_metadata.to_account_info(),
collection_authority: ctx.accounts.collection_mint.to_account_info(),
payer: ctx.accounts.payer.to_account_info(),
update_authority: ctx.accounts.collection_mint.to_account_info(),
collection_mint: ctx.accounts.collection_mint.to_account_info(),
collection_metadata: ctx.accounts.collection_metadata.to_account_info(),
collection_master_edition: ctx
.accounts
.collection_master_edition
.to_account_info(),
},
&signer_seeds,
),
None,
)?;
ctx.accounts.token_lottery.total_tickets += 1;
Ok(())
}
/**
提交随机数
**/
pub fn commit_randomness(ctx: Context<CommitRandomness>) -> Result<()> {
let clock = Clock::get()?;
let token_lottery = &mut ctx.accounts.token_lottery;
//检查权限
if ctx.accounts.payer.key() != token_lottery.authority {
return Err(ErrorCode::NotAuthorized.into());
}
let randomness_data =
RandomnessAccountData::parse(ctx.accounts.randomness_account.data.borrow()).unwrap();
//随机数不符合要求 说明这个抽奖已经揭晓了,否则就有人提前知道谁是 赢家了
if randomness_data.seed_slot != clock.slot - 1 {
return Err(ErrorCode::RandomnessAlreadyRevealed.into());
}
token_lottery.randomness_account = ctx.accounts.randomness_account.key();
Ok(())
}
/**
抽奖结果
**/
pub fn reveal_winner(ctx: Context<RevealWinner>) -> Result<()> {
let clock = Clock::get()?;
let token_lottery = &mut ctx.accounts.token_lottery;
if ctx.accounts.payer.key() != token_lottery.authority {
return Err(ErrorCode::NotAuthorized.into());
}
//无效的随机因子
if ctx.accounts.randomness_account.key() != token_lottery.randomness_account {
return Err(ErrorCode::IncorrectRandomnessAccount.into());
}
//还没到结束时间
if clock.slot < token_lottery.end_time {
return Err(ErrorCode::LotteryNotCompleted.into());
}
let randomness_data =
RandomnessAccountData::parse(ctx.accounts.randomness_account.data.borrow()).unwrap();
let reveal_random_value = randomness_data
.get_value(&clock)
.map_err(|_| ErrorCode::RandomnessNotResolved)?;
//选出获胜者
let winner = reveal_random_value[0] as u64 % token_lottery.total_tickets;
token_lottery.winner = winner;
token_lottery.winner_chosen = true;
Ok(())
}
/**
发放奖励
**/
pub fn clain_winnings(ctx: Context<ClainWinnings>) -> Result<()> {
//当前 token_lottery 已经选出 获奖励 者
require!(
ctx.accounts.token_lottery.winner_chosen,
ErrorCode::WinnerNotChosen
);
//检查 metadata 是否是属于改 集合,进行验证
require!(
ctx.accounts
.token_metadata
.collection
.as_ref()
.unwrap()
.verified,
ErrorCode::NotVerified
);
//无效的票据
require!(
ctx.accounts.token_metadata.collection.as_ref().unwrap().key
== ctx.accounts.collection_mint.key(),
ErrorCode::IncorrectTicket
);
let ticket_name = NAME.to_owned() + &ctx.accounts.token_lottery.winner.to_string();
let metadata_name = ctx.accounts.token_metadata.name.replace("\u{0}", "");
//检查票据信息
require!(metadata_name == ticket_name, ErrorCode::IncorrectTicket);
//检查一共对外卖出票数
require!(ctx.accounts.ticket_account.amount > 0, ErrorCode::NoTicket);
//转账
**ctx
.accounts
.token_lottery
.to_account_info()
.lamports
.borrow_mut() -= ctx.accounts.token_lottery.lottery_pot_amount;
**ctx.accounts.payer.to_account_info().lamports.borrow_mut() +=
ctx.accounts.token_lottery.lottery_pot_amount;
ctx.accounts.token_lottery.lottery_pot_amount = 0;
Ok(())
}
}
//操作指令 关联PAD TokenLottery 账户
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + TokenLottery::INIT_SPACE,
seeds = [b"token_lottery".as_ref()],
bump,
)]
pub token_lottery: Account<'info, TokenLottery>,
pub system_program: Program<'info, System>,
}
#[account]
#[derive(InitSpace)]
pub struct TokenLottery {
//缓存 种子,便于后期推导 PAD
pub bump: u8,
//胜利的 NFT 票据 ID
pub winner: u64,
//是否已经领取奖励
pub winner_chosen: bool,
//事件限制 开始时间
pub start_time: u64,
//事件限制 结束事件
pub end_time: u64,
//奖池
pub lottery_pot_amount: u64,
//参与价格
pub ticket_price: u64,
//一共参与票数
pub total_tickets: u64,
//授权 可以执行提交随机数 和 选出胜利者
pub authority: Pubkey,
//关联执行随机信息的账户
pub randomness_account: Pubkey,
}
#[derive(Accounts)]
pub struct InitializeLottery<'info> {
#[account(mut)]
pub payer: Signer<'info>,
//ntf 集合代币账户,只是个PDA账户没有存储功能 也没有 space, 并且权限设置为 自己本身
#[account(
init,
payer = payer,
mint::decimals = 0,
mint::authority = collection_mint,
mint::freeze_authority = collection_mint,
seeds = [b"collection_mint".as_ref()],
bump,
)]
pub collection_mint: InterfaceAccount<'info, Mint>,
//nft 账户 只是 pad 关联 账户 不需要存储, 并且权限设置为 自己本身
#[account(
init,
payer = payer,
token::mint = collection_mint,
token::authority = collection_mint_account,
seeds = [b"collection_associated_token".as_ref()],
bump,
)]
pub collection_mint_account: InterfaceAccount<'info, TokenAccount>,
//nft 设置 metadata 关联 pad 账户 不需要存储,
#[account(
mut,
seeds = [
b"metadata",
token_metadata_program.key().as_ref(),
collection_mint.key().as_ref(),
],
bump,
seeds::program = token_metadata_program.key(),
)]
///CHECK: 这个账户 将由 metadata 智能合约进行 检查, "///CHECK:" 固定写法 要不然编译不通过
pub metadata: UncheckedAccount<'info>,
///单一 NFT 账号配置,区别于 Normal Editions;支持单一NFT(Master Edition)或限量复制品(Normal Editions),适合不同类型的项目需求
#[account(
mut,
seeds = [
b"metadata",
token_metadata_program.key().as_ref(),
collection_mint.key().as_ref(),
b"edition"
],
bump,
seeds::program = token_metadata_program.key(),
)]
///CHECK: 这个账户 将由 metadata 智能合约进行 检查 "///CHECK:" 固定写法 要不然编译不通过
pub master_edition: UncheckedAccount<'info>,
//mint 元数据处理 系统账号
pub token_metadata_program: Program<'info, Metadata>,
//ATA associate_token处理 程序账号
pub associate_token_program: Program<'info, AssociatedToken>,
//ATA token处理 程序账号
pub token_program: Interface<'info, TokenInterface>,
//系统账号
pub system_program: Program<'info, System>,
pub rent: Sysvar<'info, Rent>,
}
#[derive(Accounts)]
pub struct BuyTicket<'info> {
#[account(mut)]
pub payer: Signer<'info>,
//获取 token_lottery
#[account(
mut,
seeds = [b"token_lottery".as_ref()],
bump = token_lottery.bump,
)]
pub token_lottery: Account<'info, TokenLottery>,
//获取 token_lottery
#[account(
mut,
seeds = [b"collection_mint".as_ref()],
bump,
)]
pub collection_mint: InterfaceAccount<'info, Mint>,
//创建NFT 票据
#[account(
init,
payer = payer,
seeds = [token_lottery.total_tickets.to_le_bytes().as_ref()],
bump,
mint::decimals = 0,
mint::authority = collection_mint,
mint::freeze_authority = collection_mint,
mint::token_program = token_program,
)]
pub ticket_mint: InterfaceAccount<'info, Mint>,
//购买完毕接收 nft 的账号
#[account(
init,
payer = payer,
associated_token::mint = ticket_mint,
associated_token::authority = payer,
associated_token::token_program = token_program,
)]
pub destination: InterfaceAccount<'info, TokenAccount>,
#[account(
mut,
seeds = [
b"metadata",
token_metadata_program.key().as_ref(),
collection_mint.key().as_ref(),
],
bump,
seeds::program = token_metadata_program.key(),
)]
/// CHECK 这个 Account 将由智能合约 进行验证
pub collection_metadata: UncheckedAccount<'info>,
#[account(
mut,
seeds = [
b"metadata",
token_metadata_program.key().as_ref(),
collection_mint.key().as_ref(),
b"edition"
],
bump,
seeds::program = token_metadata_program.key(),
)]
///CHECK: 这个账户 将由 metadata 智能合约进行 检查
pub collection_master_edition: UncheckedAccount<'info>,
pub associated_token_program: Program<'info, AssociatedToken>,
pub token_program: Interface<'info, TokenInterface>,
pub token_metadata_program: Program<'info, Metadata>,
pub system_program: Program<'info, System>,
//租金
pub rent: Sysvar<'info, Rent>,
}
#[derive(Accounts)]
pub struct CommitRandomness<'info> {
//付款人
#[account(mut)]
pub payer: Signer<'info>,
//获取 token_lottery
#[account(
mut,
seeds = [b"token_lottery".as_ref()],
bump = token_lottery.bump,
)]
pub token_lottery: Account<'info, TokenLottery>,
//创建随机账号 由 switchboard 处理
//命令行:cargo add switchboard_on_demand
/// CHECK 这个账号是由 Switchboard smart contract 验证
pub randomness_account: UncheckedAccount<'info>,
// 用到的系统账号
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct RevealWinner<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
mut,
seeds = [b"token_lottery".as_ref()],
bump = token_lottery.bump,
)]
pub token_lottery: Account<'info, TokenLottery>,
/// CHECK 随机数种子, 由 Switchboard 进行验证
pub randomness_account: UncheckedAccount<'info>,
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct ClainWinnings<'info> {
#[account(mut)]
pub payer: Signer<'info>,
//关联账号
#[account(
mut,
seeds = [b"token_lottery".as_ref()],
bump = token_lottery.bump,
)]
pub token_lottery: Account<'info, TokenLottery>,
//关联账号
#[account(
seeds = [token_lottery.winner.to_le_bytes().as_ref()],
bump,
)]
pub ticket_mint: InterfaceAccount<'info, Mint>,
//关联账号
#[account(
seeds = [b"collection_mint".as_ref()],
bump,
)]
pub collection_mint: InterfaceAccount<'info, Mint>,
//关联 票据元数据
#[account(
seeds = [
b"metadata",
token_metadata_program.key().as_ref(),
ticket_mint.key().as_ref(),
],
bump,
seeds::program = token_metadata_program.key(),
)]
pub token_metadata: Account<'info, MetadataAccount>,
//关联中奖账户
#[account(
associated_token::mint = ticket_mint,
associated_token::authority = payer,
associated_token::token_program = token_program,
)]
pub ticket_account: InterfaceAccount<'info, TokenAccount>,
//关联票据集合
#[account(
seeds = [
b"metadata",
token_metadata_program.key().as_ref(),
collection_mint.key().as_ref()
],
bump,
seeds::program = token_metadata_program.key(),
)]
pub collection_metadata: Account<'info, MetadataAccount>,
pub token_program: Interface<'info, TokenInterface>,
pub token_metadata_program: Program<'info, Metadata>,
pub system_program: Program<'info, System>,
}
#[error_code]
pub enum ErrorCode {
#[msg("Lottery is not open")]
LotteryNotOpen,
#[msg("Not authorized")]
NotAuthorized,
#[msg("Randomness Already Revealed")]
RandomnessAlreadyRevealed,
#[msg("Incorrect Randomness Account")]
IncorrectRandomnessAccount,
#[msg("Lottery No tCompleted")]
LotteryNotCompleted,
#[msg("Randomness Not Resolved")]
RandomnessNotResolved,
#[msg("Winner Not Chosen")]
WinnerNotChosen,
#[msg("NotVerified")]
NotVerified,
#[msg("Incorrect Ticket")]
IncorrectTicket,
#[msg("No Ticket")]
NoTicket,
}
Cargo.toml 配置文件
[package]
name = "token_lottery"
version = "0.1.0"
description = "Created with Anchor"
edition = "2021"
[lib]
crate-type = ["cdylib", "lib"]
name = "token_lottery"
[features]
default = []
cpi = ["no-entrypoint"]
no-entrypoint = []
no-idl = []
no-log-ix-name = []
#idl-build = ["anchor-lang/idl-build"]
#添加anchor-spl build
idl-build = ["anchor-lang/idl-build","anchor-spl/idl-build"]
[dependencies]
anchor-lang = "0.31.1"
anchor-spl = { version = "0.31.1",features = ["metadata"] }
switchboard-on-demand = "0.4.9"