diff --git a/token/client/src/token.rs b/token/client/src/token.rs index 5a8a6b47a68..f16812f5ae7 100644 --- a/token/client/src/token.rs +++ b/token/client/src/token.rs @@ -3811,4 +3811,76 @@ where ) .await } + + /// Initialize a token-group member on a mint + pub async fn token_group_initialize_member( + &self, + mint_authority: &Pubkey, + group_mint: &Pubkey, + group_update_authority: &Pubkey, + signing_keypairs: &S, + ) -> TokenResult { + self.process_ixs( + &[spl_token_group_interface::instruction::initialize_member( + &self.program_id, + &self.pubkey, + &self.pubkey, + mint_authority, + group_mint, + group_update_authority, + )], + signing_keypairs, + ) + .await + } + + async fn get_additional_rent_for_new_member(&self) -> TokenResult { + let account = self.get_account(self.pubkey).await?; + let account_lamports = account.lamports; + let mint_state = self.unpack_mint_info(account)?; + let new_account_len = mint_state + .try_get_account_len()? + .checked_add(ExtensionType::try_calculate_account_len::(&[ + ExtensionType::TokenGroupMember, + ])?) + .ok_or(TokenError::Program( + spl_token_2022::error::TokenError::Overflow.into(), + ))?; + let new_rent_exempt_minimum = self + .client + .get_minimum_balance_for_rent_exemption(new_account_len) + .await + .map_err(TokenError::Client)?; + Ok(new_rent_exempt_minimum.saturating_sub(account_lamports)) + } + + /// Initialize a token-group member on a mint + #[allow(clippy::too_many_arguments)] + pub async fn token_group_initialize_member_with_rent_transfer( + &self, + payer: &Pubkey, + mint_authority: &Pubkey, + group_mint: &Pubkey, + group_update_authority: &Pubkey, + signing_keypairs: &S, + ) -> TokenResult { + let additional_lamports = self.get_additional_rent_for_new_member().await?; + let mut instructions = vec![]; + if additional_lamports > 0 { + instructions.push(system_instruction::transfer( + payer, + &self.pubkey, + additional_lamports, + )); + } + instructions.push(spl_token_group_interface::instruction::initialize_member( + &self.program_id, + &self.pubkey, + &self.pubkey, + mint_authority, + group_mint, + group_update_authority, + )); + self.process_ixs(&instructions, signing_keypairs).await + } } diff --git a/token/program-2022-test/tests/token_group_initialize_member.rs b/token/program-2022-test/tests/token_group_initialize_member.rs new file mode 100644 index 00000000000..adcb1434911 --- /dev/null +++ b/token/program-2022-test/tests/token_group_initialize_member.rs @@ -0,0 +1,786 @@ +#![cfg(feature = "test-sbf")] + +mod program_test; +use { + program_test::TestContext, + solana_program_test::{processor, tokio, ProgramTest}, + solana_sdk::{ + instruction::InstructionError, signature::Signer, signer::keypair::Keypair, + transaction::TransactionError, transport::TransportError, + }, + spl_pod::bytemuck::pod_from_bytes, + spl_token_2022::{error::TokenError, extension::{group_member_pointer::GroupMemberPointer, BaseStateWithExtensions}, processor::Processor}, + spl_token_client::token::{ExtensionInitializationParams, TokenError as TokenClientError}, + spl_token_group_interface::{error::TokenGroupError, state::TokenGroupMember}, + std::sync::Arc, +}; + +fn setup_program_test() -> ProgramTest { + let mut program_test = ProgramTest::default(); + program_test.add_program( + "spl_token_2022", + spl_token_2022::id(), + processor!(Processor::process), + ); + program_test +} + +async fn setup( + test_context: &mut TestContext, + mint: Keypair, + init_params: Vec, +) { + test_context + .init_token_with_mint_keypair_and_freeze_authority(mint, init_params, None) + .await + .unwrap(); +} + +#[tokio::test] +async fn success_initialize_member() { + let group_authority = Keypair::new(); + let group_mint_keypair = Keypair::new(); + + let member1_authority = Keypair::new(); + let member1_mint_keypair = Keypair::new(); + let member2_authority = Keypair::new(); + let member2_mint_keypair = Keypair::new(); + let member3_authority = Keypair::new(); + let member3_mint_keypair = Keypair::new(); + + let program_test = setup_program_test(); + let context = program_test.start_with_context().await; + let context = Arc::new(tokio::sync::Mutex::new(context)); + + let create_context = || TestContext { + context: context.clone(), + token_context: None, + }; + + let mut group_test_context = create_context(); + setup( + &mut group_test_context, + group_mint_keypair.insecure_clone(), + vec![ExtensionInitializationParams::GroupPointer { + authority: Some(group_authority.pubkey()), + group_address: Some(group_mint_keypair.pubkey()), + }], + ) + .await; + let group_token_context = group_test_context.token_context.take().unwrap(); + + let mut member1_test_context = create_context(); + setup( + &mut member1_test_context, + member1_mint_keypair.insecure_clone(), + vec![ExtensionInitializationParams::GroupMemberPointer { + authority: Some(member1_authority.pubkey()), + member_address: Some(member1_mint_keypair.pubkey()), + }], + ) + .await; + let member1_token_context = member1_test_context.token_context.take().unwrap(); + + let mut member2_test_context = create_context(); + setup( + &mut member2_test_context, + member2_mint_keypair.insecure_clone(), + vec![ExtensionInitializationParams::GroupMemberPointer { + authority: Some(member2_authority.pubkey()), + member_address: Some(member2_mint_keypair.pubkey()), + }], + ) + .await; + let member2_token_context = member2_test_context.token_context.take().unwrap(); + + let mut member3_test_context = create_context(); + setup( + &mut member3_test_context, + member3_mint_keypair.insecure_clone(), + vec![ExtensionInitializationParams::GroupMemberPointer { + authority: Some(member3_authority.pubkey()), + member_address: Some(member3_mint_keypair.pubkey()), + }], + ) + .await; + let member3_token_context = member3_test_context.token_context.take().unwrap(); + + let payer_pubkey = context.lock().await.payer.pubkey(); + + let group_update_authority = Keypair::new(); + + group_token_context + .token + .token_group_initialize_with_rent_transfer( + &payer_pubkey, + &group_token_context.mint_authority.pubkey(), + &group_update_authority.pubkey(), + 5, + &[&group_token_context.mint_authority], + ) + .await + .unwrap(); + + // fails without more lamports for new rent-exemption + let error = member1_token_context + .token + .token_group_initialize_member( + &member1_token_context.mint_authority.pubkey(), + &group_mint_keypair.pubkey(), + &group_update_authority.pubkey(), + &[ + &member1_token_context.mint_authority, + &group_update_authority, + ], + ) + .await + .unwrap_err(); + assert_eq!( + error, + TokenClientError::Client(Box::new(TransportError::TransactionError( + TransactionError::InsufficientFundsForRent { account_index: 3 } + ))) + ); + + // fail wrong mint authority signer + let not_mint_authority = Keypair::new(); + let error = member1_token_context + .token + .token_group_initialize_member_with_rent_transfer( + &payer_pubkey, + ¬_mint_authority.pubkey(), + &group_mint_keypair.pubkey(), + &group_update_authority.pubkey(), + &[¬_mint_authority, &group_update_authority], + ) + .await + .unwrap_err(); + assert_eq!( + error, + TokenClientError::Client(Box::new(TransportError::TransactionError( + TransactionError::InstructionError( + 1, + InstructionError::Custom(TokenGroupError::IncorrectMintAuthority as u32) + ) + ))) + ); + + // fail wrong group update authority signer + let not_group_update_authority = Keypair::new(); + let error = member1_token_context + .token + .token_group_initialize_member_with_rent_transfer( + &payer_pubkey, + &member1_token_context.mint_authority.pubkey(), + &group_mint_keypair.pubkey(), + ¬_group_update_authority.pubkey(), + &[ + &member1_token_context.mint_authority, + ¬_group_update_authority, + ], + ) + .await + .unwrap_err(); + assert_eq!( + error, + TokenClientError::Client(Box::new(TransportError::TransactionError( + TransactionError::InstructionError( + 1, + InstructionError::Custom(TokenGroupError::IncorrectUpdateAuthority as u32) + ) + ))) + ); + + member1_token_context + .token + .token_group_initialize_member_with_rent_transfer( + &payer_pubkey, + &member1_token_context.mint_authority.pubkey(), + &group_mint_keypair.pubkey(), + &group_update_authority.pubkey(), + &[ + &member1_token_context.mint_authority, + &group_update_authority, + ], + ) + .await + .unwrap(); + + // check that the data is correct + let mint_info = member1_token_context.token.get_mint_info().await.unwrap(); + let member_bytes = mint_info.get_extension_bytes::().unwrap(); + let fetched_member = pod_from_bytes::(member_bytes).unwrap(); + assert_eq!( + fetched_member, + &TokenGroupMember { + mint: member1_mint_keypair.pubkey(), + group: group_mint_keypair.pubkey(), + member_number: 1.try_into().unwrap(), + } + ); + + // fail double-init + let error = member1_token_context + .token + .token_group_initialize_member_with_rent_transfer( + &payer_pubkey, + &member1_token_context.mint_authority.pubkey(), + &group_mint_keypair.pubkey(), + &group_update_authority.pubkey(), + &[ + &member1_token_context.mint_authority, + &group_update_authority, + ], + ) + .await + .unwrap_err(); + assert_eq!( + error, + TokenClientError::Client(Box::new(TransportError::TransactionError( + TransactionError::InstructionError( + 1, + InstructionError::Custom(TokenError::ExtensionAlreadyInitialized as u32) + ) + ))) + ); + + // Now the others + member2_token_context + .token + .token_group_initialize_member_with_rent_transfer( + &payer_pubkey, + &member2_token_context.mint_authority.pubkey(), + &group_mint_keypair.pubkey(), + &group_update_authority.pubkey(), + &[ + &member2_token_context.mint_authority, + &group_update_authority, + ], + ) + .await + .unwrap(); + let mint_info = member2_token_context.token.get_mint_info().await.unwrap(); + let member_bytes = mint_info.get_extension_bytes::().unwrap(); + let fetched_member = pod_from_bytes::(member_bytes).unwrap(); + assert_eq!( + fetched_member, + &TokenGroupMember { + mint: member2_mint_keypair.pubkey(), + group: group_mint_keypair.pubkey(), + member_number: 2.try_into().unwrap(), + } + ); + + member3_token_context + .token + .token_group_initialize_member_with_rent_transfer( + &payer_pubkey, + &member3_token_context.mint_authority.pubkey(), + &group_mint_keypair.pubkey(), + &group_update_authority.pubkey(), + &[ + &member3_token_context.mint_authority, + &group_update_authority, + ], + ) + .await + .unwrap(); + let mint_info = member3_token_context.token.get_mint_info().await.unwrap(); + let member_bytes = mint_info.get_extension_bytes::().unwrap(); + let fetched_member = pod_from_bytes::(member_bytes).unwrap(); + assert_eq!( + fetched_member, + &TokenGroupMember { + mint: member3_mint_keypair.pubkey(), + group: group_mint_keypair.pubkey(), + member_number: 3.try_into().unwrap(), + } + ); +} + +#[tokio::test] +async fn success_initialize_member_multiple_groups() { + let group1_authority = Keypair::new(); + let group1_mint_keypair = Keypair::new(); + let group2_authority = Keypair::new(); + let group2_mint_keypair = Keypair::new(); + let group3_authority = Keypair::new(); + let group3_mint_keypair = Keypair::new(); + + let member_authority = Keypair::new(); + let member_mint_keypair = Keypair::new(); + + let program_test = setup_program_test(); + let pt_context = program_test.start_with_context().await; + let pt_context = Arc::new(tokio::sync::Mutex::new(context)); + + let create_context = || TestContext { + context: pt_context.clone(), + token_context: None, + }; + + let mut group1_context = create_context(); + setup( + &mut group1_context, + group2_mint_keypair.insecure_clone(), + vec![ExtensionInitializationParams::GroupPointer { + authority: Some(group2_authority.pubkey()), + group_address: Some(group2_mint_keypair.pubkey()), + }], + ) + .await; + let group1_context = group1_context.token_context.take().unwrap(); + + let mut group2_context = create_context(); + setup( + &mut group2_context, + group1_mint_keypair.insecure_clone(), + vec![ExtensionInitializationParams::GroupPointer { + authority: Some(group1_authority.pubkey()), + group_address: Some(group1_mint_keypair.pubkey()), + }], + ) + .await; + let group2_context = group2_context.token_context.take().unwrap(); + + let mut group3_context = create_context(); + setup( + &mut group3_context, + group3_mint_keypair.insecure_clone(), + vec![ExtensionInitializationParams::GroupPointer { + authority: Some(group3_authority.pubkey()), + group_address: Some(group3_mint_keypair.pubkey()), + }], + ) + .await; + let group3_context = group3_context.token_context.take().unwrap(); + + // Can initialize three member pointers + let mut context = create_context(); + setup( + &mut context, + member_mint_keypair.insecure_clone(), + vec![ + ExtensionInitializationParams::GroupMemberPointer { + authority: Some(member_authority.pubkey()), + group_address: Some(group1_mint_keypair.pubkey()), + member_address: Some(member_mint_keypair.pubkey()), + }, + ExtensionInitializationParams::GroupMemberPointer { + authority: Some(member_authority.pubkey()), + group_address: Some(group2_mint_keypair.pubkey()), + member_address: Some(member_mint_keypair.pubkey()), + }, + ExtensionInitializationParams::GroupMemberPointer { + authority: Some(member_authority.pubkey()), + group_address: Some(group3_mint_keypair.pubkey()), + member_address: Some(member_mint_keypair.pubkey()), + }, + ], + ) + .await; + let context = context.token_context.take().unwrap(); + + // check that the data is correct + let mint_info = context.token.get_mint_info().await.unwrap(); + let all_member_pointer_bytes = mint_info.get_all_extension_bytes::().unwrap(); + assert_eq!(all_member_pointer_bytes.len(), 3); + assert_eq!( + pod_from_bytes::(all_member_pointer_bytes[0]).unwrap(), + &GroupMemberPointer { + authority: member_authority.pubkey(), + group_address: group1_mint_keypair.pubkey(), + member_address: member_mint_keypair.pubkey(), + } + ); + assert_eq!( + pod_from_bytes::(all_member_pointer_bytes[1]).unwrap(), + &GroupMemberPointer { + authority: member_authority.pubkey(), + group_address: group2_mint_keypair.pubkey(), + member_address: member_mint_keypair.pubkey(), + } + ); + assert_eq!( + pod_from_bytes::(all_member_pointer_bytes[2]).unwrap(), + &GroupMemberPointer { + authority: member_authority.pubkey(), + group_address: group3_mint_keypair.pubkey(), + member_address: member_mint_keypair.pubkey(), + } + ); + + let payer_pubkey = pt_context.lock().await.payer.pubkey(); + let group_update_authority = Keypair::new(); + + group1_context + .token + .token_group_initialize_with_rent_transfer( + &payer_pubkey, + &group1_context.mint_authority.pubkey(), + &group_update_authority.pubkey(), + 5, + &[&group1_context.mint_authority], + ) + .await + .unwrap(); + group2_context + .token + .token_group_initialize_with_rent_transfer( + &payer_pubkey, + &group2_context.mint_authority.pubkey(), + &group_update_authority.pubkey(), + 5, + &[&group2_context.mint_authority], + ) + .await + .unwrap(); + group3_context + .token + .token_group_initialize_with_rent_transfer( + &payer_pubkey, + &group3_context.mint_authority.pubkey(), + &group_update_authority.pubkey(), + 5, + &[&group3_context.mint_authority], + ) + .await + .unwrap(); + member_token_context + .token + .token_group_initialize_member_with_rent_transfer( + &payer_pubkey, + &member_token_context.mint_authority.pubkey(), + &group1_mint_keypair.pubkey(), + &group1_update_authority.pubkey(), + &[ + &member_token_context.mint_authority, + &group1_update_authority, + ], + ) + .await + .unwrap(); + member_token_context + .token + .token_group_initialize_member_with_rent_transfer( + &payer_pubkey, + &member_token_context.mint_authority.pubkey(), + &group2_mint_keypair.pubkey(), + &group2_update_authority.pubkey(), + &[ + &member_token_context.mint_authority, + &group2_update_authority, + ], + ) + .await + .unwrap(); + member_token_context + .token + .token_group_initialize_member_with_rent_transfer( + &payer_pubkey, + &member_token_context.mint_authority.pubkey(), + &group3_mint_keypair.pubkey(), + &group3_update_authority.pubkey(), + &[ + &member_token_context.mint_authority, + &group3_update_authority, + ], + ) + .await + .unwrap(); + + // check that the data is correct + let mint_info = member_token_context.token.get_mint_info().await.unwrap(); + let all_member_bytes = mint_info.get_all_extension_bytes::().unwrap(); + assert_eq!(all_member_bytes.len(), 3); + assert_eq!( + pod_from_bytes::(all_member_bytes[0]).unwrap(), + &TokenGroupMember { + mint: member_mint_keypair.pubkey(), + group: group1_mint_keypair.pubkey(), + member_number: 1.try_into().unwrap(), + } + ); + assert_eq!( + pod_from_bytes::(all_member_bytes[1]).unwrap(), + &TokenGroupMember { + mint: member_mint_keypair.pubkey(), + group: group2_mint_keypair.pubkey(), + member_number: 1.try_into().unwrap(), + } + ); + assert_eq!( + pod_from_bytes::(all_member_bytes[2]).unwrap(), + &TokenGroupMember { + mint: member_mint_keypair.pubkey(), + group: group3_mint_keypair.pubkey(), + member_number: 1.try_into().unwrap(), + } + ); +} + +#[tokio::test] +async fn fail_without_member_pointer() { + let group_authority = Keypair::new(); + let group_mint_keypair = Keypair::new(); + + let member_mint_keypair = Keypair::new(); + + let program_test = setup_program_test(); + let context = program_test.start_with_context().await; + let context = Arc::new(tokio::sync::Mutex::new(context)); + + let create_context = || TestContext { + context: context.clone(), + token_context: None, + }; + + let mut group_test_context = create_context(); + setup( + &mut group_test_context, + group_mint_keypair.insecure_clone(), + ExtensionInitializationParams::GroupPointer { + authority: Some(group_authority.pubkey()), + group_address: Some(group_mint_keypair.pubkey()), + }, + ) + .await; + let group_token_context = group_test_context.token_context.take().unwrap(); + + let mut member_test_context = create_context(); + member_test_context + .init_token_with_mint_keypair_and_freeze_authority(member_mint_keypair, vec![], None) + .await + .unwrap(); + let member_token_context = member_test_context.token_context.take().unwrap(); + + let payer_pubkey = context.lock().await.payer.pubkey(); + + let group_update_authority = Keypair::new(); + + group_token_context + .token + .token_group_initialize_with_rent_transfer( + &payer_pubkey, + &group_token_context.mint_authority.pubkey(), + &group_update_authority.pubkey(), + 5, + &[&group_token_context.mint_authority], + ) + .await + .unwrap(); + + let error = member_token_context + .token + .token_group_initialize_member_with_rent_transfer( + &payer_pubkey, + &member_token_context.mint_authority.pubkey(), + &group_mint_keypair.pubkey(), + &group_update_authority.pubkey(), + &[ + &member_token_context.mint_authority, + &group_update_authority, + ], + ) + .await + .unwrap_err(); + assert_eq!( + error, + TokenClientError::Client(Box::new(TransportError::TransactionError( + TransactionError::InstructionError( + 1, + InstructionError::Custom(TokenError::InvalidExtensionCombination as u32) + ) + ))) + ); +} + +#[tokio::test] +async fn fail_init_in_another_mint() { + let group_authority = Keypair::new(); + let group_mint_keypair = Keypair::new(); + + let member_authority = Keypair::new(); + let first_member_mint_keypair = Keypair::new(); + let second_member_mint_keypair = Keypair::new(); + + let program_test = setup_program_test(); + let context = program_test.start_with_context().await; + let context = Arc::new(tokio::sync::Mutex::new(context)); + + let create_context = || TestContext { + context: context.clone(), + token_context: None, + }; + + let mut group_test_context = create_context(); + setup( + &mut group_test_context, + group_mint_keypair.insecure_clone(), + ExtensionInitializationParams::GroupPointer { + authority: Some(group_authority.pubkey()), + group_address: Some(group_mint_keypair.pubkey()), + }, + ) + .await; + let group_token_context = group_test_context.token_context.take().unwrap(); + + let mut member_test_context = create_context(); + setup( + &mut member_test_context, + second_member_mint_keypair.insecure_clone(), + ExtensionInitializationParams::GroupMemberPointer { + authority: Some(member_authority.pubkey()), + member_address: Some(second_member_mint_keypair.pubkey()), + }, + ) + .await; + let member_token_context = member_test_context.token_context.take().unwrap(); + + let payer_pubkey = context.lock().await.payer.pubkey(); + + let group_update_authority = Keypair::new(); + + group_token_context + .token + .token_group_initialize_with_rent_transfer( + &payer_pubkey, + &group_token_context.mint_authority.pubkey(), + &group_update_authority.pubkey(), + 5, + &[&group_token_context.mint_authority], + ) + .await + .unwrap(); + + let error = member_token_context + .token + .process_ixs( + &[spl_token_group_interface::instruction::initialize_member( + &spl_token_2022::id(), + &first_member_mint_keypair.pubkey(), + member_token_context.token.get_address(), + &member_token_context.mint_authority.pubkey(), + &group_mint_keypair.pubkey(), + &group_update_authority.pubkey(), + )], + &[ + &member_token_context.mint_authority, + &group_update_authority, + ], + ) + .await + .unwrap_err(); + + assert_eq!( + error, + TokenClientError::Client(Box::new(TransportError::TransactionError( + TransactionError::InstructionError( + 0, + InstructionError::Custom(TokenError::MintMismatch as u32) + ) + ))) + ); +} + +#[tokio::test] +async fn fail_without_signatures() { + let group_authority = Keypair::new(); + let group_mint_keypair = Keypair::new(); + + let member_authority = Keypair::new(); + let member_mint_keypair = Keypair::new(); + + let program_test = setup_program_test(); + let context = program_test.start_with_context().await; + let context = Arc::new(tokio::sync::Mutex::new(context)); + + let create_context = || TestContext { + context: context.clone(), + token_context: None, + }; + + let mut group_test_context = create_context(); + setup( + &mut group_test_context, + group_mint_keypair.insecure_clone(), + ExtensionInitializationParams::GroupPointer { + authority: Some(group_authority.pubkey()), + group_address: Some(group_mint_keypair.pubkey()), + }, + ) + .await; + let group_token_context = group_test_context.token_context.take().unwrap(); + + let mut member_test_context = create_context(); + setup( + &mut member_test_context, + member_mint_keypair.insecure_clone(), + ExtensionInitializationParams::GroupMemberPointer { + authority: Some(member_authority.pubkey()), + member_address: Some(member_mint_keypair.pubkey()), + }, + ) + .await; + let member_token_context = member_test_context.token_context.take().unwrap(); + + let payer_pubkey = context.lock().await.payer.pubkey(); + + let group_update_authority = Keypair::new(); + + group_token_context + .token + .token_group_initialize_with_rent_transfer( + &payer_pubkey, + &group_token_context.mint_authority.pubkey(), + &group_update_authority.pubkey(), + 5, + &[&group_token_context.mint_authority], + ) + .await + .unwrap(); + + // Missing mint authority + let mut instruction = spl_token_group_interface::instruction::initialize_member( + &spl_token_2022::id(), + &member_mint_keypair.pubkey(), + member_token_context.token.get_address(), + &member_token_context.mint_authority.pubkey(), + &group_mint_keypair.pubkey(), + &group_update_authority.pubkey(), + ); + instruction.accounts[2].is_signer = false; + let error = member_token_context + .token + .process_ixs(&[instruction], &[&group_update_authority]) + .await + .unwrap_err(); + assert_eq!( + error, + TokenClientError::Client(Box::new(TransportError::TransactionError( + TransactionError::InstructionError(0, InstructionError::MissingRequiredSignature) + ))) + ); + + // Missing group update authority + let mut instruction = spl_token_group_interface::instruction::initialize_member( + &spl_token_2022::id(), + &member_mint_keypair.pubkey(), + member_token_context.token.get_address(), + &member_token_context.mint_authority.pubkey(), + &group_mint_keypair.pubkey(), + &group_update_authority.pubkey(), + ); + instruction.accounts[4].is_signer = false; + let error = member_token_context + .token + .process_ixs(&[instruction], &[&member_token_context.mint_authority]) + .await + .unwrap_err(); + assert_eq!( + error, + TokenClientError::Client(Box::new(TransportError::TransactionError( + TransactionError::InstructionError(0, InstructionError::MissingRequiredSignature) + ))) + ); +} diff --git a/token/program-2022/src/extension/mod.rs b/token/program-2022/src/extension/mod.rs index aafee9667dc..202533f4b63 100644 --- a/token/program-2022/src/extension/mod.rs +++ b/token/program-2022/src/extension/mod.rs @@ -37,7 +37,7 @@ use { bytemuck::{pod_from_bytes, pod_from_bytes_mut, pod_get_packed_len}, primitives::PodU16, }, - spl_token_group_interface::state::TokenGroup, + spl_token_group_interface::state::{TokenGroup, TokenGroupMember}, spl_type_length_value::variable_len_pack::VariableLenPack, std::{ cmp::Ordering, @@ -1170,6 +1170,8 @@ pub enum ExtensionType { /// Mint contains a pointer to another account (or the same account) that /// holds group member configurations GroupMemberPointer, + /// Mint contains token group member configurations + TokenGroupMember, /// Test variable-length mint extension #[cfg(test)] VariableLenMintTest = u16::MAX - 2, @@ -1249,6 +1251,7 @@ impl ExtensionType { ExtensionType::GroupPointer => pod_get_packed_len::(), ExtensionType::TokenGroup => pod_get_packed_len::(), ExtensionType::GroupMemberPointer => pod_get_packed_len::(), + ExtensionType::TokenGroupMember => pod_get_packed_len::(), #[cfg(test)] ExtensionType::AccountPaddingTest => pod_get_packed_len::(), #[cfg(test)] @@ -1311,7 +1314,8 @@ impl ExtensionType { | ExtensionType::TokenMetadata | ExtensionType::GroupPointer | ExtensionType::TokenGroup - | ExtensionType::GroupMemberPointer => AccountType::Mint, + | ExtensionType::GroupMemberPointer + | ExtensionType::TokenGroupMember => AccountType::Mint, ExtensionType::ImmutableOwner | ExtensionType::TransferFeeAmount | ExtensionType::ConfidentialTransferAccount diff --git a/token/program-2022/src/extension/token_group/mod.rs b/token/program-2022/src/extension/token_group/mod.rs index 986bda89999..1546ec92975 100644 --- a/token/program-2022/src/extension/token_group/mod.rs +++ b/token/program-2022/src/extension/token_group/mod.rs @@ -1,6 +1,6 @@ use { crate::extension::{Extension, ExtensionType}, - spl_token_group_interface::state::TokenGroup, + spl_token_group_interface::state::{TokenGroup, TokenGroupMember}, }; /// Instruction processor for the TokenGroup extensions @@ -9,3 +9,7 @@ pub mod processor; impl Extension for TokenGroup { const TYPE: ExtensionType = ExtensionType::TokenGroup; } + +impl Extension for TokenGroupMember { + const TYPE: ExtensionType = ExtensionType::TokenGroupMember; +} diff --git a/token/program-2022/src/extension/token_group/processor.rs b/token/program-2022/src/extension/token_group/processor.rs index 5fce2def9b8..7f56ac03ed9 100644 --- a/token/program-2022/src/extension/token_group/processor.rs +++ b/token/program-2022/src/extension/token_group/processor.rs @@ -5,8 +5,9 @@ use { check_program_account, error::TokenError, extension::{ - alloc_and_serialize, group_pointer::GroupPointer, BaseStateWithExtensions, - StateWithExtensions, StateWithExtensionsMut, + alloc_and_serialize, alloc_and_serialize_allow_repeating, + group_member_pointer::GroupMemberPointer, group_pointer::GroupPointer, + BaseStateWithExtensions, StateWithExtensions, StateWithExtensionsMut, }, state::Mint, }, @@ -24,7 +25,7 @@ use { instruction::{ InitializeGroup, TokenGroupInstruction, UpdateGroupAuthority, UpdateGroupMaxSize, }, - state::TokenGroup, + state::{TokenGroup, TokenGroupMember}, }, }; @@ -142,6 +143,80 @@ pub fn process_update_group_authority( Ok(()) } +/// Processes an [InitializeMember](enum.GroupInterfaceInstruction.html) +/// instruction +pub fn process_initialize_group_member( + _program_id: &Pubkey, + accounts: &[AccountInfo], +) -> ProgramResult { + let account_info_iter = &mut accounts.iter(); + + 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 group_info = next_account_info(account_info_iter)?; + let group_update_authority_info = next_account_info(account_info_iter)?; + + // check that the mint and member accounts are the same, since the member + // extension should only describe itself + if member_info.key != member_mint_info.key { + msg!("Group member configurations for a mint must be initialized in the mint itself."); + return Err(TokenError::MintMismatch.into()); + } + + // scope the mint authority check, since the mint is in the same account! + { + // This check isn't really needed since we'll be writing into the account, + // but auditors like it + check_program_account(member_mint_info.owner)?; + 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()); + } + + if let Ok(member_pointer) = member_mint.get_extension::() { + if Option::::from(member_pointer.group_address) != Some(*group_info.key) { + msg!( + "The group-member-pointer extension must reference the group provided in \ + the instruction" + ); + return Err(TokenError::InvalidExtensionCombination.into()); + } + } else { + msg!( + "A mint with group member configurations must have the group-member-pointer \ + extension initialized" + ); + return Err(TokenError::InvalidExtensionCombination.into()); + } + } + + // Make sure the member mint is not the same as the group mint + 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 = StateWithExtensionsMut::::unpack(&mut buffer)?; + let group = state.get_extension_mut::()?; + + 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 member = TokenGroupMember::new(member_mint_info.key, group_info.key, member_number); + alloc_and_serialize_allow_repeating::(member_info, &member)?; + + Ok(()) +} + /// Processes an [Instruction](enum.Instruction.html). pub fn process_instruction( program_id: &Pubkey, @@ -161,6 +236,9 @@ pub fn process_instruction( msg!("TokenGroupInstruction: UpdateGroupAuthority"); process_update_group_authority(program_id, accounts, data) } - _ => Err(ProgramError::InvalidInstructionData), + TokenGroupInstruction::InitializeMember(_) => { + msg!("TokenGroupInstruction: InitializeMember"); + process_initialize_group_member(program_id, accounts) + } } }