Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .github/.ghaignore
Original file line number Diff line number Diff line change
@@ -1,2 +0,0 @@
# dependency issues
tokens/token-extensions/nft-meta-data-pointer/anchor-example/anchor
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@ cluster = "localnet"
wallet = "~/.config/solana/id.json"

[scripts]
test = "pnpm ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"
test = "cargo test"
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "extension-nft-anchor",
"version": "1.0.0",
"description": "Anchor 'chop tree' game minting Token-2022 NFTs with the metadata-pointer extension. Tested with a Rust LiteSVM integration test (anchor test -> cargo test).",
"private": true,
"license": "MIT",
"scripts": {
"build": "anchor build",
"test": "anchor test",
"build-and-test": "anchor build && anchor test"
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,27 @@ custom-panic = []
[dependencies]
anchor-lang = { version = "1.0.0", features = ["init-if-needed"] }
anchor-spl = { version = "1.0.0" }
# session-keys pinned to 2.0.3 — check compatibility with Anchor 1.0/Solana 3.x
session-keys = { version = "2.0.3", features = ["no-entrypoint"] }
# Removed solana-program pin (=2.1.15) — Anchor 1.0 requires Solana 3.x deps
spl-token-2022 = { version="6", features = [ "no-entrypoint" ] }
spl-token = { version = "4.0.1", features = [ "no-entrypoint" ] }
spl-token-metadata-interface = "0.7.0"
# session-keys 3.1.1 is the first release that supports Anchor >=0.28,<2.0
# (so it builds against Anchor 1.0). Earlier 2.x releases pin Anchor <=0.30
# and fail to compile against the Anchor 1.0 / Solana 3.x API. Provides the
# gasless session-token lesson via `#[session_auth_or]` / `SessionToken`.
session-keys = { version = "3.1.1", features = ["no-entrypoint"] }
# Token-2022 + token-metadata access goes through anchor-spl's bundled
# re-exports (`anchor_spl::token_interface::spl_token_2022`, which is
# `spl-token-2022-interface`, and `anchor_spl::token_2022_extensions::
# spl_token_metadata_interface`). Pinning standalone `spl-token-2022` /
# `spl-token-metadata-interface` here pulls a second copy on a different
# `solana-pubkey` major than anchor-lang/anchor-spl use, which breaks the
# CPI builders with `Pubkey` type mismatches. Relying on anchor-spl's
# re-exports keeps a single, consistent type universe.

[dev-dependencies]
litesvm = "0.11.0"
solana-keypair = "3.0.1"
solana-signer = "3.0.0"
solana-instruction = "3.0.0"
solana-pubkey = "3.0.0"
solana-kite = "0.3.0"

[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(target_os, values("solana"))'] }
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
use anchor_lang::error_code;

// Anchor's IDL build allows only a single `#[error_code]` enum per program, so
// the game and program-level errors live in one enum.
#[error_code]
pub enum GameErrorCode {
#[msg("Not enough energy")]
NotEnoughEnergy,
#[msg("Wrong Authority")]
WrongAuthority,
}

#[error_code]
pub enum ProgramErrorCode {
#[msg("Invalid Mint account space")]
InvalidMintAccountSpace,
#[msg("Cant initialize metadata_pointer")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ pub use crate::errors::GameErrorCode;
pub use crate::state::game_data::GameData;
use crate::{state::player_data::PlayerData, NftAuthority};
use anchor_lang::prelude::*;
use anchor_spl::token_interface::{Token2022};
use anchor_lang::solana_program::program::invoke_signed;
use anchor_spl::token_2022_extensions::spl_token_metadata_interface;
use anchor_spl::token_interface::{spl_token_2022, Token2022};
use session_keys::{Session, SessionToken};
use solana_program::program::invoke_signed;

pub fn chop_tree(context: Context<ChopTree>, counter: u16, amount: u64) -> Result<()> {
// Save game_data bump on first creation (init_if_needed). See init_player.rs
Expand Down Expand Up @@ -92,7 +93,7 @@ pub struct ChopTree<'info> {
pub system_program: Program<'info, System>,
/// CHECK: Make sure the ata to the mint is actually owned by the signer
#[account(mut)]
pub mint: AccountInfo<'info>,
pub mint: UncheckedAccount<'info>,
#[account(
init_if_needed,
seeds = [b"nft_authority".as_ref()],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,28 @@
pub use crate::constants::TOKEN_METADATA_EXTENSION_SPACE;
pub use crate::errors::GameErrorCode;
pub use crate::errors::ProgramErrorCode;
pub use crate::state::game_data::GameData;
use anchor_lang::{ prelude::*, system_program };
use anchor_lang::solana_program::program::{invoke, invoke_signed};
use anchor_lang::{prelude::*, system_program};
use anchor_spl::{
associated_token::{ self, AssociatedToken },
associated_token::{self, AssociatedToken},
token_2022,
token_interface::{ spl_token_2022::instruction::AuthorityType, Token2022 },
token_2022_extensions::spl_token_metadata_interface,
token_interface::{
spl_token_2022::{self, extension::ExtensionType, instruction::AuthorityType, state::Mint},
Token2022,
},
};
use solana_program::program::{ invoke, invoke_signed };
use spl_token_2022::{ extension::ExtensionType, state::Mint };

pub fn handle_mint_nft(context: Context<MintNft>) -> Result<()> {
msg!("Mint nft with meta data extension and additional meta data");

let space = match
ExtensionType::try_calculate_account_len::<Mint>(&[ExtensionType::MetadataPointer])
{
Ok(space) => space,
Err(_) => {
return err!(ProgramErrorCode::InvalidMintAccountSpace);
}
};
let space =
match ExtensionType::try_calculate_account_len::<Mint>(&[ExtensionType::MetadataPointer]) {
Ok(space) => space,
Err(_) => {
return err!(GameErrorCode::InvalidMintAccountSpace);
}
};

// Space required for the inline SPL Token Metadata extension TLV. The
// metadata lives on the mint account itself (not a separate account)
Expand All @@ -42,58 +43,67 @@ pub fn handle_mint_nft(context: Context<MintNft>) -> Result<()> {
system_program::CreateAccount {
from: context.accounts.signer.to_account_info(),
to: context.accounts.mint.to_account_info(),
}
},
),
lamports_required,
space as u64,
&context.accounts.token_program.key()
&context.accounts.token_program.key(),
)?;

// Assign the mint to the token program
system_program::assign(
CpiContext::new(context.accounts.token_program.key(), system_program::Assign {
account_to_assign: context.accounts.mint.to_account_info(),
}),
&token_2022::ID
CpiContext::new(
context.accounts.token_program.key(),
system_program::Assign {
account_to_assign: context.accounts.mint.to_account_info(),
},
),
&token_2022::ID,
)?;

// Initialize the metadata pointer (Need to do this before initializing the mint)
let init_meta_data_pointer_ix = match
spl_token_2022::extension::metadata_pointer::instruction::initialize(
let init_meta_data_pointer_ix =
match spl_token_2022::extension::metadata_pointer::instruction::initialize(
&Token2022::id(),
&context.accounts.mint.key(),
Some(context.accounts.nft_authority.key()),
Some(context.accounts.mint.key())
)
{
Ok(ix) => ix,
Err(_) => {
return err!(ProgramErrorCode::CantInitializeMetadataPointer);
}
};
Some(context.accounts.mint.key()),
) {
Ok(ix) => ix,
Err(_) => {
return err!(GameErrorCode::CantInitializeMetadataPointer);
}
};

invoke(
&init_meta_data_pointer_ix,
&[context.accounts.mint.to_account_info(), context.accounts.nft_authority.to_account_info()]
&[
context.accounts.mint.to_account_info(),
context.accounts.nft_authority.to_account_info(),
],
)?;

// Initialize the mint cpi
let mint_cpi_ix = CpiContext::new(
context.accounts.token_program.key(),
token_2022::InitializeMint2 {
mint: context.accounts.mint.to_account_info(),
}
},
);

token_2022::initialize_mint2(mint_cpi_ix, 0, &context.accounts.nft_authority.key(), None).unwrap();
token_2022::initialize_mint2(mint_cpi_ix, 0, &context.accounts.nft_authority.key(), None)
.unwrap();

// We use a PDA as a mint authority for the metadata account because
// we want to be able to update the NFT from the program.
let seeds = b"nft_authority";
let bump = context.bumps.nft_authority;
let signer: &[&[&[u8]]] = &[&[seeds, &[bump]]];

msg!("Init metadata {0}", context.accounts.nft_authority.to_account_info().key);
msg!(
"Init metadata {0}",
context.accounts.nft_authority.to_account_info().key
);

// Init the metadata account
let init_token_meta_data_ix = &spl_token_metadata_interface::instruction::initialize(
Expand All @@ -104,7 +114,7 @@ pub fn handle_mint_nft(context: Context<MintNft>) -> Result<()> {
context.accounts.nft_authority.to_account_info().key,
"Beaver".to_string(),
"BVA".to_string(),
"https://arweave.net/MHK3Iopy0GgvDoM7LkkiAdg7pQqExuuWvedApCnzfj0".to_string()
"https://arweave.net/MHK3Iopy0GgvDoM7LkkiAdg7pQqExuuWvedApCnzfj0".to_string(),
);

invoke_signed(
Expand All @@ -113,7 +123,7 @@ pub fn handle_mint_nft(context: Context<MintNft>) -> Result<()> {
context.accounts.mint.to_account_info().clone(),
context.accounts.nft_authority.to_account_info().clone(),
],
signer
signer,
)?;

// Update the metadata account with an additional metadata field in this case the player level
Expand All @@ -123,29 +133,27 @@ pub fn handle_mint_nft(context: Context<MintNft>) -> Result<()> {
context.accounts.mint.key,
context.accounts.nft_authority.to_account_info().key,
spl_token_metadata_interface::state::Field::Key("level".to_string()),
"1".to_string()
"1".to_string(),
),
&[
context.accounts.mint.to_account_info().clone(),
context.accounts.nft_authority.to_account_info().clone(),
],
signer
signer,
)?;

// Create the associated token account
associated_token::create(
CpiContext::new(
context.accounts.associated_token_program.key(),
associated_token::Create {
payer: context.accounts.signer.to_account_info(),
associated_token: context.accounts.token_account.to_account_info(),
authority: context.accounts.signer.to_account_info(),
mint: context.accounts.mint.to_account_info(),
system_program: context.accounts.system_program.to_account_info(),
token_program: context.accounts.token_program.to_account_info(),
}
)
)?;
associated_token::create(CpiContext::new(
context.accounts.associated_token_program.key(),
associated_token::Create {
payer: context.accounts.signer.to_account_info(),
associated_token: context.accounts.token_account.to_account_info(),
authority: context.accounts.signer.to_account_info(),
mint: context.accounts.mint.to_account_info(),
system_program: context.accounts.system_program.to_account_info(),
token_program: context.accounts.token_program.to_account_info(),
},
))?;

// Mint one token to the associated token account of the player
token_2022::mint_to(
Expand All @@ -156,9 +164,9 @@ pub fn handle_mint_nft(context: Context<MintNft>) -> Result<()> {
to: context.accounts.token_account.to_account_info(),
authority: context.accounts.nft_authority.to_account_info(),
},
signer
signer,
),
1
1,
)?;

// Freeze the mint authority so no more tokens can be minted to make it an NFT
Expand All @@ -169,10 +177,10 @@ pub fn handle_mint_nft(context: Context<MintNft>) -> Result<()> {
current_authority: context.accounts.nft_authority.to_account_info(),
account_or_mint: context.accounts.mint.to_account_info(),
},
signer
signer,
),
AuthorityType::MintTokens,
None
None,
)?;

