Skip to content

Commit 2dc988f

Browse files
authored
Merge pull request #63 from quicknode/claude/token-ext-nftptr
fix(token-extensions/nft-meta-data-pointer): modernize deps + LiteSVM test; re-enable in CI
2 parents ec6e5e4 + b78db00 commit 2dc988f

10 files changed

Lines changed: 348 additions & 75 deletions

File tree

.github/.ghaignore

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +0,0 @@
1-
# dependency issues
2-
tokens/token-extensions/nft-meta-data-pointer/anchor-example/anchor

tokens/token-extensions/nft-meta-data-pointer/anchor-example/anchor/Anchor.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,4 @@ cluster = "localnet"
1313
wallet = "~/.config/solana/id.json"
1414

1515
[scripts]
16-
test = "pnpm ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"
16+
test = "cargo test"
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"name": "extension-nft-anchor",
3+
"version": "1.0.0",
4+
"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).",
5+
"private": true,
6+
"license": "MIT",
7+
"scripts": {
8+
"build": "anchor build",
9+
"test": "anchor test",
10+
"build-and-test": "anchor build && anchor test"
11+
}
12+
}

tokens/token-extensions/nft-meta-data-pointer/anchor-example/anchor/pnpm-lock.yaml

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tokens/token-extensions/nft-meta-data-pointer/anchor-example/anchor/programs/extension_nft/Cargo.toml

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,27 @@ custom-panic = []
2323
[dependencies]
2424
anchor-lang = { version = "1.0.0", features = ["init-if-needed"] }
2525
anchor-spl = { version = "1.0.0" }
26-
# session-keys pinned to 2.0.3 — check compatibility with Anchor 1.0/Solana 3.x
27-
session-keys = { version = "2.0.3", features = ["no-entrypoint"] }
28-
# Removed solana-program pin (=2.1.15) — Anchor 1.0 requires Solana 3.x deps
29-
spl-token-2022 = { version="6", features = [ "no-entrypoint" ] }
30-
spl-token = { version = "4.0.1", features = [ "no-entrypoint" ] }
31-
spl-token-metadata-interface = "0.7.0"
26+
# session-keys 3.1.1 is the first release that supports Anchor >=0.28,<2.0
27+
# (so it builds against Anchor 1.0). Earlier 2.x releases pin Anchor <=0.30
28+
# and fail to compile against the Anchor 1.0 / Solana 3.x API. Provides the
29+
# gasless session-token lesson via `#[session_auth_or]` / `SessionToken`.
30+
session-keys = { version = "3.1.1", features = ["no-entrypoint"] }
31+
# Token-2022 + token-metadata access goes through anchor-spl's bundled
32+
# re-exports (`anchor_spl::token_interface::spl_token_2022`, which is
33+
# `spl-token-2022-interface`, and `anchor_spl::token_2022_extensions::
34+
# spl_token_metadata_interface`). Pinning standalone `spl-token-2022` /
35+
# `spl-token-metadata-interface` here pulls a second copy on a different
36+
# `solana-pubkey` major than anchor-lang/anchor-spl use, which breaks the
37+
# CPI builders with `Pubkey` type mismatches. Relying on anchor-spl's
38+
# re-exports keeps a single, consistent type universe.
39+
40+
[dev-dependencies]
41+
litesvm = "0.11.0"
42+
solana-keypair = "3.0.1"
43+
solana-signer = "3.0.0"
44+
solana-instruction = "3.0.0"
45+
solana-pubkey = "3.0.0"
46+
solana-kite = "0.3.0"
3247

3348
[lints.rust]
3449
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(target_os, values("solana"))'] }

tokens/token-extensions/nft-meta-data-pointer/anchor-example/anchor/programs/extension_nft/src/errors.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
use anchor_lang::error_code;
22

