diff --git a/Cargo.lock b/Cargo.lock index 57260d5a0fe09e..8660b491ca4361 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7498,6 +7498,7 @@ dependencies = [ "spl-token", "spl-token-2022", "spl-token-group-interface", + "spl-token-metadata-interface", "thiserror", ] @@ -7852,8 +7853,10 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85a5db7e4efb1107b0b8e52a13f035437cdcb36ef99c58f6d467f089d9b2915a" dependencies = [ + "base64 0.21.7", "borsh 0.10.3", "bytemuck", + "serde", "solana-program", "solana-zk-token-sdk", "spl-program-error", @@ -7957,6 +7960,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e16aa8f64b6e0eaab3f5034e84d867c8435d8216497b4543a4978a31f4b6e8a8" dependencies = [ "borsh 0.10.3", + "serde", "solana-program", "spl-discriminator", "spl-pod", diff --git a/transaction-status/Cargo.toml b/transaction-status/Cargo.toml index 6b214d8477c876..3a6a67931f16f5 100644 --- a/transaction-status/Cargo.toml +++ b/transaction-status/Cargo.toml @@ -29,6 +29,7 @@ spl-memo = { workspace = true, features = ["no-entrypoint"] } 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, features = ["serde-traits"] } thiserror = { workspace = true } [package.metadata.docs.rs] diff --git a/transaction-status/src/parse_token.rs b/transaction-status/src/parse_token.rs index e49b8ce9dea613..124ebc5ca352e7 100644 --- a/transaction-status/src/parse_token.rs +++ b/transaction-status/src/parse_token.rs @@ -1,5 +1,8 @@ use { - self::extension::token_group::parse_token_group_instruction, + self::extension::{ + token_group::parse_token_group_instruction, + token_metadata::parse_token_metadata_instruction, + }, crate::parse_instruction::{ check_num_accounts, ParsableProgram, ParseInstructionError, ParsedInstructionEnum, }, @@ -24,6 +27,7 @@ use { }, }, spl_token_group_interface::instruction::TokenGroupInstruction, + spl_token_metadata_interface::instruction::TokenMetadataInstruction, }; mod extension; @@ -690,6 +694,14 @@ pub fn parse_token( &instruction.accounts, account_keys, ) + } else if let Ok(token_metadata_instruction) = + TokenMetadataInstruction::unpack(&instruction.data) + { + parse_token_metadata_instruction( + &token_metadata_instruction, + &instruction.accounts, + account_keys, + ) } else { Err(ParseInstructionError::InstructionNotParsable( ParsableProgram::SplToken, diff --git a/transaction-status/src/parse_token/extension/mod.rs b/transaction-status/src/parse_token/extension/mod.rs index dff938117287a3..e34ec6bd11e8af 100644 --- a/transaction-status/src/parse_token/extension/mod.rs +++ b/transaction-status/src/parse_token/extension/mod.rs @@ -13,5 +13,6 @@ pub(super) mod mint_close_authority; pub(super) mod permanent_delegate; pub(super) mod reallocate; pub(super) mod token_group; +pub(super) mod token_metadata; pub(super) mod transfer_fee; pub(super) mod transfer_hook; diff --git a/transaction-status/src/parse_token/extension/token_metadata.rs b/transaction-status/src/parse_token/extension/token_metadata.rs new file mode 100644 index 00000000000000..a6cc3f4f646e08 --- /dev/null +++ b/transaction-status/src/parse_token/extension/token_metadata.rs @@ -0,0 +1,344 @@ +use { + super::*, + spl_token_metadata_interface::instruction::{ + Emit, Initialize, RemoveKey, TokenMetadataInstruction, UpdateAuthority, UpdateField, + }, +}; + +pub(in crate::parse_token) fn parse_token_metadata_instruction( + instruction: &TokenMetadataInstruction, + account_indexes: &[u8], + account_keys: &AccountKeys, +) -> Result { + match instruction { + TokenMetadataInstruction::Initialize(metadata) => { + check_num_token_accounts(account_indexes, 1)?; + let Initialize { name, symbol, uri } = metadata; + let value = json!({ + "metadata": account_keys[account_indexes[0] as usize].to_string(), + "updateAuthority": account_keys[account_indexes[1] as usize].to_string(), + "mint": account_keys[account_indexes[2] as usize].to_string(), + "mintAuthority": account_keys[account_indexes[3] as usize].to_string(), + "name": name, + "symbol": symbol, + "uri": uri, + }); + Ok(ParsedInstructionEnum { + instruction_type: "initializeTokenMetadata".to_string(), + info: value, + }) + } + TokenMetadataInstruction::UpdateField(update) => { + check_num_token_accounts(account_indexes, 1)?; + let UpdateField { field, value } = update; + let value = json!({ + "metadata": account_keys[account_indexes[0] as usize].to_string(), + "updateAuthority": account_keys[account_indexes[1] as usize].to_string(), + "field": field, + "value": value, + }); + Ok(ParsedInstructionEnum { + instruction_type: "updateTokenMetadataField".to_string(), + info: value, + }) + } + TokenMetadataInstruction::RemoveKey(remove) => { + check_num_token_accounts(account_indexes, 1)?; + let RemoveKey { key, idempotent } = remove; + let value = json!({ + "metadata": account_keys[account_indexes[0] as usize].to_string(), + "updateAuthority": account_keys[account_indexes[1] as usize].to_string(), + "key": key, + "idempotent": *idempotent, + }); + Ok(ParsedInstructionEnum { + instruction_type: "removeTokenMetadataKey".to_string(), + info: value, + }) + } + TokenMetadataInstruction::UpdateAuthority(update) => { + check_num_token_accounts(account_indexes, 1)?; + let UpdateAuthority { new_authority } = update; + let value = json!({ + "metadata": account_keys[account_indexes[0] as usize].to_string(), + "updateAuthority": account_keys[account_indexes[1] as usize].to_string(), + "newAuthority": new_authority, + }); + Ok(ParsedInstructionEnum { + instruction_type: "updateTokenMetadataAuthority".to_string(), + info: value, + }) + } + TokenMetadataInstruction::Emit(emit) => { + check_num_token_accounts(account_indexes, 1)?; + let Emit { start, end } = emit; + let mut value = json!({ + "metadata": account_keys[account_indexes[0] as usize].to_string(), + }); + let map = value.as_object_mut().unwrap(); + if let Some(start) = *start { + map.insert("start".to_string(), json!(start)); + } + if let Some(end) = *end { + map.insert("end".to_string(), json!(end)); + } + Ok(ParsedInstructionEnum { + instruction_type: "emitTokenMetadata".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_token_metadata_instruction() { + let mint = Pubkey::new_unique(); + let mint_authority = Pubkey::new_unique(); + let update_authority = Pubkey::new_unique(); + let metadata = Pubkey::new_unique(); + + let name = "Mega Token".to_string(); + let symbol = "MEGA".to_string(); + let uri = "https://mega.com".to_string(); + + // Initialize + let ix = spl_token_metadata_interface::instruction::initialize( + &spl_token_2022::id(), + &metadata, + &update_authority, + &mint, + &mint_authority, + name.clone(), + symbol.clone(), + uri.clone(), + ); + let mut message = Message::new(&[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: "initializeTokenMetadata".to_string(), + info: json!({ + "metadata": metadata.to_string(), + "updateAuthority": update_authority.to_string(), + "mint": mint.to_string(), + "mintAuthority": mint_authority.to_string(), + "name": name, + "symbol": symbol, + "uri": uri, + }) + } + ); + + // UpdateField + // Update one of the fixed fields. + let ix = spl_token_metadata_interface::instruction::update_field( + &spl_token_2022::id(), + &metadata, + &update_authority, + spl_token_metadata_interface::state::Field::Uri, + "https://ultra-mega.com".to_string(), + ); + let mut message = Message::new(&[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: "updateTokenMetadataField".to_string(), + info: json!({ + "metadata": metadata.to_string(), + "updateAuthority": update_authority.to_string(), + "field": "uri", + "value": "https://ultra-mega.com", + }) + } + ); + // Add a new field + let ix = spl_token_metadata_interface::instruction::update_field( + &spl_token_2022::id(), + &metadata, + &update_authority, + spl_token_metadata_interface::state::Field::Key("new_field".to_string()), + "new_value".to_string(), + ); + let mut message = Message::new(&[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: "updateTokenMetadataField".to_string(), + info: json!({ + "metadata": metadata.to_string(), + "updateAuthority": update_authority.to_string(), + "field": { + "key": "new_field", + }, + "value": "new_value", + }) + } + ); + + // RemoveKey + let ix = spl_token_metadata_interface::instruction::remove_key( + &spl_token_2022::id(), + &metadata, + &update_authority, + "new_field".to_string(), + false, + ); + let mut message = Message::new(&[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: "removeTokenMetadataKey".to_string(), + info: json!({ + "metadata": metadata.to_string(), + "updateAuthority": update_authority.to_string(), + "key": "new_field", + "idempotent": false, + }) + } + ); + + // UpdateAuthority + let new_authority = Pubkey::new_unique(); + let ix = spl_token_metadata_interface::instruction::update_authority( + &spl_token_2022::id(), + &metadata, + &update_authority, + Some(new_authority).try_into().unwrap(), + ); + let mut message = Message::new(&[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: "updateTokenMetadataAuthority".to_string(), + info: json!({ + "metadata": metadata.to_string(), + "updateAuthority": update_authority.to_string(), + "newAuthority": new_authority.to_string(), + }) + } + ); + + // Emit + // Emit with start and end. + let ix = spl_token_metadata_interface::instruction::emit( + &spl_token_2022::id(), + &metadata, + Some(1), + Some(2), + ); + let mut message = Message::new(&[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: "emitTokenMetadata".to_string(), + info: json!({ + "metadata": metadata.to_string(), + "start": 1, + "end": 2, + }) + } + ); + // Emit with only start. + let ix = spl_token_metadata_interface::instruction::emit( + &spl_token_2022::id(), + &metadata, + Some(1), + None, + ); + let mut message = Message::new(&[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: "emitTokenMetadata".to_string(), + info: json!({ + "metadata": metadata.to_string(), + "start": 1, + }) + } + ); + // Emit with only end. + let ix = spl_token_metadata_interface::instruction::emit( + &spl_token_2022::id(), + &metadata, + None, + Some(2), + ); + let mut message = Message::new(&[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: "emitTokenMetadata".to_string(), + info: json!({ + "metadata": metadata.to_string(), + "end": 2, + }) + } + ); + // Emit with neither start nor end. + let ix = spl_token_metadata_interface::instruction::emit( + &spl_token_2022::id(), + &metadata, + None, + None, + ); + let mut message = Message::new(&[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: "emitTokenMetadata".to_string(), + info: json!({ + "metadata": metadata.to_string(), + }) + } + ); + } +}