diff --git a/Cargo.lock b/Cargo.lock index 9c28163494..0d63dd81fd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5259,6 +5259,7 @@ dependencies = [ "spl-pod", "spl-token", "spl-token-2022", + "spl-token-group-interface", "spl-token-metadata-interface", "thiserror", "zstd", @@ -7193,6 +7194,12 @@ dependencies = [ "syn 2.0.42", ] +[[package]] +name = "solana-security-txt" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "468aa43b7edb1f9b7b7b686d5c3aeb6630dc1708e86e31343499dd5c4d775183" + [[package]] name = "solana-send-transaction-service" version = "1.18.0" @@ -7850,9 +7857,9 @@ dependencies = [ [[package]] name = "spl-associated-token-account" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "385e31c29981488f2820b2022d8e731aae3b02e6e18e2fd854e4c9a94dc44fc3" +checksum = "992d9c64c2564cc8f63a4b508bf3ebcdf2254b0429b13cd1d31adb6162432a5f" dependencies = [ "assert_matches", "borsh 0.10.3", @@ -7958,9 +7965,9 @@ dependencies = [ [[package]] name = "spl-tlv-account-resolution" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "062e148d3eab7b165582757453632ffeef490c02c86a48bfdb4988f63eefb3b9" +checksum = "3f7020347c07892c08560d230fbb8a980316c9e198e22b198b7b9d951ff96047" dependencies = [ "bytemuck", "solana-program", @@ -7987,9 +7994,9 @@ dependencies = [ [[package]] name = "spl-token-2022" -version = "0.9.0" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4abf34a65ba420584a0c35f3903f8d727d1f13ababbdc3f714c6b065a686e86" +checksum = "d697fac19fd74ff472dfcc13f0b442dd71403178ce1de7b5d16f83a33561c059" dependencies = [ "arrayref", "bytemuck", @@ -7997,16 +8004,31 @@ dependencies = [ "num-traits", "num_enum 0.7.1", "solana-program", + "solana-security-txt", "solana-zk-token-sdk", "spl-memo", "spl-pod", "spl-token", + "spl-token-group-interface", "spl-token-metadata-interface", "spl-transfer-hook-interface", "spl-type-length-value", "thiserror", ] +[[package]] +name = "spl-token-group-interface" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b889509d49fa74a4a033ca5dae6c2307e9e918122d97e58562f5c4ffa795c75d" +dependencies = [ + "bytemuck", + "solana-program", + "spl-discriminator", + "spl-pod", + "spl-program-error", +] + [[package]] name = "spl-token-metadata-interface" version = "0.2.0" @@ -8023,9 +8045,9 @@ dependencies = [ [[package]] name = "spl-transfer-hook-interface" -version = "0.3.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "051d31803f873cabe71aec3c1b849f35248beae5d19a347d93a5c9cccc5d5a9b" +checksum = "7aabdb7c471566f6ddcee724beb8618449ea24b399e58d464d6b5bc7db550259" dependencies = [ "arrayref", "bytemuck", diff --git a/Cargo.toml b/Cargo.toml index f3e2be4aef..ca8b6905e1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -389,12 +389,13 @@ solana-zk-keygen = { path = "zk-keygen", version = "=1.18.0" } solana-zk-token-proof-program = { path = "programs/zk-token-proof", version = "=1.18.0" } solana-zk-token-sdk = { path = "zk-token-sdk", version = "=1.18.0" } solana_rbpf = "=0.8.0" -spl-associated-token-account = "=2.2.0" +spl-associated-token-account = "=2.3.0" spl-instruction-padding = "0.1" spl-memo = "=4.0.0" spl-pod = "=0.1.0" spl-token = "=4.0.0" -spl-token-2022 = "=0.9.0" +spl-token-2022 = "=1.0.0" +spl-token-group-interface = "=0.1.0" spl-token-metadata-interface = "=0.2.0" static_assertions = "1.1.0" stream-cancel = "0.8.2" diff --git a/account-decoder/Cargo.toml b/account-decoder/Cargo.toml index 3f883ddc23..7aee8478b4 100644 --- a/account-decoder/Cargo.toml +++ b/account-decoder/Cargo.toml @@ -23,6 +23,7 @@ solana-config-program = { workspace = true } solana-sdk = { workspace = true } spl-token = { workspace = true, features = ["no-entrypoint"] } spl-token-2022 = { workspace = true, features = ["no-entrypoint"] } +spl-token-group-interface = { workspace = true } spl-token-metadata-interface = { workspace = true } thiserror = { workspace = true } zstd = { workspace = true } diff --git a/account-decoder/src/parse_token_extension.rs b/account-decoder/src/parse_token_extension.rs index 39d26d83a2..a2fdef41b4 100644 --- a/account-decoder/src/parse_token_extension.rs +++ b/account-decoder/src/parse_token_extension.rs @@ -6,6 +6,7 @@ use { solana_program::pubkey::Pubkey, solana_zk_token_sdk::zk_token_elgamal::pod::ElGamalPubkey, }, + spl_token_group_interface::state::{TokenGroup, TokenGroupMember}, spl_token_metadata_interface::state::TokenMetadata, }; @@ -32,6 +33,10 @@ pub enum UiExtension { TransferHookAccount(UiTransferHookAccount), MetadataPointer(UiMetadataPointer), TokenMetadata(UiTokenMetadata), + GroupPointer(UiGroupPointer), + GroupMemberPointer(UiGroupMemberPointer), + TokenGroup(UiTokenGroup), + TokenGroupMember(UiTokenGroupMember), UnparseableExtension, } @@ -108,6 +113,22 @@ pub fn parse_extension( .get_extension::() .map(|&extension| UiExtension::TransferHookAccount(extension.into())) .unwrap_or(UiExtension::UnparseableExtension), + ExtensionType::GroupPointer => account + .get_extension::() + .map(|&extension| UiExtension::GroupPointer(extension.into())) + .unwrap_or(UiExtension::UnparseableExtension), + ExtensionType::GroupMemberPointer => account + .get_extension::() + .map(|&extension| UiExtension::GroupMemberPointer(extension.into())) + .unwrap_or(UiExtension::UnparseableExtension), + ExtensionType::TokenGroup => account + .get_extension::() + .map(|&extension| UiExtension::TokenGroup(extension.into())) + .unwrap_or(UiExtension::UnparseableExtension), + ExtensionType::TokenGroupMember => account + .get_extension::() + .map(|&extension| UiExtension::TokenGroupMember(extension.into())) + .unwrap_or(UiExtension::UnparseableExtension), } } @@ -481,3 +502,78 @@ impl From for UiTransferHookAccou } } } + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct UiGroupPointer { + pub authority: Option, + pub group_address: Option, +} + +impl From for UiGroupPointer { + fn from(group_pointer: extension::group_pointer::GroupPointer) -> Self { + let authority: Option = group_pointer.authority.into(); + let group_address: Option = group_pointer.group_address.into(); + Self { + authority: authority.map(|pubkey| pubkey.to_string()), + group_address: group_address.map(|pubkey| pubkey.to_string()), + } + } +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct UiGroupMemberPointer { + pub authority: Option, + pub member_address: Option, +} + +impl From for UiGroupMemberPointer { + fn from(member_pointer: extension::group_member_pointer::GroupMemberPointer) -> Self { + let authority: Option = member_pointer.authority.into(); + let member_address: Option = member_pointer.member_address.into(); + Self { + authority: authority.map(|pubkey| pubkey.to_string()), + member_address: member_address.map(|pubkey| pubkey.to_string()), + } + } +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct UiTokenGroup { + pub update_authority: Option, + pub mint: String, + pub size: u32, + pub max_size: u32, +} + +impl From for UiTokenGroup { + fn from(token_group: TokenGroup) -> Self { + let update_authority: Option = token_group.update_authority.into(); + Self { + update_authority: update_authority.map(|pubkey| pubkey.to_string()), + mint: token_group.mint.to_string(), + size: token_group.size.into(), + max_size: token_group.max_size.into(), + } + } +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct UiTokenGroupMember { + pub mint: String, + pub group: String, + pub member_number: u32, +} + +impl From for UiTokenGroupMember { + fn from(member: TokenGroupMember) -> Self { + Self { + mint: member.mint.to_string(), + group: member.group.to_string(), + member_number: member.member_number.into(), + } + } +} diff --git a/program-test/src/programs.rs b/program-test/src/programs.rs index ed96be7644..8d9a42790f 100644 --- a/program-test/src/programs.rs +++ b/program-test/src/programs.rs @@ -30,7 +30,7 @@ static SPL_PROGRAMS: &[(Pubkey, Pubkey, &[u8])] = &[ ( spl_token_2022::ID, solana_sdk::bpf_loader_upgradeable::ID, - include_bytes!("programs/spl_token_2022-0.9.0.so"), + include_bytes!("programs/spl_token_2022-1.0.0.so"), ), ( spl_memo_1_0::ID, diff --git a/program-test/src/programs/spl_token_2022-0.9.0.so b/program-test/src/programs/spl_token_2022-0.9.0.so deleted file mode 100644 index 704fce1190..0000000000 Binary files a/program-test/src/programs/spl_token_2022-0.9.0.so and /dev/null differ diff --git a/program-test/src/programs/spl_token_2022-1.0.0.so b/program-test/src/programs/spl_token_2022-1.0.0.so new file mode 100755 index 0000000000..796fafc4cc Binary files /dev/null and b/program-test/src/programs/spl_token_2022-1.0.0.so differ diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock index e08e5c0395..664942fcb9 100644 --- a/programs/sbf/Cargo.lock +++ b/programs/sbf/Cargo.lock @@ -4670,6 +4670,7 @@ dependencies = [ "solana-sdk", "spl-token", "spl-token-2022", + "spl-token-group-interface", "spl-token-metadata-interface", "thiserror", "zstd", @@ -6294,6 +6295,12 @@ dependencies = [ "syn 2.0.42", ] +[[package]] +name = "solana-security-txt" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "468aa43b7edb1f9b7b7b686d5c3aeb6630dc1708e86e31343499dd5c4d775183" + [[package]] name = "solana-send-transaction-service" version = "1.18.0" @@ -6776,9 +6783,9 @@ dependencies = [ [[package]] name = "spl-associated-token-account" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "385e31c29981488f2820b2022d8e731aae3b02e6e18e2fd854e4c9a94dc44fc3" +checksum = "992d9c64c2564cc8f63a4b508bf3ebcdf2254b0429b13cd1d31adb6162432a5f" dependencies = [ "assert_matches", "borsh 0.10.3", @@ -6874,9 +6881,9 @@ dependencies = [ [[package]] name = "spl-tlv-account-resolution" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "062e148d3eab7b165582757453632ffeef490c02c86a48bfdb4988f63eefb3b9" +checksum = "3f7020347c07892c08560d230fbb8a980316c9e198e22b198b7b9d951ff96047" dependencies = [ "bytemuck", "solana-program", @@ -6903,9 +6910,9 @@ dependencies = [ [[package]] name = "spl-token-2022" -version = "0.9.0" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4abf34a65ba420584a0c35f3903f8d727d1f13ababbdc3f714c6b065a686e86" +checksum = "d697fac19fd74ff472dfcc13f0b442dd71403178ce1de7b5d16f83a33561c059" dependencies = [ "arrayref", "bytemuck", @@ -6913,16 +6920,31 @@ dependencies = [ "num-traits", "num_enum 0.7.1", "solana-program", + "solana-security-txt", "solana-zk-token-sdk", "spl-memo", "spl-pod", "spl-token", + "spl-token-group-interface", "spl-token-metadata-interface", "spl-transfer-hook-interface", "spl-type-length-value", "thiserror", ] +[[package]] +name = "spl-token-group-interface" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b889509d49fa74a4a033ca5dae6c2307e9e918122d97e58562f5c4ffa795c75d" +dependencies = [ + "bytemuck", + "solana-program", + "spl-discriminator", + "spl-pod", + "spl-program-error", +] + [[package]] name = "spl-token-metadata-interface" version = "0.2.0" @@ -6939,9 +6961,9 @@ dependencies = [ [[package]] name = "spl-transfer-hook-interface" -version = "0.3.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "051d31803f873cabe71aec3c1b849f35248beae5d19a347d93a5c9cccc5d5a9b" +checksum = "7aabdb7c471566f6ddcee724beb8618449ea24b399e58d464d6b5bc7db550259" dependencies = [ "arrayref", "bytemuck", diff --git a/transaction-status/src/parse_token.rs b/transaction-status/src/parse_token.rs index ee9a04db3a..c7111ee622 100644 --- a/transaction-status/src/parse_token.rs +++ b/transaction-status/src/parse_token.rs @@ -4,9 +4,9 @@ use { }, extension::{ confidential_transfer::*, confidential_transfer_fee::*, cpi_guard::*, - default_account_state::*, interest_bearing_mint::*, memo_transfer::*, metadata_pointer::*, - mint_close_authority::*, permanent_delegate::*, reallocate::*, transfer_fee::*, - transfer_hook::*, + default_account_state::*, group_member_pointer::*, group_pointer::*, + interest_bearing_mint::*, memo_transfer::*, metadata_pointer::*, mint_close_authority::*, + permanent_delegate::*, reallocate::*, transfer_fee::*, transfer_hook::*, }, serde_json::{json, Map, Value}, solana_account_decoder::parse_token::{token_amount_to_ui_amount, UiAccountState}, @@ -233,7 +233,9 @@ pub fn parse_token( | AuthorityType::ConfidentialTransferMint | AuthorityType::TransferHookProgramId | AuthorityType::ConfidentialTransferFeeConfig - | AuthorityType::MetadataPointer => "mint", + | AuthorityType::MetadataPointer + | AuthorityType::GroupPointer + | AuthorityType::GroupMemberPointer => "mint", AuthorityType::AccountOwner | AuthorityType::CloseAccount => "account", }; let mut value = json!({ @@ -650,6 +652,30 @@ pub fn parse_token( account_keys, ) } + TokenInstruction::GroupPointerExtension => { + if instruction.data.len() < 2 { + return Err(ParseInstructionError::InstructionNotParsable( + ParsableProgram::SplToken, + )); + } + parse_group_pointer_instruction( + &instruction.data[1..], + &instruction.accounts, + account_keys, + ) + } + TokenInstruction::GroupMemberPointerExtension => { + if instruction.data.len() < 2 { + return Err(ParseInstructionError::InstructionNotParsable( + ParsableProgram::SplToken, + )); + } + parse_group_member_pointer_instruction( + &instruction.data[1..], + &instruction.accounts, + account_keys, + ) + } } } @@ -669,6 +695,8 @@ pub enum UiAuthorityType { TransferHookProgramId, ConfidentialTransferFeeConfig, MetadataPointer, + GroupPointer, + GroupMemberPointer, } impl From for UiAuthorityType { @@ -689,6 +717,8 @@ impl From for UiAuthorityType { UiAuthorityType::ConfidentialTransferFeeConfig } AuthorityType::MetadataPointer => UiAuthorityType::MetadataPointer, + AuthorityType::GroupPointer => UiAuthorityType::GroupPointer, + AuthorityType::GroupMemberPointer => UiAuthorityType::GroupMemberPointer, } } } @@ -716,6 +746,10 @@ pub enum UiExtensionType { ConfidentialTransferFeeAmount, MetadataPointer, TokenMetadata, + GroupPointer, + GroupMemberPointer, + TokenGroup, + TokenGroupMember, } impl From for UiExtensionType { @@ -747,6 +781,10 @@ impl From for UiExtensionType { } ExtensionType::MetadataPointer => UiExtensionType::MetadataPointer, ExtensionType::TokenMetadata => UiExtensionType::TokenMetadata, + ExtensionType::GroupPointer => UiExtensionType::GroupPointer, + ExtensionType::GroupMemberPointer => UiExtensionType::GroupMemberPointer, + ExtensionType::TokenGroup => UiExtensionType::TokenGroup, + ExtensionType::TokenGroupMember => UiExtensionType::TokenGroupMember, } } } diff --git a/transaction-status/src/parse_token/extension/group_member_pointer.rs b/transaction-status/src/parse_token/extension/group_member_pointer.rs new file mode 100644 index 0000000000..24d0503dc5 --- /dev/null +++ b/transaction-status/src/parse_token/extension/group_member_pointer.rs @@ -0,0 +1,189 @@ +use { + super::*, + spl_token_2022::{ + extension::group_member_pointer::instruction::*, + instruction::{decode_instruction_data, decode_instruction_type}, + }, +}; + +pub(in crate::parse_token) fn parse_group_member_pointer_instruction( + instruction_data: &[u8], + account_indexes: &[u8], + account_keys: &AccountKeys, +) -> Result { + match decode_instruction_type(instruction_data) + .map_err(|_| ParseInstructionError::InstructionNotParsable(ParsableProgram::SplToken))? + { + GroupMemberPointerInstruction::Initialize => { + check_num_token_accounts(account_indexes, 1)?; + let InitializeInstructionData { + authority, + member_address, + } = *decode_instruction_data(instruction_data).map_err(|_| { + ParseInstructionError::InstructionNotParsable(ParsableProgram::SplToken) + })?; + let mut value = json!({ + "mint": account_keys[account_indexes[0] as usize].to_string(), + }); + let map = value.as_object_mut().unwrap(); + if let Some(authority) = Option::::from(authority) { + map.insert("authority".to_string(), json!(authority.to_string())); + } + if let Some(member_address) = Option::::from(member_address) { + map.insert( + "memberAddress".to_string(), + json!(member_address.to_string()), + ); + } + Ok(ParsedInstructionEnum { + instruction_type: "initializeGroupMemberPointer".to_string(), + info: value, + }) + } + GroupMemberPointerInstruction::Update => { + check_num_token_accounts(account_indexes, 2)?; + let UpdateInstructionData { member_address } = + *decode_instruction_data(instruction_data).map_err(|_| { + ParseInstructionError::InstructionNotParsable(ParsableProgram::SplToken) + })?; + let mut value = json!({ + "mint": account_keys[account_indexes[0] as usize].to_string(), + }); + let map = value.as_object_mut().unwrap(); + if let Some(member_address) = Option::::from(member_address) { + map.insert( + "memberAddress".to_string(), + json!(member_address.to_string()), + ); + } + parse_signers( + map, + 1, + account_keys, + account_indexes, + "authority", + "multisigAuthority", + ); + Ok(ParsedInstructionEnum { + instruction_type: "updateGroupMemberPointer".to_string(), + info: value, + }) + } + } +} + +#[cfg(test)] +mod test { + use {super::*, solana_sdk::pubkey::Pubkey, spl_token_2022::solana_program::message::Message}; + + #[test] + fn test_parse_group_member_pointer_instruction() { + let mint_pubkey = Pubkey::new_unique(); + let authority = Pubkey::new_unique(); + let member_address = Pubkey::new_unique(); + + // Initialize variations + let init_ix = initialize( + &spl_token_2022::id(), + &mint_pubkey, + Some(authority), + Some(member_address), + ) + .unwrap(); + let mut message = Message::new(&[init_ix], None); + let compiled_instruction = &mut message.instructions[0]; + assert_eq!( + parse_token( + compiled_instruction, + &AccountKeys::new(&message.account_keys, None) + ) + .unwrap(), + ParsedInstructionEnum { + instruction_type: "initializeGroupMemberPointer".to_string(), + info: json!({ + "mint": mint_pubkey.to_string(), + "authority": authority.to_string(), + "memberAddress": member_address.to_string(), + }) + } + ); + + let init_ix = initialize(&spl_token_2022::id(), &mint_pubkey, None, None).unwrap(); + let mut message = Message::new(&[init_ix], None); + let compiled_instruction = &mut message.instructions[0]; + assert_eq!( + parse_token( + compiled_instruction, + &AccountKeys::new(&message.account_keys, None) + ) + .unwrap(), + ParsedInstructionEnum { + instruction_type: "initializeGroupMemberPointer".to_string(), + info: json!({ + "mint": mint_pubkey.to_string(), + }) + } + ); + + // Single owner Update + let update_ix = update( + &spl_token_2022::id(), + &mint_pubkey, + &authority, + &[], + Some(member_address), + ) + .unwrap(); + let mut message = Message::new(&[update_ix], None); + let compiled_instruction = &mut message.instructions[0]; + assert_eq!( + parse_token( + compiled_instruction, + &AccountKeys::new(&message.account_keys, None) + ) + .unwrap(), + ParsedInstructionEnum { + instruction_type: "updateGroupMemberPointer".to_string(), + info: json!({ + "mint": mint_pubkey.to_string(), + "authority": authority.to_string(), + "memberAddress": member_address.to_string(), + }) + } + ); + + // Multisig Update + let multisig_pubkey = Pubkey::new_unique(); + let multisig_signer0 = Pubkey::new_unique(); + let multisig_signer1 = Pubkey::new_unique(); + let update_ix = update( + &spl_token_2022::id(), + &mint_pubkey, + &multisig_pubkey, + &[&multisig_signer0, &multisig_signer1], + Some(member_address), + ) + .unwrap(); + let mut message = Message::new(&[update_ix], None); + let compiled_instruction = &mut message.instructions[0]; + assert_eq!( + parse_token( + compiled_instruction, + &AccountKeys::new(&message.account_keys, None) + ) + .unwrap(), + ParsedInstructionEnum { + instruction_type: "updateGroupMemberPointer".to_string(), + info: json!({ + "mint": mint_pubkey.to_string(), + "memberAddress": member_address.to_string(), + "multisigAuthority": multisig_pubkey.to_string(), + "signers": vec![ + multisig_signer0.to_string(), + multisig_signer1.to_string(), + ], + }) + } + ); + } +} diff --git a/transaction-status/src/parse_token/extension/group_pointer.rs b/transaction-status/src/parse_token/extension/group_pointer.rs new file mode 100644 index 0000000000..5800a2fd88 --- /dev/null +++ b/transaction-status/src/parse_token/extension/group_pointer.rs @@ -0,0 +1,183 @@ +use { + super::*, + spl_token_2022::{ + extension::group_pointer::instruction::*, + instruction::{decode_instruction_data, decode_instruction_type}, + }, +}; + +pub(in crate::parse_token) fn parse_group_pointer_instruction( + instruction_data: &[u8], + account_indexes: &[u8], + account_keys: &AccountKeys, +) -> Result { + match decode_instruction_type(instruction_data) + .map_err(|_| ParseInstructionError::InstructionNotParsable(ParsableProgram::SplToken))? + { + GroupPointerInstruction::Initialize => { + check_num_token_accounts(account_indexes, 1)?; + let InitializeInstructionData { + authority, + group_address, + } = *decode_instruction_data(instruction_data).map_err(|_| { + ParseInstructionError::InstructionNotParsable(ParsableProgram::SplToken) + })?; + let mut value = json!({ + "mint": account_keys[account_indexes[0] as usize].to_string(), + }); + let map = value.as_object_mut().unwrap(); + if let Some(authority) = Option::::from(authority) { + map.insert("authority".to_string(), json!(authority.to_string())); + } + if let Some(group_address) = Option::::from(group_address) { + map.insert("groupAddress".to_string(), json!(group_address.to_string())); + } + Ok(ParsedInstructionEnum { + instruction_type: "initializeGroupPointer".to_string(), + info: value, + }) + } + GroupPointerInstruction::Update => { + check_num_token_accounts(account_indexes, 2)?; + let UpdateInstructionData { group_address } = + *decode_instruction_data(instruction_data).map_err(|_| { + ParseInstructionError::InstructionNotParsable(ParsableProgram::SplToken) + })?; + let mut value = json!({ + "mint": account_keys[account_indexes[0] as usize].to_string(), + }); + let map = value.as_object_mut().unwrap(); + if let Some(group_address) = Option::::from(group_address) { + map.insert("groupAddress".to_string(), json!(group_address.to_string())); + } + parse_signers( + map, + 1, + account_keys, + account_indexes, + "authority", + "multisigAuthority", + ); + Ok(ParsedInstructionEnum { + instruction_type: "updateGroupPointer".to_string(), + info: value, + }) + } + } +} + +#[cfg(test)] +mod test { + use {super::*, solana_sdk::pubkey::Pubkey, spl_token_2022::solana_program::message::Message}; + + #[test] + fn test_parse_group_pointer_instruction() { + let mint_pubkey = Pubkey::new_unique(); + let authority = Pubkey::new_unique(); + let group_address = Pubkey::new_unique(); + + // Initialize variations + let init_ix = initialize( + &spl_token_2022::id(), + &mint_pubkey, + Some(authority), + Some(group_address), + ) + .unwrap(); + let mut message = Message::new(&[init_ix], None); + let compiled_instruction = &mut message.instructions[0]; + assert_eq!( + parse_token( + compiled_instruction, + &AccountKeys::new(&message.account_keys, None) + ) + .unwrap(), + ParsedInstructionEnum { + instruction_type: "initializeGroupPointer".to_string(), + info: json!({ + "mint": mint_pubkey.to_string(), + "authority": authority.to_string(), + "groupAddress": group_address.to_string(), + }) + } + ); + + let init_ix = initialize(&spl_token_2022::id(), &mint_pubkey, None, None).unwrap(); + let mut message = Message::new(&[init_ix], None); + let compiled_instruction = &mut message.instructions[0]; + assert_eq!( + parse_token( + compiled_instruction, + &AccountKeys::new(&message.account_keys, None) + ) + .unwrap(), + ParsedInstructionEnum { + instruction_type: "initializeGroupPointer".to_string(), + info: json!({ + "mint": mint_pubkey.to_string(), + }) + } + ); + + // Single owner Update + let update_ix = update( + &spl_token_2022::id(), + &mint_pubkey, + &authority, + &[], + Some(group_address), + ) + .unwrap(); + let mut message = Message::new(&[update_ix], None); + let compiled_instruction = &mut message.instructions[0]; + assert_eq!( + parse_token( + compiled_instruction, + &AccountKeys::new(&message.account_keys, None) + ) + .unwrap(), + ParsedInstructionEnum { + instruction_type: "updateGroupPointer".to_string(), + info: json!({ + "mint": mint_pubkey.to_string(), + "authority": authority.to_string(), + "groupAddress": group_address.to_string(), + }) + } + ); + + // Multisig Update + let multisig_pubkey = Pubkey::new_unique(); + let multisig_signer0 = Pubkey::new_unique(); + let multisig_signer1 = Pubkey::new_unique(); + let update_ix = update( + &spl_token_2022::id(), + &mint_pubkey, + &multisig_pubkey, + &[&multisig_signer0, &multisig_signer1], + Some(group_address), + ) + .unwrap(); + let mut message = Message::new(&[update_ix], None); + let compiled_instruction = &mut message.instructions[0]; + assert_eq!( + parse_token( + compiled_instruction, + &AccountKeys::new(&message.account_keys, None) + ) + .unwrap(), + ParsedInstructionEnum { + instruction_type: "updateGroupPointer".to_string(), + info: json!({ + "mint": mint_pubkey.to_string(), + "groupAddress": group_address.to_string(), + "multisigAuthority": multisig_pubkey.to_string(), + "signers": vec![ + multisig_signer0.to_string(), + multisig_signer1.to_string(), + ], + }) + } + ); + } +} diff --git a/transaction-status/src/parse_token/extension/mod.rs b/transaction-status/src/parse_token/extension/mod.rs index 8e65ddfcfc..19dd4f14b2 100644 --- a/transaction-status/src/parse_token/extension/mod.rs +++ b/transaction-status/src/parse_token/extension/mod.rs @@ -4,6 +4,8 @@ pub(super) mod confidential_transfer; pub(super) mod confidential_transfer_fee; pub(super) mod cpi_guard; pub(super) mod default_account_state; +pub(super) mod group_member_pointer; +pub(super) mod group_pointer; pub(super) mod interest_bearing_mint; pub(super) mod memo_transfer; pub(super) mod metadata_pointer;