3+
// Anchor's IDL build allows only a single `#[error_code]` enum per program, so
4+
// the game and program-level errors live in one enum.
35
#[error_code]
46
pub enum GameErrorCode {
57
#[msg("Not enough energy")]
68
NotEnoughEnergy,
79
#[msg("Wrong Authority")]
810
WrongAuthority,
9-
}
10-
11-
#[error_code]
12-
pub enum ProgramErrorCode {
1311
#[msg("Invalid Mint account space")]
1412
InvalidMintAccountSpace,
1513
#[msg("Cant initialize metadata_pointer")]

tokens/token-extensions/nft-meta-data-pointer/anchor-example/anchor/programs/extension_nft/src/instructions/chop_tree.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@ pub use crate::errors::GameErrorCode;
22
pub use crate::state::game_data::GameData;
33
use crate::{state::player_data::PlayerData, NftAuthority};
44
use anchor_lang::prelude::*;
5-
use anchor_spl::token_interface::{Token2022};
5+
use anchor_lang::solana_program::program::invoke_signed;
6+
use anchor_spl::token_2022_extensions::spl_token_metadata_interface;
7+
use anchor_spl::token_interface::{spl_token_2022, Token2022};
68
use session_keys::{Session, SessionToken};
7-
use solana_program::program::invoke_signed;
89

910
pub fn chop_tree(context: Context<ChopTree>, counter: u16, amount: u64) -> Result<()> {
1011
// Save game_data bump on first creation (init_if_needed). See init_player.rs
@@ -92,7 +93,7 @@ pub struct ChopTree<'info> {
9293
pub system_program: Program<'info, System>,
9394
/// CHECK: Make sure the ata to the mint is actually owned by the signer
9495
#[account(mut)]
95-
pub mint: AccountInfo<'info>,
96+
pub mint: UncheckedAccount<'info>,
9697
#[account(
9798
init_if_needed,
9899
seeds = [b"nft_authority".as_ref()],

tokens/token-extensions/nft-meta-data-pointer/anchor-example/anchor/programs/extension_nft/src/instructions/mint_nft.rs

Lines changed: 64 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,28 @@
11
pub use crate::constants::TOKEN_METADATA_EXTENSION_SPACE;
22
pub use crate::errors::GameErrorCode;
3-
pub use crate::errors::ProgramErrorCode;
43
pub use crate::state::game_data::GameData;
5-
use anchor_lang::{ prelude::*, system_program };
4+
use anchor_lang::solana_program::program::{invoke, invoke_signed};
5+
use anchor_lang::{prelude::*, system_program};
66
use anchor_spl::{
7-
associated_token::{ self, AssociatedToken },
7+
associated_token::{self, AssociatedToken},
88
token_2022,
9-
token_interface::{ spl_token_2022::instruction::AuthorityType, Token2022 },
9+
token_2022_extensions::spl_token_metadata_interface,
10+
token_interface::{
11+
spl_token_2022::{self, extension::ExtensionType, instruction::AuthorityType, state::Mint},
12+
Token2022,
13+
},
1014
};
11-
use solana_program::program::{ invoke, invoke_signed };
12-
use spl_token_2022::{ extension::ExtensionType, state::Mint };
1315

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

17-
let space = match
18-
ExtensionType::try_calculate_account_len::<Mint>(&[ExtensionType::MetadataPointer])
19-
{
20-
Ok(space) => space,
21-
Err(_) => {
22-
return err!(ProgramErrorCode::InvalidMintAccountSpace);
23-
}
24-
};
19+
let space =
20+
match ExtensionType::try_calculate_account_len::<Mint>(&[ExtensionType::MetadataPointer]) {
21+
Ok(space) => space,
22+
Err(_) => {
23+
return err!(GameErrorCode::InvalidMintAccountSpace);
24+
}
25+
};
2526

2627
// Space required for the inline SPL Token Metadata extension TLV. The
2728
// metadata lives on the mint account itself (not a separate account)
@@ -42,58 +43,67 @@ pub fn handle_mint_nft(context: Context<MintNft>) -> Result<()> {
4243
system_program::CreateAccount {
4344
from: context.accounts.signer.to_account_info(),
4445
to: context.accounts.mint.to_account_info(),
45-
}
46+
},
4647
),
4748
lamports_required,
4849
space as u64,
49-
&context.accounts.token_program.key()
50+
&context.accounts.token_program.key(),
5051
)?;
5152