Ok(())
Expand All @@ -186,7 +194,7 @@ pub struct MintNft<'info> {
pub token_program: Program<'info, Token2022>,
/// CHECK: We will create this one for the user
#[account(mut)]
pub token_account: AccountInfo<'info>,
pub token_account: UncheckedAccount<'info>,
#[account(mut)]
pub mint: Signer<'info>,
pub rent: Sysvar<'info, Rent>,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
// The Anchor `#[program]` macro expands to code that clippy flags as a
// diverging sub-expression; this allow is the accepted workaround in this repo.
#![allow(clippy::diverging_sub_expression)]

pub use crate::errors::GameErrorCode;
pub use anchor_lang::prelude::*;
pub use session_keys::{session_auth_or, Session, SessionError};
Expand Down Expand Up @@ -28,12 +32,15 @@ pub mod extension_nft {
// lets the player either use their session token or their main wallet. (The counter is only
// there so that the player can do multiple transactions in the same block. Without it multiple transactions
// in the same block would result in the same signature and therefore fail.)
// NOTE: the `#[session_auth_or]` macro injects code that refers to the
// context binding by the literal name `ctx`, so this handler's context
// parameter must be named `ctx` (not `context`) for the macro to expand.
#[session_auth_or(
context.accounts.player.authority.key() == context.accounts.signer.key(),
ctx.accounts.player.authority.key() == ctx.accounts.signer.key(),
GameErrorCode::WrongAuthority
)]
pub fn chop_tree(context: Context<ChopTree>, _level_seed: String, counter: u16) -> Result<()> {
chop_tree::chop_tree(context, counter, 1)
pub fn chop_tree(ctx: Context<ChopTree>, _level_seed: String, counter: u16) -> Result<()> {
chop_tree::chop_tree(ctx, counter, 1)
}

pub fn mint_nft(context: Context<MintNft>) -> Result<()> {
Expand Down
Loading
Loading