From ba909fd7d27694c4a2119d1a851a244d04853d53 Mon Sep 17 00:00:00 2001 From: Joe Date: Mon, 16 Oct 2023 13:41:02 +0200 Subject: [PATCH 1/6] token-group: create collections example --- .../workflows/pull-request-token-group.yml | 69 +++++ Cargo.lock | 16 + Cargo.toml | 1 + token-group/example/Cargo.toml | 32 ++ token-group/example/src/entrypoint.rs | 23 ++ token-group/example/src/lib.rs | 10 + token-group/example/src/processor.rs | 225 ++++++++++++++ .../example/tests/initialize_collection.rs | 154 ++++++++++ .../tests/initialize_collection_member.rs | 222 ++++++++++++++ token-group/example/tests/setup.rs | 61 ++++ .../tests/update_collection_authority.rs | 190 ++++++++++++ .../tests/update_collection_max_size.rs | 290 ++++++++++++++++++ token-group/interface/src/error.rs | 9 + 13 files changed, 1302 insertions(+) create mode 100644 .github/workflows/pull-request-token-group.yml create mode 100644 token-group/example/Cargo.toml create mode 100644 token-group/example/src/entrypoint.rs create mode 100644 token-group/example/src/lib.rs create mode 100644 token-group/example/src/processor.rs create mode 100644 token-group/example/tests/initialize_collection.rs create mode 100644 token-group/example/tests/initialize_collection_member.rs create mode 100644 token-group/example/tests/setup.rs create mode 100644 token-group/example/tests/update_collection_authority.rs create mode 100644 token-group/example/tests/update_collection_max_size.rs diff --git a/.github/workflows/pull-request-token-group.yml b/.github/workflows/pull-request-token-group.yml new file mode 100644 index 00000000000..9b40be280b9 --- /dev/null +++ b/.github/workflows/pull-request-token-group.yml @@ -0,0 +1,69 @@ +name: Token-Group Pull Request + +on: + pull_request: + paths: + - 'token-group/**' + - 'token/program-2022/**' + - 'ci/*-version.sh' + - '.github/workflows/pull-request-token-group.yml' + push: + branches: [master] + paths: + - 'token-group/**' + - 'token/program-2022/**' + - 'ci/*-version.sh' + - '.github/workflows/pull-request-token-group.yml' + +jobs: + cargo-test-sbf: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Set env vars + run: | + source ci/rust-version.sh + echo "RUST_STABLE=$rust_stable" >> $GITHUB_ENV + source ci/solana-version.sh + echo "SOLANA_VERSION=$solana_version" >> $GITHUB_ENV + + - uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ env.RUST_STABLE }} + override: true + profile: minimal + + - uses: actions/cache@v2 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + key: cargo-build-${{ hashFiles('**/Cargo.lock') }}-${{ env.RUST_STABLE}} + + - uses: actions/cache@v2 + with: + path: | + ~/.cargo/bin/rustfilt + key: cargo-sbf-bins-${{ runner.os }} + + - uses: actions/cache@v2 + with: + path: ~/.cache/solana + key: solana-${{ env.SOLANA_VERSION }} + + - name: Install dependencies + run: | + ./ci/install-build-deps.sh + ./ci/install-program-deps.sh + echo "$HOME/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH + + - name: Test token-group with "serde" activated + run: | + cargo test \ + --manifest-path=token-group/interface/Cargo.toml \ + --features serde-traits \ + -- --nocapture + + - name: Build and test example + run: ./ci/cargo-test-sbf.sh token-group/example diff --git a/Cargo.lock b/Cargo.lock index 597bab68d12..62094f314e5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7341,6 +7341,22 @@ dependencies = [ "thiserror", ] +[[package]] +name = "spl-token-group-example" +version = "0.1.0" +dependencies = [ + "solana-program", + "solana-program-test", + "solana-sdk", + "spl-discriminator 0.1.0", + "spl-pod 0.1.0", + "spl-token-2022 0.9.0", + "spl-token-client", + "spl-token-group-interface", + "spl-token-metadata-interface 0.2.0", + "spl-type-length-value 0.3.0", +] + [[package]] name = "spl-token-group-interface" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 5ac30a0132c..d4f442d410b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,6 +42,7 @@ members = [ "stake-pool/cli", "stake-pool/program", "stateless-asks/program", + "token-group/example", "token-group/interface", "token-lending/cli", "token-lending/program", diff --git a/token-group/example/Cargo.toml b/token-group/example/Cargo.toml new file mode 100644 index 00000000000..17af8a35198 --- /dev/null +++ b/token-group/example/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "spl-token-group-example" +version = "0.1.0" +description = "Solana Program Library Token Group Example" +authors = ["Solana Labs Maintainers "] +repository = "https://github.com/solana-labs/solana-program-library" +license = "Apache-2.0" +edition = "2021" + +[features] +no-entrypoint = [] +test-sbf = [] + +[dependencies] +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", features = ["no-entrypoint"] } +spl-token-group-interface = { version = "0.1.0", path = "../interface" } +spl-type-length-value = { version = "0.3.0", path = "../../libraries/type-length-value" } + +[dev-dependencies] +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"] + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] \ No newline at end of file diff --git a/token-group/example/src/entrypoint.rs b/token-group/example/src/entrypoint.rs new file mode 100644 index 00000000000..a5ae85f308e --- /dev/null +++ b/token-group/example/src/entrypoint.rs @@ -0,0 +1,23 @@ +//! Program entrypoint + +use { + crate::processor, + solana_program::{ + account_info::AccountInfo, entrypoint, entrypoint::ProgramResult, + program_error::PrintProgramError, pubkey::Pubkey, + }, + spl_token_group_interface::error::TokenGroupError, +}; + +entrypoint!(process_instruction); +fn process_instruction( + program_id: &Pubkey, + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> ProgramResult { + if let Err(error) = processor::process(program_id, accounts, instruction_data) { + error.print::(); + return Err(error); + } + Ok(()) +} \ No newline at end of file diff --git a/token-group/example/src/lib.rs b/token-group/example/src/lib.rs new file mode 100644 index 00000000000..d476bbbab83 --- /dev/null +++ b/token-group/example/src/lib.rs @@ -0,0 +1,10 @@ +//! Crate defining an example program for creating SPL token collections +//! using the SPL Token Group interface. + +#![deny(missing_docs)] +#![cfg_attr(not(test), forbid(unsafe_code))] + +pub mod processor; + +#[cfg(not(feature = "no-entrypoint"))] +mod entrypoint; diff --git a/token-group/example/src/processor.rs b/token-group/example/src/processor.rs new file mode 100644 index 00000000000..31c9ae22e39 --- /dev/null +++ b/token-group/example/src/processor.rs @@ -0,0 +1,225 @@ +//! Program state processor + +use { + solana_program::{ + account_info::{next_account_info, AccountInfo}, + entrypoint::ProgramResult, + msg, + program_error::ProgramError, + program_option::COption, + pubkey::Pubkey, + }, + spl_pod::optional_keys::OptionalNonZeroPubkey, + spl_token_2022::{extension::StateWithExtensions, state::Mint}, + spl_token_group_interface::{ + error::TokenGroupError, + instruction::{ + InitializeGroup, TokenGroupInstruction, UpdateGroupAuthority, UpdateGroupMaxSize, + }, + state::{TokenGroup, TokenGroupMember}, + }, + spl_type_length_value::state::TlvStateMut, +}; + +fn check_update_authority( + update_authority_info: &AccountInfo, + expected_update_authority: &OptionalNonZeroPubkey, +) -> Result<(), ProgramError> { + if !update_authority_info.is_signer { + return Err(ProgramError::MissingRequiredSignature); + } + let update_authority = Option::::from(*expected_update_authority) + .ok_or(TokenGroupError::ImmutableGroup)?; + if update_authority != *update_authority_info.key { + return Err(TokenGroupError::IncorrectUpdateAuthority.into()); + } + Ok(()) +} + +/// Processes an [InitializeGroup](enum.GroupInterfaceInstruction.html) +/// instruction for a `Collection` +pub fn process_initialize_collection( + _program_id: &Pubkey, + accounts: &[AccountInfo], + data: InitializeGroup, +) -> ProgramResult { + // Assumes one has already created a mint for the collection. + let account_info_iter = &mut accounts.iter(); + + // Accounts expected by this instruction: + // + // 0. `[w]` Collection (Group) + // 1. `[]` Mint + // 2. `[s]` Mint authority + let collection_info = next_account_info(account_info_iter)?; + let mint_info = next_account_info(account_info_iter)?; + let mint_authority_info = next_account_info(account_info_iter)?; + + { + // 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 mint_data = mint_info.try_borrow_data()?; + let mint = StateWithExtensions::::unpack(&mint_data)?; + + if !mint_authority_info.is_signer { + return Err(ProgramError::MissingRequiredSignature); + } + if mint.base.mint_authority.as_ref() != COption::Some(mint_authority_info.key) { + return Err(TokenGroupError::IncorrectMintAuthority.into()); + } + } + + // Allocate a TLV entry for the space and write it in + let mut buffer = collection_info.try_borrow_mut_data()?; + let mut state = TlvStateMut::unpack(&mut buffer)?; + let (collection, _) = state.init_value::(false)?; + *collection = TokenGroup::new(mint_info.key, data.update_authority, data.max_size.into()); + + Ok(()) +} + +/// Processes an +/// [UpdateGroupMaxSize](enum.GroupInterfaceInstruction.html) +/// instruction for a `Collection` +pub fn process_update_collection_max_size( + _program_id: &Pubkey, + accounts: &[AccountInfo], + data: UpdateGroupMaxSize, +) -> ProgramResult { + let account_info_iter = &mut accounts.iter(); + + // Accounts expected by this instruction: + // + // 0. `[w]` Collection (Group) + // 1. `[s]` Update authority + let collection_info = next_account_info(account_info_iter)?; + let update_authority_info = next_account_info(account_info_iter)?; + + let mut buffer = collection_info.try_borrow_mut_data()?; + let mut state = TlvStateMut::unpack(&mut buffer)?; + let collection = state.get_first_value_mut::()?; + + check_update_authority(update_authority_info, &collection.update_authority)?; + + // Update the max size (zero-copy) + collection.update_max_size(data.max_size.into())?; + + Ok(()) +} + +/// Processes an +/// [UpdateGroupAuthority](enum.GroupInterfaceInstruction.html) +/// instruction for a `Collection` +pub fn process_update_collection_authority( + _program_id: &Pubkey, + accounts: &[AccountInfo], + data: UpdateGroupAuthority, +) -> ProgramResult { + let account_info_iter = &mut accounts.iter(); + + // Accounts expected by this instruction: + // + // 0. `[w]` Collection (Group) + // 1. `[s]` Current update authority + let collection_info = next_account_info(account_info_iter)?; + let update_authority_info = next_account_info(account_info_iter)?; + + let mut buffer = collection_info.try_borrow_mut_data()?; + let mut state = TlvStateMut::unpack(&mut buffer)?; + let collection = state.get_first_value_mut::()?; + + check_update_authority(update_authority_info, &collection.update_authority)?; + + // Update the authority (zero-copy) + collection.update_authority = data.new_authority; + + Ok(()) +} + +/// Processes an [InitializeMember](enum.GroupInterfaceInstruction.html) +/// instruction for a `Collection` +pub fn process_initialize_collection_member( + _program_id: &Pubkey, + accounts: &[AccountInfo], +) -> ProgramResult { + // For this group, we are going to assume the collection has been + // initialized, and we're also assuming a mint has been created for the + // member. + // Collection members in this example can have their own separate + // metadata that differs from the metadata of the collection, since + // metadata is not involved here. + let account_info_iter = &mut accounts.iter(); + + // Accounts expected by this instruction: + // + // 0. `[w]` Collection Member (Member) + // 1. `[]` Collection Member (Member) Mint + // 2. `[s]` Collection Member (Member) Mint authority + // 3. `[w]` Collection (Group) + // 4. `[s]` Collection (Group) update authority + let member_info = next_account_info(account_info_iter)?; + let member_mint_info = next_account_info(account_info_iter)?; + let member_mint_authority_info = next_account_info(account_info_iter)?; + let collection_info = next_account_info(account_info_iter)?; + let collection_update_authority_info = next_account_info(account_info_iter)?; + + // Mint checks on the member + { + // 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 member_mint_data = member_mint_info.try_borrow_data()?; + let member_mint = StateWithExtensions::::unpack(&member_mint_data)?; + + if !member_mint_authority_info.is_signer { + return Err(ProgramError::MissingRequiredSignature); + } + if member_mint.base.mint_authority.as_ref() != COption::Some(member_mint_authority_info.key) + { + return Err(TokenGroupError::IncorrectMintAuthority.into()); + } + } + + // Increment the size of the collection + let mut buffer = collection_info.try_borrow_mut_data()?; + let mut state = TlvStateMut::unpack(&mut buffer)?; + let collection = state.get_first_value_mut::()?; + + check_update_authority( + collection_update_authority_info, + &collection.update_authority, + )?; + let member_number = collection.increment_size()?; + + // Allocate a TLV entry for the space and write it in + let mut buffer = member_info.try_borrow_mut_data()?; + let mut state = TlvStateMut::unpack(&mut buffer)?; + let (member, _) = state.init_value::(false)?; + *member = TokenGroupMember::new(member_mint_info.key, collection_info.key, member_number); + + Ok(()) +} + +/// Processes an `SplTokenGroupInstruction` +pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], input: &[u8]) -> ProgramResult { + let instruction = TokenGroupInstruction::unpack(input)?; + match instruction { + TokenGroupInstruction::InitializeGroup(data) => { + msg!("Instruction: InitializeCollection"); + process_initialize_collection(program_id, accounts, data) + } + TokenGroupInstruction::UpdateGroupMaxSize(data) => { + msg!("Instruction: UpdateCollectionMaxSize"); + process_update_collection_max_size(program_id, accounts, data) + } + TokenGroupInstruction::UpdateGroupAuthority(data) => { + msg!("Instruction: UpdateCollectionAuthority"); + process_update_collection_authority(program_id, accounts, data) + } + TokenGroupInstruction::InitializeMember(_) => { + msg!("Instruction: InitializeCollectionMember"); + process_initialize_collection_member(program_id, accounts) + } + } +} diff --git a/token-group/example/tests/initialize_collection.rs b/token-group/example/tests/initialize_collection.rs new file mode 100644 index 00000000000..0e4d162c7d6 --- /dev/null +++ b/token-group/example/tests/initialize_collection.rs @@ -0,0 +1,154 @@ +#![cfg(feature = "test-sbf")] + +mod setup; + +use { + setup::{setup_mint, setup_program_test}, + solana_program::{instruction::InstructionError, pubkey::Pubkey, system_instruction}, + solana_program_test::tokio, + solana_sdk::{ + signature::Keypair, + signer::Signer, + transaction::{Transaction, TransactionError}, + }, + spl_token_client::token::Token, + spl_token_group_interface::{instruction::initialize_group, state::TokenGroup}, + spl_type_length_value::{ + error::TlvError, + state::{TlvState, TlvStateBorrowed}, + }, +}; + +#[tokio::test] +async fn test_initialize_collection() { + let program_id = Pubkey::new_unique(); + let collection = Keypair::new(); + let collection_mint = Keypair::new(); + let collection_mint_authority = Keypair::new(); + + let collection_group_state = TokenGroup { + update_authority: None.try_into().unwrap(), + mint: collection_mint.pubkey(), + size: 0.into(), + max_size: 50.into(), + }; + + let (context, client, payer) = setup_program_test(&program_id).await; + + let token_client = Token::new( + client, + &spl_token_2022::id(), + &collection_mint.pubkey(), + Some(0), + payer.clone(), + ); + setup_mint(&token_client, &collection_mint, &collection_mint_authority).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); + + // Fail: mint authority not signer + let mut init_group_ix = initialize_group( + &program_id, + &collection.pubkey(), + &collection_mint.pubkey(), + &collection_mint_authority.pubkey(), + collection_group_state.update_authority.try_into().unwrap(), + collection_group_state.max_size.into(), + ); + init_group_ix.accounts[2].is_signer = false; + let transaction = Transaction::new_signed_with_payer( + &[ + system_instruction::create_account( + &context.payer.pubkey(), + &collection.pubkey(), + rent_lamports, + space.try_into().unwrap(), + &program_id, + ), + init_group_ix, + ], + Some(&context.payer.pubkey()), + &[&context.payer, &collection], + context.last_blockhash, + ); + assert_eq!( + context + .banks_client + .process_transaction(transaction) + .await + .unwrap_err() + .unwrap(), + TransactionError::InstructionError(1, InstructionError::MissingRequiredSignature) + ); + + // Success: create the collection + let transaction = Transaction::new_signed_with_payer( + &[ + system_instruction::create_account( + &context.payer.pubkey(), + &collection.pubkey(), + rent_lamports, + space.try_into().unwrap(), + &program_id, + ), + initialize_group( + &program_id, + &collection.pubkey(), + &collection_mint.pubkey(), + &collection_mint_authority.pubkey(), + collection_group_state.update_authority.try_into().unwrap(), + collection_group_state.max_size.into(), + ), + ], + Some(&context.payer.pubkey()), + &[&context.payer, &collection_mint_authority, &collection], + context.last_blockhash, + ); + context + .banks_client + .process_transaction(transaction) + .await + .unwrap(); + + // Fetch the collection account and ensure it matches our state + let fetched_collection_account = context + .banks_client + .get_account(collection.pubkey()) + .await + .unwrap() + .unwrap(); + let fetched_meta = TlvStateBorrowed::unpack(&fetched_collection_account.data).unwrap(); + let fetched_collection_group_state = fetched_meta.get_first_value::().unwrap(); + assert_eq!(fetched_collection_group_state, &collection_group_state); + + // Fail: can't initialize twice + let transaction = Transaction::new_signed_with_payer( + &[initialize_group( + &program_id, + &collection.pubkey(), + &collection_mint.pubkey(), + &collection_mint_authority.pubkey(), + Pubkey::new_unique().into(), // Intentionally changed + collection_group_state.max_size.into(), + )], + Some(&context.payer.pubkey()), + &[&context.payer, &collection_mint_authority], + context.last_blockhash, + ); + assert_eq!( + context + .banks_client + .process_transaction(transaction) + .await + .unwrap_err() + .unwrap(), + TransactionError::InstructionError( + 0, + InstructionError::Custom(TlvError::TypeAlreadyExists as u32) + ) + ); +} diff --git a/token-group/example/tests/initialize_collection_member.rs b/token-group/example/tests/initialize_collection_member.rs new file mode 100644 index 00000000000..89fe52a7ea5 --- /dev/null +++ b/token-group/example/tests/initialize_collection_member.rs @@ -0,0 +1,222 @@ +#![cfg(feature = "test-sbf")] + +mod setup; + +use { + setup::{setup_mint, setup_program_test}, + solana_program::{instruction::InstructionError, pubkey::Pubkey, system_instruction}, + solana_program_test::tokio, + solana_sdk::{ + signature::Keypair, + signer::Signer, + transaction::{Transaction, TransactionError}, + }, + spl_token_client::token::Token, + spl_token_group_interface::{ + instruction::{initialize_group, initialize_member}, + state::{TokenGroup, TokenGroupMember}, + }, + spl_type_length_value::state::{TlvState, TlvStateBorrowed}, +}; + +#[tokio::test] +async fn test_initialize_collection_member() { + let program_id = Pubkey::new_unique(); + let collection = Keypair::new(); + let collection_mint = Keypair::new(); + let collection_mint_authority = Keypair::new(); + let collection_update_authority = Keypair::new(); + let member = Keypair::new(); + let member_mint = Keypair::new(); + let member_mint_authority = Keypair::new(); + + let collection_group_state = TokenGroup { + update_authority: Some(collection_update_authority.pubkey()) + .try_into() + .unwrap(), + mint: collection_mint.pubkey(), + size: 30.into(), + max_size: 50.into(), + }; + + let (context, client, payer) = setup_program_test(&program_id).await; + + setup_mint( + &Token::new( + client.clone(), + &spl_token_2022::id(), + &collection_mint.pubkey(), + Some(0), + payer.clone(), + ), + &collection_mint, + &collection_mint_authority, + ) + .await; + setup_mint( + &Token::new( + client.clone(), + &spl_token_2022::id(), + &member_mint.pubkey(), + Some(0), + payer.clone(), + ), + &member_mint, + &member_mint_authority, + ) + .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(), + &collection.pubkey(), + rent_lamports, + space.try_into().unwrap(), + &program_id, + ), + initialize_group( + &program_id, + &collection.pubkey(), + &collection_mint.pubkey(), + &collection_mint_authority.pubkey(), + collection_group_state.update_authority.try_into().unwrap(), + collection_group_state.max_size.into(), + ), + ], + Some(&context.payer.pubkey()), + &[&context.payer, &collection_mint_authority, &collection], + context.last_blockhash, + ); + context + .banks_client + .process_transaction(transaction) + .await + .unwrap(); + + let member_space = TlvStateBorrowed::get_base_len() + std::mem::size_of::(); + let member_rent_lamports = rent.minimum_balance(member_space); + + // Fail: member mint authority not signer + let mut init_member_ix = initialize_member( + &program_id, + &member.pubkey(), + &member_mint.pubkey(), + &member_mint_authority.pubkey(), + &collection.pubkey(), + &collection_update_authority.pubkey(), + ); + init_member_ix.accounts[2].is_signer = false; + let transaction = Transaction::new_signed_with_payer( + &[ + system_instruction::create_account( + &context.payer.pubkey(), + &member.pubkey(), + member_rent_lamports, + member_space.try_into().unwrap(), + &program_id, + ), + init_member_ix, + ], + Some(&context.payer.pubkey()), + &[&context.payer, &member, &collection_update_authority], + context.last_blockhash, + ); + assert_eq!( + context + .banks_client + .process_transaction(transaction) + .await + .unwrap_err() + .unwrap(), + TransactionError::InstructionError(1, InstructionError::MissingRequiredSignature) + ); + + // Fail: group update authority not signer + let mut init_member_ix = initialize_member( + &program_id, + &member.pubkey(), + &member_mint.pubkey(), + &member_mint_authority.pubkey(), + &collection.pubkey(), + &collection_update_authority.pubkey(), + ); + init_member_ix.accounts[4].is_signer = false; + let transaction = Transaction::new_signed_with_payer( + &[ + system_instruction::create_account( + &context.payer.pubkey(), + &member.pubkey(), + member_rent_lamports, + member_space.try_into().unwrap(), + &program_id, + ), + init_member_ix, + ], + Some(&context.payer.pubkey()), + &[&context.payer, &member, &member_mint_authority], + context.last_blockhash, + ); + assert_eq!( + context + .banks_client + .process_transaction(transaction) + .await + .unwrap_err() + .unwrap(), + TransactionError::InstructionError(1, InstructionError::MissingRequiredSignature) + ); + + // Success: initialize member + let transaction = Transaction::new_signed_with_payer( + &[ + system_instruction::create_account( + &context.payer.pubkey(), + &member.pubkey(), + member_rent_lamports, + member_space.try_into().unwrap(), + &program_id, + ), + initialize_member( + &program_id, + &member.pubkey(), + &member_mint.pubkey(), + &member_mint_authority.pubkey(), + &collection.pubkey(), + &collection_update_authority.pubkey(), + ), + ], + Some(&context.payer.pubkey()), + &[ + &context.payer, + &member, + &member_mint_authority, + &collection_update_authority, + ], + context.last_blockhash, + ); + context + .banks_client + .process_transaction(transaction) + .await + .unwrap(); + + // Fetch the member account and ensure it matches our state + let member_account = context + .banks_client + .get_account(member.pubkey()) + .await + .unwrap() + .unwrap(); + let fetched_meta = TlvStateBorrowed::unpack(&member_account.data).unwrap(); + let fetched_collection_member_state = + fetched_meta.get_first_value::().unwrap(); + assert_eq!(fetched_collection_member_state.group, collection.pubkey()); + assert_eq!(u32::from(fetched_collection_member_state.member_number), 1); +} diff --git a/token-group/example/tests/setup.rs b/token-group/example/tests/setup.rs new file mode 100644 index 00000000000..14cb091830c --- /dev/null +++ b/token-group/example/tests/setup.rs @@ -0,0 +1,61 @@ +#![cfg(feature = "test-sbf")] + +use { + solana_program_test::{processor, tokio::sync::Mutex, ProgramTest, ProgramTestContext}, + solana_sdk::{pubkey::Pubkey, signature::Keypair, signer::Signer}, + spl_token_client::{ + client::{ + ProgramBanksClient, ProgramBanksClientProcessTransaction, ProgramClient, + SendTransaction, SimulateTransaction, + }, + token::Token, + }, + std::sync::Arc, +}; + +/// Set up a program test +pub async fn setup_program_test( + program_id: &Pubkey, +) -> ( + Arc>, + Arc>, + Arc, +) { + let mut program_test = ProgramTest::new( + "spl_token_group_example", + *program_id, + processor!(spl_token_group_example::processor::process), + ); + program_test.prefer_bpf(false); + program_test.add_program( + "spl_token_2022", + spl_token_2022::id(), + processor!(spl_token_2022::processor::Processor::process), + ); + let context = program_test.start_with_context().await; + let payer = Arc::new(context.payer.insecure_clone()); + let context = Arc::new(Mutex::new(context)); + let client: Arc> = + Arc::new(ProgramBanksClient::new_from_context( + Arc::clone(&context), + ProgramBanksClientProcessTransaction, + )); + (context, client, payer) +} + +/// Set up a Token-2022 mint +pub async fn setup_mint( + token_client: &Token, + mint_keypair: &Keypair, + mint_authority_keypair: &Keypair, +) { + token_client + .create_mint( + &mint_authority_keypair.pubkey(), + None, + vec![], + &[mint_keypair], + ) + .await + .unwrap(); +} diff --git a/token-group/example/tests/update_collection_authority.rs b/token-group/example/tests/update_collection_authority.rs new file mode 100644 index 00000000000..703c3d08878 --- /dev/null +++ b/token-group/example/tests/update_collection_authority.rs @@ -0,0 +1,190 @@ +#![cfg(feature = "test-sbf")] + +mod setup; + +use { + setup::{setup_mint, setup_program_test}, + solana_program::{instruction::InstructionError, pubkey::Pubkey, system_instruction}, + solana_program_test::tokio, + solana_sdk::{ + signature::Keypair, + signer::Signer, + transaction::{Transaction, TransactionError}, + }, + spl_token_client::token::Token, + spl_token_group_interface::{ + error::TokenGroupError, + instruction::{initialize_group, update_group_authority}, + state::TokenGroup, + }, + spl_type_length_value::state::{TlvState, TlvStateBorrowed}, +}; + +#[tokio::test] +async fn test_update_collection_authority() { + let program_id = Pubkey::new_unique(); + let collection = Keypair::new(); + let collection_mint = Keypair::new(); + let collection_mint_authority = Keypair::new(); + let collection_update_authority = Keypair::new(); + + let collection_group_state = TokenGroup { + update_authority: Some(collection_update_authority.pubkey()) + .try_into() + .unwrap(), + mint: collection_mint.pubkey(), + size: 30.into(), + max_size: 50.into(), + }; + + let (context, client, payer) = setup_program_test(&program_id).await; + + let token_client = Token::new( + client, + &spl_token_2022::id(), + &collection_mint.pubkey(), + Some(0), + payer.clone(), + ); + setup_mint(&token_client, &collection_mint, &collection_mint_authority).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(), + &collection.pubkey(), + rent_lamports, + space.try_into().unwrap(), + &program_id, + ), + initialize_group( + &program_id, + &collection.pubkey(), + &collection_mint.pubkey(), + &collection_mint_authority.pubkey(), + collection_group_state.update_authority.try_into().unwrap(), + collection_group_state.max_size.into(), + ), + ], + Some(&context.payer.pubkey()), + &[&context.payer, &collection_mint_authority, &collection], + context.last_blockhash, + ); + context + .banks_client + .process_transaction(transaction) + .await + .unwrap(); + + // Fail: update authority not signer + let mut update_ix = update_group_authority( + &program_id, + &collection.pubkey(), + &collection_update_authority.pubkey(), + None, + ); + update_ix.accounts[1].is_signer = false; + let transaction = Transaction::new_signed_with_payer( + &[update_ix], + Some(&context.payer.pubkey()), + &[&context.payer], + context.last_blockhash, + ); + assert_eq!( + context + .banks_client + .process_transaction(transaction) + .await + .unwrap_err() + .unwrap(), + TransactionError::InstructionError(0, InstructionError::MissingRequiredSignature) + ); + + // Fail: incorrect update authority + let transaction = Transaction::new_signed_with_payer( + &[update_group_authority( + &program_id, + &collection.pubkey(), + &collection.pubkey(), + None, + )], + Some(&context.payer.pubkey()), + &[&context.payer, &collection], + context.last_blockhash, + ); + assert_eq!( + context + .banks_client + .process_transaction(transaction) + .await + .unwrap_err() + .unwrap(), + TransactionError::InstructionError( + 0, + InstructionError::Custom(TokenGroupError::IncorrectUpdateAuthority as u32) + ) + ); + + // Success: update authority + let transaction = Transaction::new_signed_with_payer( + &[update_group_authority( + &program_id, + &collection.pubkey(), + &collection_update_authority.pubkey(), + None, + )], + Some(&context.payer.pubkey()), + &[&context.payer, &collection_update_authority], + context.last_blockhash, + ); + context + .banks_client + .process_transaction(transaction) + .await + .unwrap(); + + // Fetch the account and assert the new authority + let fetched_collection_account = context + .banks_client + .get_account(collection.pubkey()) + .await + .unwrap() + .unwrap(); + let fetched_meta = TlvStateBorrowed::unpack(&fetched_collection_account.data).unwrap(); + let fetched_collection_group_state = fetched_meta.get_first_value::().unwrap(); + assert_eq!( + fetched_collection_group_state.update_authority, + None.try_into().unwrap(), + ); + + // Fail: immutable group + let transaction = Transaction::new_signed_with_payer( + &[update_group_authority( + &program_id, + &collection.pubkey(), + &collection_update_authority.pubkey(), + Some(collection_update_authority.pubkey()), + )], + Some(&context.payer.pubkey()), + &[&context.payer, &collection_update_authority], + context.last_blockhash, + ); + assert_eq!( + context + .banks_client + .process_transaction(transaction) + .await + .unwrap_err() + .unwrap(), + TransactionError::InstructionError( + 0, + InstructionError::Custom(TokenGroupError::ImmutableGroup as u32) + ) + ); +} diff --git a/token-group/example/tests/update_collection_max_size.rs b/token-group/example/tests/update_collection_max_size.rs new file mode 100644 index 00000000000..96b9cfaeb80 --- /dev/null +++ b/token-group/example/tests/update_collection_max_size.rs @@ -0,0 +1,290 @@ +#![cfg(feature = "test-sbf")] + +mod setup; + +use { + setup::{setup_mint, setup_program_test}, + solana_program::{instruction::InstructionError, pubkey::Pubkey, system_instruction}, + solana_program_test::tokio, + solana_sdk::{ + account::Account as SolanaAccount, + signature::Keypair, + signer::Signer, + transaction::{Transaction, TransactionError}, + }, + spl_token_client::token::Token, + spl_token_group_interface::{ + error::TokenGroupError, + instruction::{initialize_group, update_group_max_size}, + state::TokenGroup, + }, + spl_type_length_value::state::{TlvState, TlvStateBorrowed, TlvStateMut}, +}; + +#[tokio::test] +async fn test_update_collection_max_size() { + let program_id = Pubkey::new_unique(); + let collection = Keypair::new(); + let collection_mint = Keypair::new(); + let collection_mint_authority = Keypair::new(); + let collection_update_authority = Keypair::new(); + + let collection_group_state = TokenGroup { + update_authority: Some(collection_update_authority.pubkey()) + .try_into() + .unwrap(), + mint: collection_mint.pubkey(), + size: 30.into(), + max_size: 50.into(), + }; + + let (context, client, payer) = setup_program_test(&program_id).await; + + let token_client = Token::new( + client, + &spl_token_2022::id(), + &collection_mint.pubkey(), + Some(0), + payer.clone(), + ); + setup_mint(&token_client, &collection_mint, &collection_mint_authority).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(), + &collection.pubkey(), + rent_lamports, + space.try_into().unwrap(), + &program_id, + ), + initialize_group( + &program_id, + &collection.pubkey(), + &collection_mint.pubkey(), + &collection_mint_authority.pubkey(), + collection_group_state.update_authority.try_into().unwrap(), + collection_group_state.max_size.into(), + ), + ], + Some(&context.payer.pubkey()), + &[&context.payer, &collection_mint_authority, &collection], + context.last_blockhash, + ); + context + .banks_client + .process_transaction(transaction) + .await + .unwrap(); + + // Fail: update authority not signer + let mut update_ix = + update_group_max_size(&program_id, &collection.pubkey(), &collection.pubkey(), 100); + update_ix.accounts[1].is_signer = false; + let transaction = Transaction::new_signed_with_payer( + &[update_ix], + Some(&context.payer.pubkey()), + &[&context.payer], + context.last_blockhash, + ); + assert_eq!( + context + .banks_client + .process_transaction(transaction) + .await + .unwrap_err() + .unwrap(), + TransactionError::InstructionError(0, InstructionError::MissingRequiredSignature) + ); + + // Fail: incorrect update authority + let transaction = Transaction::new_signed_with_payer( + &[update_group_max_size( + &program_id, + &collection.pubkey(), + &collection.pubkey(), + 100, + )], + Some(&context.payer.pubkey()), + &[&context.payer, &collection], + context.last_blockhash, + ); + assert_eq!( + context + .banks_client + .process_transaction(transaction) + .await + .unwrap_err() + .unwrap(), + TransactionError::InstructionError( + 0, + InstructionError::Custom(TokenGroupError::IncorrectUpdateAuthority as u32) + ) + ); + + // Fail: size exceeds new max size + let fetched_collection_account = context + .banks_client + .get_account(collection.pubkey()) + .await + .unwrap() + .unwrap(); + let mut data = fetched_collection_account.data; + let mut state = TlvStateMut::unpack(&mut data).unwrap(); + let collection_data = state.get_first_value_mut::().unwrap(); + collection_data.size = 30.into(); + context.set_account( + &collection.pubkey(), + &SolanaAccount { + data, + ..fetched_collection_account + } + .into(), + ); + let transaction = Transaction::new_signed_with_payer( + &[update_group_max_size( + &program_id, + &collection.pubkey(), + &collection_update_authority.pubkey(), + 20, + )], + Some(&context.payer.pubkey()), + &[&context.payer, &collection_update_authority], + context.last_blockhash, + ); + assert_eq!( + context + .banks_client + .process_transaction(transaction) + .await + .unwrap_err() + .unwrap(), + TransactionError::InstructionError( + 0, + InstructionError::Custom(TokenGroupError::SizeExceedsNewMaxSize as u32) + ) + ); + + // Success: update max size + let transaction = Transaction::new_signed_with_payer( + &[update_group_max_size( + &program_id, + &collection.pubkey(), + &collection_update_authority.pubkey(), + 100, + )], + Some(&context.payer.pubkey()), + &[&context.payer, &collection_update_authority], + context.last_blockhash, + ); + context + .banks_client + .process_transaction(transaction) + .await + .unwrap(); + + // Fetch the account and assert the new max size + let fetched_collection_account = context + .banks_client + .get_account(collection.pubkey()) + .await + .unwrap() + .unwrap(); + let fetched_meta = TlvStateBorrowed::unpack(&fetched_collection_account.data).unwrap(); + let fetched_collection_group_state = fetched_meta.get_first_value::().unwrap(); + assert_eq!(fetched_collection_group_state.max_size, 100.into()); +} + +// Fail: immutable group +#[tokio::test] +async fn test_update_collection_max_size_fail_immutable_group() { + let program_id = Pubkey::new_unique(); + let collection = Keypair::new(); + let collection_mint = Keypair::new(); + let collection_mint_authority = Keypair::new(); + let collection_update_authority = Keypair::new(); + + let collection_group_state = TokenGroup { + update_authority: Some(collection_update_authority.pubkey()) + .try_into() + .unwrap(), + mint: collection_mint.pubkey(), + size: 30.into(), + max_size: 50.into(), + }; + + let (context, client, payer) = setup_program_test(&program_id).await; + + let token_client = Token::new( + client, + &spl_token_2022::id(), + &collection_mint.pubkey(), + Some(0), + payer.clone(), + ); + setup_mint(&token_client, &collection_mint, &collection_mint_authority).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(), + &collection.pubkey(), + rent_lamports, + space.try_into().unwrap(), + &program_id, + ), + initialize_group( + &program_id, + &collection.pubkey(), + &collection_mint.pubkey(), + &collection_mint_authority.pubkey(), + None, + collection_group_state.max_size.into(), + ), + ], + Some(&context.payer.pubkey()), + &[&context.payer, &collection_mint_authority, &collection], + context.last_blockhash, + ); + context + .banks_client + .process_transaction(transaction) + .await + .unwrap(); + + let transaction = Transaction::new_signed_with_payer( + &[update_group_max_size( + &program_id, + &collection.pubkey(), + &collection_update_authority.pubkey(), + 100, + )], + Some(&context.payer.pubkey()), + &[&context.payer, &collection_update_authority], + context.last_blockhash, + ); + assert_eq!( + context + .banks_client + .process_transaction(transaction) + .await + .unwrap_err() + .unwrap(), + TransactionError::InstructionError( + 0, + InstructionError::Custom(TokenGroupError::ImmutableGroup as u32) + ) + ); +} diff --git a/token-group/interface/src/error.rs b/token-group/interface/src/error.rs index f2eff4d60f2..de41a6379f8 100644 --- a/token-group/interface/src/error.rs +++ b/token-group/interface/src/error.rs @@ -11,4 +11,13 @@ pub enum TokenGroupError { /// Size is greater than max size #[error("Size is greater than max size")] SizeExceedsMaxSize, + /// Group is immutable + #[error("Group is immutable")] + ImmutableGroup, + /// Incorrect mint authority has signed the instruction + #[error("Incorrect mint authority has signed the instruction")] + IncorrectMintAuthority, + /// Incorrect update authority has signed the instruction + #[error("Incorrect update authority has signed the instruction")] + IncorrectUpdateAuthority, } From e05532fa4bb0999e5f2e5d17741a88cb9770e524 Mon Sep 17 00:00:00 2001 From: Joe Date: Wed, 18 Oct 2023 12:13:10 +0200 Subject: [PATCH 2/6] cut metadata --- .github/workflows/pull-request-token-group.yml | 1 - token-group/example/src/entrypoint.rs | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/pull-request-token-group.yml b/.github/workflows/pull-request-token-group.yml index 9b40be280b9..b0dc891ba83 100644 --- a/.github/workflows/pull-request-token-group.yml +++ b/.github/workflows/pull-request-token-group.yml @@ -62,7 +62,6 @@ jobs: run: | cargo test \ --manifest-path=token-group/interface/Cargo.toml \ - --features serde-traits \ -- --nocapture - name: Build and test example diff --git a/token-group/example/src/entrypoint.rs b/token-group/example/src/entrypoint.rs index a5ae85f308e..762ec75255a 100644 --- a/token-group/example/src/entrypoint.rs +++ b/token-group/example/src/entrypoint.rs @@ -20,4 +20,4 @@ fn process_instruction( return Err(error); } Ok(()) -} \ No newline at end of file +} From e28468673b743d63f3a7efbb2678471e03228675 Mon Sep 17 00:00:00 2001 From: Joe Date: Wed, 18 Oct 2023 23:16:24 +0200 Subject: [PATCH 3/6] return to group nomenclature --- token-group/example/src/lib.rs | 2 +- token-group/example/src/processor.rs | 106 ++++++++------- ...lize_collection.rs => initialize_group.rs} | 68 +++++----- ...lection_member.rs => initialize_member.rs} | 61 +++++---- ...authority.rs => update_group_authority.rs} | 70 +++++----- ...n_max_size.rs => update_group_max_size.rs} | 121 +++++++++--------- 6 files changed, 206 insertions(+), 222 deletions(-) rename token-group/example/tests/{initialize_collection.rs => initialize_group.rs} (65%) rename token-group/example/tests/{initialize_collection_member.rs => initialize_member.rs} (78%) rename token-group/example/tests/{update_collection_authority.rs => update_group_authority.rs} (69%) rename token-group/example/tests/{update_collection_max_size.rs => update_group_max_size.rs} (65%) diff --git a/token-group/example/src/lib.rs b/token-group/example/src/lib.rs index d476bbbab83..749eb07e34a 100644 --- a/token-group/example/src/lib.rs +++ b/token-group/example/src/lib.rs @@ -1,4 +1,4 @@ -//! Crate defining an example program for creating SPL token collections +//! Crate defining an example program for creating SPL token groups //! using the SPL Token Group interface. #![deny(missing_docs)] diff --git a/token-group/example/src/processor.rs b/token-group/example/src/processor.rs index 31c9ae22e39..b0f9e8e0959 100644 --- a/token-group/example/src/processor.rs +++ b/token-group/example/src/processor.rs @@ -37,21 +37,21 @@ fn check_update_authority( } /// Processes an [InitializeGroup](enum.GroupInterfaceInstruction.html) -/// instruction for a `Collection` -pub fn process_initialize_collection( +/// instruction +pub fn process_initialize_group( _program_id: &Pubkey, accounts: &[AccountInfo], data: InitializeGroup, ) -> ProgramResult { - // Assumes one has already created a mint for the collection. + // Assumes one has already created a mint for the group. let account_info_iter = &mut accounts.iter(); // Accounts expected by this instruction: // - // 0. `[w]` Collection (Group) + // 0. `[w]` Group // 1. `[]` Mint // 2. `[s]` Mint authority - let collection_info = next_account_info(account_info_iter)?; + let group_info = next_account_info(account_info_iter)?; let mint_info = next_account_info(account_info_iter)?; let mint_authority_info = next_account_info(account_info_iter)?; @@ -71,18 +71,18 @@ pub fn process_initialize_collection( } // Allocate a TLV entry for the space and write it in - let mut buffer = collection_info.try_borrow_mut_data()?; + let mut buffer = group_info.try_borrow_mut_data()?; let mut state = TlvStateMut::unpack(&mut buffer)?; - let (collection, _) = state.init_value::(false)?; - *collection = TokenGroup::new(mint_info.key, data.update_authority, data.max_size.into()); + let (group, _) = state.init_value::(false)?; + *group = TokenGroup::new(mint_info.key, data.update_authority, data.max_size.into()); Ok(()) } /// Processes an /// [UpdateGroupMaxSize](enum.GroupInterfaceInstruction.html) -/// instruction for a `Collection` -pub fn process_update_collection_max_size( +/// instruction +pub fn process_update_group_max_size( _program_id: &Pubkey, accounts: &[AccountInfo], data: UpdateGroupMaxSize, @@ -91,27 +91,27 @@ pub fn process_update_collection_max_size( // Accounts expected by this instruction: // - // 0. `[w]` Collection (Group) + // 0. `[w]` Group // 1. `[s]` Update authority - let collection_info = next_account_info(account_info_iter)?; + let group_info = next_account_info(account_info_iter)?; let update_authority_info = next_account_info(account_info_iter)?; - let mut buffer = collection_info.try_borrow_mut_data()?; + let mut buffer = group_info.try_borrow_mut_data()?; let mut state = TlvStateMut::unpack(&mut buffer)?; - let collection = state.get_first_value_mut::()?; + let group = state.get_first_value_mut::()?; - check_update_authority(update_authority_info, &collection.update_authority)?; + check_update_authority(update_authority_info, &group.update_authority)?; // Update the max size (zero-copy) - collection.update_max_size(data.max_size.into())?; + group.update_max_size(data.max_size.into())?; Ok(()) } /// Processes an /// [UpdateGroupAuthority](enum.GroupInterfaceInstruction.html) -/// instruction for a `Collection` -pub fn process_update_collection_authority( +/// instruction +pub fn process_update_group_authority( _program_id: &Pubkey, accounts: &[AccountInfo], data: UpdateGroupAuthority, @@ -120,49 +120,46 @@ pub fn process_update_collection_authority( // Accounts expected by this instruction: // - // 0. `[w]` Collection (Group) + // 0. `[w]` Group // 1. `[s]` Current update authority - let collection_info = next_account_info(account_info_iter)?; + let group_info = next_account_info(account_info_iter)?; let update_authority_info = next_account_info(account_info_iter)?; - let mut buffer = collection_info.try_borrow_mut_data()?; + let mut buffer = group_info.try_borrow_mut_data()?; let mut state = TlvStateMut::unpack(&mut buffer)?; - let collection = state.get_first_value_mut::()?; + let group = state.get_first_value_mut::()?; - check_update_authority(update_authority_info, &collection.update_authority)?; + check_update_authority(update_authority_info, &group.update_authority)?; // Update the authority (zero-copy) - collection.update_authority = data.new_authority; + group.update_authority = data.new_authority; Ok(()) } /// Processes an [InitializeMember](enum.GroupInterfaceInstruction.html) -/// instruction for a `Collection` -pub fn process_initialize_collection_member( - _program_id: &Pubkey, - accounts: &[AccountInfo], -) -> ProgramResult { - // For this group, we are going to assume the collection has been +/// instruction +pub fn process_initialize_member(_program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { + // For this group, we are going to assume the group has been // initialized, and we're also assuming a mint has been created for the // member. - // Collection members in this example can have their own separate - // metadata that differs from the metadata of the collection, since + // Group members in this example can have their own separate + // metadata that differs from the metadata of the group, since // metadata is not involved here. let account_info_iter = &mut accounts.iter(); // Accounts expected by this instruction: // - // 0. `[w]` Collection Member (Member) - // 1. `[]` Collection Member (Member) Mint - // 2. `[s]` Collection Member (Member) Mint authority - // 3. `[w]` Collection (Group) - // 4. `[s]` Collection (Group) update authority + // 0. `[w]` Member + // 1. `[]` Member Mint + // 2. `[s]` Member Mint authority + // 3. `[w]` Group + // 4. `[s]` Group update authority let member_info = next_account_info(account_info_iter)?; let member_mint_info = next_account_info(account_info_iter)?; let member_mint_authority_info = next_account_info(account_info_iter)?; - let collection_info = next_account_info(account_info_iter)?; - let collection_update_authority_info = next_account_info(account_info_iter)?; + let group_info = next_account_info(account_info_iter)?; + let group_update_authority_info = next_account_info(account_info_iter)?; // Mint checks on the member { @@ -181,22 +178,19 @@ pub fn process_initialize_collection_member( } } - // Increment the size of the collection - let mut buffer = collection_info.try_borrow_mut_data()?; + // Increment the size of the group + let mut buffer = group_info.try_borrow_mut_data()?; let mut state = TlvStateMut::unpack(&mut buffer)?; - let collection = state.get_first_value_mut::()?; + let group = state.get_first_value_mut::()?; - check_update_authority( - collection_update_authority_info, - &collection.update_authority, - )?; - let member_number = collection.increment_size()?; + check_update_authority(group_update_authority_info, &group.update_authority)?; + let member_number = group.increment_size()?; // Allocate a TLV entry for the space and write it in let mut buffer = member_info.try_borrow_mut_data()?; let mut state = TlvStateMut::unpack(&mut buffer)?; let (member, _) = state.init_value::(false)?; - *member = TokenGroupMember::new(member_mint_info.key, collection_info.key, member_number); + *member = TokenGroupMember::new(member_mint_info.key, group_info.key, member_number); Ok(()) } @@ -206,20 +200,20 @@ pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], input: &[u8]) -> P let instruction = TokenGroupInstruction::unpack(input)?; match instruction { TokenGroupInstruction::InitializeGroup(data) => { - msg!("Instruction: InitializeCollection"); - process_initialize_collection(program_id, accounts, data) + msg!("Instruction: InitializeGroup"); + process_initialize_group(program_id, accounts, data) } TokenGroupInstruction::UpdateGroupMaxSize(data) => { - msg!("Instruction: UpdateCollectionMaxSize"); - process_update_collection_max_size(program_id, accounts, data) + msg!("Instruction: UpdateGroupMaxSize"); + process_update_group_max_size(program_id, accounts, data) } TokenGroupInstruction::UpdateGroupAuthority(data) => { - msg!("Instruction: UpdateCollectionAuthority"); - process_update_collection_authority(program_id, accounts, data) + msg!("Instruction: UpdateGroupAuthority"); + process_update_group_authority(program_id, accounts, data) } TokenGroupInstruction::InitializeMember(_) => { - msg!("Instruction: InitializeCollectionMember"); - process_initialize_collection_member(program_id, accounts) + msg!("Instruction: InitializeMember"); + process_initialize_member(program_id, accounts) } } } diff --git a/token-group/example/tests/initialize_collection.rs b/token-group/example/tests/initialize_group.rs similarity index 65% rename from token-group/example/tests/initialize_collection.rs rename to token-group/example/tests/initialize_group.rs index 0e4d162c7d6..585335d8f96 100644 --- a/token-group/example/tests/initialize_collection.rs +++ b/token-group/example/tests/initialize_group.rs @@ -20,15 +20,15 @@ use { }; #[tokio::test] -async fn test_initialize_collection() { +async fn test_initialize_group() { let program_id = Pubkey::new_unique(); - let collection = Keypair::new(); - let collection_mint = Keypair::new(); - let collection_mint_authority = Keypair::new(); + let group = Keypair::new(); + let group_mint = Keypair::new(); + let group_mint_authority = Keypair::new(); - let collection_group_state = TokenGroup { + let group_state = TokenGroup { update_authority: None.try_into().unwrap(), - mint: collection_mint.pubkey(), + mint: group_mint.pubkey(), size: 0.into(), max_size: 50.into(), }; @@ -38,11 +38,11 @@ async fn test_initialize_collection() { let token_client = Token::new( client, &spl_token_2022::id(), - &collection_mint.pubkey(), + &group_mint.pubkey(), Some(0), payer.clone(), ); - setup_mint(&token_client, &collection_mint, &collection_mint_authority).await; + setup_mint(&token_client, &group_mint, &group_mint_authority).await; let mut context = context.lock().await; @@ -53,18 +53,18 @@ async fn test_initialize_collection() { // Fail: mint authority not signer let mut init_group_ix = initialize_group( &program_id, - &collection.pubkey(), - &collection_mint.pubkey(), - &collection_mint_authority.pubkey(), - collection_group_state.update_authority.try_into().unwrap(), - collection_group_state.max_size.into(), + &group.pubkey(), + &group_mint.pubkey(), + &group_mint_authority.pubkey(), + group_state.update_authority.try_into().unwrap(), + group_state.max_size.into(), ); init_group_ix.accounts[2].is_signer = false; let transaction = Transaction::new_signed_with_payer( &[ system_instruction::create_account( &context.payer.pubkey(), - &collection.pubkey(), + &group.pubkey(), rent_lamports, space.try_into().unwrap(), &program_id, @@ -72,7 +72,7 @@ async fn test_initialize_collection() { init_group_ix, ], Some(&context.payer.pubkey()), - &[&context.payer, &collection], + &[&context.payer, &group], context.last_blockhash, ); assert_eq!( @@ -85,27 +85,27 @@ async fn test_initialize_collection() { TransactionError::InstructionError(1, InstructionError::MissingRequiredSignature) ); - // Success: create the collection + // Success: create the group let transaction = Transaction::new_signed_with_payer( &[ system_instruction::create_account( &context.payer.pubkey(), - &collection.pubkey(), + &group.pubkey(), rent_lamports, space.try_into().unwrap(), &program_id, ), initialize_group( &program_id, - &collection.pubkey(), - &collection_mint.pubkey(), - &collection_mint_authority.pubkey(), - collection_group_state.update_authority.try_into().unwrap(), - collection_group_state.max_size.into(), + &group.pubkey(), + &group_mint.pubkey(), + &group_mint_authority.pubkey(), + group_state.update_authority.try_into().unwrap(), + group_state.max_size.into(), ), ], Some(&context.payer.pubkey()), - &[&context.payer, &collection_mint_authority, &collection], + &[&context.payer, &group_mint_authority, &group], context.last_blockhash, ); context @@ -114,29 +114,29 @@ async fn test_initialize_collection() { .await .unwrap(); - // Fetch the collection account and ensure it matches our state - let fetched_collection_account = context + // Fetch the group account and ensure it matches our state + let fetched_group_account = context .banks_client - .get_account(collection.pubkey()) + .get_account(group.pubkey()) .await .unwrap() .unwrap(); - let fetched_meta = TlvStateBorrowed::unpack(&fetched_collection_account.data).unwrap(); - let fetched_collection_group_state = fetched_meta.get_first_value::().unwrap(); - assert_eq!(fetched_collection_group_state, &collection_group_state); + let fetched_meta = TlvStateBorrowed::unpack(&fetched_group_account.data).unwrap(); + let fetched_group_state = fetched_meta.get_first_value::().unwrap(); + assert_eq!(fetched_group_state, &group_state); // Fail: can't initialize twice let transaction = Transaction::new_signed_with_payer( &[initialize_group( &program_id, - &collection.pubkey(), - &collection_mint.pubkey(), - &collection_mint_authority.pubkey(), + &group.pubkey(), + &group_mint.pubkey(), + &group_mint_authority.pubkey(), Pubkey::new_unique().into(), // Intentionally changed - collection_group_state.max_size.into(), + group_state.max_size.into(), )], Some(&context.payer.pubkey()), - &[&context.payer, &collection_mint_authority], + &[&context.payer, &group_mint_authority], context.last_blockhash, ); assert_eq!( diff --git a/token-group/example/tests/initialize_collection_member.rs b/token-group/example/tests/initialize_member.rs similarity index 78% rename from token-group/example/tests/initialize_collection_member.rs rename to token-group/example/tests/initialize_member.rs index 89fe52a7ea5..c32a75571bd 100644 --- a/token-group/example/tests/initialize_collection_member.rs +++ b/token-group/example/tests/initialize_member.rs @@ -20,21 +20,19 @@ use { }; #[tokio::test] -async fn test_initialize_collection_member() { +async fn test_initialize_group_member() { let program_id = Pubkey::new_unique(); - let collection = Keypair::new(); - let collection_mint = Keypair::new(); - let collection_mint_authority = Keypair::new(); - let collection_update_authority = Keypair::new(); + let group = Keypair::new(); + let group_mint = Keypair::new(); + let group_mint_authority = Keypair::new(); + let group_update_authority = Keypair::new(); let member = Keypair::new(); let member_mint = Keypair::new(); let member_mint_authority = Keypair::new(); - let collection_group_state = TokenGroup { - update_authority: Some(collection_update_authority.pubkey()) - .try_into() - .unwrap(), - mint: collection_mint.pubkey(), + let group_state = TokenGroup { + update_authority: Some(group_update_authority.pubkey()).try_into().unwrap(), + mint: group_mint.pubkey(), size: 30.into(), max_size: 50.into(), }; @@ -45,12 +43,12 @@ async fn test_initialize_collection_member() { &Token::new( client.clone(), &spl_token_2022::id(), - &collection_mint.pubkey(), + &group_mint.pubkey(), Some(0), payer.clone(), ), - &collection_mint, - &collection_mint_authority, + &group_mint, + &group_mint_authority, ) .await; setup_mint( @@ -76,22 +74,22 @@ async fn test_initialize_collection_member() { &[ system_instruction::create_account( &context.payer.pubkey(), - &collection.pubkey(), + &group.pubkey(), rent_lamports, space.try_into().unwrap(), &program_id, ), initialize_group( &program_id, - &collection.pubkey(), - &collection_mint.pubkey(), - &collection_mint_authority.pubkey(), - collection_group_state.update_authority.try_into().unwrap(), - collection_group_state.max_size.into(), + &group.pubkey(), + &group_mint.pubkey(), + &group_mint_authority.pubkey(), + group_state.update_authority.try_into().unwrap(), + group_state.max_size.into(), ), ], Some(&context.payer.pubkey()), - &[&context.payer, &collection_mint_authority, &collection], + &[&context.payer, &group_mint_authority, &group], context.last_blockhash, ); context @@ -109,8 +107,8 @@ async fn test_initialize_collection_member() { &member.pubkey(), &member_mint.pubkey(), &member_mint_authority.pubkey(), - &collection.pubkey(), - &collection_update_authority.pubkey(), + &group.pubkey(), + &group_update_authority.pubkey(), ); init_member_ix.accounts[2].is_signer = false; let transaction = Transaction::new_signed_with_payer( @@ -125,7 +123,7 @@ async fn test_initialize_collection_member() { init_member_ix, ], Some(&context.payer.pubkey()), - &[&context.payer, &member, &collection_update_authority], + &[&context.payer, &member, &group_update_authority], context.last_blockhash, ); assert_eq!( @@ -144,8 +142,8 @@ async fn test_initialize_collection_member() { &member.pubkey(), &member_mint.pubkey(), &member_mint_authority.pubkey(), - &collection.pubkey(), - &collection_update_authority.pubkey(), + &group.pubkey(), + &group_update_authority.pubkey(), ); init_member_ix.accounts[4].is_signer = false; let transaction = Transaction::new_signed_with_payer( @@ -188,8 +186,8 @@ async fn test_initialize_collection_member() { &member.pubkey(), &member_mint.pubkey(), &member_mint_authority.pubkey(), - &collection.pubkey(), - &collection_update_authority.pubkey(), + &group.pubkey(), + &group_update_authority.pubkey(), ), ], Some(&context.payer.pubkey()), @@ -197,7 +195,7 @@ async fn test_initialize_collection_member() { &context.payer, &member, &member_mint_authority, - &collection_update_authority, + &group_update_authority, ], context.last_blockhash, ); @@ -215,8 +213,7 @@ async fn test_initialize_collection_member() { .unwrap() .unwrap(); let fetched_meta = TlvStateBorrowed::unpack(&member_account.data).unwrap(); - let fetched_collection_member_state = - fetched_meta.get_first_value::().unwrap(); - assert_eq!(fetched_collection_member_state.group, collection.pubkey()); - assert_eq!(u32::from(fetched_collection_member_state.member_number), 1); + let fetched_group_member_state = fetched_meta.get_first_value::().unwrap(); + assert_eq!(fetched_group_member_state.group, group.pubkey()); + assert_eq!(u32::from(fetched_group_member_state.member_number), 1); } diff --git a/token-group/example/tests/update_collection_authority.rs b/token-group/example/tests/update_group_authority.rs similarity index 69% rename from token-group/example/tests/update_collection_authority.rs rename to token-group/example/tests/update_group_authority.rs index 703c3d08878..adf2f00c209 100644 --- a/token-group/example/tests/update_collection_authority.rs +++ b/token-group/example/tests/update_group_authority.rs @@ -21,18 +21,16 @@ use { }; #[tokio::test] -async fn test_update_collection_authority() { +async fn test_update_group_authority() { let program_id = Pubkey::new_unique(); - let collection = Keypair::new(); - let collection_mint = Keypair::new(); - let collection_mint_authority = Keypair::new(); - let collection_update_authority = Keypair::new(); + let group = Keypair::new(); + let group_mint = Keypair::new(); + let group_mint_authority = Keypair::new(); + let group_update_authority = Keypair::new(); - let collection_group_state = TokenGroup { - update_authority: Some(collection_update_authority.pubkey()) - .try_into() - .unwrap(), - mint: collection_mint.pubkey(), + let group_state = TokenGroup { + update_authority: Some(group_update_authority.pubkey()).try_into().unwrap(), + mint: group_mint.pubkey(), size: 30.into(), max_size: 50.into(), }; @@ -42,11 +40,11 @@ async fn test_update_collection_authority() { let token_client = Token::new( client, &spl_token_2022::id(), - &collection_mint.pubkey(), + &group_mint.pubkey(), Some(0), payer.clone(), ); - setup_mint(&token_client, &collection_mint, &collection_mint_authority).await; + setup_mint(&token_client, &group_mint, &group_mint_authority).await; let mut context = context.lock().await; @@ -58,22 +56,22 @@ async fn test_update_collection_authority() { &[ system_instruction::create_account( &context.payer.pubkey(), - &collection.pubkey(), + &group.pubkey(), rent_lamports, space.try_into().unwrap(), &program_id, ), initialize_group( &program_id, - &collection.pubkey(), - &collection_mint.pubkey(), - &collection_mint_authority.pubkey(), - collection_group_state.update_authority.try_into().unwrap(), - collection_group_state.max_size.into(), + &group.pubkey(), + &group_mint.pubkey(), + &group_mint_authority.pubkey(), + group_state.update_authority.try_into().unwrap(), + group_state.max_size.into(), ), ], Some(&context.payer.pubkey()), - &[&context.payer, &collection_mint_authority, &collection], + &[&context.payer, &group_mint_authority, &group], context.last_blockhash, ); context @@ -85,8 +83,8 @@ async fn test_update_collection_authority() { // Fail: update authority not signer let mut update_ix = update_group_authority( &program_id, - &collection.pubkey(), - &collection_update_authority.pubkey(), + &group.pubkey(), + &group_update_authority.pubkey(), None, ); update_ix.accounts[1].is_signer = false; @@ -110,12 +108,12 @@ async fn test_update_collection_authority() { let transaction = Transaction::new_signed_with_payer( &[update_group_authority( &program_id, - &collection.pubkey(), - &collection.pubkey(), + &group.pubkey(), + &group.pubkey(), None, )], Some(&context.payer.pubkey()), - &[&context.payer, &collection], + &[&context.payer, &group], context.last_blockhash, ); assert_eq!( @@ -135,12 +133,12 @@ async fn test_update_collection_authority() { let transaction = Transaction::new_signed_with_payer( &[update_group_authority( &program_id, - &collection.pubkey(), - &collection_update_authority.pubkey(), + &group.pubkey(), + &group_update_authority.pubkey(), None, )], Some(&context.payer.pubkey()), - &[&context.payer, &collection_update_authority], + &[&context.payer, &group_update_authority], context.last_blockhash, ); context @@ -150,16 +148,16 @@ async fn test_update_collection_authority() { .unwrap(); // Fetch the account and assert the new authority - let fetched_collection_account = context + let fetched_group_account = context .banks_client - .get_account(collection.pubkey()) + .get_account(group.pubkey()) .await .unwrap() .unwrap(); - let fetched_meta = TlvStateBorrowed::unpack(&fetched_collection_account.data).unwrap(); - let fetched_collection_group_state = fetched_meta.get_first_value::().unwrap(); + let fetched_meta = TlvStateBorrowed::unpack(&fetched_group_account.data).unwrap(); + let fetched_group_state = fetched_meta.get_first_value::().unwrap(); assert_eq!( - fetched_collection_group_state.update_authority, + fetched_group_state.update_authority, None.try_into().unwrap(), ); @@ -167,12 +165,12 @@ async fn test_update_collection_authority() { let transaction = Transaction::new_signed_with_payer( &[update_group_authority( &program_id, - &collection.pubkey(), - &collection_update_authority.pubkey(), - Some(collection_update_authority.pubkey()), + &group.pubkey(), + &group_update_authority.pubkey(), + Some(group_update_authority.pubkey()), )], Some(&context.payer.pubkey()), - &[&context.payer, &collection_update_authority], + &[&context.payer, &group_update_authority], context.last_blockhash, ); assert_eq!( diff --git a/token-group/example/tests/update_collection_max_size.rs b/token-group/example/tests/update_group_max_size.rs similarity index 65% rename from token-group/example/tests/update_collection_max_size.rs rename to token-group/example/tests/update_group_max_size.rs index 96b9cfaeb80..af30d53f7a9 100644 --- a/token-group/example/tests/update_collection_max_size.rs +++ b/token-group/example/tests/update_group_max_size.rs @@ -22,18 +22,16 @@ use { }; #[tokio::test] -async fn test_update_collection_max_size() { +async fn test_update_group_max_size() { let program_id = Pubkey::new_unique(); - let collection = Keypair::new(); - let collection_mint = Keypair::new(); - let collection_mint_authority = Keypair::new(); - let collection_update_authority = Keypair::new(); + let group = Keypair::new(); + let group_mint = Keypair::new(); + let group_mint_authority = Keypair::new(); + let group_update_authority = Keypair::new(); - let collection_group_state = TokenGroup { - update_authority: Some(collection_update_authority.pubkey()) - .try_into() - .unwrap(), - mint: collection_mint.pubkey(), + let group_state = TokenGroup { + update_authority: Some(group_update_authority.pubkey()).try_into().unwrap(), + mint: group_mint.pubkey(), size: 30.into(), max_size: 50.into(), }; @@ -43,11 +41,11 @@ async fn test_update_collection_max_size() { let token_client = Token::new( client, &spl_token_2022::id(), - &collection_mint.pubkey(), + &group_mint.pubkey(), Some(0), payer.clone(), ); - setup_mint(&token_client, &collection_mint, &collection_mint_authority).await; + setup_mint(&token_client, &group_mint, &group_mint_authority).await; let mut context = context.lock().await; @@ -59,22 +57,22 @@ async fn test_update_collection_max_size() { &[ system_instruction::create_account( &context.payer.pubkey(), - &collection.pubkey(), + &group.pubkey(), rent_lamports, space.try_into().unwrap(), &program_id, ), initialize_group( &program_id, - &collection.pubkey(), - &collection_mint.pubkey(), - &collection_mint_authority.pubkey(), - collection_group_state.update_authority.try_into().unwrap(), - collection_group_state.max_size.into(), + &group.pubkey(), + &group_mint.pubkey(), + &group_mint_authority.pubkey(), + group_state.update_authority.try_into().unwrap(), + group_state.max_size.into(), ), ], Some(&context.payer.pubkey()), - &[&context.payer, &collection_mint_authority, &collection], + &[&context.payer, &group_mint_authority, &group], context.last_blockhash, ); context @@ -84,8 +82,7 @@ async fn test_update_collection_max_size() { .unwrap(); // Fail: update authority not signer - let mut update_ix = - update_group_max_size(&program_id, &collection.pubkey(), &collection.pubkey(), 100); + let mut update_ix = update_group_max_size(&program_id, &group.pubkey(), &group.pubkey(), 100); update_ix.accounts[1].is_signer = false; let transaction = Transaction::new_signed_with_payer( &[update_ix], @@ -107,12 +104,12 @@ async fn test_update_collection_max_size() { let transaction = Transaction::new_signed_with_payer( &[update_group_max_size( &program_id, - &collection.pubkey(), - &collection.pubkey(), + &group.pubkey(), + &group.pubkey(), 100, )], Some(&context.payer.pubkey()), - &[&context.payer, &collection], + &[&context.payer, &group], context.last_blockhash, ); assert_eq!( @@ -129,33 +126,33 @@ async fn test_update_collection_max_size() { ); // Fail: size exceeds new max size - let fetched_collection_account = context + let fetched_group_account = context .banks_client - .get_account(collection.pubkey()) + .get_account(group.pubkey()) .await .unwrap() .unwrap(); - let mut data = fetched_collection_account.data; + let mut data = fetched_group_account.data; let mut state = TlvStateMut::unpack(&mut data).unwrap(); - let collection_data = state.get_first_value_mut::().unwrap(); - collection_data.size = 30.into(); + let group_data = state.get_first_value_mut::().unwrap(); + group_data.size = 30.into(); context.set_account( - &collection.pubkey(), + &group.pubkey(), &SolanaAccount { data, - ..fetched_collection_account + ..fetched_group_account } .into(), ); let transaction = Transaction::new_signed_with_payer( &[update_group_max_size( &program_id, - &collection.pubkey(), - &collection_update_authority.pubkey(), + &group.pubkey(), + &group_update_authority.pubkey(), 20, )], Some(&context.payer.pubkey()), - &[&context.payer, &collection_update_authority], + &[&context.payer, &group_update_authority], context.last_blockhash, ); assert_eq!( @@ -175,12 +172,12 @@ async fn test_update_collection_max_size() { let transaction = Transaction::new_signed_with_payer( &[update_group_max_size( &program_id, - &collection.pubkey(), - &collection_update_authority.pubkey(), + &group.pubkey(), + &group_update_authority.pubkey(), 100, )], Some(&context.payer.pubkey()), - &[&context.payer, &collection_update_authority], + &[&context.payer, &group_update_authority], context.last_blockhash, ); context @@ -190,31 +187,29 @@ async fn test_update_collection_max_size() { .unwrap(); // Fetch the account and assert the new max size - let fetched_collection_account = context + let fetched_group_account = context .banks_client - .get_account(collection.pubkey()) + .get_account(group.pubkey()) .await .unwrap() .unwrap(); - let fetched_meta = TlvStateBorrowed::unpack(&fetched_collection_account.data).unwrap(); - let fetched_collection_group_state = fetched_meta.get_first_value::().unwrap(); - assert_eq!(fetched_collection_group_state.max_size, 100.into()); + let fetched_meta = TlvStateBorrowed::unpack(&fetched_group_account.data).unwrap(); + let fetched_group_state = fetched_meta.get_first_value::().unwrap(); + assert_eq!(fetched_group_state.max_size, 100.into()); } // Fail: immutable group #[tokio::test] -async fn test_update_collection_max_size_fail_immutable_group() { +async fn test_update_group_max_size_fail_immutable_group() { let program_id = Pubkey::new_unique(); - let collection = Keypair::new(); - let collection_mint = Keypair::new(); - let collection_mint_authority = Keypair::new(); - let collection_update_authority = Keypair::new(); + let group = Keypair::new(); + let group_mint = Keypair::new(); + let group_mint_authority = Keypair::new(); + let group_update_authority = Keypair::new(); - let collection_group_state = TokenGroup { - update_authority: Some(collection_update_authority.pubkey()) - .try_into() - .unwrap(), - mint: collection_mint.pubkey(), + let group_state = TokenGroup { + update_authority: Some(group_update_authority.pubkey()).try_into().unwrap(), + mint: group_mint.pubkey(), size: 30.into(), max_size: 50.into(), }; @@ -224,11 +219,11 @@ async fn test_update_collection_max_size_fail_immutable_group() { let token_client = Token::new( client, &spl_token_2022::id(), - &collection_mint.pubkey(), + &group_mint.pubkey(), Some(0), payer.clone(), ); - setup_mint(&token_client, &collection_mint, &collection_mint_authority).await; + setup_mint(&token_client, &group_mint, &group_mint_authority).await; let mut context = context.lock().await; @@ -240,22 +235,22 @@ async fn test_update_collection_max_size_fail_immutable_group() { &[ system_instruction::create_account( &context.payer.pubkey(), - &collection.pubkey(), + &group.pubkey(), rent_lamports, space.try_into().unwrap(), &program_id, ), initialize_group( &program_id, - &collection.pubkey(), - &collection_mint.pubkey(), - &collection_mint_authority.pubkey(), + &group.pubkey(), + &group_mint.pubkey(), + &group_mint_authority.pubkey(), None, - collection_group_state.max_size.into(), + group_state.max_size.into(), ), ], Some(&context.payer.pubkey()), - &[&context.payer, &collection_mint_authority, &collection], + &[&context.payer, &group_mint_authority, &group], context.last_blockhash, ); context @@ -267,12 +262,12 @@ async fn test_update_collection_max_size_fail_immutable_group() { let transaction = Transaction::new_signed_with_payer( &[update_group_max_size( &program_id, - &collection.pubkey(), - &collection_update_authority.pubkey(), + &group.pubkey(), + &group_update_authority.pubkey(), 100, )], Some(&context.payer.pubkey()), - &[&context.payer, &collection_update_authority], + &[&context.payer, &group_update_authority], context.last_blockhash, ); assert_eq!( From 2ce7dd414f3f581f8441b8d40a7d488715be0348 Mon Sep 17 00:00:00 2001 From: Joe Date: Thu, 19 Oct 2023 13:56:19 +0200 Subject: [PATCH 4/6] add multi-group comment --- token-group/example/src/processor.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/token-group/example/src/processor.rs b/token-group/example/src/processor.rs index b0f9e8e0959..e9604e1a44f 100644 --- a/token-group/example/src/processor.rs +++ b/token-group/example/src/processor.rs @@ -189,6 +189,8 @@ pub fn process_initialize_member(_program_id: &Pubkey, accounts: &[AccountInfo]) // Allocate a TLV entry for the space and write it in let mut buffer = member_info.try_borrow_mut_data()?; let mut state = TlvStateMut::unpack(&mut buffer)?; + // Note if `allow_repetition: true` is instead used here, one can initialize + // the same token as a member of multiple groups! let (member, _) = state.init_value::(false)?; *member = TokenGroupMember::new(member_mint_info.key, group_info.key, member_number); From 87667aabf44a3aa41f8871f0fdd2edd780362ef9 Mon Sep 17 00:00:00 2001 From: Joe Date: Fri, 20 Oct 2023 15:10:23 +0200 Subject: [PATCH 5/6] address ze nits --- .../workflows/pull-request-token-group.yml | 2 +- token-group/example/Cargo.toml | 6 ++--- token-group/example/src/lib.rs | 2 +- token-group/example/tests/initialize_group.rs | 7 +----- .../example/tests/initialize_member.rs | 11 +++++----- .../example/tests/update_group_authority.rs | 11 +++++----- .../example/tests/update_group_max_size.rs | 22 +++++++++---------- 7 files changed, 26 insertions(+), 35 deletions(-) diff --git a/.github/workflows/pull-request-token-group.yml b/.github/workflows/pull-request-token-group.yml index b0dc891ba83..fce03fe5d22 100644 --- a/.github/workflows/pull-request-token-group.yml +++ b/.github/workflows/pull-request-token-group.yml @@ -58,7 +58,7 @@ jobs: ./ci/install-program-deps.sh echo "$HOME/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH - - name: Test token-group with "serde" activated + - name: Test token-group interface run: | cargo test \ --manifest-path=token-group/interface/Cargo.toml \ diff --git a/token-group/example/Cargo.toml b/token-group/example/Cargo.toml index 17af8a35198..a1c1849ed06 100644 --- a/token-group/example/Cargo.toml +++ b/token-group/example/Cargo.toml @@ -12,15 +12,15 @@ no-entrypoint = [] test-sbf = [] [dependencies] -solana-program = "1.16.16" +solana-program = "1.17.2" spl-pod = { version = "0.1.0", path = "../../libraries/pod" } spl-token-2022 = { version = "0.9.0", path = "../../token/program-2022", features = ["no-entrypoint"] } spl-token-group-interface = { version = "0.1.0", path = "../interface" } spl-type-length-value = { version = "0.3.0", path = "../../libraries/type-length-value" } [dev-dependencies] -solana-program-test = "1.16.16" -solana-sdk = "1.16.16" +solana-program-test = "1.17.2" +solana-sdk = "1.17.2" 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" } diff --git a/token-group/example/src/lib.rs b/token-group/example/src/lib.rs index 749eb07e34a..bd0da38115d 100644 --- a/token-group/example/src/lib.rs +++ b/token-group/example/src/lib.rs @@ -2,7 +2,7 @@ //! using the SPL Token Group interface. #![deny(missing_docs)] -#![cfg_attr(not(test), forbid(unsafe_code))] +#![forbid(unsafe_code)] pub mod processor; diff --git a/token-group/example/tests/initialize_group.rs b/token-group/example/tests/initialize_group.rs index 585335d8f96..a6461324eb8 100644 --- a/token-group/example/tests/initialize_group.rs +++ b/token-group/example/tests/initialize_group.rs @@ -26,12 +26,7 @@ async fn test_initialize_group() { let group_mint = Keypair::new(); let group_mint_authority = Keypair::new(); - let group_state = TokenGroup { - update_authority: None.try_into().unwrap(), - mint: group_mint.pubkey(), - size: 0.into(), - max_size: 50.into(), - }; + let group_state = TokenGroup::new(&group_mint.pubkey(), None.try_into().unwrap(), 50); let (context, client, payer) = setup_program_test(&program_id).await; diff --git a/token-group/example/tests/initialize_member.rs b/token-group/example/tests/initialize_member.rs index c32a75571bd..386c2e7cd52 100644 --- a/token-group/example/tests/initialize_member.rs +++ b/token-group/example/tests/initialize_member.rs @@ -30,12 +30,11 @@ async fn test_initialize_group_member() { let member_mint = Keypair::new(); let member_mint_authority = Keypair::new(); - let group_state = TokenGroup { - update_authority: Some(group_update_authority.pubkey()).try_into().unwrap(), - mint: group_mint.pubkey(), - size: 30.into(), - max_size: 50.into(), - }; + let group_state = TokenGroup::new( + &group_mint.pubkey(), + Some(group_update_authority.pubkey()).try_into().unwrap(), + 50, + ); let (context, client, payer) = setup_program_test(&program_id).await; diff --git a/token-group/example/tests/update_group_authority.rs b/token-group/example/tests/update_group_authority.rs index adf2f00c209..35c74ec8682 100644 --- a/token-group/example/tests/update_group_authority.rs +++ b/token-group/example/tests/update_group_authority.rs @@ -28,12 +28,11 @@ async fn test_update_group_authority() { let group_mint_authority = Keypair::new(); let group_update_authority = Keypair::new(); - let group_state = TokenGroup { - update_authority: Some(group_update_authority.pubkey()).try_into().unwrap(), - mint: group_mint.pubkey(), - size: 30.into(), - max_size: 50.into(), - }; + let group_state = TokenGroup::new( + &group_mint.pubkey(), + Some(group_update_authority.pubkey()).try_into().unwrap(), + 50, + ); let (context, client, payer) = setup_program_test(&program_id).await; diff --git a/token-group/example/tests/update_group_max_size.rs b/token-group/example/tests/update_group_max_size.rs index af30d53f7a9..412335bfc18 100644 --- a/token-group/example/tests/update_group_max_size.rs +++ b/token-group/example/tests/update_group_max_size.rs @@ -29,12 +29,11 @@ async fn test_update_group_max_size() { let group_mint_authority = Keypair::new(); let group_update_authority = Keypair::new(); - let group_state = TokenGroup { - update_authority: Some(group_update_authority.pubkey()).try_into().unwrap(), - mint: group_mint.pubkey(), - size: 30.into(), - max_size: 50.into(), - }; + let group_state = TokenGroup::new( + &group_mint.pubkey(), + Some(group_update_authority.pubkey()).try_into().unwrap(), + 50, + ); let (context, client, payer) = setup_program_test(&program_id).await; @@ -207,12 +206,11 @@ async fn test_update_group_max_size_fail_immutable_group() { let group_mint_authority = Keypair::new(); let group_update_authority = Keypair::new(); - let group_state = TokenGroup { - update_authority: Some(group_update_authority.pubkey()).try_into().unwrap(), - mint: group_mint.pubkey(), - size: 30.into(), - max_size: 50.into(), - }; + let group_state = TokenGroup::new( + &group_mint.pubkey(), + Some(group_update_authority.pubkey()).try_into().unwrap(), + 50, + ); let (context, client, payer) = setup_program_test(&program_id).await; From 2563c92b1af3743e395b9adccbb4e312431c091a Mon Sep 17 00:00:00 2001 From: Joe Date: Fri, 20 Oct 2023 15:15:45 +0200 Subject: [PATCH 6/6] add `MemberAccountIsGroupAccount` error and checks, tests --- token-group/example/src/processor.rs | 5 +++ .../example/tests/initialize_member.rs | 42 +++++++++++++++++++ token-group/interface/src/error.rs | 3 ++ 3 files changed, 50 insertions(+) diff --git a/token-group/example/src/processor.rs b/token-group/example/src/processor.rs index e9604e1a44f..fd1dd0c6647 100644 --- a/token-group/example/src/processor.rs +++ b/token-group/example/src/processor.rs @@ -178,6 +178,11 @@ pub fn process_initialize_member(_program_id: &Pubkey, accounts: &[AccountInfo]) } } + // Make sure the member account is not the same as the group accout + if member_info.key == group_info.key { + return Err(TokenGroupError::MemberAccountIsGroupAccount.into()); + } + // Increment the size of the group let mut buffer = group_info.try_borrow_mut_data()?; let mut state = TlvStateMut::unpack(&mut buffer)?; diff --git a/token-group/example/tests/initialize_member.rs b/token-group/example/tests/initialize_member.rs index 386c2e7cd52..b0f7193d3cc 100644 --- a/token-group/example/tests/initialize_member.rs +++ b/token-group/example/tests/initialize_member.rs @@ -13,6 +13,7 @@ use { }, spl_token_client::token::Token, spl_token_group_interface::{ + error::TokenGroupError, instruction::{initialize_group, initialize_member}, state::{TokenGroup, TokenGroupMember}, }, @@ -170,6 +171,47 @@ async fn test_initialize_group_member() { TransactionError::InstructionError(1, InstructionError::MissingRequiredSignature) ); + // Fail: member account is group account + let transaction = Transaction::new_signed_with_payer( + &[ + system_instruction::create_account( + &context.payer.pubkey(), + &member.pubkey(), + member_rent_lamports, + member_space.try_into().unwrap(), + &program_id, + ), + initialize_member( + &program_id, + &member.pubkey(), + &member_mint.pubkey(), + &member_mint_authority.pubkey(), + &member.pubkey(), + &group_update_authority.pubkey(), + ), + ], + Some(&context.payer.pubkey()), + &[ + &context.payer, + &member, + &member_mint_authority, + &group_update_authority, + ], + context.last_blockhash, + ); + assert_eq!( + context + .banks_client + .process_transaction(transaction) + .await + .unwrap_err() + .unwrap(), + TransactionError::InstructionError( + 1, + InstructionError::Custom(TokenGroupError::MemberAccountIsGroupAccount as u32) + ) + ); + // Success: initialize member let transaction = Transaction::new_signed_with_payer( &[ diff --git a/token-group/interface/src/error.rs b/token-group/interface/src/error.rs index de41a6379f8..6840a060863 100644 --- a/token-group/interface/src/error.rs +++ b/token-group/interface/src/error.rs @@ -20,4 +20,7 @@ pub enum TokenGroupError { /// Incorrect update authority has signed the instruction #[error("Incorrect update authority has signed the instruction")] IncorrectUpdateAuthority, + /// Member account should not be the same as the group account + #[error("Member account should not be the same as the group account")] + MemberAccountIsGroupAccount, }