5253
// Assign the mint to the token program
5354
system_program::assign(
54-
CpiContext::new(context.accounts.token_program.key(), system_program::Assign {
55-
account_to_assign: context.accounts.mint.to_account_info(),
56-
}),
57-
&token_2022::ID
55+
CpiContext::new(
56+
context.accounts.token_program.key(),
57+
system_program::Assign {
58+
account_to_assign: context.accounts.mint.to_account_info(),
59+
},
60+
),
61+
&token_2022::ID,
5862
)?;
5963

6064
// Initialize the metadata pointer (Need to do this before initializing the mint)
61-
let init_meta_data_pointer_ix = match
62-
spl_token_2022::extension::metadata_pointer::instruction::initialize(
65+
let init_meta_data_pointer_ix =
66+
match spl_token_2022::extension::metadata_pointer::instruction::initialize(
6367
&Token2022::id(),
6468
&context.accounts.mint.key(),
6569
Some(context.accounts.nft_authority.key()),
66-
Some(context.accounts.mint.key())
67-
)
68-
{
69-
Ok(ix) => ix,
70-
Err(_) => {
71-
return err!(ProgramErrorCode::CantInitializeMetadataPointer);
72-
}
73-
};
70+
Some(context.accounts.mint.key()),
71+
) {
72+
Ok(ix) => ix,
73+
Err(_) => {
74+
return err!(GameErrorCode::CantInitializeMetadataPointer);
75+
}
76+
};
7477

7578
invoke(
7679
&init_meta_data_pointer_ix,
77-
&[context.accounts.mint.to_account_info(), context.accounts.nft_authority.to_account_info()]
80+
&[
81+
context.accounts.mint.to_account_info(),
82+
context.accounts.nft_authority.to_account_info(),
83+
],
7884
)?;
7985

8086
// Initialize the mint cpi
8187
let mint_cpi_ix = CpiContext::new(
8288
context.accounts.token_program.key(),
8389
token_2022::InitializeMint2 {
8490
mint: context.accounts.mint.to_account_info(),
85-
}
91+
},
8692
);
8793

88-
token_2022::initialize_mint2(mint_cpi_ix, 0, &context.accounts.nft_authority.key(), None).unwrap();
94+
token_2022::initialize_mint2(mint_cpi_ix, 0, &context.accounts.nft_authority.key(), None)
95+
.unwrap();
8996

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

96-
msg!("Init metadata {0}", context.accounts.nft_authority.to_account_info().key);
103+
msg!(
104+
"Init metadata {0}",
105+
context.accounts.nft_authority.to_account_info().key
106+
);
97107

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

110120
invoke_signed(
@@ -113,7 +123,7 @@ pub fn handle_mint_nft(context: Context<MintNft>) -> Result<()> {
113123
context.accounts.mint.to_account_info().clone(),
114124
context.accounts.nft_authority.to_account_info().clone(),
115125
],
116-
signer
126+
signer,
117127
)?;
118128

119129
// Update the metadata account with an additional metadata field in this case the player level
@@ -123,29 +133,27 @@ pub fn handle_mint_nft(context: Context<MintNft>) -> Result<()> {
123133
context.accounts.mint.key,
124134
context.accounts.nft_authority.to_account_info().key,
125135
spl_token_metadata_interface::state::Field::Key("level".to_string()),
126-
"1".to_string()
136+
"1".to_string(),
127137
),
128138
&[
129139
context.accounts.mint.to_account_info().clone(),
130140
context.accounts.nft_authority.to_account_info().clone(),
131141
],
132-
signer
142+
signer,
133143
)?;
134144

