From f22c5167964e1f65b06729c4fd5aee8bab7106ad Mon Sep 17 00:00:00 2001 From: Joe Date: Tue, 17 Oct 2023 16:55:20 +0200 Subject: [PATCH] add editions processor example --- token-group/example/Cargo.toml | 2 +- token-group/example/src/processor.rs | 136 +++++++- .../example/tests/initialize_collection.rs | 8 +- .../tests/initialize_collection_member.rs | 2 + .../example/tests/initialize_edition.rs | 314 ++++++++++++++++++ token-group/example/tests/setup.rs | 44 ++- .../tests/update_collection_authority.rs | 8 +- .../tests/update_collection_max_size.rs | 16 +- 8 files changed, 520 insertions(+), 10 deletions(-) create mode 100644 token-group/example/tests/initialize_edition.rs diff --git a/token-group/example/Cargo.toml b/token-group/example/Cargo.toml index 37f44c4b9fa..227d5e4cde1 100644 --- a/token-group/example/Cargo.toml +++ b/token-group/example/Cargo.toml @@ -16,6 +16,7 @@ solana-program = "1.16.16" spl-pod = { version = "0.1.0", path = "../../libraries/pod" } spl-token-2022 = { version = "0.9.0", path = "../../token/program-2022" } spl-token-group-interface = { version = "0.1.0", path = "../interface" } +spl-token-metadata-interface = { version = "0.2", path = "../../token-metadata/interface" } spl-type-length-value = { version = "0.3.0", path = "../../libraries/type-length-value" } [dev-dependencies] @@ -23,7 +24,6 @@ solana-program-test = "1.16.16" solana-sdk = "1.16.16" spl-discriminator = { version = "0.1.0", path = "../../libraries/discriminator" } spl-token-client = { version = "0.7", path = "../../token/client" } -spl-token-metadata-interface = { version = "0.2", path = "../../token-metadata/interface" } [lib] crate-type = ["cdylib", "lib"] diff --git a/token-group/example/src/processor.rs b/token-group/example/src/processor.rs index de23ff72f98..ef74ad5738f 100644 --- a/token-group/example/src/processor.rs +++ b/token-group/example/src/processor.rs @@ -5,12 +5,18 @@ use { account_info::{next_account_info, AccountInfo}, entrypoint::ProgramResult, msg, + program::invoke, program_error::ProgramError, program_option::COption, pubkey::Pubkey, }, spl_pod::optional_keys::OptionalNonZeroPubkey, - spl_token_2022::{extension::StateWithExtensions, state::Mint}, + spl_token_2022::{ + extension::{ + metadata_pointer::MetadataPointer, BaseStateWithExtensions, StateWithExtensions, + }, + state::Mint, + }, spl_token_group_interface::{ error::TokenGroupError, instruction::{ @@ -18,6 +24,7 @@ use { }, state::{TokenGroup, TokenGroupMember}, }, + spl_token_metadata_interface::state::TokenMetadata, spl_type_length_value::state::TlvStateMut, }; @@ -201,6 +208,122 @@ pub fn process_initialize_collection_member( Ok(()) } +/// Processes a [InitializeMember](enum.GroupInterfaceInstruction.html) +/// instruction for an `Edition`. +/// +/// This function demonstrates using this interface for editions as well. +fn process_initialize_edition_reprint( + _program_id: &Pubkey, + accounts: &[AccountInfo], +) -> ProgramResult { + // Here we are going to assume the original has been created and + // initialized as a group, then we can use the original to print "reprints" + // from it. + // We're also assuming a mint _and_ metadata have been created for _both_ + // the original and the reprint. + let account_info_iter = &mut accounts.iter(); + + // Accounts expected by this instruction: + // + // 0. `[w]` Reprint (Member) + // 1. `[]` Reprint (Member) Mint + // 2. `[s]` Reprint (Member) Mint authority + // 3. `[w]` Original (Group) + // 4. `[s]` Original (Group) update authority + let reprint_info = next_account_info(account_info_iter)?; + // Note this particular example _also_ requires the mint to be writable! + let reprint_mint_info = next_account_info(account_info_iter)?; + let reprint_mint_authority_info = next_account_info(account_info_iter)?; + let original_info = next_account_info(account_info_iter)?; + let original_update_authority_info = next_account_info(account_info_iter)?; + + // Additional accounts expected by this instruction: + // + // 5. `[]` Original (Group) Mint + // 6. `[]` SPL Token 2022 program + let original_mint_info = next_account_info(account_info_iter)?; + let _program_2022_info = next_account_info(account_info_iter)?; + + // Mint & metadata checks on the original + let original_token_metadata = { + // IMPORTANT: this example program is designed to work with any + // program that implements the SPL token interface, so there is no + // ownership check on the mint account. + let original_mint_data = original_mint_info.try_borrow_data()?; + let original_mint = StateWithExtensions::::unpack(&original_mint_data)?; + + // Make sure the metadata pointer is pointing to the mint itself + let metadata_pointer = original_mint.get_extension::()?; + let metadata_pointer_address = Option::::from(metadata_pointer.metadata_address); + if metadata_pointer_address != Some(*original_mint_info.key) { + return Err(ProgramError::InvalidAccountData); + } + + // Extract the token metadata + original_mint.get_variable_len_extension::()? + }; + + // Mint checks on the reprint + { + // IMPORTANT: this example program is designed to work with any + // program that implements the SPL token interface, so there is no + // ownership check on the mint account. + let reprint_mint_data = reprint_mint_info.try_borrow_data()?; + let reprint_mint = StateWithExtensions::::unpack(&reprint_mint_data)?; + + if !reprint_mint_authority_info.is_signer { + return Err(ProgramError::MissingRequiredSignature); + } + if reprint_mint.base.mint_authority.as_ref() + != COption::Some(reprint_mint_authority_info.key) + { + return Err(TokenGroupError::IncorrectAuthority.into()); + } + + // Make sure the metadata pointer is pointing to the mint itself + let metadata_pointer = reprint_mint.get_extension::()?; + let metadata_pointer_address = Option::::from(metadata_pointer.metadata_address); + if metadata_pointer_address != Some(*reprint_mint_info.key) { + return Err(ProgramError::InvalidAccountData); + } + } + + // Increment the size of the editions + let mut buffer = original_info.try_borrow_mut_data()?; + let mut state = TlvStateMut::unpack(&mut buffer)?; + let original = state.get_first_value_mut::()?; + + check_update_authority(original_update_authority_info, &original.update_authority)?; + let reprint_number = original.increment_size()?; + + // Allocate a TLV entry for the space and write it in + let mut buffer = reprint_info.try_borrow_mut_data()?; + let mut state = TlvStateMut::unpack(&mut buffer)?; + let (reprint, _) = state.init_value::(false)?; + *reprint = TokenGroupMember::new(*original_info.key, reprint_number); + + // Use the original metadata to initialize the reprint metadata + let cpi_instruction = spl_token_metadata_interface::instruction::initialize( + &spl_token_2022::id(), + reprint_mint_info.key, + original_update_authority_info.key, + reprint_mint_info.key, + reprint_mint_authority_info.key, + original_token_metadata.name, + original_token_metadata.symbol, + original_token_metadata.uri, + ); + let cpi_account_infos = &[ + reprint_mint_info.clone(), + original_update_authority_info.clone(), + reprint_mint_info.clone(), + reprint_mint_authority_info.clone(), + ]; + invoke(&cpi_instruction, cpi_account_infos)?; + + Ok(()) +} + /// Processes an `SplTokenGroupInstruction` pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], input: &[u8]) -> ProgramResult { let instruction = TokenGroupInstruction::unpack(input)?; @@ -218,8 +341,15 @@ pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], input: &[u8]) -> P process_update_collection_authority(program_id, accounts, data) } TokenGroupInstruction::InitializeMember(_) => { - msg!("Instruction: InitializeCollectionMember"); - process_initialize_collection_member(program_id, accounts) + // For demonstration purposes, we'll use the number of accounts + // provided to determine which type of member to initialize. + if accounts.len() == 5 { + msg!("Instruction: InitializeCollectionMember"); + process_initialize_collection_member(program_id, accounts) + } else { + msg!("Instruction: InitializeEdition"); + process_initialize_edition_reprint(program_id, accounts) + } } } } diff --git a/token-group/example/tests/initialize_collection.rs b/token-group/example/tests/initialize_collection.rs index c49a12a2288..fc5a43612f6 100644 --- a/token-group/example/tests/initialize_collection.rs +++ b/token-group/example/tests/initialize_collection.rs @@ -41,7 +41,13 @@ async fn test_initialize_collection() { Some(0), payer.clone(), ); - setup_mint(&token_client, &collection_mint, &collection_mint_authority).await; + setup_mint( + &token_client, + &collection_mint, + &collection_mint_authority, + vec![], + ) + .await; let mut context = context.lock().await; diff --git a/token-group/example/tests/initialize_collection_member.rs b/token-group/example/tests/initialize_collection_member.rs index 7ad6c0bfeff..7c0a444d479 100644 --- a/token-group/example/tests/initialize_collection_member.rs +++ b/token-group/example/tests/initialize_collection_member.rs @@ -50,6 +50,7 @@ async fn test_initialize_collection_member() { ), &collection_mint, &collection_mint_authority, + vec![], ) .await; setup_mint( @@ -62,6 +63,7 @@ async fn test_initialize_collection_member() { ), &member_mint, &member_mint_authority, + vec![], ) .await; diff --git a/token-group/example/tests/initialize_edition.rs b/token-group/example/tests/initialize_edition.rs new file mode 100644 index 00000000000..262273ca9aa --- /dev/null +++ b/token-group/example/tests/initialize_edition.rs @@ -0,0 +1,314 @@ +#![cfg(feature = "test-sbf")] + +mod setup; + +use { + setup::{setup_mint, setup_mint_and_metadata, setup_program_test}, + solana_program::{ + borsh0_10::get_instance_packed_len, + instruction::{AccountMeta, Instruction, InstructionError}, + pubkey::Pubkey, + system_instruction, + }, + solana_program_test::tokio, + solana_sdk::{ + signature::Keypair, + signer::Signer, + transaction::{Transaction, TransactionError}, + }, + spl_token_2022::{ + extension::{BaseStateWithExtensions, StateWithExtensions}, + state::Mint, + }, + spl_token_client::token::{ExtensionInitializationParams, Token}, + spl_token_group_interface::{ + instruction::{initialize_group, initialize_member}, + state::{TokenGroup, TokenGroupMember}, + }, + spl_token_metadata_interface::state::TokenMetadata, + spl_type_length_value::state::{TlvState, TlvStateBorrowed}, +}; + +fn initialize_edition_reprint( + program_id: &Pubkey, + reprint: &Pubkey, + reprint_mint: &Pubkey, + reprint_mint_authority: &Pubkey, + original: &Pubkey, + original_update_authority: &Pubkey, + original_mint: &Pubkey, +) -> Instruction { + let mut ix = initialize_member( + program_id, + reprint, + reprint_mint, + reprint_mint_authority, + original, + original_update_authority, + ); + // Our program requires the reprint mint to be writable + ix.accounts[1].is_writable = true; + ix.accounts.extend_from_slice(&[ + AccountMeta::new_readonly(*original_mint, false), + AccountMeta::new_readonly(spl_token_2022::id(), false), + ]); + ix +} + +#[tokio::test] +async fn test_initialize_edition_reprint() { + let program_id = Pubkey::new_unique(); + let original = Keypair::new(); + let original_mint = Keypair::new(); + let original_mint_authority = Keypair::new(); + let original_update_authority = Keypair::new(); + let reprint = Keypair::new(); + let reprint_mint = Keypair::new(); + let reprint_mint_authority = Keypair::new(); + + let original_metadata_state = TokenMetadata { + update_authority: None.try_into().unwrap(), + mint: original_mint.pubkey(), + name: "The Coolest Collection".to_string(), + symbol: "COOL".to_string(), + uri: "https://cool.com".to_string(), + additional_metadata: vec![], + }; + let original_group_state = TokenGroup { + update_authority: Some(original_update_authority.pubkey()).try_into().unwrap(), + size: 30.into(), + max_size: 50.into(), + }; + + let (context, client, payer) = setup_program_test(&program_id).await; + + setup_mint_and_metadata( + &Token::new( + client.clone(), + &spl_token_2022::id(), + &original_mint.pubkey(), + Some(0), + payer.clone(), + ), + &original_mint, + &original_mint_authority, + &original_metadata_state, + payer.clone(), + ) + .await; + // Add the metadata pointer extension ahead of time + setup_mint( + &Token::new( + client.clone(), + &spl_token_2022::id(), + &reprint_mint.pubkey(), + Some(0), + payer.clone(), + ), + &reprint_mint, + &reprint_mint_authority, + vec![ExtensionInitializationParams::MetadataPointer { + authority: Some(reprint_mint_authority.pubkey()), + metadata_address: Some(reprint_mint.pubkey()), + }], + ) + .await; + + let mut context = context.lock().await; + + let rent = context.banks_client.get_rent().await.unwrap(); + let space = TlvStateBorrowed::get_base_len() + std::mem::size_of::(); + let rent_lamports = rent.minimum_balance(space); + + let transaction = Transaction::new_signed_with_payer( + &[ + system_instruction::create_account( + &context.payer.pubkey(), + &original.pubkey(), + rent_lamports, + space.try_into().unwrap(), + &program_id, + ), + initialize_group( + &program_id, + &original.pubkey(), + &original_mint.pubkey(), + &original_mint_authority.pubkey(), + original_group_state.update_authority.try_into().unwrap(), + original_group_state.max_size.into(), + ), + ], + Some(&context.payer.pubkey()), + &[&context.payer, &original_mint_authority, &original], + context.last_blockhash, + ); + context + .banks_client + .process_transaction(transaction) + .await + .unwrap(); + + let metadata_space = TlvStateBorrowed::get_base_len() + + get_instance_packed_len(&original_metadata_state).unwrap(); + let metadata_rent_lamports = rent.minimum_balance(metadata_space); + + let reprint_space = TlvStateBorrowed::get_base_len() + std::mem::size_of::(); + let reprint_rent_lamports = rent.minimum_balance(reprint_space); + + // Fail: reprint mint authority not signer + let mut init_reprint_ix = initialize_edition_reprint( + &program_id, + &reprint.pubkey(), + &reprint_mint.pubkey(), + &reprint_mint_authority.pubkey(), + &original.pubkey(), + &original_update_authority.pubkey(), + &original_mint.pubkey(), + ); + init_reprint_ix.accounts[2].is_signer = false; + let transaction = Transaction::new_signed_with_payer( + &[ + system_instruction::create_account( + &context.payer.pubkey(), + &reprint.pubkey(), + reprint_rent_lamports, + reprint_space.try_into().unwrap(), + &program_id, + ), + // Fund the mint with rent for metadata + system_instruction::transfer( + &context.payer.pubkey(), + &reprint_mint.pubkey(), + metadata_rent_lamports, + ), + init_reprint_ix, + ], + Some(&context.payer.pubkey()), + &[&context.payer, &reprint, &original_update_authority], + context.last_blockhash, + ); + assert_eq!( + context + .banks_client + .process_transaction(transaction) + .await + .unwrap_err() + .unwrap(), + TransactionError::InstructionError(2, InstructionError::MissingRequiredSignature) + ); + + // Fail: group update authority not signer + let mut init_reprint_ix = initialize_edition_reprint( + &program_id, + &reprint.pubkey(), + &reprint_mint.pubkey(), + &reprint_mint_authority.pubkey(), + &original.pubkey(), + &original_update_authority.pubkey(), + &original_mint.pubkey(), + ); + init_reprint_ix.accounts[4].is_signer = false; + let transaction = Transaction::new_signed_with_payer( + &[ + system_instruction::create_account( + &context.payer.pubkey(), + &reprint.pubkey(), + reprint_rent_lamports, + reprint_space.try_into().unwrap(), + &program_id, + ), + // Fund the mint with rent for metadata + system_instruction::transfer( + &context.payer.pubkey(), + &reprint_mint.pubkey(), + metadata_rent_lamports, + ), + init_reprint_ix, + ], + Some(&context.payer.pubkey()), + &[&context.payer, &reprint, &reprint_mint_authority], + context.last_blockhash, + ); + assert_eq!( + context + .banks_client + .process_transaction(transaction) + .await + .unwrap_err() + .unwrap(), + TransactionError::InstructionError(2, InstructionError::MissingRequiredSignature) + ); + + // Success: initialize edition reprint + let transaction = Transaction::new_signed_with_payer( + &[ + system_instruction::create_account( + &context.payer.pubkey(), + &reprint.pubkey(), + reprint_rent_lamports, + reprint_space.try_into().unwrap(), + &program_id, + ), + // Fund the mint with rent for metadata + system_instruction::transfer( + &context.payer.pubkey(), + &reprint_mint.pubkey(), + metadata_rent_lamports, + ), + initialize_edition_reprint( + &program_id, + &reprint.pubkey(), + &reprint_mint.pubkey(), + &reprint_mint_authority.pubkey(), + &original.pubkey(), + &original_update_authority.pubkey(), + &original_mint.pubkey(), + ), + ], + Some(&context.payer.pubkey()), + &[ + &context.payer, + &reprint, + &reprint_mint_authority, + &original_update_authority, + ], + context.last_blockhash, + ); + context + .banks_client + .process_transaction(transaction) + .await + .unwrap(); + + // Fetch the reprint account and ensure it matches our state + let reprint_account = context + .banks_client + .get_account(reprint.pubkey()) + .await + .unwrap() + .unwrap(); + let fetched_meta = TlvStateBorrowed::unpack(&reprint_account.data).unwrap(); + let fetched_original_reprint_state = + fetched_meta.get_first_value::().unwrap(); + assert_eq!(fetched_original_reprint_state.group, original.pubkey()); + assert_eq!(u32::from(fetched_original_reprint_state.member_number), 1); + + // Fetch the reprint's metadata and ensure it matches our original + let reprint_mint_account = context + .banks_client + .get_account(reprint_mint.pubkey()) + .await + .unwrap() + .unwrap(); + let fetched_reprint_meta = + StateWithExtensions::::unpack(&reprint_mint_account.data).unwrap(); + let fetched_reprint_metadata = fetched_reprint_meta + .get_variable_len_extension::() + .unwrap(); + assert_eq!(fetched_reprint_metadata.name, original_metadata_state.name); + assert_eq!( + fetched_reprint_metadata.symbol, + original_metadata_state.symbol + ); + assert_eq!(fetched_reprint_metadata.uri, original_metadata_state.uri); +} diff --git a/token-group/example/tests/setup.rs b/token-group/example/tests/setup.rs index 14cb091830c..73187287f39 100644 --- a/token-group/example/tests/setup.rs +++ b/token-group/example/tests/setup.rs @@ -8,8 +8,9 @@ use { ProgramBanksClient, ProgramBanksClientProcessTransaction, ProgramClient, SendTransaction, SimulateTransaction, }, - token::Token, + token::{ExtensionInitializationParams, Token}, }, + spl_token_metadata_interface::state::TokenMetadata, std::sync::Arc, }; @@ -48,14 +49,53 @@ pub async fn setup_mint( token_client: &Token, mint_keypair: &Keypair, mint_authority_keypair: &Keypair, + extensions: Vec, ) { token_client .create_mint( &mint_authority_keypair.pubkey(), None, - vec![], + extensions, &[mint_keypair], ) .await .unwrap(); } + +/// Set up a Token-2022 mint and metadata +/// +/// Note: Not every test uses this function, so we need to ignore the +/// lint warning. +#[allow(dead_code)] +pub async fn setup_mint_and_metadata( + token_client: &Token, + mint_keypair: &Keypair, + mint_authority_keypair: &Keypair, + token_metadata: &TokenMetadata, + payer: Arc, +) { + token_client + .create_mint( + &mint_authority_keypair.pubkey(), + None, + vec![ExtensionInitializationParams::MetadataPointer { + authority: Some(mint_authority_keypair.pubkey()), + metadata_address: Some(mint_keypair.pubkey()), + }], + &[mint_keypair], + ) + .await + .unwrap(); + token_client + .token_metadata_initialize_with_rent_transfer( + &payer.pubkey(), + &mint_authority_keypair.pubkey(), // Also the metadata update authority + &mint_authority_keypair.pubkey(), + token_metadata.name.clone(), + token_metadata.symbol.clone(), + token_metadata.uri.clone(), + &[&payer, mint_authority_keypair], + ) + .await + .unwrap(); +} diff --git a/token-group/example/tests/update_collection_authority.rs b/token-group/example/tests/update_collection_authority.rs index a57a5818a5d..2135d37ce9a 100644 --- a/token-group/example/tests/update_collection_authority.rs +++ b/token-group/example/tests/update_collection_authority.rs @@ -45,7 +45,13 @@ async fn test_update_collection_authority() { Some(0), payer.clone(), ); - setup_mint(&token_client, &collection_mint, &collection_mint_authority).await; + setup_mint( + &token_client, + &collection_mint, + &collection_mint_authority, + vec![], + ) + .await; let mut context = context.lock().await; diff --git a/token-group/example/tests/update_collection_max_size.rs b/token-group/example/tests/update_collection_max_size.rs index b4be2db480f..501dac6a2c3 100644 --- a/token-group/example/tests/update_collection_max_size.rs +++ b/token-group/example/tests/update_collection_max_size.rs @@ -46,7 +46,13 @@ async fn test_update_collection_max_size() { Some(0), payer.clone(), ); - setup_mint(&token_client, &collection_mint, &collection_mint_authority).await; + setup_mint( + &token_client, + &collection_mint, + &collection_mint_authority, + vec![], + ) + .await; let mut context = context.lock().await; @@ -226,7 +232,13 @@ async fn test_update_collection_max_size_fail_immutable_group() { Some(0), payer.clone(), ); - setup_mint(&token_client, &collection_mint, &collection_mint_authority).await; + setup_mint( + &token_client, + &collection_mint, + &collection_mint_authority, + vec![], + ) + .await; let mut context = context.lock().await;