diff --git a/program-libs/account-checks/Cargo.toml b/program-libs/account-checks/Cargo.toml index 337d1c85bf..74da010402 100644 --- a/program-libs/account-checks/Cargo.toml +++ b/program-libs/account-checks/Cargo.toml @@ -15,6 +15,7 @@ solana = [ "solana-account-info", "solana-pubkey", "msg", + "std" ] msg = ["dep:solana-msg"] pinocchio = ["dep:pinocchio"] diff --git a/program-libs/compressed-account/src/pubkey.rs b/program-libs/compressed-account/src/pubkey.rs index 3d10d7a60c..2dd83f9062 100644 --- a/program-libs/compressed-account/src/pubkey.rs +++ b/program-libs/compressed-account/src/pubkey.rs @@ -210,6 +210,9 @@ pub trait AsPubkey { fn to_pubkey_bytes(&self) -> [u8; 32]; #[cfg(feature = "anchor")] fn to_anchor_pubkey(&self) -> anchor_lang::prelude::Pubkey; + fn to_light_pubkey(&self) -> Pubkey { + self.to_pubkey_bytes().into() + } } impl AsPubkey for Pubkey { @@ -220,6 +223,9 @@ impl AsPubkey for Pubkey { fn to_anchor_pubkey(&self) -> anchor_lang::prelude::Pubkey { self.into() } + fn to_light_pubkey(&self) -> Pubkey { + *self + } } #[cfg(feature = "anchor")] diff --git a/program-libs/ctoken-types/src/instructions/mint_action/builder.rs b/program-libs/ctoken-types/src/instructions/mint_action/builder.rs new file mode 100644 index 0000000000..14944d6dd5 --- /dev/null +++ b/program-libs/ctoken-types/src/instructions/mint_action/builder.rs @@ -0,0 +1,161 @@ +use light_compressed_account::instruction_data::{ + compressed_proof::CompressedProof, + traits::{InstructionDiscriminator, LightInstructionData}, +}; + +use crate::instructions::mint_action::{ + Action, CompressedMintInstructionData, CompressedMintWithContext, CpiContext, CreateMint, + MintActionCompressedInstructionData, MintToCTokenAction, MintToCompressedAction, + RemoveMetadataKeyAction, UpdateAuthority, UpdateMetadataAuthorityAction, + UpdateMetadataFieldAction, +}; + +/// Discriminator for MintAction instruction +pub const MINT_ACTION_DISCRIMINATOR: u8 = 103; + +impl InstructionDiscriminator for MintActionCompressedInstructionData { + fn discriminator(&self) -> &'static [u8] { + &[MINT_ACTION_DISCRIMINATOR] + } +} + +impl LightInstructionData for MintActionCompressedInstructionData {} + +impl MintActionCompressedInstructionData { + pub fn new( + mint_with_context: CompressedMintWithContext, + proof: Option, + ) -> Self { + Self { + leaf_index: mint_with_context.leaf_index, + prove_by_index: mint_with_context.prove_by_index, + root_index: mint_with_context.root_index, + compressed_address: mint_with_context.address, + token_pool_bump: 0, + token_pool_index: 0, + create_mint: None, + actions: Vec::new(), + proof, + cpi_context: None, + mint: mint_with_context.mint, + } + } + + pub fn new_mint( + compressed_address: [u8; 32], + root_index: u16, + proof: CompressedProof, + mint: CompressedMintInstructionData, + ) -> Self { + Self { + leaf_index: 0, + prove_by_index: false, + root_index, + compressed_address, + token_pool_bump: 0, + token_pool_index: 0, + create_mint: Some(CreateMint::default()), + actions: Vec::new(), + proof: Some(proof), + cpi_context: None, + mint, + } + } + + pub fn new_mint_write_to_cpi_context( + compressed_address: [u8; 32], + root_index: u16, + mint: CompressedMintInstructionData, + cpi_context: CpiContext, + ) -> Self { + Self { + leaf_index: 0, + prove_by_index: false, + root_index, + compressed_address, + token_pool_bump: 0, + token_pool_index: 0, + create_mint: Some(CreateMint::default()), + actions: Vec::new(), + proof: None, // Proof is verified with execution not write + cpi_context: Some(cpi_context), + mint, + } + } + + #[must_use = "with_mint_to_compressed returns a new value"] + pub fn with_mint_to_compressed(mut self, action: MintToCompressedAction) -> Self { + self.actions.push(Action::MintToCompressed(action)); + self + } + + #[must_use = "with_mint_to_ctoken returns a new value"] + pub fn with_mint_to_ctoken(mut self, action: MintToCTokenAction) -> Self { + self.actions.push(Action::MintToCToken(action)); + self + } + + #[must_use = "with_update_mint_authority returns a new value"] + pub fn with_update_mint_authority(mut self, authority: UpdateAuthority) -> Self { + self.actions.push(Action::UpdateMintAuthority(authority)); + self + } + + #[must_use = "with_update_freeze_authority returns a new value"] + pub fn with_update_freeze_authority(mut self, authority: UpdateAuthority) -> Self { + self.actions.push(Action::UpdateFreezeAuthority(authority)); + self + } + + #[must_use = "with_update_metadata_field returns a new value"] + pub fn with_update_metadata_field(mut self, action: UpdateMetadataFieldAction) -> Self { + self.actions.push(Action::UpdateMetadataField(action)); + self + } + + #[must_use = "with_update_metadata_authority returns a new value"] + pub fn with_update_metadata_authority(mut self, action: UpdateMetadataAuthorityAction) -> Self { + self.actions.push(Action::UpdateMetadataAuthority(action)); + self + } + + #[must_use = "with_remove_metadata_key returns a new value"] + pub fn with_remove_metadata_key(mut self, action: RemoveMetadataKeyAction) -> Self { + self.actions.push(Action::RemoveMetadataKey(action)); + self + } + + #[must_use = "with_cpi_context returns a new value"] + pub fn with_cpi_context(mut self, cpi_context: CpiContext) -> Self { + self.cpi_context = Some(cpi_context); + self + } + + #[must_use = "write_to_cpi_context_first returns a new value"] + pub fn write_to_cpi_context_first(mut self) -> Self { + if let Some(ref mut ctx) = self.cpi_context { + ctx.first_set_context = true; + ctx.set_context = false; + } else { + self.cpi_context = Some(CpiContext { + first_set_context: true, + ..Default::default() + }); + } + self + } + + #[must_use = "write_to_cpi_context_set returns a new value"] + pub fn write_to_cpi_context_set(mut self) -> Self { + if let Some(ref mut ctx) = self.cpi_context { + ctx.set_context = true; + ctx.first_set_context = false; + } else { + self.cpi_context = Some(CpiContext { + set_context: true, + ..Default::default() + }); + } + self + } +} diff --git a/program-libs/ctoken-types/src/instructions/mint_action/mint_to_compressed.rs b/program-libs/ctoken-types/src/instructions/mint_action/mint_to_compressed.rs index 8244a3773f..7956556419 100644 --- a/program-libs/ctoken-types/src/instructions/mint_action/mint_to_compressed.rs +++ b/program-libs/ctoken-types/src/instructions/mint_action/mint_to_compressed.rs @@ -1,4 +1,4 @@ -use light_compressed_account::Pubkey; +use light_compressed_account::{pubkey::AsPubkey, Pubkey}; use light_zero_copy::ZeroCopy; use crate::{AnchorDeserialize, AnchorSerialize}; @@ -10,9 +10,27 @@ pub struct Recipient { pub amount: u64, } +impl Recipient { + pub fn new(recipient: impl AsPubkey, amount: u64) -> Self { + Self { + recipient: recipient.to_light_pubkey(), + amount, + } + } +} + #[repr(C)] #[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize, ZeroCopy)] pub struct MintToCompressedAction { pub token_account_version: u8, pub recipients: Vec, } + +impl MintToCompressedAction { + pub fn new(recipients: Vec) -> Self { + Self { + token_account_version: 3, + recipients, + } + } +} diff --git a/program-libs/ctoken-types/src/instructions/mint_action/mod.rs b/program-libs/ctoken-types/src/instructions/mint_action/mod.rs index 5a0df3d5cd..cc3282f313 100644 --- a/program-libs/ctoken-types/src/instructions/mint_action/mod.rs +++ b/program-libs/ctoken-types/src/instructions/mint_action/mod.rs @@ -13,3 +13,4 @@ pub use mint_to_compressed::*; pub use mint_to_ctoken::*; pub use update_metadata::*; pub use update_mint::*; +mod builder; diff --git a/program-tests/compressed-token-test/tests/mint/cpi_context.rs b/program-tests/compressed-token-test/tests/mint/cpi_context.rs index a82d20f7d5..1c2194d452 100644 --- a/program-tests/compressed-token-test/tests/mint/cpi_context.rs +++ b/program-tests/compressed-token-test/tests/mint/cpi_context.rs @@ -1,17 +1,19 @@ use anchor_lang::InstructionData; use compressed_token_test::ID as WRAPPER_PROGRAM_ID; use light_client::indexer::Indexer; +use light_compressed_account::instruction_data::traits::LightInstructionData; use light_compressed_token_sdk::instructions::{ derive_compressed_mint_address, find_spl_mint_address, - mint_action::instruction::{ - create_mint_action_cpi, CreateMintCpiWriteInputs, MintActionInputs, - MintActionInputsCpiWrite, - }, + get_mint_action_instruction_account_metas, get_mint_action_instruction_account_metas_cpi_write, + mint_action::{MintActionMetaConfig, MintActionMetaConfigCpiWrite}, }; use light_ctoken_types::{ - instructions::mint_action::{CompressedMintInstructionData, CompressedMintWithContext}, + instructions::mint_action::{ + CompressedMintInstructionData, CompressedMintWithContext, CpiContext, + MintActionCompressedInstructionData, + }, state::CompressedMintMetadata, - COMPRESSED_TOKEN_PROGRAM_ID, + CMINT_ADDRESS_TREE, COMPRESSED_TOKEN_PROGRAM_ID, }; use light_program_test::{utils::assert::assert_rpc_error, LightProgramTest, ProgramTestConfig}; use light_test_utils::Rpc; @@ -25,12 +27,16 @@ use solana_sdk::{ struct TestSetup { rpc: LightProgramTest, - mint_action_inputs: MintActionInputsCpiWrite, + compressed_mint_inputs: CompressedMintWithContext, payer: Keypair, mint_seed: Keypair, mint_authority: Keypair, compressed_mint_address: [u8; 32], cpi_context_pubkey: Pubkey, + address_tree: Pubkey, + address_tree_index: u8, + output_queue: Pubkey, + output_queue_index: u8, } async fn test_setup() -> TestSetup { @@ -46,9 +52,10 @@ async fn test_setup() -> TestSetup { let address_tree_info = rpc.get_address_tree_v2(); let address_tree = address_tree_info.tree; - // Get CPI context from test accounts + // Get CPI context and state tree info from test accounts let tree_info = rpc.test_accounts.v2_state_trees[0]; let cpi_context_pubkey = tree_info.cpi_context; + let output_queue = tree_info.output_queue; // 2. Create mint parameters let mint_seed = Keypair::new(); @@ -61,7 +68,7 @@ async fn test_setup() -> TestSetup { derive_compressed_mint_address(&mint_seed.pubkey(), &address_tree); let (spl_mint_pda, _) = find_spl_mint_address(&mint_seed.pubkey()); - // 3. Build mint action instruction using SDK + // 3. Build compressed mint inputs let compressed_mint_inputs = CompressedMintWithContext { leaf_index: 0, prove_by_index: false, @@ -81,28 +88,18 @@ async fn test_setup() -> TestSetup { }, }; - let create_mint_inputs = CreateMintCpiWriteInputs { - compressed_mint_inputs, - mint_seed: mint_seed.pubkey(), - authority: mint_authority.pubkey(), - payer: payer.pubkey(), - cpi_context_pubkey, - first_set_context: true, - address_tree_index: 1, - output_queue_index: 0, - assigned_account_index: 0, - }; - - let mint_action_inputs = MintActionInputsCpiWrite::new_create_mint(create_mint_inputs); - TestSetup { rpc, - mint_action_inputs, + compressed_mint_inputs, payer, mint_seed, mint_authority, compressed_mint_address, cpi_context_pubkey, + address_tree, + address_tree_index: 1, + output_queue, + output_queue_index: 0, } } @@ -111,29 +108,64 @@ async fn test_setup() -> TestSetup { async fn test_write_to_cpi_context_create_mint() { let TestSetup { mut rpc, - mint_action_inputs, + compressed_mint_inputs, payer, mint_seed, mint_authority, compressed_mint_address, cpi_context_pubkey, + address_tree, + address_tree_index, + output_queue: _, + output_queue_index, } = test_setup().await; - // Get the compressed token program instruction - let ctoken_instruction = - light_compressed_token_sdk::instructions::mint_action::instruction::mint_action_cpi_write( - mint_action_inputs, - ) - .expect("Failed to build mint action instruction"); + // Build instruction data using new builder API + let instruction_data = MintActionCompressedInstructionData::new_mint( + compressed_mint_inputs.address, + compressed_mint_inputs.root_index, + CompressedProof::default(), + compressed_mint_inputs.mint.clone(), + ) + .with_cpi_context(CpiContext { + set_context: false, + first_set_context: true, + in_tree_index: address_tree_index, + in_queue_index: 0, + out_queue_index: output_queue_index, + token_out_queue_index: 0, + assigned_account_index: 0, + read_only_address_trees: [0; 4], + address_tree_pubkey: address_tree.to_bytes(), + }); + + // Build account metas using helper + let config = MintActionMetaConfigCpiWrite { + fee_payer: payer.pubkey(), + mint_signer: Some(mint_seed.pubkey()), + authority: mint_authority.pubkey(), + cpi_context: cpi_context_pubkey, + mint_needs_to_sign: true, + }; - // Build wrapper program instruction - // The wrapper just passes through all accounts and instruction data + let account_metas = get_mint_action_instruction_account_metas_cpi_write(config); - // Build the wrapper instruction using Anchor's InstructionData - let wrapper_ix_data = compressed_token_test::instruction::WriteToCpiContextMintAction { - inputs: ctoken_instruction.data.clone(), + // Serialize instruction data + let data = instruction_data + .data() + .expect("Failed to serialize instruction data"); + + // Build compressed token instruction + let ctoken_instruction = Instruction { + program_id: Pubkey::new_from_array(COMPRESSED_TOKEN_PROGRAM_ID), + accounts: account_metas, + data: data.clone(), }; + // Build wrapper instruction using Anchor's InstructionData + let wrapper_ix_data = + compressed_token_test::instruction::WriteToCpiContextMintAction { inputs: data }; + let wrapper_instruction = Instruction { program_id: WRAPPER_PROGRAM_ID, accounts: vec![AccountMeta::new_readonly( @@ -195,30 +227,67 @@ async fn test_write_to_cpi_context_create_mint() { async fn test_write_to_cpi_context_invalid_address_tree() { let TestSetup { mut rpc, - mut mint_action_inputs, + compressed_mint_inputs, payer, mint_seed, mint_authority, compressed_mint_address: _, - cpi_context_pubkey: _, + cpi_context_pubkey, + address_tree: _, + address_tree_index, + output_queue: _, + output_queue_index, } = test_setup().await; // Swap the address tree pubkey to a random one (this should fail validation) let invalid_address_tree = Pubkey::new_unique(); - mint_action_inputs.cpi_context.address_tree_pubkey = invalid_address_tree.to_bytes(); - // Get the compressed token program instruction - let ctoken_instruction = - light_compressed_token_sdk::instructions::mint_action::instruction::mint_action_cpi_write( - mint_action_inputs, - ) - .expect("Failed to build mint action instruction"); + // Build instruction data with invalid address tree + let instruction_data = MintActionCompressedInstructionData::new_mint( + compressed_mint_inputs.address, + compressed_mint_inputs.root_index, + CompressedProof::default(), + compressed_mint_inputs.mint.clone(), + ) + .with_cpi_context(CpiContext { + set_context: false, + first_set_context: true, + in_tree_index: address_tree_index, + in_queue_index: 0, + out_queue_index: output_queue_index, + token_out_queue_index: 0, + assigned_account_index: 0, + read_only_address_trees: [0; 4], + address_tree_pubkey: invalid_address_tree.to_bytes(), + }); + + // Build account metas using helper + let config = MintActionMetaConfigCpiWrite { + fee_payer: payer.pubkey(), + mint_signer: Some(mint_seed.pubkey()), + authority: mint_authority.pubkey(), + cpi_context: cpi_context_pubkey, + mint_needs_to_sign: true, + }; + + let account_metas = get_mint_action_instruction_account_metas_cpi_write(config); + + // Serialize instruction data + let data = instruction_data + .data() + .expect("Failed to serialize instruction data"); - // Build wrapper program instruction - let wrapper_ix_data = compressed_token_test::instruction::WriteToCpiContextMintAction { - inputs: ctoken_instruction.data.clone(), + // Build compressed token instruction + let ctoken_instruction = Instruction { + program_id: Pubkey::new_from_array(COMPRESSED_TOKEN_PROGRAM_ID), + accounts: account_metas, + data: data.clone(), }; + // Build wrapper instruction + let wrapper_ix_data = + compressed_token_test::instruction::WriteToCpiContextMintAction { inputs: data }; + let wrapper_instruction = Instruction { program_id: WRAPPER_PROGRAM_ID, accounts: vec![AccountMeta::new_readonly( @@ -250,31 +319,68 @@ async fn test_write_to_cpi_context_invalid_address_tree() { async fn test_write_to_cpi_context_invalid_compressed_address() { let TestSetup { mut rpc, - mut mint_action_inputs, + compressed_mint_inputs, payer, mint_seed, mint_authority, compressed_mint_address: _, - cpi_context_pubkey: _, + cpi_context_pubkey, + address_tree, + address_tree_index, + output_queue: _, + output_queue_index, } = test_setup().await; // Swap the compressed address to a random one (this should fail validation) - // Keep the correct address_tree_pubkey (CMINT_ADDRESS_TREE) but provide wrong address + // Keep the correct address_tree_pubkey but provide wrong address let invalid_compressed_address = [42u8; 32]; - mint_action_inputs.compressed_mint_inputs.address = invalid_compressed_address; - // Get the compressed token program instruction - let ctoken_instruction = - light_compressed_token_sdk::instructions::mint_action::instruction::mint_action_cpi_write( - mint_action_inputs, - ) - .expect("Failed to build mint action instruction"); + // Build instruction data with invalid compressed address + let instruction_data = MintActionCompressedInstructionData::new_mint( + invalid_compressed_address, + compressed_mint_inputs.root_index, + CompressedProof::default(), + compressed_mint_inputs.mint.clone(), + ) + .with_cpi_context(CpiContext { + set_context: false, + first_set_context: true, + in_tree_index: address_tree_index, + in_queue_index: 0, + out_queue_index: output_queue_index, + token_out_queue_index: 0, + assigned_account_index: 0, + read_only_address_trees: [0; 4], + address_tree_pubkey: address_tree.to_bytes(), + }); - // Build wrapper program instruction - let wrapper_ix_data = compressed_token_test::instruction::WriteToCpiContextMintAction { - inputs: ctoken_instruction.data.clone(), + // Build account metas using helper + let config = MintActionMetaConfigCpiWrite { + fee_payer: payer.pubkey(), + mint_signer: Some(mint_seed.pubkey()), + authority: mint_authority.pubkey(), + cpi_context: cpi_context_pubkey, + mint_needs_to_sign: true, }; + let account_metas = get_mint_action_instruction_account_metas_cpi_write(config); + + // Serialize instruction data + let data = instruction_data + .data() + .expect("Failed to serialize instruction data"); + + // Build compressed token instruction + let ctoken_instruction = Instruction { + program_id: Pubkey::new_from_array(COMPRESSED_TOKEN_PROGRAM_ID), + accounts: account_metas, + data: data.clone(), + }; + + // Build wrapper instruction + let wrapper_ix_data = + compressed_token_test::instruction::WriteToCpiContextMintAction { inputs: data }; + let wrapper_instruction = Instruction { program_id: WRAPPER_PROGRAM_ID, accounts: vec![AccountMeta::new_readonly( @@ -306,17 +412,20 @@ async fn test_write_to_cpi_context_invalid_compressed_address() { async fn test_execute_cpi_context_invalid_tree_index() { let TestSetup { mut rpc, - mint_action_inputs, + compressed_mint_inputs, payer, mint_seed, mint_authority, compressed_mint_address: _, cpi_context_pubkey, + address_tree: _, + address_tree_index: _, + output_queue, + output_queue_index: _, } = test_setup().await; - // Now try to execute with invalid in_tree_index (should fail) // Build execute mode CPI context with invalid tree index - let execute_cpi_context = light_ctoken_types::instructions::mint_action::CpiContext { + let execute_cpi_context = CpiContext { set_context: false, first_set_context: false, // Execute mode in_tree_index: 5, // Invalid! Should be 1 @@ -325,40 +434,50 @@ async fn test_execute_cpi_context_invalid_tree_index() { token_out_queue_index: 0, assigned_account_index: 0, read_only_address_trees: [0; 4], - address_tree_pubkey: light_ctoken_types::CMINT_ADDRESS_TREE, + address_tree_pubkey: CMINT_ADDRESS_TREE, }; - // Get tree info for execute mode - let tree_info = rpc.test_accounts.v2_state_trees[0]; + // Build instruction data for execute mode - must mark as create_mint + let instruction_data = MintActionCompressedInstructionData::new_mint( + compressed_mint_inputs.address, + compressed_mint_inputs.root_index, + CompressedProof::default(), + compressed_mint_inputs.mint.clone(), + ) + .with_cpi_context(execute_cpi_context); + + // Build account metas using regular MintActionMetaConfig for execute mode + let mut config = MintActionMetaConfig::new_create_mint( + &instruction_data, + mint_authority.pubkey(), + mint_seed.pubkey(), + payer.pubkey(), + Pubkey::new_from_array(CMINT_ADDRESS_TREE), + output_queue, + ) + .expect("Failed to create meta config"); - // Build MintActionInputs for execute mode - let execute_inputs = MintActionInputs { - compressed_mint_inputs: mint_action_inputs.compressed_mint_inputs.clone(), - mint_seed: mint_seed.pubkey(), - mint_bump: None, - create_mint: true, - authority: mint_action_inputs.authority, - payer: mint_action_inputs.payer, - proof: Some(CompressedProof::default()), - actions: vec![], - address_tree_pubkey: Pubkey::new_from_array(light_ctoken_types::CMINT_ADDRESS_TREE), - input_queue: None, - output_queue: tree_info.output_queue, - tokens_out_queue: None, - token_pool: None, - }; + // Set CPI context for execute mode + config.with_cpi_context = Some(cpi_context_pubkey); - let execute_instruction = create_mint_action_cpi( - execute_inputs, - Some(execute_cpi_context), - Some(cpi_context_pubkey), - ) - .expect("Failed to build execute instruction"); + let account_metas = get_mint_action_instruction_account_metas(config, &compressed_mint_inputs); - let execute_wrapper_ix_data = compressed_token_test::instruction::ExecuteCpiContextMintAction { - inputs: execute_instruction.data.clone(), + // Serialize instruction data + let data = instruction_data + .data() + .expect("Failed to serialize instruction data"); + + // Build compressed token instruction + let execute_instruction = Instruction { + program_id: Pubkey::new_from_array(COMPRESSED_TOKEN_PROGRAM_ID), + accounts: account_metas, + data: data.clone(), }; + // Build wrapper instruction + let execute_wrapper_ix_data = + compressed_token_test::instruction::ExecuteCpiContextMintAction { inputs: data }; + let execute_wrapper_instruction = Instruction { program_id: WRAPPER_PROGRAM_ID, accounts: vec![AccountMeta::new_readonly( diff --git a/program-tests/compressed-token-test/tests/mint/edge_cases.rs b/program-tests/compressed-token-test/tests/mint/edge_cases.rs index 0ab7e7ffc5..fb696bc888 100644 --- a/program-tests/compressed-token-test/tests/mint/edge_cases.rs +++ b/program-tests/compressed-token-test/tests/mint/edge_cases.rs @@ -12,7 +12,10 @@ use light_program_test::{LightProgramTest, ProgramTestConfig}; use light_test_utils::{ assert_mint_action::assert_mint_action, mint_assert::assert_compressed_mint_account, Rpc, }; -use light_token_client::actions::create_mint; +use light_token_client::{ + actions::create_mint, + instructions::mint_action::{MintActionType, MintToRecipient}, +}; use serial_test::serial; use solana_sdk::{signature::Keypair, signer::Signer}; @@ -169,65 +172,66 @@ async fn functional_all_in_one_instruction() { // Build all actions for a single instruction let actions = vec![ // 1. MintToCompressed - mint to compressed account - light_compressed_token_sdk::instructions::mint_action::MintActionType::MintTo { - recipients: vec![light_compressed_token_sdk::instructions::mint_action::MintToRecipient { + MintActionType::MintTo { + recipients: vec![MintToRecipient { recipient: Keypair::new().pubkey(), amount: 1000u64, }], token_account_version: 2, }, // 2. MintToCToken - mint to decompressed account - light_compressed_token_sdk::instructions::mint_action::MintActionType::MintToCToken { + MintActionType::MintToCToken { account: light_compressed_token_sdk::instructions::derive_ctoken_ata( &recipient.pubkey(), &spl_mint_pda, - ).0, + ) + .0, amount: 2000u64, }, // 3. UpdateMintAuthority - light_compressed_token_sdk::instructions::mint_action::MintActionType::UpdateMintAuthority { + MintActionType::UpdateMintAuthority { new_authority: Some(new_mint_authority.pubkey()), }, // 4. UpdateFreezeAuthority - light_compressed_token_sdk::instructions::mint_action::MintActionType::UpdateFreezeAuthority { + MintActionType::UpdateFreezeAuthority { new_authority: Some(new_freeze_authority.pubkey()), }, // 5. UpdateMetadataField - update the name - light_compressed_token_sdk::instructions::mint_action::MintActionType::UpdateMetadataField { + MintActionType::UpdateMetadataField { extension_index: 0, field_type: 0, // Name field key: vec![], value: "Updated Token Name".as_bytes().to_vec(), }, // 6. UpdateMetadataField - update the symbol - light_compressed_token_sdk::instructions::mint_action::MintActionType::UpdateMetadataField { + MintActionType::UpdateMetadataField { extension_index: 0, field_type: 1, // Symbol field key: vec![], value: "UPDATED".as_bytes().to_vec(), }, // 7. UpdateMetadataField - update the URI - light_compressed_token_sdk::instructions::mint_action::MintActionType::UpdateMetadataField { + MintActionType::UpdateMetadataField { extension_index: 0, field_type: 2, // URI field key: vec![], value: "https://updated.example.com/token.json".as_bytes().to_vec(), }, // 8. UpdateMetadataField - update the first additional metadata field - light_compressed_token_sdk::instructions::mint_action::MintActionType::UpdateMetadataField { + MintActionType::UpdateMetadataField { extension_index: 0, field_type: 3, // Custom key field key: vec![1, 2, 3, 4], value: "updated_value".as_bytes().to_vec(), }, // 9. RemoveMetadataKey - remove the second additional metadata key - light_compressed_token_sdk::instructions::mint_action::MintActionType::RemoveMetadataKey { + MintActionType::RemoveMetadataKey { extension_index: 0, key: vec![4, 5, 6, 7], idempotent: 0, }, // 10. UpdateMetadataAuthority - light_compressed_token_sdk::instructions::mint_action::MintActionType::UpdateMetadataAuthority { + MintActionType::UpdateMetadataAuthority { extension_index: 0, new_authority: new_metadata_authority.pubkey(), }, diff --git a/program-tests/compressed-token-test/tests/mint/failing.rs b/program-tests/compressed-token-test/tests/mint/failing.rs index 31ec33b0ae..e492ef273b 100644 --- a/program-tests/compressed-token-test/tests/mint/failing.rs +++ b/program-tests/compressed-token-test/tests/mint/failing.rs @@ -10,7 +10,10 @@ use light_program_test::{utils::assert::assert_rpc_error, LightProgramTest, Prog use light_test_utils::{ assert_mint_action::assert_mint_action, mint_assert::assert_compressed_mint_account, Rpc, }; -use light_token_client::actions::create_mint; +use light_token_client::{ + actions::create_mint, + instructions::mint_action::{MintActionType, MintToRecipient}, +}; use serial_test::serial; use solana_sdk::{ instruction::AccountMeta, signature::Keypair, signer::Signer, transaction::Transaction, @@ -182,10 +185,12 @@ async fn functional_and_failing_tests() { let result = light_token_client::actions::mint_to_compressed( &mut rpc, spl_mint_pda, - vec![light_ctoken_types::instructions::mint_action::Recipient { - recipient: Keypair::new().pubkey().to_bytes().into(), - amount: 1000u64, - }], + vec![ + light_ctoken_types::instructions::mint_action::Recipient::new( + Keypair::new().pubkey(), + 1000u64, + ), + ], light_ctoken_types::state::TokenDataVersion::V2, &invalid_mint_authority, // Invalid authority &payer, @@ -214,14 +219,11 @@ async fn functional_and_failing_tests() { ) .unwrap(); - let recipient = Keypair::new().pubkey().to_bytes().into(); + let recipient = Keypair::new().pubkey(); let result = light_token_client::actions::mint_to_compressed( &mut rpc, spl_mint_pda, - vec![light_ctoken_types::instructions::mint_action::Recipient { - recipient, - amount: 1000u64, - }], + vec![light_ctoken_types::instructions::mint_action::Recipient::new(recipient, 1000u64)], light_ctoken_types::state::TokenDataVersion::V2, &mint_authority, // Valid authority &payer, @@ -235,17 +237,13 @@ async fn functional_and_failing_tests() { &mut rpc, compressed_mint_address, pre_compressed_mint, - vec![ - light_compressed_token_sdk::instructions::mint_action::MintActionType::MintTo { - recipients: vec![ - light_compressed_token_sdk::instructions::mint_action::MintToRecipient { - recipient: recipient.into(), - amount: 1000u64, - }, - ], - token_account_version: light_ctoken_types::state::TokenDataVersion::V2 as u8, - }, - ], + vec![MintActionType::MintTo { + recipients: vec![MintToRecipient { + recipient, + amount: 1000u64, + }], + token_account_version: light_ctoken_types::state::TokenDataVersion::V2 as u8, + }], ) .await; } @@ -313,7 +311,7 @@ async fn functional_and_failing_tests() { &mut rpc, compressed_mint_address, pre_compressed_mint, - vec![light_compressed_token_sdk::instructions::mint_action::MintActionType::UpdateMintAuthority { + vec![MintActionType::UpdateMintAuthority { new_authority: Some(new_mint_authority.pubkey()), }], ) @@ -386,7 +384,7 @@ async fn functional_and_failing_tests() { &mut rpc, compressed_mint_address, pre_compressed_mint, - vec![light_compressed_token_sdk::instructions::mint_action::MintActionType::UpdateFreezeAuthority { + vec![MintActionType::UpdateFreezeAuthority { new_authority: Some(new_freeze_authority.pubkey()), }], ) @@ -417,10 +415,12 @@ async fn functional_and_failing_tests() { &invalid_mint_authority, // Invalid authority &payer, vec![], // No compressed recipients - vec![light_ctoken_types::instructions::mint_action::Recipient { - recipient: recipient.pubkey().to_bytes().into(), - amount: 1000u64, - }], // Mint to decompressed + vec![ + light_ctoken_types::instructions::mint_action::Recipient::new( + recipient.pubkey(), + 1000u64, + ), + ], // Mint to decompressed None, // No mint authority update None, // No freeze authority update None, // Not creating new mint @@ -478,10 +478,12 @@ async fn functional_and_failing_tests() { &new_mint_authority, // Valid NEW authority after update &payer, vec![], // No compressed recipients - vec![light_ctoken_types::instructions::mint_action::Recipient { - recipient: recipient2.pubkey().to_bytes().into(), - amount: 2000u64, - }], // Mint to decompressed + vec![ + light_ctoken_types::instructions::mint_action::Recipient::new( + recipient2.pubkey(), + 2000u64, + ), + ], // Mint to decompressed None, // No mint authority update None, // No freeze authority update None, // Not creating new mint @@ -495,7 +497,7 @@ async fn functional_and_failing_tests() { &mut rpc, compressed_mint_address, pre_compressed_mint, - vec![light_compressed_token_sdk::instructions::mint_action::MintActionType::MintToCToken { + vec![MintActionType::MintToCToken { account: recipient_ata, amount: 2000u64, }], @@ -512,7 +514,7 @@ async fn functional_and_failing_tests() { mint_seed: mint_seed.pubkey(), authority: invalid_metadata_authority.pubkey(), // Invalid authority payer: payer.pubkey(), - actions: vec![light_compressed_token_sdk::instructions::mint_action::MintActionType::UpdateMetadataField { + actions: vec![MintActionType::UpdateMetadataField { extension_index: 0, field_type: 0, // 0 = Name field key: vec![], // Empty for Name field @@ -548,7 +550,7 @@ async fn functional_and_failing_tests() { ) .unwrap(); - let actions = vec![light_compressed_token_sdk::instructions::mint_action::MintActionType::UpdateMetadataField { + let actions = vec![MintActionType::UpdateMetadataField { extension_index: 0, field_type: 0, // 0 = Name field key: vec![], // Empty for Name field @@ -595,7 +597,7 @@ async fn functional_and_failing_tests() { mint_seed: mint_seed.pubkey(), authority: invalid_metadata_authority.pubkey(), // Invalid authority payer: payer.pubkey(), - actions: vec![light_compressed_token_sdk::instructions::mint_action::MintActionType::UpdateMetadataAuthority { + actions: vec![MintActionType::UpdateMetadataAuthority { extension_index: 0, new_authority: Keypair::new().pubkey(), }], @@ -629,7 +631,7 @@ async fn functional_and_failing_tests() { ) .unwrap(); - let actions = vec![light_compressed_token_sdk::instructions::mint_action::MintActionType::UpdateMetadataAuthority { + let actions = vec![MintActionType::UpdateMetadataAuthority { extension_index: 0, new_authority: new_metadata_authority.pubkey(), }]; @@ -674,10 +676,10 @@ async fn functional_and_failing_tests() { mint_seed: mint_seed.pubkey(), authority: invalid_metadata_authority.pubkey(), // Invalid authority payer: payer.pubkey(), - actions: vec![light_compressed_token_sdk::instructions::mint_action::MintActionType::RemoveMetadataKey { + actions: vec![MintActionType::RemoveMetadataKey { extension_index: 0, - key: vec![1,2,3,4], // The key we added in additional_metadata - idempotent: 0, // 0 = false + key: vec![1, 2, 3, 4], // The key we added in additional_metadata + idempotent: 0, // 0 = false }], new_mint: None, }, @@ -709,10 +711,10 @@ async fn functional_and_failing_tests() { ) .unwrap(); - let actions = vec![light_compressed_token_sdk::instructions::mint_action::MintActionType::RemoveMetadataKey { + let actions = vec![MintActionType::RemoveMetadataKey { extension_index: 0, - key: vec![1,2,3,4], // The key we added in additional_metadata - idempotent: 0, // 0 = false + key: vec![1, 2, 3, 4], // The key we added in additional_metadata + idempotent: 0, // 0 = false }]; let result = light_token_client::actions::mint_action( @@ -762,10 +764,10 @@ async fn functional_and_failing_tests() { ) .unwrap(); - let actions = vec![light_compressed_token_sdk::instructions::mint_action::MintActionType::RemoveMetadataKey { + let actions = vec![MintActionType::RemoveMetadataKey { extension_index: 0, - key: vec![1,2,3,4], // Same key, already removed - idempotent: 1, // 1 = true (won't error if key doesn't exist) + key: vec![1, 2, 3, 4], // Same key, already removed + idempotent: 1, // 1 = true (won't error if key doesn't exist) }]; let result = light_token_client::actions::mint_action( diff --git a/program-tests/compressed-token-test/tests/mint/random.rs b/program-tests/compressed-token-test/tests/mint/random.rs index 8a76cb5813..f57d21fa7b 100644 --- a/program-tests/compressed-token-test/tests/mint/random.rs +++ b/program-tests/compressed-token-test/tests/mint/random.rs @@ -9,7 +9,10 @@ use light_program_test::{LightProgramTest, ProgramTestConfig}; use light_test_utils::{ assert_mint_action::assert_mint_action, mint_assert::assert_compressed_mint_account, Rpc, }; -use light_token_client::actions::create_mint; +use light_token_client::{ + actions::create_mint, + instructions::mint_action::{MintActionType, MintToRecipient}, +}; use serial_test::serial; use solana_sdk::{signature::Keypair, signer::Signer}; @@ -190,70 +193,58 @@ async fn test_random_mint_action() { let mut recipients = Vec::new(); for _ in 0..num_recipients { - recipients.push( - light_compressed_token_sdk::instructions::mint_action::MintToRecipient { - recipient: Keypair::new().pubkey(), - amount: rng.gen_range(1..=100000), - } - ); + recipients.push(MintToRecipient { + recipient: Keypair::new().pubkey(), + amount: rng.gen_range(1..=100000), + }); } total_recipients += num_recipients; - actions.push( - light_compressed_token_sdk::instructions::mint_action::MintActionType::MintTo { - recipients, - token_account_version: rng.gen_range(1..=3), - } - ); + actions.push(MintActionType::MintTo { + recipients, + token_account_version: rng.gen_range(1..=3), + }); } } // 30% chance: MintToCToken 300..=599 => { // Randomly select one of the 5 pre-created ATAs let ata_index = rng.gen_range(0..ctoken_atas.len()); - actions.push( - light_compressed_token_sdk::instructions::mint_action::MintActionType::MintToCToken { - account: ctoken_atas[ata_index], - amount: rng.gen_range(1..=100000), - } - ); + actions.push(MintActionType::MintToCToken { + account: ctoken_atas[ata_index], + amount: rng.gen_range(1..=100000), + }); } // 10% chance: Update Name 600..=699 => { let name = random_string(&mut rng, 1, 32); - actions.push( - light_compressed_token_sdk::instructions::mint_action::MintActionType::UpdateMetadataField { - extension_index: 0, - field_type: 0, // Name field - key: vec![], - value: name, - } - ); + actions.push(MintActionType::UpdateMetadataField { + extension_index: 0, + field_type: 0, // Name field + key: vec![], + value: name, + }); } // 10% chance: Update Symbol 700..=799 => { let symbol = random_string(&mut rng, 1, 10); - actions.push( - light_compressed_token_sdk::instructions::mint_action::MintActionType::UpdateMetadataField { - extension_index: 0, - field_type: 1, // Symbol field - key: vec![], - value: symbol, - } - ); + actions.push(MintActionType::UpdateMetadataField { + extension_index: 0, + field_type: 1, // Symbol field + key: vec![], + value: symbol, + }); } // 10% chance: Update URI 800..=899 => { let uri = random_string(&mut rng, 10, 200); - actions.push( - light_compressed_token_sdk::instructions::mint_action::MintActionType::UpdateMetadataField { - extension_index: 0, - field_type: 2, // URI field - key: vec![], - value: uri, - } - ); + actions.push(MintActionType::UpdateMetadataField { + extension_index: 0, + field_type: 2, // URI field + key: vec![], + value: uri, + }); } // 9.9% chance: Update Custom Metadata 900..=998 => { @@ -263,14 +254,12 @@ async fn test_random_mint_action() { let key = available_keys[key_index].clone(); let value = random_bytes(&mut rng, 1, 64); - actions.push( - light_compressed_token_sdk::instructions::mint_action::MintActionType::UpdateMetadataField { - extension_index: 0, - field_type: 3, // Custom field - key, - value, - } - ); + actions.push(MintActionType::UpdateMetadataField { + extension_index: 0, + field_type: 3, // Custom field + key, + value, + }); } } // 0.1% chance: Remove Custom Metadata Key @@ -280,24 +269,24 @@ async fn test_random_mint_action() { let key_index = rng.gen_range(0..available_keys.len()); let key = available_keys.remove(key_index); - actions.push( - light_compressed_token_sdk::instructions::mint_action::MintActionType::RemoveMetadataKey { - extension_index: 0, - key, - idempotent: if available_keys.is_empty() { 1 } else { rng.gen_bool(0.5) as u8 }, // 50% chance idempotent when keys exist, always when none left - } - ); + actions.push(MintActionType::RemoveMetadataKey { + extension_index: 0, + key, + idempotent: if available_keys.is_empty() { + 1 + } else { + rng.gen_bool(0.5) as u8 + }, // 50% chance idempotent when keys exist, always when none left + }); } else { // No keys left, try to remove a random key (always idempotent) let random_key = vec![rng.gen::(), rng.gen::()]; - actions.push( - light_compressed_token_sdk::instructions::mint_action::MintActionType::RemoveMetadataKey { - extension_index: 0, // Only TokenMetadata extension exists (index 0) - key: random_key, - idempotent: 1, // Always idempotent when no keys exist - } - ); + actions.push(MintActionType::RemoveMetadataKey { + extension_index: 0, // Only TokenMetadata extension exists (index 0) + key: random_key, + idempotent: 1, // Always idempotent when no keys exist + }); } } // This should never happen since we generate 0..1000, but added for completeness @@ -320,8 +309,6 @@ async fn test_random_mint_action() { // Fix action ordering: remove any UpdateMetadataField actions that come after RemoveMetadataKey for the same key use std::collections::HashSet; - use light_compressed_token_sdk::instructions::mint_action::MintActionType; - let mut removed_keys: HashSet> = HashSet::new(); let mut i = 0; diff --git a/program-tests/compressed-token-test/tests/transfer2/compress_failing.rs b/program-tests/compressed-token-test/tests/transfer2/compress_failing.rs index 3832349ee0..1c0c4335c7 100644 --- a/program-tests/compressed-token-test/tests/transfer2/compress_failing.rs +++ b/program-tests/compressed-token-test/tests/transfer2/compress_failing.rs @@ -110,10 +110,7 @@ async fn setup_compression_test(token_amount: u64) -> Result 0 { - vec![Recipient { - recipient: owner.pubkey().to_bytes().into(), - amount: source_token_amount, - }] + vec![Recipient::new(owner.pubkey(), source_token_amount)] } else { vec![] }; @@ -744,10 +741,7 @@ async fn test_too_many_mints() { .unwrap(); // Create mint and mint tokens to source CToken ATA - let decompressed_recipients = vec![Recipient { - recipient: context.owner.pubkey().to_bytes().into(), - amount: 1000, - }]; + let decompressed_recipients = vec![Recipient::new(context.owner.pubkey(), 1000)]; light_token_client::actions::mint_action_comprehensive( &mut context.rpc, diff --git a/program-tests/compressed-token-test/tests/transfer2/shared.rs b/program-tests/compressed-token-test/tests/transfer2/shared.rs index 0cf021a141..b9fb618418 100644 --- a/program-tests/compressed-token-test/tests/transfer2/shared.rs +++ b/program-tests/compressed-token-test/tests/transfer2/shared.rs @@ -19,9 +19,12 @@ use light_test_utils::{ }; use light_token_client::{ actions::{create_mint, mint_to_compressed}, - instructions::transfer2::{ - create_generic_transfer2_instruction, ApproveInput, CompressAndCloseInput, CompressInput, - DecompressInput, Transfer2InstructionType, TransferInput, + instructions::{ + mint_action::MintActionType, + transfer2::{ + create_generic_transfer2_instruction, ApproveInput, CompressAndCloseInput, + CompressInput, DecompressInput, Transfer2InstructionType, TransferInput, + }, }, }; use solana_sdk::{pubkey::Pubkey, signature::Keypair, signer::Signer, transaction::Transaction}; @@ -499,7 +502,7 @@ impl TestContext { mint_seed: mint_seed.pubkey(), authority: mint_authority.pubkey(), payer: payer.pubkey(), - actions: vec![light_compressed_token_sdk::instructions::mint_action::MintActionType::MintToCToken { + actions: vec![MintActionType::MintToCToken { account: ata, amount, }], @@ -508,7 +511,9 @@ impl TestContext { mint_authority, &payer, None, - ).await.unwrap(); + ) + .await + .unwrap(); } ctoken_atas.insert((*signer_index, *mint_index), ata); diff --git a/program-tests/compressed-token-test/tests/transfer2/transfer_failing.rs b/program-tests/compressed-token-test/tests/transfer2/transfer_failing.rs index 28cee6908a..c0ba240901 100644 --- a/program-tests/compressed-token-test/tests/transfer2/transfer_failing.rs +++ b/program-tests/compressed-token-test/tests/transfer2/transfer_failing.rs @@ -75,10 +75,7 @@ async fn setup_transfer_test( // Mint tokens to owner if amount > 0 if token_amount > 0 { - let recipients = vec![Recipient { - recipient: owner.pubkey().into(), - amount: token_amount, - }]; + let recipients = vec![Recipient::new(owner.pubkey(), token_amount)]; mint_to_compressed( &mut rpc, @@ -686,10 +683,7 @@ async fn setup_transfer_test_with_delegate( // Mint tokens to owner if amount > 0 if token_amount > 0 { - let recipients = vec![Recipient { - recipient: owner.pubkey().into(), - amount: token_amount, - }]; + let recipients = vec![Recipient::new(owner.pubkey(), token_amount)]; mint_to_compressed( &mut rpc, diff --git a/program-tests/utils/src/assert_mint_action.rs b/program-tests/utils/src/assert_mint_action.rs index a19dda082a..6ef5e04d0f 100644 --- a/program-tests/utils/src/assert_mint_action.rs +++ b/program-tests/utils/src/assert_mint_action.rs @@ -2,12 +2,12 @@ use std::collections::HashMap; use anchor_lang::prelude::borsh::BorshDeserialize; use light_client::indexer::Indexer; -use light_compressed_token_sdk::instructions::mint_action::MintActionType; use light_ctoken_types::state::{ extensions::{AdditionalMetadata, ExtensionStruct}, CToken, CompressedMint, }; use light_program_test::{LightProgramTest, Rpc}; +use light_token_client::instructions::mint_action::MintActionType; use solana_sdk::pubkey::Pubkey; /// Assert that mint actions produce the expected state changes @@ -50,9 +50,6 @@ pub async fn assert_mint_action( MintActionType::UpdateFreezeAuthority { new_authority } => { expected_mint.base.freeze_authority = new_authority.map(Into::into); } - MintActionType::CreateSplMint { .. } => { - expected_mint.metadata.spl_mint_initialized = true; - } MintActionType::UpdateMetadataField { extension_index, field_type, @@ -95,14 +92,14 @@ pub async fn assert_mint_action( if let Some(ExtensionStruct::TokenMetadata(ref mut metadata)) = extensions.get_mut(*extension_index as usize) { - metadata.update_authority = (*new_authority).into(); + metadata.update_authority = new_authority.into(); } } } MintActionType::RemoveMetadataKey { extension_index, key, - .. + idempotent: _, } => { if let Some(ref mut extensions) = expected_mint.extensions { if let Some(ExtensionStruct::TokenMetadata(ref mut metadata)) = diff --git a/programs/compressed-token/program/Cargo.toml b/programs/compressed-token/program/Cargo.toml index 01fa8407ba..672b979bf6 100644 --- a/programs/compressed-token/program/Cargo.toml +++ b/programs/compressed-token/program/Cargo.toml @@ -52,7 +52,7 @@ spl-pod = { workspace = true } light-zero-copy = { workspace = true, features = ["mut", "std", "derive"] } zerocopy = { workspace = true } anchor-compressed-token = { path = "../anchor", features = ["cpi"] } -light-account-checks = { workspace = true, features = ["solana", "pinocchio"] } +light-account-checks = { workspace = true, features = ["solana", "pinocchio", "msg"] } light-sdk = { workspace = true } borsh = { workspace = true } light-sdk-types = { workspace = true } diff --git a/sdk-libs/compressed-token-sdk/src/ctoken_instruction.rs b/sdk-libs/compressed-token-sdk/src/ctoken_instruction.rs new file mode 100644 index 0000000000..de77dee71b --- /dev/null +++ b/sdk-libs/compressed-token-sdk/src/ctoken_instruction.rs @@ -0,0 +1,23 @@ +use solana_instruction::Instruction; +use solana_program_error::ProgramError; + +pub trait CTokenInstruction: Sized { + type ExecuteAccounts<'info, A: light_account_checks::AccountInfoTrait + Clone + 'info>; + + type CpiWriteAccounts<'info, A: light_account_checks::AccountInfoTrait + Clone + 'info>; + + fn instruction( + self, + accounts: &Self::ExecuteAccounts<'_, A>, + ) -> Result; + + fn instruction_write_to_cpi_context_first( + self, + accounts: &Self::CpiWriteAccounts<'_, A>, + ) -> Result; + + fn instruction_write_to_cpi_context_set( + self, + accounts: &Self::CpiWriteAccounts<'_, A>, + ) -> Result; +} diff --git a/sdk-libs/compressed-token-sdk/src/error.rs b/sdk-libs/compressed-token-sdk/src/error.rs index f067db9542..ea201474ce 100644 --- a/sdk-libs/compressed-token-sdk/src/error.rs +++ b/sdk-libs/compressed-token-sdk/src/error.rs @@ -57,6 +57,12 @@ pub enum TokenSdkError { UseRegularSplTransfer, #[error("Cannot determine account type")] CannotDetermineAccountType, + #[error("MintActionMetaConfig::new_create_mint requires create_mint data")] + CreateMintDataRequired, + #[error("MintActionMetaConfig::new requires existing mint (create_mint must be None)")] + CreateMintMustBeNone, + #[error("MintActionMetaConfig::new_cpi_context requires cpi_context data")] + CpiContextRequired, #[error(transparent)] CompressedTokenTypes(#[from] LightTokenSdkTypeError), #[error(transparent)] @@ -109,6 +115,9 @@ impl From for u32 { TokenSdkError::IncompleteSplBridgeConfig => 17021, TokenSdkError::UseRegularSplTransfer => 17022, TokenSdkError::CannotDetermineAccountType => 17023, + TokenSdkError::CreateMintDataRequired => 17024, + TokenSdkError::CreateMintMustBeNone => 17025, + TokenSdkError::CpiContextRequired => 17026, TokenSdkError::CompressedTokenTypes(e) => e.into(), TokenSdkError::CTokenError(e) => e.into(), TokenSdkError::LightSdkTypesError(e) => e.into(), diff --git a/sdk-libs/compressed-token-sdk/src/instructions/approve/instruction.rs b/sdk-libs/compressed-token-sdk/src/instructions/approve/instruction.rs index ab2542adb1..970f1963fd 100644 --- a/sdk-libs/compressed-token-sdk/src/instructions/approve/instruction.rs +++ b/sdk-libs/compressed-token-sdk/src/instructions/approve/instruction.rs @@ -75,7 +75,6 @@ pub fn create_approve_instruction(inputs: ApproveInputs) -> Result inputs.change_compressed_account_merkle_tree, ); - // Get account metas using the dedicated function let account_metas = get_approve_instruction_account_metas(meta_config); Ok(Instruction { diff --git a/sdk-libs/compressed-token-sdk/src/instructions/batch_compress/instruction.rs b/sdk-libs/compressed-token-sdk/src/instructions/batch_compress/instruction.rs index e284424286..814eb12707 100644 --- a/sdk-libs/compressed-token-sdk/src/instructions/batch_compress/instruction.rs +++ b/sdk-libs/compressed-token-sdk/src/instructions/batch_compress/instruction.rs @@ -77,7 +77,6 @@ pub fn create_batch_compress_instruction(inputs: BatchCompressInputs) -> Result< sol_pool_pda: inputs.sol_pool_pda, }; - // Get account metas that match MintToInstruction structure let account_metas = get_batch_compress_instruction_account_metas(meta_config); Ok(Instruction { diff --git a/sdk-libs/compressed-token-sdk/src/instructions/create_compressed_mint/instruction.rs b/sdk-libs/compressed-token-sdk/src/instructions/create_compressed_mint/instruction.rs index a3706648ef..f903078e36 100644 --- a/sdk-libs/compressed-token-sdk/src/instructions/create_compressed_mint/instruction.rs +++ b/sdk-libs/compressed-token-sdk/src/instructions/create_compressed_mint/instruction.rs @@ -1,9 +1,11 @@ -use light_compressed_account::instruction_data::compressed_proof::CompressedProof; +use light_compressed_account::instruction_data::{ + compressed_proof::CompressedProof, traits::LightInstructionData, +}; use light_ctoken_types::{ self, instructions::{ extensions::ExtensionInstructionData, - mint_action::{CompressedMintWithContext, CpiContext}, + mint_action::{CompressedMintInstructionData, CompressedMintWithContext, CpiContext}, }, COMPRESSED_MINT_SEED, }; @@ -14,13 +16,13 @@ use solana_pubkey::Pubkey; use crate::{ error::{Result, TokenSdkError}, instructions::mint_action::{ - create_mint_action_cpi, mint_action_cpi_write, MintActionInputs, MintActionInputsCpiWrite, + get_mint_action_instruction_account_metas, + get_mint_action_instruction_account_metas_cpi_write, MintActionMetaConfig, + MintActionMetaConfigCpiWrite, }, AnchorDeserialize, AnchorSerialize, }; -pub const CREATE_COMPRESSED_MINT_DISCRIMINATOR: u8 = 100; - /// Input struct for creating a compressed mint instruction #[derive(Debug, Clone, AnchorDeserialize, AnchorSerialize)] pub struct CreateCompressedMintInputs { @@ -44,68 +46,75 @@ pub fn create_compressed_mint_cpi( cpi_context: Option, cpi_context_pubkey: Option, ) -> Result { - // Build CompressedMintWithContext from the input parameters + let compressed_mint_instruction_data = CompressedMintInstructionData { + supply: 0, + decimals: input.decimals, + metadata: light_ctoken_types::state::CompressedMintMetadata { + version: input.version, + mint: find_spl_mint_address(&input.mint_signer) + .0 + .to_bytes() + .into(), + spl_mint_initialized: false, + }, + mint_authority: Some(input.mint_authority.to_bytes().into()), + freeze_authority: input.freeze_authority.map(|auth| auth.to_bytes().into()), + extensions: input.extensions, + }; + let compressed_mint_with_context = CompressedMintWithContext { address: mint_address, - mint: light_ctoken_types::instructions::mint_action::CompressedMintInstructionData { - supply: 0, - decimals: input.decimals, - metadata: light_ctoken_types::state::CompressedMintMetadata { - version: input.version, - mint: find_spl_mint_address(&input.mint_signer) - .0 - .to_bytes() - .into(), - spl_mint_initialized: false, - }, - mint_authority: Some(input.mint_authority.to_bytes().into()), - freeze_authority: input.freeze_authority.map(|auth| auth.to_bytes().into()), - extensions: input.extensions, - }, - leaf_index: 0, // Default value for new mint + mint: compressed_mint_instruction_data, + leaf_index: 0, prove_by_index: false, root_index: input.address_merkle_tree_root_index, }; - // Convert create_compressed_mint CpiContext to mint_actions CpiContext if present - let mint_action_cpi_context = cpi_context.map(|ctx| { - light_ctoken_types::instructions::mint_action::CpiContext { - set_context: ctx.set_context, - first_set_context: ctx.first_set_context, - in_tree_index: 0, // Default for create mint - in_queue_index: 0, - out_queue_index: 0, - token_out_queue_index: 0, - assigned_account_index: 0, // Default for create mint - ..Default::default() - } - }); - - // Create mint action inputs for compressed mint creation - let mint_action_inputs = MintActionInputs { - compressed_mint_inputs: compressed_mint_with_context, - mint_seed: input.mint_signer, - create_mint: true, // Key difference - we're creating a new compressed mint - mint_bump: None, - authority: input.mint_authority, - payer: input.payer, - proof: Some(input.proof), - actions: Vec::new(), // Empty - just creating mint, no additional actions - address_tree_pubkey: input.address_tree_pubkey, // Address tree for new mint address - input_queue: None, // Not needed for create_mint: true - output_queue: input.output_queue, - tokens_out_queue: None, // No tokens being minted - token_pool: None, // Not needed for simple compressed mint creation + let mut instruction_data = light_ctoken_types::instructions::mint_action::MintActionCompressedInstructionData::new_mint( + mint_address, + input.address_merkle_tree_root_index, + input.proof, + compressed_mint_with_context.mint.clone(), + ); + + if let Some(ctx) = cpi_context { + instruction_data = instruction_data.with_cpi_context(ctx); + } + + let meta_config = if cpi_context_pubkey.is_some() { + MintActionMetaConfig::new_cpi_context( + &instruction_data, + input.mint_authority, + input.payer, + cpi_context_pubkey.unwrap(), + )? + } else { + MintActionMetaConfig::new_create_mint( + &instruction_data, + input.mint_authority, + input.mint_signer, + input.payer, + input.address_tree_pubkey, + input.output_queue, + )? }; - create_mint_action_cpi( - mint_action_inputs, - mint_action_cpi_context, - cpi_context_pubkey, - ) + let account_metas = + get_mint_action_instruction_account_metas(meta_config, &compressed_mint_with_context); + + let data = instruction_data + .data() + .map_err(|_| TokenSdkError::SerializationError)?; + + Ok(Instruction { + program_id: solana_pubkey::Pubkey::new_from_array( + light_ctoken_types::COMPRESSED_TOKEN_PROGRAM_ID, + ), + accounts: account_metas, + data, + }) } -/// Input struct for creating a compressed mint instruction #[derive(Debug, Clone, AnchorDeserialize, AnchorSerialize)] pub struct CreateCompressedMintInputsCpiWrite { pub decimals: u8, @@ -120,6 +129,7 @@ pub struct CreateCompressedMintInputsCpiWrite { pub extensions: Option>, pub version: u8, } + pub fn create_compressed_mint_cpi_write( input: CreateCompressedMintInputsCpiWrite, ) -> Result { @@ -131,55 +141,49 @@ pub fn create_compressed_mint_cpi_write( return Err(TokenSdkError::InvalidAccountData); } - // Build CompressedMintWithContext from the input parameters - let compressed_mint_with_context = CompressedMintWithContext { - address: input.mint_address, - mint: light_ctoken_types::instructions::mint_action::CompressedMintInstructionData { - supply: 0, - decimals: input.decimals, - metadata: light_ctoken_types::state::CompressedMintMetadata { - version: input.version, - mint: find_spl_mint_address(&input.mint_signer) - .0 - .to_bytes() - .into(), - spl_mint_initialized: false, - }, - mint_authority: Some(input.mint_authority.to_bytes().into()), - freeze_authority: input.freeze_authority.map(|auth| auth.to_bytes().into()), - extensions: input.extensions, + let compressed_mint_instruction_data = CompressedMintInstructionData { + supply: 0, + decimals: input.decimals, + metadata: light_ctoken_types::state::CompressedMintMetadata { + version: input.version, + mint: find_spl_mint_address(&input.mint_signer) + .0 + .to_bytes() + .into(), + spl_mint_initialized: false, }, - leaf_index: 0, // Default value for new mint - prove_by_index: false, - root_index: input.address_merkle_tree_root_index, + mint_authority: Some(input.mint_authority.to_bytes().into()), + freeze_authority: input.freeze_authority.map(|auth| auth.to_bytes().into()), + extensions: input.extensions, }; - // Convert create_compressed_mint CpiContext to mint_actions CpiContext - let mint_action_cpi_context = light_ctoken_types::instructions::mint_action::CpiContext { - set_context: input.cpi_context.set_context, - first_set_context: input.cpi_context.first_set_context, - in_tree_index: 0, // Default for create mint - in_queue_index: 0, - out_queue_index: 0, - token_out_queue_index: 0, - assigned_account_index: 0, // Default for create mint - ..Default::default() - }; + let instruction_data = light_ctoken_types::instructions::mint_action::MintActionCompressedInstructionData::new_mint_write_to_cpi_context( + input.mint_address, + input.address_merkle_tree_root_index, + compressed_mint_instruction_data,input.cpi_context + ); - // Create mint action inputs for compressed mint creation (CPI write mode) - let mint_action_inputs = MintActionInputsCpiWrite { - compressed_mint_inputs: compressed_mint_with_context, - mint_seed: Some(input.mint_signer), - mint_bump: None, - create_mint: true, // Key difference - we're creating a new compressed mint + let meta_config = MintActionMetaConfigCpiWrite { + fee_payer: input.payer, + mint_signer: Some(input.mint_signer), authority: input.mint_authority, - payer: input.payer, - actions: Vec::new(), // Empty - just creating mint, no additional actions - cpi_context: mint_action_cpi_context, - cpi_context_pubkey: input.cpi_context_pubkey, + cpi_context: input.cpi_context_pubkey, + mint_needs_to_sign: true, // Always true for create mint }; - mint_action_cpi_write(mint_action_inputs) + let account_metas = get_mint_action_instruction_account_metas_cpi_write(meta_config); + + let data = instruction_data + .data() + .map_err(|_| TokenSdkError::SerializationError)?; + + Ok(Instruction { + program_id: solana_pubkey::Pubkey::new_from_array( + light_ctoken_types::COMPRESSED_TOKEN_PROGRAM_ID, + ), + accounts: account_metas, + data, + }) } /// Creates a compressed mint instruction with automatic mint address derivation diff --git a/sdk-libs/compressed-token-sdk/src/instructions/create_compressed_mint/mod.rs b/sdk-libs/compressed-token-sdk/src/instructions/create_compressed_mint/mod.rs index 9569f03812..cbc4175976 100644 --- a/sdk-libs/compressed-token-sdk/src/instructions/create_compressed_mint/mod.rs +++ b/sdk-libs/compressed-token-sdk/src/instructions/create_compressed_mint/mod.rs @@ -7,5 +7,5 @@ pub use account_metas::{ pub use instruction::{ create_compressed_mint, create_compressed_mint_cpi, create_compressed_mint_cpi_write, derive_cmint_from_spl_mint, derive_compressed_mint_address, find_spl_mint_address, - CreateCompressedMintInputs, CREATE_COMPRESSED_MINT_DISCRIMINATOR, + CreateCompressedMintInputs, }; diff --git a/sdk-libs/compressed-token-sdk/src/instructions/create_spl_mint.rs b/sdk-libs/compressed-token-sdk/src/instructions/create_spl_mint.rs deleted file mode 100644 index ddff45f1de..0000000000 --- a/sdk-libs/compressed-token-sdk/src/instructions/create_spl_mint.rs +++ /dev/null @@ -1,71 +0,0 @@ -use light_compressed_token_types::ValidityProof; -use light_ctoken_types::instructions::mint_action::CompressedMintWithContext; -use solana_instruction::Instruction; -use solana_pubkey::Pubkey; - -use crate::{ - error::Result, - instructions::mint_action::{create_mint_action, MintActionInputs, MintActionType, TokenPool}, -}; - -pub const POOL_SEED: &[u8] = b"pool"; - -pub struct CreateSplMintInputs { - pub mint_signer: Pubkey, - pub mint_bump: u8, - pub compressed_mint_inputs: CompressedMintWithContext, - pub payer: Pubkey, - pub input_merkle_tree: Pubkey, - pub input_output_queue: Pubkey, - pub output_queue: Pubkey, - pub mint_authority: Pubkey, - pub proof: ValidityProof, - pub token_pool: TokenPool, -} - -/// Creates an SPL mint instruction using the mint_action instruction as a wrapper -/// This maintains the same API as before but uses mint_action under the hood -pub fn create_spl_mint_instruction(inputs: CreateSplMintInputs) -> Result { - create_spl_mint_instruction_with_bump(inputs, Pubkey::default(), false) -} - -/// Creates an SPL mint instruction with explicit token pool and CPI context options -/// This is now a wrapper around the mint_action instruction -pub fn create_spl_mint_instruction_with_bump( - inputs: CreateSplMintInputs, - _token_pool_pda: Pubkey, // Unused in mint_action, kept for API compatibility - _cpi_context: bool, // Unused in mint_action, kept for API compatibility -) -> Result { - let CreateSplMintInputs { - mint_signer, - mint_bump, - compressed_mint_inputs, - proof, - payer, - input_merkle_tree, // Used for existing compressed mint - input_output_queue, // Used for existing compressed mint input queue - output_queue, - mint_authority, - token_pool, - } = inputs; - - // Create the mint_action instruction with CreateSplMint action - let mint_action_inputs = MintActionInputs { - compressed_mint_inputs, - mint_seed: mint_signer, - create_mint: false, // The compressed mint already exists - mint_bump: Some(mint_bump), - authority: mint_authority, - payer, - proof: proof.0, - actions: vec![MintActionType::CreateSplMint { mint_bump }], - // Use input_merkle_tree since we're operating on existing compressed mint - address_tree_pubkey: input_merkle_tree, - input_queue: Some(input_output_queue), // Input queue for existing compressed mint - output_queue, - tokens_out_queue: None, // No tokens being minted in CreateSplMint - token_pool: Some(token_pool), // Required for CreateSplMint action - }; - - create_mint_action(mint_action_inputs) -} diff --git a/sdk-libs/compressed-token-sdk/src/instructions/mint_action/account_metas.rs b/sdk-libs/compressed-token-sdk/src/instructions/mint_action/account_metas.rs index 9a8933b8db..2f364dcfc3 100644 --- a/sdk-libs/compressed-token-sdk/src/instructions/mint_action/account_metas.rs +++ b/sdk-libs/compressed-token-sdk/src/instructions/mint_action/account_metas.rs @@ -1,14 +1,12 @@ use light_program_profiler::profile; use solana_instruction::AccountMeta; use solana_pubkey::Pubkey; -use spl_token_2022; use crate::instructions::CTokenDefaultAccounts; -/// Account metadata configuration for mint action instruction -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone)] pub struct MintActionMetaConfig { - pub fee_payer: Option, + pub fee_payer: Pubkey, pub mint_signer: Option, pub authority: Pubkey, pub tree_pubkey: Pubkey, // address tree when create_mint, input state tree when not @@ -25,6 +23,150 @@ pub struct MintActionMetaConfig { pub ctoken_accounts: Vec, // For mint_to_ctoken actions } +impl MintActionMetaConfig { + pub fn new_create_mint( + instruction_data: &light_ctoken_types::instructions::mint_action::MintActionCompressedInstructionData, + authority: Pubkey, + mint_signer: Pubkey, + fee_payer: Pubkey, + address_tree: Pubkey, + output_queue: Pubkey, + ) -> crate::error::Result { + if instruction_data.create_mint.is_none() { + return Err(crate::error::TokenSdkError::CreateMintDataRequired); + } + + let (has_mint_to_actions, ctoken_accounts) = + Self::analyze_actions(&instruction_data.actions); + let spl_mint_initialized = instruction_data.mint.metadata.spl_mint_initialized; + + Ok(Self { + fee_payer, + mint_signer: Some(mint_signer), + authority, + tree_pubkey: address_tree, + input_queue: None, + output_queue, + tokens_out_queue: if has_mint_to_actions { + Some(output_queue) + } else { + None + }, + with_lamports: false, + spl_mint_initialized, + has_mint_to_actions, + with_cpi_context: None, + create_mint: true, + with_mint_signer: true, + mint_needs_to_sign: true, + ctoken_accounts, + }) + } + + pub fn new( + instruction_data: &light_ctoken_types::instructions::mint_action::MintActionCompressedInstructionData, + authority: Pubkey, + fee_payer: Pubkey, + state_tree: Pubkey, + input_queue: Pubkey, + output_queue: Pubkey, + ) -> crate::error::Result { + if instruction_data.create_mint.is_some() { + return Err(crate::error::TokenSdkError::CreateMintMustBeNone); + } + + let (has_mint_to_actions, ctoken_accounts) = + Self::analyze_actions(&instruction_data.actions); + + Ok(Self { + fee_payer, + mint_signer: None, + authority, + tree_pubkey: state_tree, + input_queue: Some(input_queue), + output_queue, + tokens_out_queue: if has_mint_to_actions { + Some(output_queue) + } else { + None + }, + with_lamports: false, + spl_mint_initialized: false, + has_mint_to_actions, + with_cpi_context: None, + create_mint: false, + with_mint_signer: false, + mint_needs_to_sign: false, + ctoken_accounts, + }) + } + + pub fn new_cpi_context( + instruction_data: &light_ctoken_types::instructions::mint_action::MintActionCompressedInstructionData, + authority: Pubkey, + fee_payer: Pubkey, + cpi_context_pubkey: Pubkey, + ) -> crate::error::Result { + if instruction_data.cpi_context.is_none() { + return Err(crate::error::TokenSdkError::CpiContextRequired); + } + + let (has_mint_to_actions, ctoken_accounts) = + Self::analyze_actions(&instruction_data.actions); + let spl_mint_initialized = instruction_data.mint.metadata.spl_mint_initialized; + let create_mint = instruction_data.create_mint.is_some(); + + Ok(Self { + fee_payer, + mint_signer: None, + authority, + tree_pubkey: Pubkey::default(), + input_queue: None, + output_queue: Pubkey::default(), + tokens_out_queue: None, + with_lamports: false, + spl_mint_initialized, + has_mint_to_actions, + with_cpi_context: Some(cpi_context_pubkey), + create_mint, + with_mint_signer: create_mint, + mint_needs_to_sign: create_mint, + ctoken_accounts, + }) + } + + pub fn with_tokens_out_queue(mut self, queue: Pubkey) -> Self { + self.tokens_out_queue = Some(queue); + self + } + + pub fn with_ctoken_accounts(mut self, accounts: Vec) -> Self { + self.ctoken_accounts = accounts; + self + } + + fn analyze_actions( + actions: &[light_ctoken_types::instructions::mint_action::Action], + ) -> (bool, Vec) { + let mut has_mint_to_actions = false; + let ctoken_accounts = Vec::new(); + + for action in actions { + match action { + light_ctoken_types::instructions::mint_action::Action::MintToCompressed(_) => { + has_mint_to_actions = true; + } + light_ctoken_types::instructions::mint_action::Action::MintToCToken(_) => { + has_mint_to_actions = true; + } + _ => {} + } + } + + (has_mint_to_actions, ctoken_accounts) + } +} + /// Get the account metas for a mint action instruction #[profile] pub fn get_mint_action_instruction_account_metas( @@ -34,14 +176,11 @@ pub fn get_mint_action_instruction_account_metas( let default_pubkeys = CTokenDefaultAccounts::default(); let mut metas = Vec::new(); - // Static accounts (before CPI accounts offset) - // light_system_program (always required) metas.push(AccountMeta::new_readonly( default_pubkeys.light_system_program, false, )); - // mint_signer (conditional) - matches onchain logic: with_mint_signer = create_mint() | has_CreateSplMint_action if config.with_mint_signer { if let Some(mint_signer) = config.mint_signer { metas.push(AccountMeta::new_readonly( @@ -51,76 +190,56 @@ pub fn get_mint_action_instruction_account_metas( } } - // authority (always signer as per program requirement) metas.push(AccountMeta::new_readonly(config.authority, true)); - // For decompressed mints, add SPL mint and token program accounts - // These need to come right after authority to match processor expectations if config.spl_mint_initialized { - // mint - either derived from mint_signer (for creation) or from existing mint data if let Some(mint_signer) = config.mint_signer { - // For mint creation - derive from mint_signer let (spl_mint_pda, _) = crate::instructions::find_spl_mint_address(&mint_signer); - metas.push(AccountMeta::new(spl_mint_pda, false)); // mutable: true, signer: false + metas.push(AccountMeta::new(spl_mint_pda, false)); - // token_pool_pda (derived from mint) let (token_pool_pda, _) = crate::token_pool::find_token_pool_pda_with_index(&spl_mint_pda, 0); metas.push(AccountMeta::new(token_pool_pda, false)); } else { - // For existing mint operations - use the mint from compressed mint inputs let spl_mint_pubkey = solana_pubkey::Pubkey::from(compressed_mint_inputs.mint.metadata.mint.to_bytes()); - metas.push(AccountMeta::new(spl_mint_pubkey, false)); // mutable: true, signer: false + metas.push(AccountMeta::new(spl_mint_pubkey, false)); - // token_pool_pda (derived from the mint) let (token_pool_pda, _) = crate::token_pool::find_token_pool_pda_with_index(&spl_mint_pubkey, 0); metas.push(AccountMeta::new(token_pool_pda, false)); } - // token_program (use spl_token_2022 program ID) metas.push(AccountMeta::new_readonly(spl_token_2022::ID, false)); } - // LightSystemAccounts in exact order expected by validate_and_parse: - - // fee_payer (signer, mutable) - only add if provided - if let Some(fee_payer) = config.fee_payer { - metas.push(AccountMeta::new(fee_payer, true)); - } + metas.push(AccountMeta::new(config.fee_payer, true)); - // cpi_authority_pda metas.push(AccountMeta::new_readonly( default_pubkeys.cpi_authority_pda, false, )); - // registered_program_pda metas.push(AccountMeta::new_readonly( default_pubkeys.registered_program_pda, false, )); - // account_compression_authority metas.push(AccountMeta::new_readonly( default_pubkeys.account_compression_authority, false, )); - // account_compression_program metas.push(AccountMeta::new_readonly( default_pubkeys.account_compression_program, false, )); - // system_program metas.push(AccountMeta::new_readonly( default_pubkeys.system_program, false, )); - // sol_pool_pda (optional for lamports operations) if config.with_lamports { metas.push(AccountMeta::new( Pubkey::new_from_array(light_sdk::constants::SOL_POOL_PDA), @@ -128,38 +247,25 @@ pub fn get_mint_action_instruction_account_metas( )); } - // sol_decompression_recipient (optional - not used in mint_action, but needed for account order) - // Skip this as decompress_sol is false in mint_action - - // cpi_context (optional) if let Some(cpi_context) = config.with_cpi_context { metas.push(AccountMeta::new(cpi_context, false)); } - // After LightSystemAccounts, add the remaining accounts to match onchain expectations: - - // out_output_queue (mutable) - always required metas.push(AccountMeta::new(config.output_queue, false)); - // in_merkle_tree (always required) - // When create_mint=true: this is the address tree for creating new mint addresses - // When create_mint=false: this is the state tree containing the existing compressed mint metas.push(AccountMeta::new(config.tree_pubkey, false)); - // in_output_queue - only when NOT creating mint if !config.create_mint { if let Some(input_queue) = config.input_queue { metas.push(AccountMeta::new(input_queue, false)); } } - // tokens_out_queue - only when we have MintTo actions if config.has_mint_to_actions { let tokens_out_queue = config.tokens_out_queue.unwrap_or(config.output_queue); metas.push(AccountMeta::new(tokens_out_queue, false)); } - // Add decompressed token accounts as remaining accounts for MintToCToken actions for token_account in &config.ctoken_accounts { metas.push(AccountMeta::new(*token_account, false)); } @@ -185,16 +291,11 @@ pub fn get_mint_action_instruction_account_metas_cpi_write( let default_pubkeys = CTokenDefaultAccounts::default(); let mut metas = Vec::new(); - // The order must match mint_action on-chain program expectations: - // [light_system_program, mint_signer, authority, fee_payer, cpi_authority_pda, cpi_context] - - // light_system_program (always required) - index 0 metas.push(AccountMeta::new_readonly( default_pubkeys.light_system_program, false, )); - // mint_signer (optional signer - only when creating mint and creating SPL mint) - index 1 if let Some(mint_signer) = config.mint_signer { metas.push(AccountMeta::new_readonly( mint_signer, @@ -202,19 +303,15 @@ pub fn get_mint_action_instruction_account_metas_cpi_write( )); } - // authority (signer) - index 2 metas.push(AccountMeta::new_readonly(config.authority, true)); - // fee_payer (signer, mutable) - index 3 (this is what the program checks for) metas.push(AccountMeta::new(config.fee_payer, true)); - // cpi_authority_pda - index 4 metas.push(AccountMeta::new_readonly( default_pubkeys.cpi_authority_pda, false, )); - // cpi_context (mutable) - index 5 metas.push(AccountMeta::new(config.cpi_context, false)); metas diff --git a/sdk-libs/compressed-token-sdk/src/instructions/mint_action/cpi_accounts.rs b/sdk-libs/compressed-token-sdk/src/instructions/mint_action/cpi_accounts.rs index fda057f385..b2edcfbd49 100644 --- a/sdk-libs/compressed-token-sdk/src/instructions/mint_action/cpi_accounts.rs +++ b/sdk-libs/compressed-token-sdk/src/instructions/mint_action/cpi_accounts.rs @@ -4,30 +4,47 @@ use light_ctoken_types::COMPRESSED_TOKEN_PROGRAM_ID; use light_program_profiler::profile; use light_sdk_types::{ ACCOUNT_COMPRESSION_AUTHORITY_PDA, ACCOUNT_COMPRESSION_PROGRAM_ID, LIGHT_SYSTEM_PROGRAM_ID, - REGISTERED_PROGRAM_PDA, SOL_POOL_PDA, + REGISTERED_PROGRAM_PDA, }; use solana_instruction::AccountMeta; use solana_msg::msg; use crate::error::TokenSdkError; +#[derive(Debug, Clone, Default, Copy)] +pub struct MintActionCpiAccountsConfig { + pub with_cpi_context: bool, + pub create_mint: bool, // true = address tree, false = state tree + pub mint_to_compressed: bool, // true = tokens_out_queue required +} + +impl MintActionCpiAccountsConfig { + pub fn create_mint() -> Self { + Self { + with_cpi_context: false, + create_mint: true, + mint_to_compressed: false, + } + } + + pub fn mint_to_compressed(self) -> Self { + Self { + with_cpi_context: self.with_cpi_context, + create_mint: self.create_mint, + mint_to_compressed: true, + } + } +} + /// Parsed MintAction CPI accounts for structured access #[derive(Debug)] pub struct MintActionCpiAccounts<'a, A: AccountInfoTrait + Clone> { - // Programs (in order) pub compressed_token_program: &'a A, pub light_system_program: &'a A, - // Mint-specific accounts - pub mint_signer: Option<&'a A>, // Required when creating mint or SPL mint - pub authority: &'a A, // Always required to sign - - // Decompressed mint accounts (conditional group - all or none) - pub mint: Option<&'a A>, // SPL mint account (when decompressed) - pub token_pool_pda: Option<&'a A>, // Token pool PDA (when decompressed) - pub token_program: Option<&'a A>, // SPL Token 2022 (when decompressed) + pub mint_signer: Option<&'a A>, + pub authority: &'a A, - // Core Light system accounts pub fee_payer: &'a A, pub compressed_token_cpi_authority: &'a A, pub registered_program_pda: &'a A, @@ -35,143 +52,84 @@ pub struct MintActionCpiAccounts<'a, A: AccountInfoTrait + Clone> { pub account_compression_program: &'a A, pub system_program: &'a A, - // Optional system accounts - pub sol_pool_pda: Option<&'a A>, // For lamports operations - pub cpi_context: Option<&'a A>, // For CPI context + pub cpi_context: Option<&'a A>, - // Tree/Queue accounts (always present in execute mode) pub out_output_queue: &'a A, - pub in_merkle_tree: &'a A, // Address tree when creating, state tree otherwise - pub in_output_queue: Option<&'a A>, // When mint exists (not creating) - pub tokens_out_queue: Option<&'a A>, // For MintTo actions + pub in_merkle_tree: &'a A, + pub in_output_queue: Option<&'a A>, + pub tokens_out_queue: Option<&'a A>, - // Remaining accounts for MintToCToken actions pub ctoken_accounts: &'a [A], } impl<'a, A: AccountInfoTrait + Clone> MintActionCpiAccounts<'a, A> { - // TODO: add a config and derive config from instruction data - /// Parse accounts for mint_action CPI with full configuration - /// Following the exact order expected by the on-chain program #[profile] #[inline(always)] #[track_caller] pub fn try_from_account_infos_full( accounts: &'a [A], - with_mint_signer: bool, - spl_mint_initialized: bool, - with_lamports: bool, - with_cpi_context: bool, - create_mint: bool, // true = address tree, false = state tree - has_mint_to_actions: bool, // true = tokens_out_queue required + config: MintActionCpiAccountsConfig, ) -> Result { let mut iter = AccountIterator::new(accounts); - // 1. Compressed token program (always required) let compressed_token_program = iter.next_checked_pubkey("compressed_token_program", COMPRESSED_TOKEN_PROGRAM_ID)?; - // 2. Light system program (always required) let light_system_program = iter.next_checked_pubkey("light_system_program", LIGHT_SYSTEM_PROGRAM_ID)?; - // 3. Mint signer (conditional - when creating mint or SPL mint) - let mint_signer = iter.next_option("mint_signer", with_mint_signer)?; + let mint_signer = iter.next_option("mint_signer", config.create_mint)?; - // 4. Authority (always required, must be signer) let authority = iter.next_account("authority")?; if !authority.is_signer() { msg!("Authority must be a signer"); return Err(AccountError::InvalidSigner.into()); } - // 5-7. Decompressed mint accounts (conditional group) - let (mint, token_pool_pda, token_program) = if spl_mint_initialized { - let mint = Some(iter.next_account("mint")?); - let pool = Some(iter.next_account("token_pool_pda")?); - let program = Some(iter.next_account("token_program")?); - - // Validate SPL Token 2022 program - if let Some(prog) = program { - if prog.key() != spl_token_2022::ID.to_bytes() { - msg!( - "Invalid token program. Expected SPL Token 2022 ({:?}), got {:?}", - spl_token_2022::ID, - prog.pubkey() - ); - return Err(AccountError::InvalidProgramId.into()); - } - } - - (mint, pool, program) - } else { - (None, None, None) - }; - - // 8. Fee payer (always required, must be signer and mutable) let fee_payer = iter.next_account("fee_payer")?; if !fee_payer.is_signer() || !fee_payer.is_writable() { msg!("Fee payer must be a signer and mutable"); return Err(AccountError::InvalidSigner.into()); } - // 9. CPI authority PDA let compressed_token_cpi_authority = iter.next_checked_pubkey("compressed_token_cpi_authority", CPI_AUTHORITY_PDA)?; - // 10. Registered program PDA let registered_program_pda = iter.next_checked_pubkey("registered_program_pda", REGISTERED_PROGRAM_PDA)?; - // 11. Account compression authority let account_compression_authority = iter.next_checked_pubkey( "account_compression_authority", ACCOUNT_COMPRESSION_AUTHORITY_PDA, )?; - // 12. Account compression program let account_compression_program = iter.next_checked_pubkey( "account_compression_program", ACCOUNT_COMPRESSION_PROGRAM_ID, )?; - // 13. System program let system_program = iter.next_checked_pubkey("system_program", [0u8; 32])?; - // 14. SOL pool PDA (optional - for lamports operations) - let sol_pool_pda = if with_lamports { - Some(iter.next_checked_pubkey("sol_pool_pda", SOL_POOL_PDA)?) - } else { - None - }; - - // 15. CPI context (optional) - let cpi_context = iter.next_option_mut("cpi_context", with_cpi_context)?; + let cpi_context = iter.next_option_mut("cpi_context", config.with_cpi_context)?; - // 16. Out output queue (always required) let out_output_queue = iter.next_account("out_output_queue")?; if !out_output_queue.is_writable() { msg!("Out output queue must be mutable"); return Err(AccountError::AccountMutable.into()); } - // 17. In merkle tree (always required) - // When create_mint=true: this is the address tree for creating new mint addresses - // When create_mint=false: this is the state tree containing the existing compressed mint let in_merkle_tree = iter.next_account("in_merkle_tree")?; if !in_merkle_tree.is_writable() { msg!("In merkle tree must be mutable"); return Err(AccountError::AccountMutable.into()); } - // Validate tree ownership if !in_merkle_tree.is_owned_by(&ACCOUNT_COMPRESSION_PROGRAM_ID) { msg!("In merkle tree must be owned by account compression program"); return Err(AccountError::AccountOwnedByWrongProgram.into()); } - // 18. In output queue (conditional - when mint exists, not creating) - let in_output_queue = iter.next_option_mut("in_output_queue", !create_mint)?; + let in_output_queue = iter.next_option_mut("in_output_queue", !config.create_mint)?; if let Some(queue) = in_output_queue { if !queue.is_owned_by(&ACCOUNT_COMPRESSION_PROGRAM_ID) { msg!("In output queue must be owned by account compression program"); @@ -179,8 +137,8 @@ impl<'a, A: AccountInfoTrait + Clone> MintActionCpiAccounts<'a, A> { } } - // 19. Tokens out queue (conditional - for MintTo actions) - let tokens_out_queue = iter.next_option_mut("tokens_out_queue", has_mint_to_actions)?; + let tokens_out_queue = + iter.next_option_mut("tokens_out_queue", config.mint_to_compressed)?; if let Some(queue) = tokens_out_queue { if !queue.is_owned_by(&ACCOUNT_COMPRESSION_PROGRAM_ID) { msg!("Tokens out queue must be owned by account compression program"); @@ -188,7 +146,6 @@ impl<'a, A: AccountInfoTrait + Clone> MintActionCpiAccounts<'a, A> { } } - // 20+. Decompressed token accounts (remaining accounts for MintToCToken) let ctoken_accounts = iter.remaining_unchecked()?; Ok(Self { @@ -196,16 +153,12 @@ impl<'a, A: AccountInfoTrait + Clone> MintActionCpiAccounts<'a, A> { light_system_program, mint_signer, authority, - mint, - token_pool_pda, - token_program, fee_payer, compressed_token_cpi_authority, registered_program_pda, account_compression_authority, account_compression_program, system_program, - sol_pool_pda, cpi_context, out_output_queue, in_merkle_tree, @@ -219,58 +172,9 @@ impl<'a, A: AccountInfoTrait + Clone> MintActionCpiAccounts<'a, A> { #[inline(always)] #[track_caller] pub fn try_from_account_infos(accounts: &'a [A]) -> Result { - Self::try_from_account_infos_full( - accounts, false, // with_mint_signer - false, // spl_mint_initialized - false, // with_lamports - false, // with_cpi_context - false, // create_mint - false, // has_mint_to_actions - ) - } - - /// Parse for creating a new mint - #[inline(always)] - #[track_caller] - pub fn try_from_account_infos_create_mint( - accounts: &'a [A], - with_mint_signer: bool, - spl_mint_initialized: bool, - with_lamports: bool, - has_mint_to_actions: bool, - ) -> Result { - Self::try_from_account_infos_full( - accounts, - with_mint_signer, - spl_mint_initialized, - with_lamports, - false, // with_cpi_context - true, // create_mint - has_mint_to_actions, - ) + Self::try_from_account_infos_full(accounts, MintActionCpiAccountsConfig::default()) } - /// Parse for updating an existing mint - #[inline(always)] - #[track_caller] - pub fn try_from_account_infos_update_mint( - accounts: &'a [A], - spl_mint_initialized: bool, - with_lamports: bool, - has_mint_to_actions: bool, - ) -> Result { - Self::try_from_account_infos_full( - accounts, - false, // with_mint_signer - spl_mint_initialized, - with_lamports, - false, // with_cpi_context - false, // create_mint - has_mint_to_actions, - ) - } - - /// Get tree/queue pubkeys #[profile] #[inline(always)] pub fn tree_queue_pubkeys(&self) -> Vec<[u8; 32]> { @@ -287,35 +191,19 @@ impl<'a, A: AccountInfoTrait + Clone> MintActionCpiAccounts<'a, A> { pubkeys } - /// Convert to account infos for CPI (excludes compressed_token_program) #[profile] #[inline(always)] pub fn to_account_infos(&self) -> Vec { let mut accounts = Vec::with_capacity(20 + self.ctoken_accounts.len()); - // Start with light_system_program accounts.push(self.light_system_program.clone()); - // Add mint_signer if present if let Some(signer) = self.mint_signer { accounts.push(signer.clone()); } - // Authority accounts.push(self.authority.clone()); - // Decompressed mint accounts - if let Some(mint) = self.mint { - accounts.push(mint.clone()); - } - if let Some(pool) = self.token_pool_pda { - accounts.push(pool.clone()); - } - if let Some(program) = self.token_program { - accounts.push(program.clone()); - } - - // Core Light system accounts accounts.extend_from_slice( &[ self.fee_payer.clone(), @@ -327,15 +215,10 @@ impl<'a, A: AccountInfoTrait + Clone> MintActionCpiAccounts<'a, A> { ][..], ); - // Optional system accounts - if let Some(pool) = self.sol_pool_pda { - accounts.push(pool.clone()); - } if let Some(context) = self.cpi_context { accounts.push(context.clone()); } - // Tree/Queue accounts accounts.push(self.out_output_queue.clone()); accounts.push(self.in_merkle_tree.clone()); @@ -346,7 +229,6 @@ impl<'a, A: AccountInfoTrait + Clone> MintActionCpiAccounts<'a, A> { accounts.push(queue.clone()); } - // Decompressed token accounts for account in self.ctoken_accounts { accounts.push(account.clone()); } @@ -354,29 +236,17 @@ impl<'a, A: AccountInfoTrait + Clone> MintActionCpiAccounts<'a, A> { accounts } - /// Convert to AccountMeta vector for instruction building #[profile] #[inline(always)] - pub fn to_account_metas(&self, include_compressed_token_program: bool) -> Vec { - let mut metas = Vec::with_capacity(21 + self.ctoken_accounts.len()); - - // Optionally include compressed_token_program - if include_compressed_token_program { - metas.push(AccountMeta { - pubkey: self.compressed_token_program.key().into(), - is_writable: false, - is_signer: false, - }); - } + pub fn to_account_metas(&self) -> Vec { + let mut metas = Vec::with_capacity(15 + self.ctoken_accounts.len()); - // Light system program metas.push(AccountMeta { pubkey: self.light_system_program.key().into(), is_writable: false, is_signer: false, }); - // Mint signer if present if let Some(signer) = self.mint_signer { metas.push(AccountMeta { pubkey: signer.key().into(), @@ -385,37 +255,12 @@ impl<'a, A: AccountInfoTrait + Clone> MintActionCpiAccounts<'a, A> { }); } - // Authority metas.push(AccountMeta { pubkey: self.authority.key().into(), is_writable: false, is_signer: true, }); - // Decompressed mint accounts - if let Some(mint) = self.mint { - metas.push(AccountMeta { - pubkey: mint.key().into(), - is_writable: true, - is_signer: false, - }); - } - if let Some(pool) = self.token_pool_pda { - metas.push(AccountMeta { - pubkey: pool.key().into(), - is_writable: true, - is_signer: false, - }); - } - if let Some(program) = self.token_program { - metas.push(AccountMeta { - pubkey: program.key().into(), - is_writable: false, - is_signer: false, - }); - } - - // Core Light system accounts metas.push(AccountMeta { pubkey: self.fee_payer.key().into(), is_writable: true, @@ -447,14 +292,6 @@ impl<'a, A: AccountInfoTrait + Clone> MintActionCpiAccounts<'a, A> { is_signer: false, }); - // Optional system accounts - if let Some(pool) = self.sol_pool_pda { - metas.push(AccountMeta { - pubkey: pool.key().into(), - is_writable: true, - is_signer: false, - }); - } if let Some(context) = self.cpi_context { metas.push(AccountMeta { pubkey: context.key().into(), @@ -463,7 +300,6 @@ impl<'a, A: AccountInfoTrait + Clone> MintActionCpiAccounts<'a, A> { }); } - // Tree/Queue accounts metas.push(AccountMeta { pubkey: self.out_output_queue.key().into(), is_writable: true, @@ -490,7 +326,6 @@ impl<'a, A: AccountInfoTrait + Clone> MintActionCpiAccounts<'a, A> { }); } - // Decompressed token accounts for account in self.ctoken_accounts { metas.push(AccountMeta { pubkey: account.key().into(), @@ -498,7 +333,6 @@ impl<'a, A: AccountInfoTrait + Clone> MintActionCpiAccounts<'a, A> { is_signer: false, }); } - metas } } diff --git a/sdk-libs/compressed-token-sdk/src/instructions/mint_action/instruction.rs b/sdk-libs/compressed-token-sdk/src/instructions/mint_action/instruction.rs index c53dd82098..5d9ce41c7e 100644 --- a/sdk-libs/compressed-token-sdk/src/instructions/mint_action/instruction.rs +++ b/sdk-libs/compressed-token-sdk/src/instructions/mint_action/instruction.rs @@ -1,798 +1,147 @@ -use light_compressed_account::instruction_data::compressed_proof::CompressedProof; +use light_compressed_account::instruction_data::traits::LightInstructionData; use light_ctoken_types::{ - self, - instructions::mint_action::{ - Action, CompressedMintWithContext, CpiContext, CreateMint, CreateSplMintAction, - MintActionCompressedInstructionData, MintToCompressedAction, Recipient, - RemoveMetadataKeyAction, UpdateAuthority, UpdateMetadataAuthorityAction, - UpdateMetadataFieldAction, - }, + instructions::mint_action::MintActionCompressedInstructionData, COMPRESSED_TOKEN_PROGRAM_ID, }; -use light_program_profiler::profile; use solana_instruction::Instruction; use solana_msg::msg; -use solana_pubkey::Pubkey; +use solana_program_error::ProgramError; use crate::{ - error::{Result, TokenSdkError}, - instructions::mint_action::account_metas::{ - get_mint_action_instruction_account_metas, - get_mint_action_instruction_account_metas_cpi_write, MintActionMetaConfig, - MintActionMetaConfigCpiWrite, - }, - AnchorDeserialize, AnchorSerialize, + ctoken_instruction::CTokenInstruction, + error::TokenSdkError, + instructions::mint_action::{cpi_accounts::MintActionCpiAccounts, MintActionCpiWriteAccounts}, }; -pub const MINT_ACTION_DISCRIMINATOR: u8 = 103; - -/// Input parameters for creating a new mint -#[derive(Debug, Clone)] -pub struct CreateMintInputs { - pub compressed_mint_inputs: CompressedMintWithContext, - pub mint_seed: Pubkey, - pub authority: Pubkey, - pub payer: Pubkey, - pub proof: Option, - pub address_tree: Pubkey, - pub output_queue: Pubkey, -} - -/// Input parameters for working with an existing mint -#[derive(Debug, Clone)] -pub struct WithMintInputs { - pub compressed_mint_inputs: CompressedMintWithContext, - pub mint_seed: Pubkey, - pub authority: Pubkey, - pub payer: Pubkey, - pub proof: Option, - pub state_tree: Pubkey, - pub input_queue: Pubkey, - pub output_queue: Pubkey, - pub token_pool: Option, // Required if mint is decompressed -} - -/// Input struct for creating a mint action instruction -#[derive(Debug, Clone, AnchorDeserialize, AnchorSerialize, PartialEq)] -pub struct MintActionInputs { - pub compressed_mint_inputs: CompressedMintWithContext, - pub mint_seed: Pubkey, - pub create_mint: bool, // Whether we're creating a new compressed mint - pub mint_bump: Option, // Bump seed for creating SPL mint - pub authority: Pubkey, - pub payer: Pubkey, - pub proof: Option, - pub actions: Vec, - pub address_tree_pubkey: Pubkey, - pub input_queue: Option, // Input queue for existing compressed mint operations - pub output_queue: Pubkey, - pub tokens_out_queue: Option, // Output queue for new token accounts - pub token_pool: Option, -} - -impl MintActionInputs { - /// Create a new compressed mint (starting point for new mints) - pub fn new_create_mint(inputs: CreateMintInputs) -> Self { - Self { - compressed_mint_inputs: inputs.compressed_mint_inputs, - mint_seed: inputs.mint_seed, - create_mint: true, - mint_bump: None, - authority: inputs.authority, - payer: inputs.payer, - proof: inputs.proof, - actions: Vec::new(), - address_tree_pubkey: inputs.address_tree, - input_queue: None, - output_queue: inputs.output_queue, - tokens_out_queue: None, - token_pool: None, - } - } - - /// Start with an existing mint (starting point for existing mints) - pub fn new_with_mint(inputs: WithMintInputs) -> Self { - Self { - compressed_mint_inputs: inputs.compressed_mint_inputs, - mint_seed: inputs.mint_seed, - create_mint: false, - mint_bump: None, - authority: inputs.authority, - payer: inputs.payer, - proof: inputs.proof, - actions: Vec::new(), - address_tree_pubkey: inputs.state_tree, - input_queue: Some(inputs.input_queue), - output_queue: inputs.output_queue, - tokens_out_queue: None, - token_pool: inputs.token_pool, - } - } - - /// Add CreateSplMint action - creates SPL mint and token pool - pub fn add_create_spl_mint(mut self, mint_bump: u8, token_pool: TokenPool) -> Self { - self.actions - .push(MintActionType::CreateSplMint { mint_bump }); - self.token_pool = Some(token_pool); - self - } - - /// Add MintTo action - mint tokens to compressed token accounts - pub fn add_mint_to( - mut self, - recipients: Vec, - token_account_version: u8, - tokens_out_queue: Option, - ) -> Self { - self.actions.push(MintActionType::MintTo { - recipients, - token_account_version, - }); - // Set tokens_out_queue if not already set - if self.tokens_out_queue.is_none() { - self.tokens_out_queue = tokens_out_queue.or(Some(self.output_queue)); - } - self - } - - /// Add MintToCToken action - mint to SPL token accounts - pub fn add_mint_to_decompressed(mut self, account: Pubkey, amount: u64) -> Self { - self.actions - .push(MintActionType::MintToCToken { account, amount }); - self - } - - /// Add UpdateMintAuthority action - pub fn add_update_mint_authority(mut self, new_authority: Option) -> Self { - self.actions - .push(MintActionType::UpdateMintAuthority { new_authority }); - self - } - - /// Add UpdateFreezeAuthority action - pub fn add_update_freeze_authority(mut self, new_authority: Option) -> Self { - self.actions - .push(MintActionType::UpdateFreezeAuthority { new_authority }); - self - } - - /// Add UpdateMetadataField action - pub fn add_update_metadata_field( - mut self, - extension_index: u8, - field_type: u8, - key: Vec, - value: Vec, - ) -> Self { - self.actions.push(MintActionType::UpdateMetadataField { - extension_index, - field_type, - key, - value, - }); - self - } - - /// Add UpdateMetadataAuthority action - pub fn add_update_metadata_authority( - mut self, - extension_index: u8, - new_authority: Pubkey, - ) -> Self { - self.actions.push(MintActionType::UpdateMetadataAuthority { - extension_index, - new_authority, - }); - self - } - - /// Add RemoveMetadataKey action - pub fn add_remove_metadata_key( - mut self, - extension_index: u8, - key: Vec, - idempotent: u8, - ) -> Self { - self.actions.push(MintActionType::RemoveMetadataKey { - extension_index, - key, - idempotent, - }); - self - } -} - -/// High-level action types for the mint action instruction -#[derive(Debug, Clone, AnchorDeserialize, AnchorSerialize, PartialEq)] -pub enum MintActionType { - CreateSplMint { - mint_bump: u8, - }, - MintTo { - recipients: Vec, - token_account_version: u8, - }, - UpdateMintAuthority { - new_authority: Option, - }, - UpdateFreezeAuthority { - new_authority: Option, - }, - MintToCToken { - account: Pubkey, - amount: u64, - }, - UpdateMetadataField { - extension_index: u8, - field_type: u8, - key: Vec, - value: Vec, - }, - UpdateMetadataAuthority { - extension_index: u8, - new_authority: Pubkey, - }, - RemoveMetadataKey { - extension_index: u8, - key: Vec, - idempotent: u8, - }, -} - -#[derive(Debug, Clone, AnchorDeserialize, AnchorSerialize, PartialEq)] -pub struct MintToRecipient { - pub recipient: Pubkey, - pub amount: u64, -} - -#[derive(Debug, Clone, AnchorDeserialize, AnchorSerialize, PartialEq)] -pub struct TokenPool { - pub pubkey: Pubkey, - pub bump: u8, - pub index: u8, -} -// TODO: remove duplicate code -/// Creates a mint action instruction -#[profile] -pub fn create_mint_action_cpi( - input: MintActionInputs, - cpi_context: Option, - cpi_context_pubkey: Option, -) -> Result { - // Convert high-level actions to program-level actions - let mut program_actions = Vec::new(); - let create_mint = if input.create_mint { - Some(CreateMint::default()) - } else { - None - }; - - // Check for lamports, decompressed status, and mint actions before moving - let with_lamports = false; - let spl_mint_initialized = input - .actions - .iter() - .any(|action| matches!(action, MintActionType::CreateSplMint { .. })) - || input - .compressed_mint_inputs - .mint - .metadata - .spl_mint_initialized; - let has_mint_to_actions = input.actions.iter().any(|action| { - matches!( - action, - MintActionType::MintTo { .. } | MintActionType::MintToCToken { .. } - ) - }); - // Match onchain logic: with_mint_signer = create_mint() | has_CreateSplMint_action - let with_mint_signer = create_mint.is_some() - || input - .actions - .iter() - .any(|action| matches!(action, MintActionType::CreateSplMint { .. })); - - // Only require mint to sign when creating a new compressed mint - let mint_needs_to_sign = create_mint.is_some(); - - // Collect decompressed accounts for account index mapping - let mut decompressed_accounts: Vec = Vec::new(); - let mut decompressed_account_index = 0u8; - - for action in input.actions { - match action { - MintActionType::CreateSplMint { mint_bump: bump } => { - program_actions.push(Action::CreateSplMint(CreateSplMintAction { - mint_bump: bump, - })); - } - MintActionType::MintTo { - recipients, - token_account_version, - } => { - // TODO: cleanup once lamports are removed. - let program_recipients: Vec<_> = recipients - .into_iter() - .map(|r| Recipient { - recipient: r.recipient.to_bytes().into(), - amount: r.amount, - }) - .collect(); - - program_actions.push(Action::MintToCompressed(MintToCompressedAction { - token_account_version, - recipients: program_recipients, - })); - } - MintActionType::UpdateMintAuthority { new_authority } => { - program_actions.push(Action::UpdateMintAuthority(UpdateAuthority { - new_authority: new_authority.map(|auth| auth.to_bytes().into()), - })); - } - MintActionType::UpdateFreezeAuthority { new_authority } => { - program_actions.push(Action::UpdateFreezeAuthority(UpdateAuthority { - new_authority: new_authority.map(|auth| auth.to_bytes().into()), - })); +impl CTokenInstruction for MintActionCompressedInstructionData { + type ExecuteAccounts<'info, A: light_account_checks::AccountInfoTrait + Clone + 'info> = + MintActionCpiAccounts<'info, A>; + type CpiWriteAccounts<'info, A: light_account_checks::AccountInfoTrait + Clone + 'info> = + MintActionCpiWriteAccounts<'info, A>; + + fn instruction( + self, + accounts: &Self::ExecuteAccounts<'_, A>, + ) -> Result { + if let Some(ref cpi_ctx) = self.cpi_context { + if cpi_ctx.set_context || cpi_ctx.first_set_context { + msg!( + "CPI context write operations not supported in instruction(). Use instruction_write_to_cpi_context_first() or instruction_write_to_cpi_context_set() instead" + ); + return Err(ProgramError::from(TokenSdkError::InvalidAccountData)); } - MintActionType::MintToCToken { account, amount } => { - use light_ctoken_types::instructions::mint_action::MintToCTokenAction; + } - // Add account to decompressed accounts list and get its index - decompressed_accounts.push(account); - let current_index = decompressed_account_index; - decompressed_account_index += 1; + let data = self.data().map_err(ProgramError::from)?; - program_actions.push(Action::MintToCToken(MintToCTokenAction { - account_index: current_index, - amount, - })); - } - MintActionType::UpdateMetadataField { - extension_index, - field_type, - key, - value, - } => { - program_actions.push(Action::UpdateMetadataField(UpdateMetadataFieldAction { - extension_index, - field_type, - key, - value, - })); - } - MintActionType::UpdateMetadataAuthority { - extension_index, - new_authority, - } => { - program_actions.push(Action::UpdateMetadataAuthority( - UpdateMetadataAuthorityAction { - extension_index, - new_authority: new_authority.to_bytes().into(), - }, - )); - } - MintActionType::RemoveMetadataKey { - extension_index, - key, - idempotent, - } => { - program_actions.push(Action::RemoveMetadataKey(RemoveMetadataKeyAction { - extension_index, - key, - idempotent, - })); - } - } + Ok(Instruction { + program_id: COMPRESSED_TOKEN_PROGRAM_ID.into(), + accounts: accounts.to_account_metas(), + data, + }) } - // Create account meta config first (before moving compressed_mint_inputs) - let meta_config = MintActionMetaConfig { - fee_payer: Some(input.payer), - mint_signer: if with_mint_signer { - Some(input.mint_seed) + fn instruction_write_to_cpi_context_first( + self, + accounts: &Self::CpiWriteAccounts<'_, A>, + ) -> Result { + let mut instruction_data = self; + if let Some(ref mut cpi_ctx) = instruction_data.cpi_context { + cpi_ctx.first_set_context = true; + cpi_ctx.set_context = false; } else { - None - }, - authority: input.authority, - tree_pubkey: input.address_tree_pubkey, - input_queue: input.input_queue, - output_queue: input.output_queue, - tokens_out_queue: input.tokens_out_queue, - with_lamports, - spl_mint_initialized, - has_mint_to_actions, - with_cpi_context: cpi_context_pubkey, - create_mint: create_mint.is_some(), - with_mint_signer, - mint_needs_to_sign, - ctoken_accounts: decompressed_accounts, - }; - - // Get account metas (before moving compressed_mint_inputs) - let accounts = - get_mint_action_instruction_account_metas(meta_config, &input.compressed_mint_inputs); - msg!("account metas {:?}", accounts); - let instruction_data = MintActionCompressedInstructionData { - create_mint, - leaf_index: input.compressed_mint_inputs.leaf_index, - prove_by_index: input.compressed_mint_inputs.prove_by_index, - root_index: input.compressed_mint_inputs.root_index, - compressed_address: input.compressed_mint_inputs.address, - mint: input.compressed_mint_inputs.mint, - token_pool_bump: input.token_pool.as_ref().map_or(0, |tp| tp.bump), - token_pool_index: input.token_pool.as_ref().map_or(0, |tp| tp.index), - actions: program_actions, - proof: input.proof, - cpi_context, - }; - - // Serialize instruction data - let data_vec = instruction_data - .try_to_vec() - .map_err(|_| TokenSdkError::SerializationError)?; - - Ok(Instruction { - program_id: Pubkey::new_from_array(light_ctoken_types::COMPRESSED_TOKEN_PROGRAM_ID), - accounts, - data: [vec![MINT_ACTION_DISCRIMINATOR], data_vec].concat(), - }) -} - -/// Creates a mint action instruction without CPI context -pub fn create_mint_action(input: MintActionInputs) -> Result { - create_mint_action_cpi(input, None, None) -} - -/// Input struct for creating a mint action CPI write instruction -#[derive(Debug, Clone, AnchorDeserialize, AnchorSerialize, PartialEq)] -pub struct MintActionInputsCpiWrite { - pub compressed_mint_inputs: - light_ctoken_types::instructions::mint_action::CompressedMintWithContext, - pub mint_seed: Option, // Optional - only when creating mint and when creating SPL mint - pub mint_bump: Option, // Bump seed for creating SPL mint - pub create_mint: bool, // Whether we're creating a new mint - pub authority: Pubkey, - pub payer: Pubkey, - pub actions: Vec, - //pub input_queue: Option, // Input queue for existing compressed mint operations - pub cpi_context: light_ctoken_types::instructions::mint_action::CpiContext, - pub cpi_context_pubkey: Pubkey, -} - -/// Input parameters for creating a new mint in CPI write mode -#[derive(Debug, Clone)] -pub struct CreateMintCpiWriteInputs { - pub compressed_mint_inputs: - light_ctoken_types::instructions::mint_action::CompressedMintWithContext, - pub mint_seed: Pubkey, - pub authority: Pubkey, - pub payer: Pubkey, - pub cpi_context_pubkey: Pubkey, - pub first_set_context: bool, - pub address_tree_index: u8, - pub output_queue_index: u8, - pub assigned_account_index: u8, -} - -/// Input parameters for working with an existing mint in CPI write mode -#[derive(Debug, Clone)] -pub struct WithMintCpiWriteInputs { - pub compressed_mint_inputs: - light_ctoken_types::instructions::mint_action::CompressedMintWithContext, - pub authority: Pubkey, - pub payer: Pubkey, - pub cpi_context_pubkey: Pubkey, - pub first_set_context: bool, - pub state_tree_index: u8, - pub input_queue_index: u8, - pub output_queue_index: u8, - pub assigned_account_index: u8, -} - -impl MintActionInputsCpiWrite { - /// Create a new compressed mint for CPI write (starting point for new mints) - pub fn new_create_mint(inputs: CreateMintCpiWriteInputs) -> Self { - let cpi_context = light_ctoken_types::instructions::mint_action::CpiContext { - set_context: false, // For CPI write, we're reading from context - first_set_context: inputs.first_set_context, - in_tree_index: inputs.address_tree_index, // For create_mint, this is the address tree - in_queue_index: 0, // Not used for create_mint - out_queue_index: inputs.output_queue_index, - token_out_queue_index: 0, // Set when adding MintTo action - assigned_account_index: inputs.assigned_account_index, - read_only_address_trees: [0; 4], - address_tree_pubkey: light_ctoken_types::CMINT_ADDRESS_TREE, - }; - - Self { - compressed_mint_inputs: inputs.compressed_mint_inputs, - mint_seed: Some(inputs.mint_seed), - mint_bump: None, - create_mint: true, - authority: inputs.authority, - payer: inputs.payer, - actions: Vec::new(), - cpi_context, - cpi_context_pubkey: inputs.cpi_context_pubkey, + instruction_data.cpi_context = + Some(light_ctoken_types::instructions::mint_action::CpiContext { + first_set_context: true, + ..Default::default() + }); } - } - /// Start with an existing mint for CPI write (starting point for existing mints) - pub fn new_with_mint(inputs: WithMintCpiWriteInputs) -> Self { - let cpi_context = light_ctoken_types::instructions::mint_action::CpiContext { - set_context: false, // For CPI write, we're reading from context - first_set_context: inputs.first_set_context, - in_tree_index: inputs.state_tree_index, - in_queue_index: inputs.input_queue_index, - out_queue_index: inputs.output_queue_index, - token_out_queue_index: 0, // Set when adding MintTo action - assigned_account_index: inputs.assigned_account_index, - ..Default::default() - }; - - Self { - compressed_mint_inputs: inputs.compressed_mint_inputs, - mint_seed: None, - mint_bump: None, - create_mint: false, - authority: inputs.authority, - payer: inputs.payer, - actions: Vec::new(), - cpi_context, - cpi_context_pubkey: inputs.cpi_context_pubkey, - } + build_cpi_write_instruction(instruction_data, accounts) } - /// Add MintTo action - mint tokens to compressed token accounts - /// Returns error if mint is decompressed (no SPL mint modifications in CPI write) - pub fn add_mint_to( - mut self, - recipients: Vec, - token_account_version: u8, - token_out_queue_index: u8, // Index for token output queue - ) -> Result { - // Cannot mint if the mint is decompressed - // In CPI write, we cannot modify SPL mint supply - if self - .compressed_mint_inputs - .mint - .metadata - .spl_mint_initialized - { - return Err(TokenSdkError::CannotMintWithDecompressedInCpiWrite); + fn instruction_write_to_cpi_context_set( + self, + accounts: &Self::CpiWriteAccounts<'_, A>, + ) -> Result { + let mut instruction_data = self; + if let Some(ref mut cpi_ctx) = instruction_data.cpi_context { + cpi_ctx.set_context = true; + cpi_ctx.first_set_context = false; + } else { + instruction_data.cpi_context = + Some(light_ctoken_types::instructions::mint_action::CpiContext { + set_context: true, + ..Default::default() + }); } - // Set token_out_queue_index for the MintTo action - self.cpi_context.token_out_queue_index = token_out_queue_index; - - self.actions.push(MintActionType::MintTo { - recipients, - token_account_version, - }); - Ok(self) - } - - /// Add UpdateMintAuthority action - pub fn add_update_mint_authority(mut self, new_authority: Option) -> Self { - self.actions - .push(MintActionType::UpdateMintAuthority { new_authority }); - self - } - - /// Add UpdateFreezeAuthority action - pub fn add_update_freeze_authority(mut self, new_authority: Option) -> Self { - self.actions - .push(MintActionType::UpdateFreezeAuthority { new_authority }); - self - } - - /// Add UpdateMetadataField action - pub fn add_update_metadata_field( - mut self, - extension_index: u8, - field_type: u8, - key: Vec, - value: Vec, - ) -> Self { - self.actions.push(MintActionType::UpdateMetadataField { - extension_index, - field_type, - key, - value, - }); - self - } - - /// Add UpdateMetadataAuthority action - pub fn add_update_metadata_authority( - mut self, - extension_index: u8, - new_authority: Pubkey, - ) -> Self { - self.actions.push(MintActionType::UpdateMetadataAuthority { - extension_index, - new_authority, - }); - self - } - - /// Add RemoveMetadataKey action - pub fn add_remove_metadata_key( - mut self, - extension_index: u8, - key: Vec, - idempotent: u8, - ) -> Self { - self.actions.push(MintActionType::RemoveMetadataKey { - extension_index, - key, - idempotent, - }); - self + build_cpi_write_instruction(instruction_data, accounts) } } -/// Creates a mint action CPI write instruction (for use in CPI context) -pub fn mint_action_cpi_write(input: MintActionInputsCpiWrite) -> Result { - use light_ctoken_types::instructions::mint_action::MintActionCompressedInstructionData; - if input - .compressed_mint_inputs - .mint - .metadata - .spl_mint_initialized - || input - .actions - .iter() - .any(|action| matches!(action, MintActionType::CreateSplMint { .. })) - { - return Err(TokenSdkError::CannotMintWithDecompressedInCpiWrite); - } - // Validate CPI context - if !input.cpi_context.first_set_context && !input.cpi_context.set_context { - return Err(TokenSdkError::InvalidAccountData); - } - - // Convert high-level actions to program-level actions - let mut program_actions = Vec::new(); - let create_mint = if input.create_mint { - Some(CreateMint::default()) - } else { - None - }; - - let with_mint_signer = create_mint.is_some(); - - // Only require mint to sign when creating a new compressed mint - let mint_needs_to_sign = create_mint.is_some(); - - for action in input.actions { - match action { - MintActionType::MintTo { - recipients, - token_account_version, - } => { - let program_recipients: Vec<_> = recipients - .into_iter() - .map( - |r| light_ctoken_types::instructions::mint_action::Recipient { - recipient: r.recipient.to_bytes().into(), - amount: r.amount, - }, - ) - .collect(); - - program_actions.push( - light_ctoken_types::instructions::mint_action::Action::MintToCompressed( - light_ctoken_types::instructions::mint_action::MintToCompressedAction { - token_account_version, - recipients: program_recipients, - }, - ), - ); - } - MintActionType::UpdateMintAuthority { new_authority } => { - program_actions.push( - light_ctoken_types::instructions::mint_action::Action::UpdateMintAuthority( - light_ctoken_types::instructions::mint_action::UpdateAuthority { - new_authority: new_authority.map(|auth| auth.to_bytes().into()), - }, - ), - ); - } - MintActionType::UpdateFreezeAuthority { new_authority } => { - program_actions.push( - light_ctoken_types::instructions::mint_action::Action::UpdateFreezeAuthority( - light_ctoken_types::instructions::mint_action::UpdateAuthority { - new_authority: new_authority.map(|auth| auth.to_bytes().into()), - }, - ), - ); - } - MintActionType::UpdateMetadataField { - extension_index, - field_type, - key, - value, - } => { - program_actions.push( - light_ctoken_types::instructions::mint_action::Action::UpdateMetadataField( - UpdateMetadataFieldAction { - extension_index, - field_type, - key, - value, - }, - ), - ); - } - MintActionType::UpdateMetadataAuthority { - extension_index, - new_authority, - } => { - program_actions.push( - light_ctoken_types::instructions::mint_action::Action::UpdateMetadataAuthority( - UpdateMetadataAuthorityAction { - extension_index, - new_authority: new_authority.to_bytes().into(), - }, - ), - ); - } - MintActionType::RemoveMetadataKey { - extension_index, - key, - idempotent, - } => { - program_actions.push( - light_ctoken_types::instructions::mint_action::Action::RemoveMetadataKey( - RemoveMetadataKeyAction { - extension_index, - key, - idempotent, - }, - ), - ); +/// Helper function for building CPI write instructions +#[inline(always)] +fn build_cpi_write_instruction( + instruction_data: MintActionCompressedInstructionData, + accounts: &MintActionCpiWriteAccounts, +) -> Result { + let data = instruction_data.data().map_err(ProgramError::from)?; + Ok(Instruction { + program_id: COMPRESSED_TOKEN_PROGRAM_ID.into(), + accounts: { + let mut account_metas = Vec::with_capacity( + 6 + accounts.recipient_token_accounts.len() + + if accounts.mint_signer.is_some() { 1 } else { 0 }, + ); + + account_metas.push(solana_instruction::AccountMeta { + pubkey: accounts.light_system_program.key().into(), + is_writable: false, + is_signer: false, + }); + + if let Some(mint_signer) = accounts.mint_signer { + account_metas.push(solana_instruction::AccountMeta { + pubkey: mint_signer.key().into(), + is_writable: false, + is_signer: true, + }); } - _ => return Err(TokenSdkError::CannotMintWithDecompressedInCpiWrite), - } - } - let instruction_data = MintActionCompressedInstructionData { - create_mint, - leaf_index: input.compressed_mint_inputs.leaf_index, - prove_by_index: input.compressed_mint_inputs.prove_by_index, - root_index: input.compressed_mint_inputs.root_index, - compressed_address: input.compressed_mint_inputs.address, - mint: input.compressed_mint_inputs.mint, - token_pool_bump: 0, // Not used in CPI write context - token_pool_index: 0, // Not used in CPI write context - actions: program_actions, - proof: None, // No proof for CPI write context - cpi_context: Some(input.cpi_context), - }; + account_metas.push(solana_instruction::AccountMeta { + pubkey: accounts.authority.key().into(), + is_writable: false, + is_signer: true, + }); + + account_metas.push(solana_instruction::AccountMeta { + pubkey: accounts.fee_payer.key().into(), + is_writable: true, + is_signer: true, + }); + + account_metas.push(solana_instruction::AccountMeta { + pubkey: accounts.cpi_authority_pda.key().into(), + is_writable: false, + is_signer: false, + }); + + account_metas.push(solana_instruction::AccountMeta { + pubkey: accounts.cpi_context.key().into(), + is_writable: true, + is_signer: false, + }); + + for acc in &accounts.recipient_token_accounts { + account_metas.push(solana_instruction::AccountMeta { + pubkey: acc.key().into(), + is_writable: true, + is_signer: false, + }); + } - // Create account meta config for CPI write - let meta_config = MintActionMetaConfigCpiWrite { - fee_payer: input.payer, - mint_signer: if with_mint_signer { - input.mint_seed - } else { - None + account_metas }, - authority: input.authority, - cpi_context: input.cpi_context_pubkey, - mint_needs_to_sign, - }; - - // Get account metas - let accounts = get_mint_action_instruction_account_metas_cpi_write(meta_config); - - // Serialize instruction data - let data_vec = instruction_data - .try_to_vec() - .map_err(|_| TokenSdkError::SerializationError)?; - - Ok(Instruction { - program_id: Pubkey::new_from_array(light_ctoken_types::COMPRESSED_TOKEN_PROGRAM_ID), - accounts, - data: [vec![MINT_ACTION_DISCRIMINATOR], data_vec].concat(), + data, }) } diff --git a/sdk-libs/compressed-token-sdk/src/instructions/mint_action/mod.rs b/sdk-libs/compressed-token-sdk/src/instructions/mint_action/mod.rs index 4c13d293be..22ca0b3f41 100644 --- a/sdk-libs/compressed-token-sdk/src/instructions/mint_action/mod.rs +++ b/sdk-libs/compressed-token-sdk/src/instructions/mint_action/mod.rs @@ -7,11 +7,6 @@ pub use account_metas::{ MintActionMetaConfig, MintActionMetaConfigCpiWrite, }; pub use cpi_accounts::MintActionCpiAccounts; -pub use instruction::{ - create_mint_action, create_mint_action_cpi, mint_action_cpi_write, CreateMintCpiWriteInputs, - CreateMintInputs, MintActionInputs, MintActionInputsCpiWrite, MintActionType, MintToRecipient, - TokenPool, WithMintCpiWriteInputs, WithMintInputs, MINT_ACTION_DISCRIMINATOR, -}; use light_account_checks::AccountInfoTrait; use light_sdk::cpi::CpiSigner; @@ -38,8 +33,6 @@ impl MintActionCpiWriteAccounts<'_, T> { } pub fn to_account_infos(&self) -> Vec { - // The order must match mint_action on-chain program expectations: - // [light_system_program, mint_signer, authority, fee_payer, cpi_authority_pda, cpi_context, ...recipient_token_accounts] let mut accounts = Vec::new(); accounts.push(self.light_system_program.clone()); @@ -53,7 +46,6 @@ impl MintActionCpiWriteAccounts<'_, T> { accounts.push(self.cpi_authority_pda.clone()); accounts.push(self.cpi_context.clone()); - // Add recipient token accounts as remaining accounts for token_account in &self.recipient_token_accounts { accounts.push((*token_account).clone()); } diff --git a/sdk-libs/compressed-token-sdk/src/instructions/mint_to_compressed/instruction.rs b/sdk-libs/compressed-token-sdk/src/instructions/mint_to_compressed/instruction.rs index fb6d37961b..96ec982fdd 100644 --- a/sdk-libs/compressed-token-sdk/src/instructions/mint_to_compressed/instruction.rs +++ b/sdk-libs/compressed-token-sdk/src/instructions/mint_to_compressed/instruction.rs @@ -1,3 +1,4 @@ +use light_compressed_account::instruction_data::traits::LightInstructionData; pub use light_compressed_token_types::account_infos::mint_to_compressed::DecompressedMintConfig; use light_compressed_token_types::CompressedProof; use light_ctoken_types::instructions::mint_action::{ @@ -8,9 +9,8 @@ use solana_pubkey::Pubkey; use crate::{ error::{Result, TokenSdkError}, - instructions::mint_action::{ - create_mint_action_cpi, MintActionInputs, MintActionType, MintToRecipient, - }, + instructions::mint_action::{get_mint_action_instruction_account_metas, MintActionMetaConfig}, + TokenPool, }; pub const MINT_TO_COMPRESSED_DISCRIMINATOR: u8 = 101; @@ -32,7 +32,7 @@ pub struct MintToCompressedInputs { pub token_account_version: u8, pub cpi_context_pubkey: Option, /// Required if the mint is decompressed - pub token_pool: Option, + pub token_pool: Option, } /// Create a mint_to_compressed instruction (wrapper around mint_action) @@ -53,71 +53,56 @@ pub fn create_mint_to_compressed_instruction( proof, token_account_version, cpi_context_pubkey, - token_pool, + token_pool: _, } = inputs; - // Convert Recipients to MintToRecipients - let mint_to_recipients: Vec = recipients - .into_iter() - .map(|recipient| MintToRecipient { - recipient: solana_pubkey::Pubkey::from(recipient.recipient.to_bytes()), - amount: recipient.amount, - }) - .collect(); + let mint_to_action = light_ctoken_types::instructions::mint_action::MintToCompressedAction { + token_account_version, + recipients, + }; - // Create mint action inputs - // For existing mint operations, we don't need a mint_seed since we can use the SPL mint directly - // from the compressed_mint_inputs data. We use a dummy value that won't be used. - let mint_action_inputs = MintActionInputs { - compressed_mint_inputs, - mint_seed: solana_pubkey::Pubkey::default(), // Dummy value, not used for existing mints - create_mint: false, // Never creating mint in mint_to_compressed - mint_bump: None, // No mint creation - authority: mint_authority, - payer, - proof, - actions: vec![MintActionType::MintTo { - recipients: mint_to_recipients, - token_account_version, // From inputs parameter - }], - address_tree_pubkey: state_merkle_tree, // State tree where compressed mint is stored - input_queue: Some(input_queue), // Input queue from compressed mint tree - output_queue: output_queue_cmint, // Output queue for updated compressed mint - tokens_out_queue: Some(output_queue_tokens), // Output queue for new token accounts - token_pool, // Required if the mint is decompressed for SPL operations - /* - cpi_context: cpi_context.map(|ctx| { - light_ctoken_types::instructions::mint_action::CpiContext { - set_context: ctx.set_context, - first_set_context: ctx.first_set_context, - in_tree_index: ctx.in_tree_index, - in_queue_index: ctx.in_queue_index, - out_queue_index: ctx.out_queue_index, - token_out_queue_index: ctx.token_out_queue_index, - assigned_account_index: 0, // Default value for mint operation - } - }), - cpi_context_pubkey,*/ + let mut instruction_data = + light_ctoken_types::instructions::mint_action::MintActionCompressedInstructionData::new( + compressed_mint_inputs.clone(), + proof, + ) + .with_mint_to_compressed(mint_to_action); + + if let Some(ctx) = cpi_context { + instruction_data = instruction_data.with_cpi_context(ctx); + } + + let meta_config = if cpi_context_pubkey.is_some() { + MintActionMetaConfig::new_cpi_context( + &instruction_data, + mint_authority, + payer, + cpi_context_pubkey.unwrap(), + )? + } else { + MintActionMetaConfig::new( + &instruction_data, + mint_authority, + payer, + state_merkle_tree, + input_queue, + output_queue_cmint, + )? + .with_tokens_out_queue(output_queue_tokens) }; - // Use mint_action instruction internally - create_mint_action_cpi( - mint_action_inputs, - cpi_context.map(|ctx| { - light_ctoken_types::instructions::mint_action::CpiContext { - set_context: ctx.set_context, - first_set_context: ctx.first_set_context, - in_tree_index: ctx.in_tree_index, - in_queue_index: ctx.in_queue_index, - out_queue_index: ctx.out_queue_index, - token_out_queue_index: ctx.token_out_queue_index, - assigned_account_index: 0, // Default value for mint operation - ..Default::default() - } - }), - cpi_context_pubkey, - ) - .map_err(|e| { - TokenSdkError::CpiError(format!("Failed to create mint_action instruction: {:?}", e)) + let account_metas = + get_mint_action_instruction_account_metas(meta_config, &compressed_mint_inputs); + + let data = instruction_data + .data() + .map_err(|_| TokenSdkError::SerializationError)?; + + Ok(Instruction { + program_id: solana_pubkey::Pubkey::new_from_array( + light_ctoken_types::COMPRESSED_TOKEN_PROGRAM_ID, + ), + accounts: account_metas, + data, }) } diff --git a/sdk-libs/compressed-token-sdk/src/instructions/mod.rs b/sdk-libs/compressed-token-sdk/src/instructions/mod.rs index a1f016526d..b2e9c96602 100644 --- a/sdk-libs/compressed-token-sdk/src/instructions/mod.rs +++ b/sdk-libs/compressed-token-sdk/src/instructions/mod.rs @@ -5,7 +5,6 @@ pub mod close; pub mod compress_and_close; pub mod create_associated_token_account; pub mod create_compressed_mint; -mod create_spl_mint; pub mod create_token_account; pub mod ctoken_accounts; pub mod decompress_full; @@ -34,7 +33,6 @@ pub use compress_and_close::{ }; pub use create_associated_token_account::*; pub use create_compressed_mint::*; -pub use create_spl_mint::*; pub use create_token_account::{ create_compressible_token_account_instruction, create_ctoken_account_signed, create_token_account, CreateCompressibleTokenAccount, @@ -42,11 +40,8 @@ pub use create_token_account::{ pub use ctoken_accounts::*; pub use decompress_full::{decompress_full_ctoken_accounts_with_indices, DecompressFullIndices}; pub use mint_action::{ - create_mint_action, create_mint_action_cpi, get_mint_action_instruction_account_metas, - get_mint_action_instruction_account_metas_cpi_write, mint_action_cpi_write, - CreateMintCpiWriteInputs, CreateMintInputs, MintActionInputs, MintActionInputsCpiWrite, - MintActionMetaConfig, MintActionMetaConfigCpiWrite, MintActionType, MintToRecipient, TokenPool, - WithMintCpiWriteInputs, WithMintInputs, MINT_ACTION_DISCRIMINATOR, + get_mint_action_instruction_account_metas, get_mint_action_instruction_account_metas_cpi_write, + MintActionCpiAccounts, MintActionMetaConfig, MintActionMetaConfigCpiWrite, }; pub use mint_to_compressed::{ create_mint_to_compressed_instruction, get_mint_to_compressed_instruction_account_metas, diff --git a/sdk-libs/compressed-token-sdk/src/instructions/transfer2/instruction.rs b/sdk-libs/compressed-token-sdk/src/instructions/transfer2/instruction.rs index 30cc41a77e..d49233a03b 100644 --- a/sdk-libs/compressed-token-sdk/src/instructions/transfer2/instruction.rs +++ b/sdk-libs/compressed-token-sdk/src/instructions/transfer2/instruction.rs @@ -130,7 +130,6 @@ pub fn create_transfer2_instruction(inputs: Transfer2Inputs) -> Result, ) -> Result { - // Convert UpdateMintCpiContext to mint_action CpiContext if needed - let mint_action_cpi_context = cpi_context.map(|update_cpi_ctx| { - CpiContext { - set_context: update_cpi_ctx.set_context, - first_set_context: update_cpi_ctx.first_set_context, - in_tree_index: update_cpi_ctx.in_tree_index, - in_queue_index: update_cpi_ctx.in_queue_index, - out_queue_index: update_cpi_ctx.out_queue_index, - token_out_queue_index: 0, // Default value - not used for authority updates - assigned_account_index: 0, // Default value - mint account index for authority updates - ..Default::default() - } - }); + let mut instruction_data = + light_ctoken_types::instructions::mint_action::MintActionCompressedInstructionData::new( + input.compressed_mint_inputs.clone(), + input.proof, + ); + + let update_authority = light_ctoken_types::instructions::mint_action::UpdateAuthority { + new_authority: input.new_authority.map(|auth| auth.to_bytes().into()), + }; - // Create the appropriate action based on authority type - let actions = match input.authority_type { + instruction_data = match input.authority_type { CompressedMintAuthorityType::MintTokens => { - vec![MintActionType::UpdateMintAuthority { - new_authority: input.new_authority, - }] + instruction_data.with_update_mint_authority(update_authority) } CompressedMintAuthorityType::FreezeAccount => { - vec![MintActionType::UpdateFreezeAuthority { - new_authority: input.new_authority, - }] + instruction_data.with_update_freeze_authority(update_authority) } }; - // Create mint action inputs for authority update - let mint_action_inputs = MintActionInputs { - compressed_mint_inputs: input.compressed_mint_inputs, - mint_seed: Pubkey::default(), // Not needed for authority updates - create_mint: false, // We're updating an existing mint - mint_bump: None, - authority: input.authority, - payer: input.payer, - proof: input.proof, - actions, - address_tree_pubkey: input.in_merkle_tree, // Use in_merkle_tree as the state tree - input_queue: Some(input.in_output_queue), - output_queue: input.out_output_queue, - tokens_out_queue: None, // Not needed for authority updates - token_pool: None, // Not needed for authority updates - }; + if let Some(ctx) = cpi_context { + instruction_data = instruction_data.with_cpi_context(ctx); + } + + let meta_config = MintActionMetaConfig::new( + &instruction_data, + input.authority, + input.payer, + input.in_merkle_tree, + input.in_output_queue, + input.out_output_queue, + )?; - create_mint_action_cpi(mint_action_inputs, mint_action_cpi_context, None) + let account_metas = + get_mint_action_instruction_account_metas(meta_config, &input.compressed_mint_inputs); + + let data = instruction_data + .data() + .map_err(|_| TokenSdkError::SerializationError)?; + + Ok(Instruction { + program_id: solana_pubkey::Pubkey::new_from_array( + light_ctoken_types::COMPRESSED_TOKEN_PROGRAM_ID, + ), + accounts: account_metas, + data, + }) } /// Creates an update compressed mint instruction without CPI context @@ -111,44 +114,46 @@ pub fn create_update_compressed_mint_cpi_write( return Err(TokenSdkError::InvalidAccountData); } - // Convert UpdateMintCpiContext to mint_action CpiContext - let mint_action_cpi_context = light_ctoken_types::instructions::mint_action::CpiContext { - set_context: inputs.cpi_context.set_context, - first_set_context: inputs.cpi_context.first_set_context, - in_tree_index: inputs.cpi_context.in_tree_index, - in_queue_index: inputs.cpi_context.in_queue_index, - out_queue_index: inputs.cpi_context.out_queue_index, - token_out_queue_index: 0, // Default value - not used for authority updates - assigned_account_index: 0, // Default value - mint account index for authority updates - ..Default::default() + let mut instruction_data = + light_ctoken_types::instructions::mint_action::MintActionCompressedInstructionData::new( + inputs.compressed_mint_inputs.clone(), + None, // No proof for CPI write + ); + + let update_authority = light_ctoken_types::instructions::mint_action::UpdateAuthority { + new_authority: inputs.new_authority.map(|auth| auth.to_bytes().into()), }; - // Create the appropriate action based on authority type - let actions = match inputs.authority_type { + instruction_data = match inputs.authority_type { CompressedMintAuthorityType::MintTokens => { - vec![MintActionType::UpdateMintAuthority { - new_authority: inputs.new_authority, - }] + instruction_data.with_update_mint_authority(update_authority) } CompressedMintAuthorityType::FreezeAccount => { - vec![MintActionType::UpdateFreezeAuthority { - new_authority: inputs.new_authority, - }] + instruction_data.with_update_freeze_authority(update_authority) } }; - // Create mint action inputs for CPI write - let mint_action_inputs = MintActionInputsCpiWrite { - compressed_mint_inputs: inputs.compressed_mint_inputs, - mint_seed: None, // Not needed for authority updates - mint_bump: None, - create_mint: false, // We're updating an existing mint + instruction_data = instruction_data.with_cpi_context(inputs.cpi_context); + + let meta_config = MintActionMetaConfigCpiWrite { + fee_payer: inputs.payer, + mint_signer: None, // Not needed for authority updates authority: inputs.authority, - payer: inputs.payer, - actions, - cpi_context: mint_action_cpi_context, - cpi_context_pubkey: inputs.cpi_context_pubkey, + cpi_context: inputs.cpi_context_pubkey, + mint_needs_to_sign: false, }; - mint_action_cpi_write(mint_action_inputs) + let account_metas = get_mint_action_instruction_account_metas_cpi_write(meta_config); + + let data = instruction_data + .data() + .map_err(|_| TokenSdkError::SerializationError)?; + + Ok(Instruction { + program_id: solana_pubkey::Pubkey::new_from_array( + light_ctoken_types::COMPRESSED_TOKEN_PROGRAM_ID, + ), + accounts: account_metas, + data, + }) } diff --git a/sdk-libs/compressed-token-sdk/src/lib.rs b/sdk-libs/compressed-token-sdk/src/lib.rs index c38cdd1255..c2d5da50f3 100644 --- a/sdk-libs/compressed-token-sdk/src/lib.rs +++ b/sdk-libs/compressed-token-sdk/src/lib.rs @@ -1,6 +1,7 @@ pub mod account; pub mod account2; pub mod ctoken; +pub mod ctoken_instruction; pub mod error; pub mod instructions; pub mod pack; @@ -14,8 +15,10 @@ use anchor_lang::{AnchorDeserialize, AnchorSerialize}; #[cfg(not(feature = "anchor"))] use borsh::{BorshDeserialize as AnchorDeserialize, BorshSerialize as AnchorSerialize}; // Re-export +pub use ctoken_instruction::CTokenInstruction; pub use light_compressed_token_types::*; pub use pack::{compat, Pack, Unpack}; +pub use token_pool::TokenPool; pub use utils::{ account_meta_from_account_info, is_ctoken_account, AccountInfoToCompress, PackedCompressedTokenDataWithContext, diff --git a/sdk-libs/compressed-token-sdk/src/token_pool.rs b/sdk-libs/compressed-token-sdk/src/token_pool.rs index e354eaf268..e5a010e000 100644 --- a/sdk-libs/compressed-token-sdk/src/token_pool.rs +++ b/sdk-libs/compressed-token-sdk/src/token_pool.rs @@ -2,7 +2,14 @@ use light_compressed_token_types::constants::POOL_SEED; use light_ctoken_types::COMPRESSED_TOKEN_PROGRAM_ID; use solana_pubkey::Pubkey; -use crate::instructions::mint_action::TokenPool; +use crate::{AnchorDeserialize, AnchorSerialize}; + +#[derive(Debug, Clone, AnchorDeserialize, AnchorSerialize, PartialEq)] +pub struct TokenPool { + pub pubkey: Pubkey, + pub bump: u8, + pub index: u8, +} /// Derive the token pool pda for a given mint pub fn get_token_pool_pda(mint: &Pubkey) -> Pubkey { diff --git a/sdk-libs/compressed-token-sdk/tests/mint_action_cpi_accounts_tests.rs b/sdk-libs/compressed-token-sdk/tests/mint_action_cpi_accounts_tests.rs index b191bde46e..2a3c92967f 100644 --- a/sdk-libs/compressed-token-sdk/tests/mint_action_cpi_accounts_tests.rs +++ b/sdk-libs/compressed-token-sdk/tests/mint_action_cpi_accounts_tests.rs @@ -1,12 +1,14 @@ #![cfg(test)] use light_account_checks::account_info::test_account_info::pinocchio::get_account_info; -use light_compressed_token_sdk::instructions::mint_action::MintActionCpiAccounts; +use light_compressed_token_sdk::instructions::mint_action::{ + cpi_accounts::MintActionCpiAccountsConfig, MintActionCpiAccounts, +}; use light_compressed_token_types::CPI_AUTHORITY_PDA; use light_ctoken_types::COMPRESSED_TOKEN_PROGRAM_ID; use light_sdk_types::{ ACCOUNT_COMPRESSION_AUTHORITY_PDA, ACCOUNT_COMPRESSION_PROGRAM_ID, LIGHT_SYSTEM_PROGRAM_ID, - REGISTERED_PROGRAM_PDA, SOL_POOL_PDA, + REGISTERED_PROGRAM_PDA, }; use pinocchio::account_info::AccountInfo; @@ -119,15 +121,18 @@ fn test_successful_parsing_minimal() { false, vec![], ), // in_merkle_tree + create_test_account( + pubkey_unique(), + ACCOUNT_COMPRESSION_PROGRAM_ID, + false, + true, + false, + vec![], + ), // in_output_queue ]; - // Use create_mint variant which doesn't require in_output_queue - let result = MintActionCpiAccounts::::try_from_account_infos_create_mint( - &accounts, false, // with_mint_signer - false, // spl_mint_initialized - false, // with_lamports - false, // has_mint_to_actions - ); + // Use default config (no special options) + let result = MintActionCpiAccounts::::try_from_account_infos(&accounts); assert!(result.is_ok()); let parsed = result.unwrap(); @@ -138,21 +143,14 @@ fn test_successful_parsing_minimal() { assert_eq!(*parsed.light_system_program.key(), LIGHT_SYSTEM_PROGRAM_ID); assert!(parsed.mint_signer.is_none()); assert!(parsed.authority.is_signer()); - assert!(parsed.mint.is_none()); - assert!(parsed.token_pool_pda.is_none()); - assert!(parsed.token_program.is_none()); - assert!(parsed.sol_pool_pda.is_none()); assert!(parsed.cpi_context.is_none()); - assert!(parsed.in_output_queue.is_none()); + assert!(parsed.in_output_queue.is_some()); // Required for default config assert!(parsed.tokens_out_queue.is_none()); assert_eq!(parsed.ctoken_accounts.len(), 0); } #[test] fn test_successful_parsing_with_all_options() { - let mint_signer = pubkey_unique(); - let mint = pubkey_unique(); - let token_pool = pubkey_unique(); let cpi_context = pubkey_unique(); let accounts = vec![ @@ -173,21 +171,8 @@ fn test_successful_parsing_with_all_options() { true, vec![], ), - // Mint signer (optional) - create_test_account(mint_signer, [0u8; 32], true, false, false, vec![]), // Authority create_test_account(pubkey_unique(), [0u8; 32], true, false, false, vec![]), - // Decompressed mint accounts - create_test_account(mint, [0u8; 32], false, true, false, vec![]), - create_test_account(token_pool, [0u8; 32], false, true, false, vec![]), - create_test_account( - spl_token_2022::ID.to_bytes(), - [0u8; 32], - false, - false, - true, - vec![], - ), // Fee payer create_test_account(pubkey_unique(), [0u8; 32], true, true, false, vec![]), // Core system accounts @@ -217,8 +202,6 @@ fn test_successful_parsing_with_all_options() { vec![], ), create_test_account([0u8; 32], [0u8; 32], false, false, true, vec![]), - // SOL pool (optional) - create_test_account(SOL_POOL_PDA, [0u8; 32], false, true, false, vec![]), // CPI context (optional) create_test_account(cpi_context, [0u8; 32], false, true, false, vec![]), // Tree/Queue accounts @@ -260,28 +243,17 @@ fn test_successful_parsing_with_all_options() { ]; let result = MintActionCpiAccounts::::try_from_account_infos_full( - &accounts, true, // with_mint_signer - true, // spl_mint_initialized - true, // with_lamports - true, // with_cpi_context - false, // create_mint - true, // has_mint_to_actions + &accounts, + MintActionCpiAccountsConfig { + with_cpi_context: true, + create_mint: false, + mint_to_compressed: true, + }, ); assert!(result.is_ok()); let parsed = result.unwrap(); - assert!(parsed.mint_signer.is_some()); - assert_eq!(*parsed.mint_signer.unwrap().key(), mint_signer); - assert!(parsed.mint.is_some()); - assert_eq!(*parsed.mint.unwrap().key(), mint); - assert!(parsed.token_pool_pda.is_some()); - assert_eq!(*parsed.token_pool_pda.unwrap().key(), token_pool); - assert!(parsed.token_program.is_some()); - assert_eq!( - *parsed.token_program.unwrap().key(), - spl_token_2022::ID.to_bytes() - ); - assert!(parsed.sol_pool_pda.is_some()); + assert!(parsed.mint_signer.is_none()); // Not needed when updating mint assert!(parsed.cpi_context.is_some()); assert!(parsed.in_output_queue.is_some()); assert!(parsed.tokens_out_queue.is_some()); @@ -362,11 +334,9 @@ fn test_successful_create_mint() { ), // address tree (for create_mint) ]; - let result = MintActionCpiAccounts::::try_from_account_infos_create_mint( - &accounts, true, // with_mint_signer - false, // spl_mint_initialized - false, // with_lamports - false, // has_mint_to_actions + let result = MintActionCpiAccounts::::try_from_account_infos_full( + &accounts, + MintActionCpiAccountsConfig::create_mint(), ); assert!(result.is_ok()); @@ -453,10 +423,13 @@ fn test_successful_update_mint() { ), // in_output_queue (required for update) ]; - let result = MintActionCpiAccounts::::try_from_account_infos_update_mint( - &accounts, false, // spl_mint_initialized - false, // with_lamports - false, // has_mint_to_actions + let result = MintActionCpiAccounts::::try_from_account_infos_full( + &accounts, + MintActionCpiAccountsConfig { + with_cpi_context: false, + create_mint: false, + mint_to_compressed: false, + }, ); assert!(result.is_ok()); @@ -743,90 +716,6 @@ fn test_fee_payer_not_signer() { assert!(result.is_err()); } -#[test] -fn test_invalid_spl_token_program() { - let wrong_token_program = pubkey_unique(); - - let accounts = vec![ - create_test_account( - COMPRESSED_TOKEN_PROGRAM_ID, - [0u8; 32], - false, - false, - true, - vec![], - ), - create_test_account( - LIGHT_SYSTEM_PROGRAM_ID, - [0u8; 32], - false, - false, - true, - vec![], - ), - // Mint signer - create_test_account(pubkey_unique(), [0u8; 32], true, false, false, vec![]), - // Authority - create_test_account(pubkey_unique(), [0u8; 32], true, false, false, vec![]), - // Decompressed mint accounts (with wrong token program) - create_test_account(pubkey_unique(), [0u8; 32], false, true, false, vec![]), - create_test_account(pubkey_unique(), [0u8; 32], false, true, false, vec![]), - create_test_account(wrong_token_program, [0u8; 32], false, false, true, vec![]), // Wrong! - // Rest of accounts... - create_test_account(pubkey_unique(), [0u8; 32], true, true, false, vec![]), - create_test_account(CPI_AUTHORITY_PDA, [0u8; 32], false, false, false, vec![]), - create_test_account( - REGISTERED_PROGRAM_PDA, - [0u8; 32], - false, - false, - false, - vec![], - ), - create_test_account( - ACCOUNT_COMPRESSION_AUTHORITY_PDA, - [0u8; 32], - false, - false, - false, - vec![], - ), - create_test_account( - ACCOUNT_COMPRESSION_PROGRAM_ID, - [0u8; 32], - false, - false, - true, - vec![], - ), - create_test_account([0u8; 32], [0u8; 32], false, false, true, vec![]), - create_test_account( - pubkey_unique(), - ACCOUNT_COMPRESSION_PROGRAM_ID, - false, - true, - false, - vec![], - ), - create_test_account( - pubkey_unique(), - ACCOUNT_COMPRESSION_PROGRAM_ID, - false, - true, - false, - vec![], - ), - ]; - - let result = MintActionCpiAccounts::::try_from_account_infos_full( - &accounts, true, // with_mint_signer - true, // spl_mint_initialized - false, false, false, false, - ); - assert!(result.is_err()); - assert!(result.is_err()); -} - #[test] fn test_invalid_tree_ownership() { let wrong_owner = pubkey_unique(); @@ -962,8 +851,13 @@ fn test_invalid_queue_ownership() { create_test_account(pubkey_unique(), wrong_owner, false, true, false, vec![]), ]; - let result = MintActionCpiAccounts::::try_from_account_infos_update_mint( - &accounts, false, false, false, + let result = MintActionCpiAccounts::::try_from_account_infos_full( + &accounts, + MintActionCpiAccountsConfig { + with_cpi_context: false, + create_mint: false, + mint_to_compressed: false, + }, ); assert!(result.is_err()); assert!(result.is_err()); @@ -1033,33 +927,28 @@ fn test_helper_methods() { false, vec![], ), + create_test_account( + pubkey_unique(), + ACCOUNT_COMPRESSION_PROGRAM_ID, + false, + true, + false, + vec![], + ), // in_output_queue ]; - let parsed = MintActionCpiAccounts::::try_from_account_infos_create_mint( - &accounts, false, // with_mint_signer - false, // spl_mint_initialized - false, // with_lamports - false, // has_mint_to_actions - ) - .unwrap(); + let parsed = MintActionCpiAccounts::::try_from_account_infos(&accounts).unwrap(); // Test tree_queue_pubkeys() let tree_pubkeys = parsed.tree_queue_pubkeys(); - assert_eq!(tree_pubkeys.len(), 2); // out_output_queue and in_merkle_tree + assert_eq!(tree_pubkeys.len(), 3); // out_output_queue, in_merkle_tree, and in_output_queue // Test to_account_infos() let account_infos = parsed.to_account_infos(); assert!(!account_infos.is_empty()); assert_eq!(*account_infos[0].key(), LIGHT_SYSTEM_PROGRAM_ID); // First should be light_system_program - // Test to_account_metas() - let metas_with_program = parsed.to_account_metas(true); - assert_eq!( - metas_with_program[0].pubkey, - COMPRESSED_TOKEN_PROGRAM_ID.into() - ); - - let metas_without_program = parsed.to_account_metas(false); + let metas_without_program = parsed.to_account_metas(); assert_eq!( metas_without_program[0].pubkey, LIGHT_SYSTEM_PROGRAM_ID.into() diff --git a/sdk-libs/token-client/src/actions/create_spl_mint.rs b/sdk-libs/token-client/src/actions/create_spl_mint.rs deleted file mode 100644 index d732362786..0000000000 --- a/sdk-libs/token-client/src/actions/create_spl_mint.rs +++ /dev/null @@ -1,68 +0,0 @@ -use std::collections::HashSet; - -use light_client::{ - indexer::Indexer, - rpc::{Rpc, RpcError}, -}; -use solana_keypair::Keypair; -use solana_signature::Signature; -use solana_signer::Signer; - -use crate::instructions::create_spl_mint::create_spl_mint_instruction; - -/// Creates an SPL mint from a compressed mint and sends the transaction -/// -/// This function: -/// - Creates the create_spl_mint instruction using the instruction helper -/// - Handles signer deduplication (payer and mint_authority may be the same) -/// - Builds and sends the transaction -/// - Returns the transaction signature -/// -/// # Arguments -/// * `rpc` - RPC client with indexer access -/// * `compressed_mint_address` - Address of the compressed mint to convert to SPL mint -/// * `mint_seed` - Keypair used as seed for the SPL mint PDA -/// * `mint_authority` - Keypair that can mint tokens (must be able to sign) -/// * `payer` - Keypair for transaction fees (must be able to sign) -/// -/// # Returns -/// Returns the transaction signature on success -pub async fn create_spl_mint( - rpc: &mut R, - compressed_mint_address: [u8; 32], - mint_seed: &Keypair, - mint_authority: &Keypair, - payer: &Keypair, -) -> Result { - // Create the instruction - let instruction = create_spl_mint_instruction( - rpc, - compressed_mint_address, - mint_seed, - mint_authority.pubkey(), - payer.pubkey(), - ) - .await?; - - // Deduplicate signers (payer and mint_authority might be the same) - let mut unique_signers = HashSet::new(); - let mut signers = Vec::new(); - - // Always include payer - if unique_signers.insert(payer.pubkey()) { - signers.push(payer); - } - - // Include mint_authority if different from payer - if unique_signers.insert(mint_authority.pubkey()) { - signers.push(mint_authority); - } - println!("unique_signers {:?}", unique_signers); - - // Create and send the transaction - let signature = rpc - .create_and_send_transaction(&[instruction], &payer.pubkey(), &signers) - .await?; - - Ok(signature) -} diff --git a/sdk-libs/token-client/src/actions/mint_action.rs b/sdk-libs/token-client/src/actions/mint_action.rs index 6a87558638..f50f5615cb 100644 --- a/sdk-libs/token-client/src/actions/mint_action.rs +++ b/sdk-libs/token-client/src/actions/mint_action.rs @@ -2,17 +2,16 @@ use light_client::{ indexer::Indexer, rpc::{Rpc, RpcError}, }; -use light_compressed_token_sdk::instructions::{ - derive_compressed_mint_address, - mint_action::{MintActionType, MintToRecipient}, -}; +use light_compressed_token_sdk::instructions::derive_compressed_mint_address; use light_ctoken_types::instructions::mint_action::Recipient; use solana_keypair::Keypair; use solana_pubkey::Pubkey; use solana_signature::Signature; use solana_signer::Signer; -use crate::instructions::mint_action::{create_mint_action_instruction, MintActionParams}; +use crate::instructions::mint_action::{ + create_mint_action_instruction, MintActionParams, MintActionType, MintToRecipient, +}; /// Executes a mint action that can perform multiple operations in a single instruction /// @@ -142,6 +141,6 @@ pub async fn mint_action_comprehensive( actions, new_mint, }; - + println!("params {:?}", params); mint_action(rpc, params, authority, payer, mint_signer).await } diff --git a/sdk-libs/token-client/src/actions/mod.rs b/sdk-libs/token-client/src/actions/mod.rs index f6deb2f05d..d343e691e3 100644 --- a/sdk-libs/token-client/src/actions/mod.rs +++ b/sdk-libs/token-client/src/actions/mod.rs @@ -1,13 +1,11 @@ mod create_compressible_token_account; mod create_mint; -mod create_spl_mint; mod ctoken_transfer; mod mint_action; mod mint_to_compressed; pub mod transfer2; pub use create_compressible_token_account::*; pub use create_mint::*; -pub use create_spl_mint::*; pub use ctoken_transfer::*; pub use mint_action::*; pub use mint_to_compressed::*; diff --git a/sdk-libs/token-client/src/instructions/create_spl_mint.rs b/sdk-libs/token-client/src/instructions/create_spl_mint.rs deleted file mode 100644 index 5ac5a40ce0..0000000000 --- a/sdk-libs/token-client/src/instructions/create_spl_mint.rs +++ /dev/null @@ -1,118 +0,0 @@ -use borsh::BorshDeserialize; -use light_client::{ - indexer::Indexer, - rpc::{Rpc, RpcError}, -}; -use light_compressed_token_sdk::{ - instructions::{ - create_spl_mint_instruction as sdk_create_spl_mint_instruction, find_spl_mint_address, - CreateSplMintInputs, - }, - token_pool::derive_token_pool, -}; -use light_ctoken_types::{ - instructions::mint_action::CompressedMintWithContext, state::CompressedMint, -}; -use solana_instruction::Instruction; -use solana_keypair::Keypair; -use solana_pubkey::Pubkey; -use solana_signer::Signer; - -/// Creates a create_spl_mint instruction with automatic RPC integration -/// -/// This function automatically: -/// - Fetches the compressed mint account data -/// - Gets validity proof for the compressed mint -/// - Derives the necessary PDAs and tree information -/// - Constructs the complete instruction -/// -/// # Arguments -/// * `rpc` - RPC client with indexer access -/// * `compressed_mint_address` - Address of the compressed mint to convert to SPL mint -/// * `mint_seed` - Keypair used as seed for the SPL mint PDA -/// * `mint_authority` - Authority that can mint tokens -/// * `payer` - Transaction fee payer -/// -/// # Returns -/// Returns a configured `Instruction` ready for transaction execution -pub async fn create_spl_mint_instruction( - rpc: &mut R, - compressed_mint_address: [u8; 32], - mint_seed: &Keypair, - mint_authority: Pubkey, - payer: Pubkey, -) -> Result { - // Get the compressed mint account - let compressed_mint_account = rpc - .get_compressed_account(compressed_mint_address, None) - .await? - .value - .ok_or(RpcError::AccountDoesNotExist(format!( - "{:?}", - compressed_mint_address - )))?; - - // Deserialize the compressed mint data - let compressed_mint: CompressedMint = BorshDeserialize::deserialize( - &mut compressed_mint_account - .data - .as_ref() - .ok_or_else(|| { - RpcError::CustomError("Compressed mint account has no data".to_string()) - })? - .data - .as_slice(), - ) - .map_err(|e| RpcError::CustomError(format!("Failed to deserialize compressed mint: {}", e)))?; - - // Get validity proof for the compressed mint - let proof_result = rpc - .get_validity_proof(vec![compressed_mint_account.hash], vec![], None) - .await? - .value; - - // Derive SPL mint PDA and bump - let (spl_mint_pda, mint_bump) = find_spl_mint_address(&mint_seed.pubkey()); - - // Derive token pool for the SPL mint - let token_pool = derive_token_pool(&spl_mint_pda, 0); - - // Get tree and queue information - let input_tree = compressed_mint_account.tree_info.tree; - let input_queue = compressed_mint_account.tree_info.queue; - - // Get a separate output queue for the new compressed mint state - let output_tree_info = rpc.get_random_state_tree_info()?; - let output_queue = output_tree_info.queue; - - // Prepare compressed mint inputs - let compressed_mint_inputs = CompressedMintWithContext { - leaf_index: compressed_mint_account.leaf_index, - prove_by_index: true, - root_index: proof_result.accounts[0] - .root_index - .root_index() - .unwrap_or_default(), - address: compressed_mint_address, - mint: compressed_mint.try_into().map_err(|e| { - RpcError::CustomError(format!("Failed to create SPL mint instruction: {}", e)) - })?, - }; - - // Create the instruction using the SDK function - let instruction = sdk_create_spl_mint_instruction(CreateSplMintInputs { - mint_signer: mint_seed.pubkey(), - mint_bump, - compressed_mint_inputs, - proof: proof_result.proof, - payer, - input_merkle_tree: input_tree, - input_output_queue: input_queue, - output_queue, - mint_authority, - token_pool, - }) - .map_err(|e| RpcError::CustomError(format!("Failed to create SPL mint instruction: {}", e)))?; - println!("instruction {:?}", instruction); - Ok(instruction) -} diff --git a/sdk-libs/token-client/src/instructions/mint_action.rs b/sdk-libs/token-client/src/instructions/mint_action.rs index 6fa6dad77a..bd57618abc 100644 --- a/sdk-libs/token-client/src/instructions/mint_action.rs +++ b/sdk-libs/token-client/src/instructions/mint_action.rs @@ -3,25 +3,69 @@ use light_client::{ indexer::Indexer, rpc::{Rpc, RpcError}, }; -use light_compressed_token_sdk::{ - instructions::{ - create_mint_action, derive_compressed_mint_address, find_spl_mint_address, - mint_action::{MintActionInputs, MintActionType, MintToRecipient}, - }, - token_pool::derive_token_pool, +use light_compressed_account::instruction_data::traits::LightInstructionData; +use light_compressed_token_sdk::instructions::{ + derive_compressed_mint_address, find_spl_mint_address, + get_mint_action_instruction_account_metas, mint_action::MintActionMetaConfig, }; use light_ctoken_types::{ instructions::{ extensions::{token_metadata::TokenMetadataInstructionData, ExtensionInstructionData}, - mint_action::CompressedMintWithContext, + mint_action::{ + CompressedMintWithContext, MintActionCompressedInstructionData, MintToCTokenAction, + MintToCompressedAction, Recipient, RemoveMetadataKeyAction, UpdateAuthority, + UpdateMetadataAuthorityAction, UpdateMetadataFieldAction, + }, }, state::CompressedMint, + COMPRESSED_TOKEN_PROGRAM_ID, }; use solana_instruction::Instruction; use solana_keypair::Keypair; use solana_pubkey::Pubkey; use solana_signer::Signer; +// Backwards compatibility types for token-client +#[derive(Debug, Clone, PartialEq)] +pub struct MintToRecipient { + pub recipient: Pubkey, + pub amount: u64, +} + +/// High-level action types for the mint action instruction (backwards compatibility) +#[derive(Debug, Clone, PartialEq)] +pub enum MintActionType { + MintTo { + recipients: Vec, + token_account_version: u8, + }, + UpdateMintAuthority { + new_authority: Option, + }, + UpdateFreezeAuthority { + new_authority: Option, + }, + MintToCToken { + account: Pubkey, + amount: u64, + }, + UpdateMetadataField { + extension_index: u8, + field_type: u8, + key: Vec, + value: Vec, + }, + UpdateMetadataAuthority { + extension_index: u8, + new_authority: Pubkey, + }, + RemoveMetadataKey { + extension_index: u8, + key: Vec, + idempotent: u8, + }, +} + /// Parameters for creating a new mint #[derive(Debug)] pub struct NewMint { @@ -41,7 +85,7 @@ pub struct MintActionParams { pub authority: Pubkey, pub payer: Pubkey, pub actions: Vec, - /// Required if any action is CreateSplMint + /// Required if any action is creating a mint pub new_mint: Option, } @@ -147,62 +191,133 @@ pub async fn create_mint_action_instruction( rpc_proof_result.accounts[0].tree_info, ) }; - println!("compressed_mint_inputs {:?}", compressed_mint_inputs); - // Get mint bump from find_spl_mint_address if we're creating a compressed mint - let mint_bump = if is_creating_mint { - Some(find_spl_mint_address(¶ms.mint_seed).1) + + // Build instruction data using builder pattern + let mut instruction_data = if is_creating_mint { + MintActionCompressedInstructionData::new_mint( + params.compressed_mint_address, + compressed_mint_inputs.root_index, + proof.ok_or_else(|| { + RpcError::CustomError("Proof is required for mint creation".to_string()) + })?, + compressed_mint_inputs.mint.clone(), + ) } else { - None + MintActionCompressedInstructionData::new(compressed_mint_inputs.clone(), proof) }; - // Check if we need token_pool (for SPL operations) - let needs_token_pool = params.actions.iter().any(|action| { - matches!( - action, - MintActionType::CreateSplMint { .. } | MintActionType::MintToCToken { .. } - ) - }) || compressed_mint_inputs.mint.metadata.spl_mint_initialized; + // Convert and add actions using builder pattern + // Collect decompressed token accounts for MintToCToken actions + let mut ctoken_accounts = Vec::new(); + let mut ctoken_account_index = 0u8; + + for action in params.actions { + instruction_data = match action { + MintActionType::MintTo { + recipients, + token_account_version, + } => { + // Convert MintToRecipient (solana_sdk::Pubkey) to Recipient ([u8; 32]) + let ctoken_recipients: Vec = recipients + .into_iter() + .map(|r| Recipient::new(r.recipient, r.amount)) + .collect(); + instruction_data.with_mint_to_compressed(MintToCompressedAction { + token_account_version, + recipients: ctoken_recipients, + }) + } + MintActionType::MintToCToken { account, amount } => { + // Add account to the list and use its index + ctoken_accounts.push(account); + let current_index = ctoken_account_index; + ctoken_account_index += 1; + + instruction_data.with_mint_to_ctoken(MintToCTokenAction { + account_index: current_index, + amount, + }) + } + MintActionType::UpdateMintAuthority { new_authority } => instruction_data + .with_update_mint_authority(UpdateAuthority { + new_authority: new_authority.map(|a| a.to_bytes().into()), + }), + MintActionType::UpdateFreezeAuthority { new_authority } => instruction_data + .with_update_freeze_authority(UpdateAuthority { + new_authority: new_authority.map(|a| a.to_bytes().into()), + }), + MintActionType::UpdateMetadataField { + extension_index, + field_type, + key, + value, + } => instruction_data.with_update_metadata_field(UpdateMetadataFieldAction { + extension_index, + field_type, + key, + value, + }), + MintActionType::UpdateMetadataAuthority { + extension_index, + new_authority, + } => instruction_data.with_update_metadata_authority(UpdateMetadataAuthorityAction { + extension_index, + new_authority: new_authority.to_bytes().into(), + }), + MintActionType::RemoveMetadataKey { + extension_index, + key, + idempotent, + } => instruction_data.with_remove_metadata_key(RemoveMetadataKeyAction { + extension_index, + key, + idempotent, + }), + }; + } - let token_pool = if needs_token_pool { - let mint = find_spl_mint_address(¶ms.mint_seed).0; - Some(derive_token_pool(&mint, 0)) + // Build account metas configuration + let mut config = if is_creating_mint { + MintActionMetaConfig::new_create_mint( + &instruction_data, + params.authority, + params.mint_seed, + params.payer, // fee_payer + address_tree_pubkey, + state_tree_info.queue, + ) + .map_err(|e| RpcError::CustomError(format!("Failed to create meta config: {:?}", e)))? } else { - None + MintActionMetaConfig::new( + &instruction_data, + params.authority, + params.payer, // fee_payer + state_tree_info.tree, + state_tree_info.queue, + state_tree_info.queue, + ) + .map_err(|e| RpcError::CustomError(format!("Failed to create meta config: {:?}", e)))? }; - // Create the mint action instruction inputs - let instruction_inputs = MintActionInputs { - compressed_mint_inputs, - mint_seed: params.mint_seed, - create_mint: is_creating_mint, - mint_bump, - authority: params.authority, - payer: params.payer, - proof, - actions: params.actions, - // address_tree when create_mint, input state tree when not - address_tree_pubkey: if is_creating_mint { - address_tree_pubkey - } else { - state_tree_info.tree - }, - // input_queue only needed when operating on existing mint - input_queue: if is_creating_mint { - None - } else { - Some(state_tree_info.queue) - }, - output_queue: state_tree_info.queue, - tokens_out_queue: Some(state_tree_info.queue), // Output queue for tokens - token_pool, - }; + // Add ctoken accounts if any MintToCToken actions were present + if !ctoken_accounts.is_empty() { + config = config.with_ctoken_accounts(ctoken_accounts); + } + + // Get account metas + let account_metas = get_mint_action_instruction_account_metas(config, &compressed_mint_inputs); - // Create the instruction using the SDK - let instruction = create_mint_action(instruction_inputs).map_err(|e| { - RpcError::CustomError(format!("Failed to create mint action instruction: {:?}", e)) - })?; + // Serialize instruction data + let data = instruction_data + .data() + .map_err(|e| RpcError::CustomError(format!("Failed to serialize instruction: {:?}", e)))?; - Ok(instruction) + // Build final instruction + Ok(Instruction { + program_id: COMPRESSED_TOKEN_PROGRAM_ID.into(), + accounts: account_metas, + data, + }) } /// Helper function to create a comprehensive mint action instruction @@ -223,13 +338,14 @@ pub async fn create_comprehensive_mint_action_instruction( let address_tree_pubkey = rpc.get_address_tree_v2().tree; let compressed_mint_address = derive_compressed_mint_address(&mint_seed.pubkey(), &address_tree_pubkey); - let (_, mint_bump) = find_spl_mint_address(&mint_seed.pubkey()); // Build actions let mut actions = Vec::new(); if create_spl_mint { - actions.push(MintActionType::CreateSplMint { mint_bump }); + return Err(RpcError::CustomError( + "CreateSplMint is no longer supported".to_string(), + )); } if !mint_to_recipients.is_empty() { diff --git a/sdk-libs/token-client/src/instructions/mod.rs b/sdk-libs/token-client/src/instructions/mod.rs index a3b1af946f..0fce0b5387 100644 --- a/sdk-libs/token-client/src/instructions/mod.rs +++ b/sdk-libs/token-client/src/instructions/mod.rs @@ -1,5 +1,4 @@ pub mod create_mint; -pub mod create_spl_mint; pub mod mint_action; pub mod mint_to_compressed; pub mod transfer2; diff --git a/sdk-tests/sdk-compressible-test/src/instructions/create_user_record_and_game_session.rs b/sdk-tests/sdk-compressible-test/src/instructions/create_user_record_and_game_session.rs index 2d82dc0884..258b68b7c3 100644 --- a/sdk-tests/sdk-compressible-test/src/instructions/create_user_record_and_game_session.rs +++ b/sdk-tests/sdk-compressible-test/src/instructions/create_user_record_and_game_session.rs @@ -1,10 +1,13 @@ use anchor_lang::{ prelude::*, - solana_program::{program::invoke, sysvar::clock::Clock}, + solana_program::{instruction::Instruction, program::invoke, sysvar::clock::Clock}, }; +use light_compressed_account::instruction_data::traits::LightInstructionData; use light_compressed_token_sdk::instructions::{ - create_mint_action_cpi, find_spl_mint_address, MintActionInputs, + find_spl_mint_address, + mint_action::{get_mint_action_instruction_account_metas, MintActionMetaConfig}, }; +use light_ctoken_types::instructions::mint_action::{MintToCompressedAction, Recipient}; use light_sdk::{ compressible::{ compress_account_on_init::prepare_compressed_account_on_init, CompressibleConfig, @@ -114,60 +117,46 @@ pub fn create_user_record_and_game_session<'info>( let mint = find_spl_mint_address(&ctx.accounts.mint_signer.key()).0; let (_, token_account_address) = get_ctoken_signer_seeds(&ctx.accounts.user.key(), &mint); - let actions = vec![ - light_compressed_token_sdk::instructions::mint_action::MintActionType::MintTo { - recipients: vec![ - light_compressed_token_sdk::instructions::mint_action::MintToRecipient { - recipient: token_account_address, // TRY: THE DECOMPRESS TOKEN ACCOUNT ADDRES IS THE OWNER OF ITS COMPRESSIBLED VERSION. - amount: 1000, // Mint the full supply to the user - }, - light_compressed_token_sdk::instructions::mint_action::MintToRecipient { - recipient: get_ctoken_signer2_seeds(&ctx.accounts.user.key()).1, - amount: 1000, - }, - light_compressed_token_sdk::instructions::mint_action::MintToRecipient { - recipient: get_ctoken_signer3_seeds(&ctx.accounts.user.key()).1, - amount: 1000, - }, - light_compressed_token_sdk::instructions::mint_action::MintToRecipient { - recipient: get_ctoken_signer4_seeds( - &ctx.accounts.user.key(), - &ctx.accounts.user.key(), - ) - .1, // user as fee_payer - amount: 1000, - }, - light_compressed_token_sdk::instructions::mint_action::MintToRecipient { - recipient: get_ctoken_signer5_seeds(&ctx.accounts.user.key(), &mint, 42).1, // Fixed index 42 - amount: 1000, - }, - ], - token_account_version: 3, - }, - ]; - let output_queue = *cpi_accounts.tree_accounts().unwrap()[0].key; // Same tree as PDA let address_tree_pubkey = *cpi_accounts.tree_accounts().unwrap()[1].key; // Same tree as PDA - let mint_action_inputs = MintActionInputs { - compressed_mint_inputs: compression_params.mint_with_context.clone(), - mint_seed: ctx.accounts.mint_signer.key(), - mint_bump: Some(compression_params.mint_bump), - create_mint: true, - authority: ctx.accounts.mint_authority.key(), - payer: ctx.accounts.user.key(), - proof: compression_params.proof.into(), - actions, - input_queue: None, // Not needed for create_mint: true - output_queue, - tokens_out_queue: Some(output_queue), // For MintTo actions - address_tree_pubkey, - token_pool: None, // Not needed for simple compressed mint creation - }; - - let mint_action_instruction = create_mint_action_cpi( - mint_action_inputs, - Some(light_ctoken_types::instructions::mint_action::CpiContext { + let proof = compression_params.proof.0.unwrap_or_default(); + let mut instruction_data = + light_ctoken_types::instructions::mint_action::MintActionCompressedInstructionData::new_mint( + compression_params.mint_with_context.address, + 0, // root_index + proof, + compression_params.mint_with_context.mint.clone(), + ) + .with_mint_to_compressed(MintToCompressedAction::new(vec![ + Recipient::new( + token_account_address, // TRY: THE DECOMPRESS TOKEN ACCOUNT ADDRESS IS THE OWNER OF ITS COMPRESSIBLED VERSION. + 1000, // Mint the full supply to the user + ), + Recipient::new( + get_ctoken_signer2_seeds(&ctx.accounts.user.key()).1, + 1000, + ), + Recipient::new( + get_ctoken_signer3_seeds(&ctx.accounts.user.key()).1, + 1000, + ), + Recipient::new( + get_ctoken_signer4_seeds( + &ctx.accounts.user.key(), + &ctx.accounts.user.key(), + ) + .1, // user as fee_payer + 1000, + ), + Recipient::new( + get_ctoken_signer5_seeds(&ctx.accounts.user.key(), &mint, 42).1, // Fixed index 42 + 1000, + ), + ])); + + instruction_data = instruction_data.with_cpi_context( + light_ctoken_types::instructions::mint_action::CpiContext { address_tree_pubkey: address_tree_pubkey.to_bytes(), set_context: false, first_set_context: false, @@ -177,11 +166,37 @@ pub fn create_user_record_and_game_session<'info>( token_out_queue_index: 0, assigned_account_index: 2, read_only_address_trees: [0; 4], - }), - Some(cpi_context_pubkey), + }, + ); + + // Build account meta config + let mut config = MintActionMetaConfig::new_create_mint( + &instruction_data, + ctx.accounts.mint_authority.key(), + ctx.accounts.mint_signer.key(), + ctx.accounts.user.key(), // fee_payer + address_tree_pubkey, + output_queue, ) .unwrap(); + // Set CPI context + config.with_cpi_context = Some(cpi_context_pubkey); + + // Get account metas + let account_metas = + get_mint_action_instruction_account_metas(config, &compression_params.mint_with_context); + + // Serialize instruction data + let data = instruction_data.data().unwrap(); + + // Build instruction + let mint_action_instruction = Instruction { + program_id: Pubkey::new_from_array(light_ctoken_types::COMPRESSED_TOKEN_PROGRAM_ID), + accounts: account_metas, + data, + }; + // Get all account infos needed for the mint action let mut account_infos = cpi_accounts.to_account_infos(); account_infos.push( diff --git a/sdk-tests/sdk-token-test/src/ctoken_pda/mint.rs b/sdk-tests/sdk-token-test/src/ctoken_pda/mint.rs index 3e2693d0dc..7d7e26089c 100644 --- a/sdk-tests/sdk-token-test/src/ctoken_pda/mint.rs +++ b/sdk-tests/sdk-token-test/src/ctoken_pda/mint.rs @@ -1,7 +1,9 @@ use anchor_lang::{prelude::*, solana_program::program::invoke}; -use light_compressed_token_sdk::instructions::{ - mint_action::{CreateMintCpiWriteInputs, MintActionCpiWriteAccounts, MintActionType}, - mint_action_cpi_write, MintActionInputsCpiWrite, +use light_compressed_token_sdk::{ + ctoken_instruction::CTokenInstruction, instructions::mint_action::MintActionCpiWriteAccounts, +}; +use light_ctoken_types::instructions::mint_action::{ + MintActionCompressedInstructionData, MintToCompressedAction, UpdateAuthority, }; use light_sdk::cpi::v2::CpiAccounts; @@ -13,25 +15,29 @@ pub fn process_mint_action<'a, 'info>( input: &ChainedCtokenInstructionData, cpi_accounts: &CpiAccounts<'a, 'info>, ) -> Result<()> { - let actions = vec![ - MintActionType::MintTo { - recipients: input.token_recipients.clone(), - token_account_version: 2, - }, - MintActionType::UpdateMintAuthority { - new_authority: input.final_mint_authority, - }, - ]; + // Build instruction data using builder pattern + let mut instruction_data = MintActionCompressedInstructionData::new_mint( + input.compressed_mint_with_context.address, + input.compressed_mint_with_context.root_index, + light_compressed_account::instruction_data::compressed_proof::CompressedProof::default(), // Dummy proof for CPI write + input.compressed_mint_with_context.mint.clone(), + ); + + // Add MintToCompressed action + instruction_data = instruction_data.with_mint_to_compressed(MintToCompressedAction { + token_account_version: 2, + recipients: input.token_recipients.clone(), + }); + + // Add UpdateMintAuthority action + instruction_data = instruction_data.with_update_mint_authority(UpdateAuthority { + new_authority: input + .final_mint_authority + .map(|auth| auth.to_bytes().into()), + }); - let mint_action_inputs = MintActionInputsCpiWrite { - compressed_mint_inputs: input.compressed_mint_with_context.clone(), - mint_seed: Some(ctx.accounts.mint_seed.key()), - mint_bump: None, - create_mint: true, - authority: ctx.accounts.mint_authority.key(), - payer: ctx.accounts.payer.key(), - actions, - cpi_context: light_ctoken_types::instructions::mint_action::CpiContext { + instruction_data = instruction_data.with_cpi_context( + light_ctoken_types::instructions::mint_action::CpiContext { set_context: false, first_set_context: true, in_tree_index: 0, @@ -41,33 +47,9 @@ pub fn process_mint_action<'a, 'info>( assigned_account_index: 0, ..Default::default() }, - cpi_context_pubkey: *cpi_accounts.cpi_context().unwrap().key, - }; - - // Build using the new builder pattern - let mint_action_inputs2 = MintActionInputsCpiWrite::new_create_mint(CreateMintCpiWriteInputs { - compressed_mint_inputs: input.compressed_mint_with_context.clone(), - mint_seed: ctx.accounts.mint_seed.key(), - authority: ctx.accounts.mint_authority.key(), - payer: ctx.accounts.payer.key(), - cpi_context_pubkey: *cpi_accounts.cpi_context().unwrap().key, - first_set_context: true, - address_tree_index: 0, - output_queue_index: 1, - assigned_account_index: 0, - }) - .add_mint_to( - input.token_recipients.clone(), - 2, // token_account_version - 1, // token_out_queue_index - ) - .unwrap() // add_mint_to returns Result in CPI write mode - .add_update_mint_authority(input.final_mint_authority); + ); - // Assert that the builder produces the same result as manual construction - assert_eq!(mint_action_inputs, mint_action_inputs2); - - let mint_action_instruction = mint_action_cpi_write(mint_action_inputs).unwrap(); + // Build account structure for CPI write let mint_action_account_infos = MintActionCpiWriteAccounts { light_system_program: cpi_accounts.system_program().unwrap(), mint_signer: Some(ctx.accounts.mint_seed.as_ref()), @@ -79,6 +61,11 @@ pub fn process_mint_action<'a, 'info>( recipient_token_accounts: vec![], }; + // Build instruction using trait method for CPI write (first set context) + let mint_action_instruction = instruction_data + .instruction_write_to_cpi_context_first(&mint_action_account_infos) + .unwrap(); + invoke( &mint_action_instruction, &mint_action_account_infos.to_account_infos(), diff --git a/sdk-tests/sdk-token-test/src/mint_compressed_tokens_cpi_write.rs b/sdk-tests/sdk-token-test/src/mint_compressed_tokens_cpi_write.rs index 8587f4f36f..2bd739ab0f 100644 --- a/sdk-tests/sdk-token-test/src/mint_compressed_tokens_cpi_write.rs +++ b/sdk-tests/sdk-token-test/src/mint_compressed_tokens_cpi_write.rs @@ -1,18 +1,19 @@ use anchor_lang::{prelude::*, solana_program::program::invoke}; -use light_compressed_token_sdk::instructions::{ - mint_action::{MintActionCpiWriteAccounts, MintActionType}, - mint_action_cpi_write, - transfer2::Transfer2CpiAccounts, - MintActionInputsCpiWrite, MintToRecipient, +use light_compressed_token_sdk::{ + ctoken_instruction::CTokenInstruction, + instructions::{mint_action::MintActionCpiWriteAccounts, transfer2::Transfer2CpiAccounts}, +}; +use light_ctoken_types::instructions::mint_action::{ + CompressedMintWithContext, MintActionCompressedInstructionData, MintToCompressedAction, + Recipient, }; -use light_ctoken_types::instructions::mint_action::CompressedMintWithContext; use crate::Generic; #[derive(AnchorSerialize, AnchorDeserialize, Clone)] pub struct MintCompressedTokensCpiWriteParams { pub compressed_mint_with_context: CompressedMintWithContext, - pub recipients: Vec, + pub recipients: Vec, pub cpi_context: light_ctoken_types::instructions::mint_action::CpiContext, pub cpi_context_pubkey: Pubkey, } @@ -24,25 +25,18 @@ pub fn process_mint_compressed_tokens_cpi_write<'info>( params: MintCompressedTokensCpiWriteParams, cpi_accounts: &Transfer2CpiAccounts<'_, AccountInfo<'info>>, ) -> Result<()> { - let actions = vec![MintActionType::MintTo { - recipients: params.recipients, + // Build instruction data using builder pattern + let instruction_data = MintActionCompressedInstructionData::new( + params.compressed_mint_with_context, + None, // No proof for CPI write + ) + .with_mint_to_compressed(MintToCompressedAction { token_account_version: 2, - }]; - - let mint_action_inputs = MintActionInputsCpiWrite { - compressed_mint_inputs: params.compressed_mint_with_context, - mint_seed: None, // Not needed for existing mint - mint_bump: None, // Not needed for existing mint - create_mint: false, // Using existing mint - authority: ctx.accounts.signer.key(), - payer: ctx.accounts.signer.key(), - actions, - cpi_context: params.cpi_context, - cpi_context_pubkey: *cpi_accounts.cpi_context.unwrap().key, - }; - - let mint_action_instruction = mint_action_cpi_write(mint_action_inputs).unwrap(); + recipients: params.recipients, + }) + .with_cpi_context(params.cpi_context); + // Build account structure for CPI write let mint_action_account_infos = MintActionCpiWriteAccounts { authority: ctx.accounts.signer.as_ref(), light_system_program: cpi_accounts.light_system_program, @@ -54,6 +48,19 @@ pub fn process_mint_compressed_tokens_cpi_write<'info>( recipient_token_accounts: vec![], }; + // Determine which CPI write method to use based on cpi_context flags + let mint_action_instruction = if instruction_data + .cpi_context + .as_ref() + .map(|c| c.first_set_context) + .unwrap_or(false) + { + instruction_data.instruction_write_to_cpi_context_first(&mint_action_account_infos) + } else { + instruction_data.instruction_write_to_cpi_context_set(&mint_action_account_infos) + } + .unwrap(); + invoke( &mint_action_instruction, &mint_action_account_infos.to_account_infos(), diff --git a/sdk-tests/sdk-token-test/src/pda_ctoken/mint.rs b/sdk-tests/sdk-token-test/src/pda_ctoken/mint.rs index 6532add838..fe20a8eb2c 100644 --- a/sdk-tests/sdk-token-test/src/pda_ctoken/mint.rs +++ b/sdk-tests/sdk-token-test/src/pda_ctoken/mint.rs @@ -1,6 +1,10 @@ use anchor_lang::{prelude::*, solana_program::program::invoke}; -use light_compressed_token_sdk::instructions::{ - create_mint_action_cpi, CreateMintInputs, MintActionInputs, +use light_compressed_token_sdk::{ + ctoken_instruction::CTokenInstruction, instructions::mint_action::MintActionCpiAccounts, +}; +use light_ctoken_types::instructions::mint_action::{ + MintActionCompressedInstructionData, MintToCTokenAction, MintToCompressedAction, + UpdateAuthority, }; use light_sdk_types::cpi_accounts::v2::CpiAccounts; @@ -11,46 +15,63 @@ pub fn process_mint_action<'a, 'info>( input: &ChainedCtokenInstructionData, cpi_accounts: &CpiAccounts<'a, AccountInfo<'info>>, ) -> Result<()> { - // Derive the output queue pubkey - use the same tree as the PDA creation - let address_tree_pubkey = *cpi_accounts.tree_accounts().unwrap()[0].key; // Same tree as PDA - let output_queue = *cpi_accounts.tree_accounts().unwrap()[1].key; // Same tree as PDA - - // Build using the new builder pattern - let mint_action_inputs = MintActionInputs::new_create_mint(CreateMintInputs { - compressed_mint_inputs: input.compressed_mint_with_context.clone(), - mint_seed: ctx.accounts.mint_seed.key(), - authority: ctx.accounts.mint_authority.key(), - payer: ctx.accounts.payer.key(), - proof: input.pda_creation.proof.into(), - address_tree: address_tree_pubkey, - output_queue, - }) - .add_mint_to( - input.token_recipients.clone(), - 2, // token_account_version - Some(output_queue), + // Build instruction data using builder pattern + // ValidityProof is a wrapper around Option + let compressed_proof = input.pda_creation.proof.0.unwrap(); + let instruction_data = MintActionCompressedInstructionData::new_mint( + input.compressed_mint_with_context.address, + input.compressed_mint_with_context.root_index, + compressed_proof, + input.compressed_mint_with_context.mint.clone(), ) - .add_mint_to_decompressed( - ctx.accounts.token_account.key(), - input.token_recipients[0].amount, - ) - .add_update_mint_authority(input.final_mint_authority); + .with_mint_to_compressed(MintToCompressedAction { + token_account_version: 2, + recipients: input.token_recipients.clone(), + }) + .with_mint_to_ctoken(MintToCTokenAction { + account_index: 0, // Index in remaining accounts + amount: input.token_recipients[0].amount, + }) + .with_update_mint_authority(UpdateAuthority { + new_authority: input + .final_mint_authority + .map(|auth| auth.to_bytes().into()), + }) + .with_cpi_context(light_ctoken_types::instructions::mint_action::CpiContext { + set_context: false, + first_set_context: false, + in_tree_index: 1, + in_queue_index: 0, + out_queue_index: 0, + token_out_queue_index: 0, + assigned_account_index: 1, + ..Default::default() + }); - let mint_action_instruction = create_mint_action_cpi( - mint_action_inputs, - Some(light_ctoken_types::instructions::mint_action::CpiContext { - set_context: false, - first_set_context: false, - in_tree_index: 1, - in_queue_index: 0, - out_queue_index: 0, - token_out_queue_index: 0, - assigned_account_index: 1, - ..Default::default() - }), - Some(*cpi_accounts.cpi_context().unwrap().key), - ) - .unwrap(); + // Build account structure for CPI - manually construct from CpiAccounts + let tree_accounts = cpi_accounts.tree_accounts().unwrap(); + let ctoken_accounts_vec = vec![ctx.accounts.token_account.to_account_info()]; + let mint_action_accounts = MintActionCpiAccounts { + compressed_token_program: ctx.accounts.ctoken_program.as_ref(), + light_system_program: cpi_accounts.system_program().unwrap(), + mint_signer: Some(ctx.accounts.mint_seed.as_ref()), + authority: ctx.accounts.mint_authority.as_ref(), + fee_payer: ctx.accounts.payer.as_ref(), + compressed_token_cpi_authority: ctx.accounts.ctoken_cpi_authority.as_ref(), + registered_program_pda: cpi_accounts.registered_program_pda().unwrap(), + account_compression_authority: cpi_accounts.account_compression_authority().unwrap(), + account_compression_program: cpi_accounts.account_compression_program().unwrap(), + system_program: cpi_accounts.system_program().unwrap(), + cpi_context: cpi_accounts.cpi_context().ok(), + out_output_queue: &tree_accounts[1], // output queue + in_merkle_tree: &tree_accounts[0], // address tree + in_output_queue: None, // Not needed for create + tokens_out_queue: Some(&tree_accounts[0]), // Same as output queue for mint_to + ctoken_accounts: &ctoken_accounts_vec, // For MintToCToken + }; + + // Build instruction using trait method + let mint_action_instruction = instruction_data.instruction(&mint_action_accounts).unwrap(); // Get all account infos needed for the mint action let mut account_infos = cpi_accounts.to_account_infos(); diff --git a/sdk-tests/sdk-token-test/src/pda_ctoken/processor.rs b/sdk-tests/sdk-token-test/src/pda_ctoken/processor.rs index d4488f613e..551e177124 100644 --- a/sdk-tests/sdk-token-test/src/pda_ctoken/processor.rs +++ b/sdk-tests/sdk-token-test/src/pda_ctoken/processor.rs @@ -1,6 +1,6 @@ use anchor_lang::prelude::*; -use light_compressed_token_sdk::{instructions::mint_action::MintToRecipient, ValidityProof}; -use light_ctoken_types::instructions::mint_action::CompressedMintWithContext; +use light_compressed_token_sdk::ValidityProof; +use light_ctoken_types::instructions::mint_action::{CompressedMintWithContext, Recipient}; use super::{ create_pda::process_create_escrow_pda_with_cpi_context, mint::process_mint_action, PdaCToken, @@ -8,7 +8,7 @@ use super::{ #[derive(Debug, Clone, AnchorDeserialize, AnchorSerialize)] pub struct ChainedCtokenInstructionData { pub compressed_mint_with_context: CompressedMintWithContext, - pub token_recipients: Vec, + pub token_recipients: Vec, pub final_mint_authority: Option, pub pda_creation: PdaCreationData, pub output_tree_index: u8, diff --git a/sdk-tests/sdk-token-test/tests/compress_and_close_cpi.rs b/sdk-tests/sdk-token-test/tests/compress_and_close_cpi.rs index 4561e4d6af..e6f6ca48b5 100644 --- a/sdk-tests/sdk-token-test/tests/compress_and_close_cpi.rs +++ b/sdk-tests/sdk-token-test/tests/compress_and_close_cpi.rs @@ -117,10 +117,7 @@ async fn setup_compress_and_close_test( let decompressed_recipients = owners .iter() - .map(|owner| Recipient { - recipient: owner.pubkey().into(), - amount: mint_amount, - }) + .map(|owner| Recipient::new(owner.pubkey(), mint_amount)) .collect::>(); println!("decompressed_recipients {:?}", decompressed_recipients); // Create the mint and mint to the existing ATAs @@ -464,7 +461,6 @@ async fn test_compress_and_close_cpi_with_context() { // Import required types for minting use anchor_lang::AnchorDeserialize; - use light_compressed_token_sdk::instructions::MintToRecipient; use light_ctoken_types::instructions::mint_action::CompressedMintWithContext; use sdk_token_test::mint_compressed_tokens_cpi_write::MintCompressedTokensCpiWriteParams; @@ -507,10 +503,7 @@ async fn test_compress_and_close_cpi_with_context() { .unwrap(); // Create mint params to populate CPI context - let mint_recipients = vec![MintToRecipient { - recipient: ctx.owners[0].pubkey(), - amount: 500, // Mint some additional tokens - }]; + let mint_recipients = vec![Recipient::new(ctx.owners[0].pubkey(), 500)]; // Deserialize the mint data use light_ctoken_types::state::CompressedMint; diff --git a/sdk-tests/sdk-token-test/tests/ctoken_pda.rs b/sdk-tests/sdk-token-test/tests/ctoken_pda.rs index 2d0fc5ed0e..f7bf1c1c08 100644 --- a/sdk-tests/sdk-token-test/tests/ctoken_pda.rs +++ b/sdk-tests/sdk-token-test/tests/ctoken_pda.rs @@ -2,16 +2,13 @@ use anchor_lang::{AnchorDeserialize, InstructionData, ToAccountMetas}; use light_client::indexer::Indexer; use light_compressed_account::{address::derive_address, hash_to_bn254_field_size_be}; use light_compressed_token_sdk::{ - instructions::{ - create_compressed_mint::find_spl_mint_address, derive_compressed_mint_address, - mint_action::MintToRecipient, - }, + instructions::{create_compressed_mint::find_spl_mint_address, derive_compressed_mint_address}, CPI_AUTHORITY_PDA, }; use light_ctoken_types::{ instructions::{ extensions::token_metadata::TokenMetadataInstructionData, - mint_action::{CompressedMintInstructionData, CompressedMintWithContext}, + mint_action::{CompressedMintInstructionData, CompressedMintWithContext, Recipient}, }, state::{extensions::AdditionalMetadata, CompressedMintMetadata}, COMPRESSED_TOKEN_PROGRAM_ID, @@ -212,10 +209,10 @@ pub async fn create_mint( }, }; - let token_recipients = vec![MintToRecipient { - recipient: payer.pubkey(), - amount: 1000u64, // Mint 1000 tokens - }]; + let token_recipients = vec![Recipient::new( + payer.pubkey(), + 1000u64, // Mint 1000 tokens + )]; let pda_creation = PdaCreationData { amount: pda_amount, diff --git a/sdk-tests/sdk-token-test/tests/decompress_full_cpi.rs b/sdk-tests/sdk-token-test/tests/decompress_full_cpi.rs index 81ae71b986..a554169508 100644 --- a/sdk-tests/sdk-token-test/tests/decompress_full_cpi.rs +++ b/sdk-tests/sdk-token-test/tests/decompress_full_cpi.rs @@ -5,7 +5,7 @@ use anchor_lang::{AnchorDeserialize, InstructionData}; const TEST_INPUT_RANGE: [usize; 4] = [1, 2, 3, 4]; use light_compressed_token_sdk::instructions::{ - decompress_full::DecompressFullAccounts, find_spl_mint_address, MintToRecipient, + decompress_full::DecompressFullAccounts, find_spl_mint_address, }; use light_ctoken_types::instructions::mint_action::{CompressedMintWithContext, Recipient}; use light_program_test::{Indexer, LightProgramTest, ProgramTestConfig, Rpc}; @@ -102,10 +102,7 @@ async fn setup_decompress_full_test(num_inputs: usize) -> (LightProgramTest, Tes let compressed_amount_per_account = total_compressed_amount / num_inputs as u64; let compressed_recipients: Vec = (0..num_inputs) - .map(|_| Recipient { - recipient: owner.pubkey().into(), - amount: compressed_amount_per_account, - }) + .map(|_| Recipient::new(owner.pubkey(), compressed_amount_per_account)) .collect(); println!( @@ -331,10 +328,7 @@ async fn test_decompress_full_cpi_with_context() { let mut remaining_accounts = PackedAccounts::default(); // let output_tree_info = rpc.get_random_state_tree_info().unwrap(); - let mint_recipients = vec![MintToRecipient { - recipient: ctx.owner.pubkey(), - amount: 500, // Mint some additional tokens - }]; + let mint_recipients = vec![Recipient::new(ctx.owner.pubkey(), 500)]; let address_tree_info = rpc.get_address_tree_v2(); let compressed_mint_address = diff --git a/sdk-tests/sdk-token-test/tests/pda_ctoken.rs b/sdk-tests/sdk-token-test/tests/pda_ctoken.rs index 426fcf07de..ca2e7997f3 100644 --- a/sdk-tests/sdk-token-test/tests/pda_ctoken.rs +++ b/sdk-tests/sdk-token-test/tests/pda_ctoken.rs @@ -12,14 +12,13 @@ use light_compressed_token_sdk::{ }, create_compressed_mint::find_spl_mint_address, derive_compressed_mint_address, - mint_action::MintToRecipient, }, CPI_AUTHORITY_PDA, }; use light_ctoken_types::{ instructions::{ extensions::token_metadata::TokenMetadataInstructionData, - mint_action::{CompressedMintInstructionData, CompressedMintWithContext}, + mint_action::{CompressedMintInstructionData, CompressedMintWithContext, Recipient}, }, state::{extensions::AdditionalMetadata, CompressedMintMetadata}, COMPRESSED_TOKEN_PROGRAM_ID, @@ -284,10 +283,10 @@ pub async fn create_mint( }, }; - let token_recipients = vec![MintToRecipient { - recipient: payer.pubkey(), - amount: 1000u64, // Mint 1000 tokens - }]; + let token_recipients = vec![Recipient::new( + payer.pubkey(), + 1000u64, // Mint 1000 tokens + )]; let pda_creation = PdaCreationData { amount: pda_amount, diff --git a/sdk-tests/sdk-token-test/tests/test_compress_full_and_close.rs b/sdk-tests/sdk-token-test/tests/test_compress_full_and_close.rs index 8c40939788..b7672ba7dc 100644 --- a/sdk-tests/sdk-token-test/tests/test_compress_full_and_close.rs +++ b/sdk-tests/sdk-token-test/tests/test_compress_full_and_close.rs @@ -141,10 +141,7 @@ async fn test_compress_full_and_close() { cpi_context_pubkey: None, proof: None, compressed_mint_inputs, - recipients: vec![Recipient { - recipient: recipient.into(), - amount: mint_amount, - }], + recipients: vec![Recipient::new(recipient, mint_amount)], mint_authority, payer: payer.pubkey(), state_merkle_tree: compressed_mint_account.tree_info.tree,