135145
// Create the associated token account
136-
associated_token::create(
137-
CpiContext::new(
138-
context.accounts.associated_token_program.key(),
139-
associated_token::Create {
140-
payer: context.accounts.signer.to_account_info(),
141-
associated_token: context.accounts.token_account.to_account_info(),
142-
authority: context.accounts.signer.to_account_info(),
143-
mint: context.accounts.mint.to_account_info(),
144-
system_program: context.accounts.system_program.to_account_info(),
145-
token_program: context.accounts.token_program.to_account_info(),
146-
}
147-
)
148-
)?;
146+
associated_token::create(CpiContext::new(
147+
context.accounts.associated_token_program.key(),
148+
associated_token::Create {
149+
payer: context.accounts.signer.to_account_info(),
150+
associated_token: context.accounts.token_account.to_account_info(),
151+
authority: context.accounts.signer.to_account_info(),
152+
mint: context.accounts.mint.to_account_info(),
153+
system_program: context.accounts.system_program.to_account_info(),
154+
token_program: context.accounts.token_program.to_account_info(),
155+
},
156+
))?;
149157

150158
// Mint one token to the associated token account of the player
151159
token_2022::mint_to(
@@ -156,9 +164,9 @@ pub fn handle_mint_nft(context: Context<MintNft>) -> Result<()> {
156164
to: context.accounts.token_account.to_account_info(),
157165
authority: context.accounts.nft_authority.to_account_info(),
158166
},
159-
signer
167+
signer,
160168
),
161-
1
169+
1,
162170
)?;
163171

164172
// Freeze the mint authority so no more tokens can be minted to make it an NFT
@@ -169,10 +177,10 @@ pub fn handle_mint_nft(context: Context<MintNft>) -> Result<()> {
169177
current_authority: context.accounts.nft_authority.to_account_info(),
170178
account_or_mint: context.accounts.mint.to_account_info(),
171179
},
172-
signer
180+
signer,
173181
),
174182
AuthorityType::MintTokens,
175-
None
183+
None,
176184
)?;
177185

178186
Ok(())
@@ -186,7 +194,7 @@ pub struct MintNft<'info> {
186194
pub token_program: Program<'info, Token2022>,
187195
/// CHECK: We will create this one for the user
188196
#[account(mut)]
189-
pub token_account: AccountInfo<'info>,
197+
pub token_account: UncheckedAccount<'info>,
190198
#[account(mut)]
191199
pub mint: Signer<'info>,
192200
pub rent: Sysvar<'info, Rent>,

tokens/token-extensions/nft-meta-data-pointer/anchor-example/anchor/programs/extension_nft/src/lib.rs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
// The Anchor `#[program]` macro expands to code that clippy flags as a
2+
// diverging sub-expression; this allow is the accepted workaround in this repo.
3+
#![allow(clippy::diverging_sub_expression)]
4+
15
pub use crate::errors::GameErrorCode;
26
pub use anchor_lang::prelude::*;
37
pub use session_keys::{session_auth_or, Session, SessionError};
@@ -28,12 +32,15 @@ pub mod extension_nft {
2832
// lets the player either use their session token or their main wallet. (The counter is only
2933
// there so that the player can do multiple transactions in the same block. Without it multiple transactions
3034
// in the same block would result in the same signature and therefore fail.)
35+
// NOTE: the `#[session_auth_or]` macro injects code that refers to the
36+
// context binding by the literal name `ctx`, so this handler's context
37+
// parameter must be named `ctx` (not `context`) for the macro to expand.
3138
#[session_auth_or(
32-
context.accounts.player.authority.key() == context.accounts.signer.key(),
39+
ctx.accounts.player.authority.key() == ctx.accounts.signer.key(),
3340
GameErrorCode::WrongAuthority
3441
)]
35-
pub fn chop_tree(context: Context<ChopTree>, _level_seed: String, counter: u16) -> Result<()> {
36-
chop_tree::chop_tree(context, counter, 1)
42+
pub fn chop_tree(ctx: Context<ChopTree>, _level_seed: String, counter: u16) -> Result<()> {
43+
chop_tree::chop_tree(ctx, counter, 1)
3744
}
3845

3946
pub fn mint_nft(context: Context<MintNft>) -> Result<()> {

0 commit comments

Comments
 (0)