// file: kb_decoder_spl_token_2022/src/token.rs // version: 13 //! Bounded Token-2022 wire, account and authority decoding. use base64::Engine; // rust-rules: trait-import use sha2::Digest; // rust-rules: trait-import #[derive(Clone, Debug)] struct ParsedInstruction { code: &'static str, tag: u8, parameters: serde_json::Value, consumed: usize, } #[derive(Clone, Debug)] struct ResolvedAccount { position: usize, account_index: u64, account_key: std::string::String, signer: bool, writable: bool, source: serde_json::Value, } pub(crate) fn entry_for_tag(tag: u8) -> std::option::Option<(u8, &'static str, bool)> { return crate::INSTRUCTION_ENTRIES .iter() .copied() .find(|(candidate, _, _)| return *candidate == tag); } pub(crate) fn payload_tag( input: &kb_store_core::CoreInstructionReplayInput, ) -> std::option::Option { let bytes = decode_payload(input).ok(); return bytes.and_then(|value| return value.first().copied()); } pub(crate) fn decode( input: &kb_store_core::CoreInstructionReplayInput, ) -> kb_decoder_api::DecoderExecutionResult { let bytes = match decode_payload(input) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { return failed( std::option::Option::None, "token_payload_unavailable", error.to_string(), ); }, }; let tag = match bytes.first().copied() { std::option::Option::Some(value) => value, std::option::Option::None => { return failed( std::option::Option::None, "token_payload_empty", "Token-2022 instruction payload is empty".to_string(), ); }, }; let recognized_code = crate::entry_for_tag(tag) .map(|(_, code, _)| return code) .or_else(|| return embedded_interface_code(bytes.as_slice())); if recognized_code.is_none() { return kb_decoder_api::DecoderExecutionResult { status: kb_decoder_api::DecoderOutcomeStatus::Unsupported, recognized_entry_code: std::option::Option::None, observations: std::vec::Vec::new(), diagnostics: std::vec![kb_decoder_api::DecoderDiagnostic { code: "unknown_token_instruction_tag_or_interface_discriminator".to_string(), message: format!( "unknown Token-2022 tag or embedded interface discriminator; payloadPrefixHex={}", hex_prefix(bytes.as_slice()) ), retriable: false, }], }; } let parsed = match parse_instruction(bytes.as_slice(), false) { std::result::Result::Ok(value) => value, std::result::Result::Err(message) => { return failed( recognized_code, "malformed_token_instruction", format!("{message}; payloadPrefixHex={}", hex_prefix(bytes.as_slice())), ); }, }; let accounts = match resolve_accounts(input) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { return failed( std::option::Option::Some(parsed.code), "token_accounts_malformed", error.to_string(), ); }, }; let (parameters, batch_children) = if parsed.code == "batch" { match batch_observations(input, bytes.as_slice(), accounts.as_slice()) { std::result::Result::Ok((children, manifest)) => (manifest, children), std::result::Result::Err(message) => { return failed( std::option::Option::Some("batch"), "malformed_token_batch", message, ); }, } } else { (parsed.parameters.clone(), std::vec::Vec::new()) }; let account_projection = project_accounts(parsed.code, accounts.as_slice()); let suffix_length = bytes.len().saturating_sub(parsed.consumed); let authority = authority_projection(parsed.code, accounts.as_slice()); let semantic_diagnostics = semantic_diagnostics(parsed.code, parameters.clone(), accounts.as_slice(), suffix_length); let committed = !input.transaction_failed; let parent = observation( input, parsed.code, parsed.tag, parameters, account_projection, authority, semantic_diagnostics, bytes.as_slice(), suffix_length, committed, format!("token_2022:{}:0", parsed.code), input.instruction_path.clone(), event_family(parsed.code), ); let mut observations = std::vec![parent]; observations.extend(batch_children); return kb_decoder_api::DecoderExecutionResult { status: kb_decoder_api::DecoderOutcomeStatus::Decoded, recognized_entry_code: std::option::Option::Some(parsed.code.to_string()), observations, diagnostics: std::vec::Vec::new(), }; } fn decode_payload( input: &kb_store_core::CoreInstructionReplayInput, ) -> kb_core::Result> { let payload = match input.instruction_payload_json.as_ref() { std::option::Option::Some(value) => value, std::option::Option::None => { return std::result::Result::Err(kb_core::Error::invalid_state( "Token-2022 instruction payload is not retained", )); }, }; let encoded = match payload.get("dataBase64").and_then(serde_json::Value::as_str) { std::option::Option::Some(value) => value, std::option::Option::None => { return std::result::Result::Err(kb_core::Error::invalid_state( "Token-2022 payload does not contain dataBase64", )); }, }; let maximum_encoded = crate::MAX_INSTRUCTION_BYTES .saturating_mul(4) .saturating_div(3) .saturating_add(4); if encoded.len() > maximum_encoded { return std::result::Result::Err(kb_core::Error::invalid_state(format!( "Token-2022 base64 payload exceeds {maximum_encoded} bytes" ))); } let decoded = match base64::engine::general_purpose::STANDARD.decode(encoded.as_bytes()) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { return std::result::Result::Err(kb_core::Error::invalid_state(format!( "Token-2022 payload is not valid base64: {error}" ))); }, }; if decoded.len() > crate::MAX_INSTRUCTION_BYTES { return std::result::Result::Err(kb_core::Error::invalid_state(format!( "Token-2022 payload exceeds {} decoded bytes", crate::MAX_INSTRUCTION_BYTES ))); } return std::result::Result::Ok(decoded); } fn parse_instruction( bytes: &[u8], nested_in_batch: bool, ) -> std::result::Result { if embedded_interface_code(bytes).is_some() { return parse_embedded_interface_instruction(bytes); } let tag = match bytes.first().copied() { std::option::Option::Some(value) => value, std::option::Option::None => { return std::result::Result::Err("empty instruction".to_string()); }, }; let code = match crate::entry_for_tag(tag) { std::option::Option::Some((_, value, _)) => value, std::option::Option::None => { return std::result::Result::Err(format!("unknown instruction tag {tag}")); }, }; if nested_in_batch && tag == 255 { return std::result::Result::Err("nested Batch instruction is forbidden".to_string()); } let result = match tag { 0 | 20 => parse_initialize_mint(bytes, code, tag), 1 | 5 | 9 | 10 | 11 | 17 | 21 | 22 | 38 => std::result::Result::Ok(ParsedInstruction { code, tag, parameters: serde_json::json!({}), consumed: 1, }), 255 => std::result::Result::Ok(ParsedInstruction { code, tag, parameters: serde_json::json!({}), consumed: bytes.len(), }), 2 | 19 => parse_one_u8(bytes, code, tag, "m"), 3 | 4 | 7 | 8 | 23 => parse_one_u64(bytes, code, tag, "amountRaw"), 6 => parse_set_authority(bytes, code, tag), 12 | 13 | 14 | 15 => parse_amount_decimals(bytes, code, tag), 16 | 18 => parse_pubkey_parameter(bytes, code, tag, "owner"), 24 => parse_ui_amount(bytes, code, tag), 29 => parse_reallocate(bytes, code, tag), 31 | 32 => std::result::Result::Ok(ParsedInstruction { code, tag, parameters: serde_json::json!({}), consumed: 1, }), 35 => parse_pubkey_parameter(bytes, code, tag, "delegate"), 45 => parse_unwrap_lamports(bytes, code, tag), 26 | 27 | 28 | 30 | 33 | 34 | 36 | 37 | 39 | 40 | 41 | 42 | 43 | 44 | 46 => { parse_extension_envelope(bytes, code, tag) }, _ => std::result::Result::Err(format!("unsupported instruction tag {tag}")), }; return result; } const TOKEN_METADATA_INITIALIZE_DISCRIMINATOR: [u8; 8] = [0xd2, 0xe1, 0x1e, 0xa2, 0x58, 0xb8, 0x4d, 0x8d]; const TOKEN_METADATA_UPDATE_FIELD_DISCRIMINATOR: [u8; 8] = [0xdd, 0xe9, 0x31, 0x2d, 0xb5, 0xca, 0xdc, 0xc8]; const TOKEN_METADATA_REMOVE_KEY_DISCRIMINATOR: [u8; 8] = [0xea, 0x12, 0x20, 0x38, 0x59, 0x8d, 0x25, 0xb5]; const TOKEN_METADATA_UPDATE_AUTHORITY_DISCRIMINATOR: [u8; 8] = [0xd7, 0xe4, 0xa6, 0xe4, 0x54, 0x64, 0x56, 0x7b]; const TOKEN_METADATA_EMIT_DISCRIMINATOR: [u8; 8] = [0xfa, 0xa6, 0xb4, 0xfa, 0x0d, 0x0c, 0xb8, 0x46]; const TOKEN_GROUP_INITIALIZE_DISCRIMINATOR: [u8; 8] = [0x79, 0x71, 0x6c, 0x27, 0x36, 0x33, 0x00, 0x04]; const TOKEN_GROUP_UPDATE_MAX_SIZE_DISCRIMINATOR: [u8; 8] = [0x6c, 0x25, 0xab, 0x8f, 0xf8, 0x1e, 0x12, 0x6e]; const TOKEN_GROUP_UPDATE_AUTHORITY_DISCRIMINATOR: [u8; 8] = [0xa1, 0x69, 0x58, 0x01, 0xed, 0xdd, 0xd8, 0xcb]; const TOKEN_GROUP_INITIALIZE_MEMBER_DISCRIMINATOR: [u8; 8] = [0x98, 0x20, 0xde, 0xb0, 0xdf, 0xed, 0x74, 0x86]; fn embedded_interface_code(bytes: &[u8]) -> std::option::Option<&'static str> { let discriminator = match bytes.get(..8) { std::option::Option::Some(value) => value, std::option::Option::None => return std::option::Option::None, }; return match discriminator { value if value == TOKEN_METADATA_INITIALIZE_DISCRIMINATOR => { std::option::Option::Some("initialize_token_metadata") }, value if value == TOKEN_METADATA_UPDATE_FIELD_DISCRIMINATOR => { std::option::Option::Some("update_token_metadata_field") }, value if value == TOKEN_METADATA_REMOVE_KEY_DISCRIMINATOR => { std::option::Option::Some("remove_token_metadata_key") }, value if value == TOKEN_METADATA_UPDATE_AUTHORITY_DISCRIMINATOR => { std::option::Option::Some("update_token_metadata_authority") }, value if value == TOKEN_METADATA_EMIT_DISCRIMINATOR => { std::option::Option::Some("emit_token_metadata") }, value if value == TOKEN_GROUP_INITIALIZE_DISCRIMINATOR => { std::option::Option::Some("initialize_token_group") }, value if value == TOKEN_GROUP_UPDATE_MAX_SIZE_DISCRIMINATOR => { std::option::Option::Some("update_token_group_max_size") }, value if value == TOKEN_GROUP_UPDATE_AUTHORITY_DISCRIMINATOR => { std::option::Option::Some("update_token_group_authority") }, value if value == TOKEN_GROUP_INITIALIZE_MEMBER_DISCRIMINATOR => { std::option::Option::Some("initialize_token_group_member") }, _ => std::option::Option::None, }; } macro_rules! token_value_or_return { ($expression:expr) => { match $expression { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), } }; } fn parse_embedded_interface_instruction( bytes: &[u8], ) -> std::result::Result { let code = match embedded_interface_code(bytes) { std::option::Option::Some(value) => value, std::option::Option::None => { return std::result::Result::Err( "unknown embedded interface discriminator".to_string(), ); }, }; let mut offset = 8_usize; let parameters = match code { "initialize_token_metadata" => { let name = token_value_or_return!(read_borsh_string(bytes, &mut offset, "name")); let symbol = token_value_or_return!(read_borsh_string(bytes, &mut offset, "symbol")); let uri = token_value_or_return!(read_borsh_string(bytes, &mut offset, "uri")); serde_json::json!({"name":name,"symbol":symbol,"uri":uri}) }, "update_token_metadata_field" => { let field_tag = token_value_or_return!(read_u8(bytes, &mut offset, "metadata field")); let field = match field_tag { 0 => serde_json::json!({"kind":"name"}), 1 => serde_json::json!({"kind":"symbol"}), 2 => serde_json::json!({"kind":"uri"}), 3 => { serde_json::json!({"kind":"key","key":token_value_or_return!(read_borsh_string(bytes, &mut offset, "field key"))}) }, _ => { return std::result::Result::Err(format!( "unknown Token Metadata field discriminant {field_tag}" )); }, }; let value = token_value_or_return!(read_borsh_string(bytes, &mut offset, "value")); serde_json::json!({"field":field,"value":value}) }, "remove_token_metadata_key" => { let idempotent = match token_value_or_return!(read_u8(bytes, &mut offset, "idempotent")) { 0 => false, 1 => true, value => return std::result::Result::Err(format!("invalid Borsh bool {value}")), }; let key = token_value_or_return!(read_borsh_string(bytes, &mut offset, "key")); serde_json::json!({"idempotent":idempotent,"key":key}) }, "update_token_metadata_authority" => { let authority = token_value_or_return!(read_nullable_pubkey(bytes, &mut offset, "new authority")); serde_json::json!({"newAuthority":authority}) }, "emit_token_metadata" => { let start = token_value_or_return!(read_borsh_option_u64(bytes, &mut offset, "start")); let end = token_value_or_return!(read_borsh_option_u64(bytes, &mut offset, "end")); serde_json::json!({"start":start,"end":end}) }, "initialize_token_group" => { let update_authority = token_value_or_return!(read_nullable_pubkey( bytes, &mut offset, "update authority" )); let max_size = token_value_or_return!(read_u64(bytes, &mut offset, "max size")); serde_json::json!({"updateAuthority":update_authority,"maxSize":max_size.to_string()}) }, "update_token_group_max_size" => { let max_size = token_value_or_return!(read_u64(bytes, &mut offset, "max size")); serde_json::json!({"maxSize":max_size.to_string()}) }, "update_token_group_authority" => { let authority = token_value_or_return!(read_nullable_pubkey(bytes, &mut offset, "new authority")); serde_json::json!({"newAuthority":authority}) }, "initialize_token_group_member" => serde_json::json!({}), _ => { return std::result::Result::Err(format!( "unsupported embedded interface instruction {code}" )); }, }; if offset != bytes.len() { return std::result::Result::Err(format!( "{code} requires exact wire length; trailingBytes={}", bytes.len().saturating_sub(offset) )); } return std::result::Result::Ok(ParsedInstruction { code, tag: bytes[0], parameters, consumed: offset, }); } fn read_u8( bytes: &[u8], offset: &mut usize, label: &str, ) -> std::result::Result { let value = match bytes.get(*offset).copied() { std::option::Option::Some(value) => value, std::option::Option::None => return std::result::Result::Err(format!("truncated {label}")), }; *offset = offset.saturating_add(1); return std::result::Result::Ok(value); } fn read_u64( bytes: &[u8], offset: &mut usize, label: &str, ) -> std::result::Result { let end = offset.saturating_add(8); let slice = match bytes.get(*offset..end) { std::option::Option::Some(value) => value, std::option::Option::None => return std::result::Result::Err(format!("truncated {label}")), }; *offset = end; return std::result::Result::Ok(u64::from_le_bytes([ slice[0], slice[1], slice[2], slice[3], slice[4], slice[5], slice[6], slice[7], ])); } fn read_borsh_string( bytes: &[u8], offset: &mut usize, label: &str, ) -> std::result::Result { let length_end = offset.saturating_add(4); let length_bytes = match bytes.get(*offset..length_end) { std::option::Option::Some(value) => value, std::option::Option::None => { return std::result::Result::Err(format!("truncated {label} length")); }, }; let length = u32::from_le_bytes([length_bytes[0], length_bytes[1], length_bytes[2], length_bytes[3]]) as usize; if length > crate::MAX_METADATA_STRING_BYTES { return std::result::Result::Err(format!( "{label} exceeds {} bytes", crate::MAX_METADATA_STRING_BYTES )); } let start = length_end; let end = start.saturating_add(length); let value = match bytes.get(start..end) { std::option::Option::Some(value) => value, std::option::Option::None => return std::result::Result::Err(format!("truncated {label}")), }; let text = match std::str::from_utf8(value) { std::result::Result::Ok(value) => value.to_string(), std::result::Result::Err(_) => { return std::result::Result::Err(format!("{label} is not valid UTF-8")); }, }; *offset = end; return std::result::Result::Ok(text); } fn read_nullable_pubkey( bytes: &[u8], offset: &mut usize, label: &str, ) -> std::result::Result { let end = offset.saturating_add(32); let value = match bytes.get(*offset..end) { std::option::Option::Some(value) => value, std::option::Option::None => return std::result::Result::Err(format!("truncated {label}")), }; *offset = end; if value.iter().all(|byte| return *byte == 0) { return std::result::Result::Ok(serde_json::Value::Null); } return std::result::Result::Ok(serde_json::Value::String(bs58::encode(value).into_string())); } fn read_borsh_option_u64( bytes: &[u8], offset: &mut usize, label: &str, ) -> std::result::Result { let tag = match read_u8(bytes, offset, label) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; return match tag { 0 => std::result::Result::Ok(serde_json::Value::Null), 1 => { let value = match read_u64(bytes, offset, label) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; std::result::Result::Ok(serde_json::Value::String(value.to_string())) }, value => std::result::Result::Err(format!("invalid Borsh option tag {value} for {label}")), }; } fn parse_reallocate( bytes: &[u8], code: &'static str, tag: u8, ) -> std::result::Result { if bytes.is_empty() || bytes.len().saturating_sub(1) % 2 != 0 { return std::result::Result::Err(format!( "{code} requires a complete u16 extension type sequence" )); } let mut extension_types = std::vec::Vec::new(); for chunk in bytes[1..].chunks_exact(2) { extension_types.push(u16::from_le_bytes([chunk[0], chunk[1]])); } return std::result::Result::Ok(ParsedInstruction { code, tag, parameters: serde_json::json!({"extensionTypes": extension_types}), consumed: bytes.len(), }); } fn parse_extension_envelope( bytes: &[u8], code: &'static str, tag: u8, ) -> std::result::Result { return match tag { 26 => parse_transfer_fee_instruction(bytes, tag), 27 => parse_confidential_transfer_instruction(bytes, tag), 28 => parse_default_account_state_instruction(bytes, tag), 30 => parse_toggle_extension_instruction( bytes, tag, "memo_transfer", "enable_required_transfer_memos", "disable_required_transfer_memos", ), 33 => parse_interest_bearing_instruction(bytes, tag), 34 => parse_toggle_extension_instruction( bytes, tag, "cpi_guard", "enable_cpi_guard", "disable_cpi_guard", ), 36 => parse_pointer_extension_instruction( bytes, tag, "transfer_hook", "initialize_transfer_hook", "update_transfer_hook_program_id", "authority", "programId", ), 37 => parse_confidential_transfer_fee_instruction(bytes, tag), 39 => parse_pointer_extension_instruction( bytes, tag, "metadata_pointer", "initialize_metadata_pointer", "update_metadata_pointer", "authority", "metadataAddress", ), 40 => parse_pointer_extension_instruction( bytes, tag, "group_pointer", "initialize_group_pointer", "update_group_pointer", "authority", "groupAddress", ), 41 => parse_pointer_extension_instruction( bytes, tag, "group_member_pointer", "initialize_group_member_pointer", "update_group_member_pointer", "authority", "memberAddress", ), 42 => parse_confidential_mint_burn_instruction(bytes, tag), 43 => parse_scaled_ui_amount_instruction(bytes, tag), 44 => parse_pausable_instruction(bytes, tag), 46 => parse_permissioned_burn_instruction(bytes, tag), _ => parse_opaque_extension_envelope(bytes, code, tag), }; } fn parse_confidential_transfer_instruction( bytes: &[u8], tag: u8, ) -> std::result::Result { let sub = match bytes.get(1).copied() { std::option::Option::Some(value) => value, std::option::Option::None => { return std::result::Result::Err( "confidential transfer instruction requires a sub-discriminant".to_string(), ); }, }; let parsed = match sub { 0 => { if bytes.len() != 67 { return std::result::Result::Err(format!( "initialize_confidential_transfer_mint requires exactly 67 bytes; received {}", bytes.len() )); } let auto_approve = match bytes[34] { 0 => false, 1 => true, value => { return std::result::Result::Err(format!( "initialize_confidential_transfer_mint has invalid Bool value {value}" )); }, }; ParsedInstruction { code: "initialize_confidential_transfer_mint", tag, parameters: serde_json::json!({ "extension": "confidential_transfer", "subDiscriminant": sub, "authority": optional_nonzero_pubkey(&bytes[2..34]), "autoApproveNewAccounts": auto_approve, "auditorElgamalPubkeyBase64": base64::engine::general_purpose::STANDARD.encode(&bytes[35..67]), "auditorElgamalPubkeyPresent": bytes[35..67].iter().any(|value| return *value != 0), }), consumed: 67, } }, 1 => { if bytes.len() != 35 { return std::result::Result::Err(format!( "update_confidential_transfer_mint requires exactly 35 bytes; received {}", bytes.len() )); } let auto_approve = match bytes[2] { 0 => false, 1 => true, value => { return std::result::Result::Err(format!( "update_confidential_transfer_mint has invalid Bool value {value}" )); }, }; ParsedInstruction { code: "update_confidential_transfer_mint", tag, parameters: serde_json::json!({ "extension": "confidential_transfer", "subDiscriminant": sub, "autoApproveNewAccounts": auto_approve, "auditorElgamalPubkeyBase64": base64::engine::general_purpose::STANDARD.encode(&bytes[3..35]), "auditorElgamalPubkeyPresent": bytes[3..35].iter().any(|value| return *value != 0), }), consumed: 35, } }, 3 => ParsedInstruction { code: "approve_confidential_transfer_account", tag, parameters: serde_json::json!({"extension":"confidential_transfer","subDiscriminant":sub}), consumed: 2, }, 4 => { if bytes.len() != 3 { return std::result::Result::Err(format!( "empty_confidential_transfer_account requires exactly 3 bytes; received {}", bytes.len() )); } ParsedInstruction { code: "empty_confidential_transfer_account", tag, parameters: serde_json::json!({ "extension":"confidential_transfer", "subDiscriminant":sub, "proofInstructionOffset": i8::from_le_bytes([bytes[2]]), "proofLocation": if bytes[2] == 0 { "context_state_account" } else { "instruction_offset" }, }), consumed: 3, } }, 5 => { if bytes.len() != 11 { return std::result::Result::Err(format!( "deposit_confidential_tokens requires exactly 11 bytes; received {}", bytes.len() )); } let amount = u64::from_le_bytes([ bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7], bytes[8], bytes[9], ]); ParsedInstruction { code: "deposit_confidential_tokens", tag, parameters: serde_json::json!({ "extension":"confidential_transfer", "subDiscriminant":sub, "amountRaw": amount.to_string(), "decimals": bytes[10], }), consumed: 11, } }, 8 => { if bytes.len() != 46 { return std::result::Result::Err(format!( "apply_confidential_pending_balance requires exactly 46 bytes; received {}", bytes.len() )); } let counter = u64::from_le_bytes([ bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7], bytes[8], bytes[9], ]); ParsedInstruction { code: "apply_confidential_pending_balance", tag, parameters: serde_json::json!({ "extension":"confidential_transfer", "subDiscriminant":sub, "expectedPendingBalanceCreditCounter": counter.to_string(), "newDecryptableAvailableBalanceBase64": base64::engine::general_purpose::STANDARD.encode(&bytes[10..46]), }), consumed: 46, } }, 9 | 10 | 11 | 12 => { if bytes.len() != 2 { return std::result::Result::Err(format!( "confidential credit toggle requires exactly 2 bytes; received {}", bytes.len() )); } let (code, credit_kind, enabled) = match sub { 9 => ("enable_confidential_credits", "confidential", true), 10 => ("disable_confidential_credits", "confidential", false), 11 => ("enable_non_confidential_credits", "non_confidential", true), 12 => ("disable_non_confidential_credits", "non_confidential", false), _ => { return std::result::Result::Err("unreachable confidential toggle".to_string()); }, }; ParsedInstruction { code, tag, parameters: serde_json::json!({ "extension":"confidential_transfer", "subDiscriminant":sub, "creditKind": credit_kind, "enabled": enabled, }), consumed: 2, } }, 2 => { if bytes.len() != 47 { return std::result::Result::Err(format!( "configure_confidential_transfer_account requires exactly 47 bytes; received {}", bytes.len() )); } let maximum_pending_balance_credit_counter = u64::from_le_bytes([ bytes[38], bytes[39], bytes[40], bytes[41], bytes[42], bytes[43], bytes[44], bytes[45], ]); ParsedInstruction { code: "configure_confidential_transfer_account", tag, parameters: serde_json::json!({ "extension":"confidential_transfer", "subDiscriminant":sub, "decryptableZeroBalanceBase64":base64::engine::general_purpose::STANDARD.encode(&bytes[2..38]), "maximumPendingBalanceCreditCounter":maximum_pending_balance_credit_counter.to_string(), "proofInstructionOffset":i8::from_le_bytes([bytes[46]]), "proofLocation":if bytes[46] == 0 { "context_state_account" } else { "instruction_offset" }, }), consumed: 47, } }, 6 => { if bytes.len() != 49 { return std::result::Result::Err(format!( "withdraw_confidential_tokens requires exactly 49 bytes; received {}", bytes.len() )); } let amount = u64::from_le_bytes([ bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7], bytes[8], bytes[9], ]); ParsedInstruction { code: "withdraw_confidential_tokens", tag, parameters: serde_json::json!({ "extension":"confidential_transfer", "subDiscriminant":sub, "amountRaw":amount.to_string(), "decimals":bytes[10], "newDecryptableAvailableBalanceBase64":base64::engine::general_purpose::STANDARD.encode(&bytes[11..47]), "equalityProofInstructionOffset":i8::from_le_bytes([bytes[47]]), "rangeProofInstructionOffset":i8::from_le_bytes([bytes[48]]), }), consumed: 49, } }, 7 => { if bytes.len() != 169 { return std::result::Result::Err(format!( "transfer_confidential_tokens requires exactly 169 bytes; received {}", bytes.len() )); } ParsedInstruction { code: "transfer_confidential_tokens", tag, parameters: serde_json::json!({ "extension":"confidential_transfer", "subDiscriminant":sub, "newSourceDecryptableAvailableBalanceBase64":base64::engine::general_purpose::STANDARD.encode(&bytes[2..38]), "transferAmountAuditorCiphertextLoBase64":base64::engine::general_purpose::STANDARD.encode(&bytes[38..102]), "transferAmountAuditorCiphertextHiBase64":base64::engine::general_purpose::STANDARD.encode(&bytes[102..166]), "equalityProofInstructionOffset":i8::from_le_bytes([bytes[166]]), "ciphertextValidityProofInstructionOffset":i8::from_le_bytes([bytes[167]]), "rangeProofInstructionOffset":i8::from_le_bytes([bytes[168]]), }), consumed: 169, } }, 13 => { if bytes.len() != 171 { return std::result::Result::Err(format!( "transfer_confidential_tokens_with_fee requires exactly 171 bytes; received {}", bytes.len() )); } ParsedInstruction { code: "transfer_confidential_tokens_with_fee", tag, parameters: serde_json::json!({ "extension":"confidential_transfer", "subDiscriminant":sub, "newSourceDecryptableAvailableBalanceBase64":base64::engine::general_purpose::STANDARD.encode(&bytes[2..38]), "transferAmountAuditorCiphertextLoBase64":base64::engine::general_purpose::STANDARD.encode(&bytes[38..102]), "transferAmountAuditorCiphertextHiBase64":base64::engine::general_purpose::STANDARD.encode(&bytes[102..166]), "equalityProofInstructionOffset":i8::from_le_bytes([bytes[166]]), "transferAmountCiphertextValidityProofInstructionOffset":i8::from_le_bytes([bytes[167]]), "feeSigmaProofInstructionOffset":i8::from_le_bytes([bytes[168]]), "feeCiphertextValidityProofInstructionOffset":i8::from_le_bytes([bytes[169]]), "rangeProofInstructionOffset":i8::from_le_bytes([bytes[170]]), }), consumed: 171, } }, 14 => { if bytes.len() != 2 { return std::result::Result::Err(format!( "configure_confidential_transfer_account_with_registry requires exactly 2 bytes; received {}", bytes.len() )); } ParsedInstruction { code: "configure_confidential_transfer_account_with_registry", tag, parameters: serde_json::json!({ "extension":"confidential_transfer", "subDiscriminant":sub, "proofLocation":"elgamal_registry_account", "ownerSignatureRequired":false, }), consumed: 2, } }, value => { return std::result::Result::Err(format!( "unknown confidential transfer sub-discriminant {value}" )); }, }; if bytes.len() != parsed.consumed { return std::result::Result::Err(format!( "{} requires exactly {} bytes; received {}", parsed.code, parsed.consumed, bytes.len() )); } return std::result::Result::Ok(parsed); } fn parse_confidential_transfer_fee_instruction( bytes: &[u8], tag: u8, ) -> std::result::Result { let sub = match bytes.get(1).copied() { std::option::Option::Some(value) => value, std::option::Option::None => { return std::result::Result::Err( "confidential transfer fee instruction requires a sub-discriminant".to_string(), ); }, }; let parsed = match sub { 0 => { if bytes.len() != 66 { return std::result::Result::Err(format!( "initialize_confidential_transfer_fee_config requires exactly 66 bytes; received {}", bytes.len() )); } ParsedInstruction { code: "initialize_confidential_transfer_fee_config", tag, parameters: serde_json::json!({ "extension":"confidential_transfer_fee", "subDiscriminant":sub, "authority":optional_nonzero_pubkey(&bytes[2..34]), "withdrawWithheldAuthorityElgamalPubkeyBase64":base64::engine::general_purpose::STANDARD.encode(&bytes[34..66]), }), consumed: 66, } }, 1 => { if bytes.len() != 39 { return std::result::Result::Err(format!( "withdraw_confidential_withheld_tokens_from_mint requires exactly 39 bytes; received {}", bytes.len() )); } ParsedInstruction { code: "withdraw_confidential_withheld_tokens_from_mint", tag, parameters: serde_json::json!({ "extension":"confidential_transfer_fee", "subDiscriminant":sub, "proofInstructionOffset":i8::from_le_bytes([bytes[2]]), "proofLocation":if bytes[2] == 0 { "context_state_account" } else { "instruction_offset" }, "newDecryptableAvailableBalanceBase64":base64::engine::general_purpose::STANDARD.encode(&bytes[3..39]), }), consumed: 39, } }, 2 => { if bytes.len() != 40 { return std::result::Result::Err(format!( "withdraw_confidential_withheld_tokens_from_accounts requires exactly 40 bytes; received {}", bytes.len() )); } ParsedInstruction { code: "withdraw_confidential_withheld_tokens_from_accounts", tag, parameters: serde_json::json!({ "extension":"confidential_transfer_fee", "subDiscriminant":sub, "numTokenAccounts":bytes[2], "proofInstructionOffset":i8::from_le_bytes([bytes[3]]), "proofLocation":if bytes[3] == 0 { "context_state_account" } else { "instruction_offset" }, "newDecryptableAvailableBalanceBase64":base64::engine::general_purpose::STANDARD.encode(&bytes[4..40]), }), consumed: 40, } }, 3 | 4 | 5 => { if bytes.len() != 2 { return std::result::Result::Err(format!( "confidential transfer fee toggle or harvest instruction requires exactly 2 bytes; received {}", bytes.len() )); } let code = match sub { 3 => "harvest_confidential_withheld_tokens_to_mint", 4 => "enable_confidential_harvest_to_mint", 5 => "disable_confidential_harvest_to_mint", _ => { return std::result::Result::Err( "unreachable confidential transfer fee leaf".to_string(), ); }, }; ParsedInstruction { code, tag, parameters: serde_json::json!({ "extension":"confidential_transfer_fee", "subDiscriminant":sub, }), consumed: 2, } }, value => { return std::result::Result::Err(format!( "unknown confidential transfer fee sub-discriminant {value}" )); }, }; if bytes.len() != parsed.consumed { return std::result::Result::Err(format!( "{} requires exactly {} bytes; received {}", parsed.code, parsed.consumed, bytes.len() )); } return std::result::Result::Ok(parsed); } fn parse_confidential_mint_burn_instruction( bytes: &[u8], tag: u8, ) -> std::result::Result { let sub = match bytes.get(1).copied() { std::option::Option::Some(value) => value, std::option::Option::None => { return std::result::Result::Err( "confidential mint/burn instruction requires a sub-discriminant".to_string(), ); }, }; let expected = match sub { 0 => 70, 1 => 35, 2 => 38, 3 | 4 => 169, 5 => 2, value => { return std::result::Result::Err(format!( "unknown confidential mint/burn sub-discriminant {value}" )); }, }; if bytes.len() != expected { return std::result::Result::Err(format!( "confidential mint/burn sub-discriminant {sub} requires exactly {expected} bytes; received {}", bytes.len() )); } return match sub { 0 => std::result::Result::Ok(ParsedInstruction { code: "initialize_confidential_mint_burn", tag, parameters: serde_json::json!({ "extension":"confidential_mint_burn", "subDiscriminant":sub, "supplyElgamalPubkeyBase64":base64::engine::general_purpose::STANDARD.encode(&bytes[2..34]), "decryptableSupplyBase64":base64::engine::general_purpose::STANDARD.encode(&bytes[34..70]), }), consumed: 70, }), 1 => std::result::Result::Ok(ParsedInstruction { code: "rotate_confidential_supply_elgamal_pubkey", tag, parameters: serde_json::json!({ "extension":"confidential_mint_burn", "subDiscriminant":sub, "newSupplyElgamalPubkeyBase64":base64::engine::general_purpose::STANDARD.encode(&bytes[2..34]), "proofInstructionOffset":i8::from_le_bytes([bytes[34]]), "proofLocation":if bytes[34] == 0 { "context_state_account" } else { "instruction_offset" }, }), consumed: 35, }), 2 => std::result::Result::Ok(ParsedInstruction { code: "update_confidential_decryptable_supply", tag, parameters: serde_json::json!({ "extension":"confidential_mint_burn", "subDiscriminant":sub, "newDecryptableSupplyBase64":base64::engine::general_purpose::STANDARD.encode(&bytes[2..38]), }), consumed: 38, }), 3 | 4 => std::result::Result::Ok(ParsedInstruction { code: if sub == 3 { "confidential_mint" } else { "confidential_burn" }, tag, parameters: serde_json::json!({ "extension":"confidential_mint_burn", "subDiscriminant":sub, "newDecryptableBalanceBase64":base64::engine::general_purpose::STANDARD.encode(&bytes[2..38]), "amountAuditorCiphertextLoBase64":base64::engine::general_purpose::STANDARD.encode(&bytes[38..102]), "amountAuditorCiphertextHiBase64":base64::engine::general_purpose::STANDARD.encode(&bytes[102..166]), "equalityProofInstructionOffset":i8::from_le_bytes([bytes[166]]), "ciphertextValidityProofInstructionOffset":i8::from_le_bytes([bytes[167]]), "rangeProofInstructionOffset":i8::from_le_bytes([bytes[168]]), }), consumed: 169, }), 5 => std::result::Result::Ok(ParsedInstruction { code: "apply_pending_confidential_burn", tag, parameters: serde_json::json!({ "extension":"confidential_mint_burn", "subDiscriminant":sub, }), consumed: 2, }), _ => std::result::Result::Err( "unreachable confidential mint/burn sub-discriminant".to_string(), ), }; } fn parse_scaled_ui_amount_instruction( bytes: &[u8], tag: u8, ) -> std::result::Result { let sub = match bytes.get(1).copied() { std::option::Option::Some(value) => value, std::option::Option::None => { return std::result::Result::Err( "scaled UI amount instruction requires a sub-discriminant".to_string(), ); }, }; return match sub { 0 => { if bytes.len() != 42 { return std::result::Result::Err(format!( "initialize_scaled_ui_amount requires exactly 42 bytes; received {}", bytes.len() )); } let multiplier_bits = u64::from_le_bytes([ bytes[34], bytes[35], bytes[36], bytes[37], bytes[38], bytes[39], bytes[40], bytes[41], ]); std::result::Result::Ok(ParsedInstruction { code: "initialize_scaled_ui_amount", tag, parameters: serde_json::json!({ "extension": "scaled_ui_amount", "subDiscriminant": sub, "authority": optional_nonzero_pubkey(&bytes[2..34]), "multiplierBitsHex": format!("{multiplier_bits:016x}"), "multiplierFiniteDecimal": finite_f64_decimal(multiplier_bits), }), consumed: 42, }) }, 1 => { if bytes.len() != 18 { return std::result::Result::Err(format!( "update_scaled_ui_amount_multiplier requires exactly 18 bytes; received {}", bytes.len() )); } let multiplier_bits = u64::from_le_bytes([ bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7], bytes[8], bytes[9], ]); let effective_timestamp = i64::from_le_bytes([ bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15], bytes[16], bytes[17], ]); std::result::Result::Ok(ParsedInstruction { code: "update_scaled_ui_amount_multiplier", tag, parameters: serde_json::json!({ "extension": "scaled_ui_amount", "subDiscriminant": sub, "multiplierBitsHex": format!("{multiplier_bits:016x}"), "multiplierFiniteDecimal": finite_f64_decimal(multiplier_bits), "effectiveTimestamp": effective_timestamp.to_string(), }), consumed: 18, }) }, value => { std::result::Result::Err(format!("unknown scaled UI amount sub-discriminant {value}")) }, }; } fn parse_pausable_instruction( bytes: &[u8], tag: u8, ) -> std::result::Result { let sub = match bytes.get(1).copied() { std::option::Option::Some(value) => value, std::option::Option::None => { return std::result::Result::Err( "pausable instruction requires a sub-discriminant".to_string(), ); }, }; return match sub { 0 => { if bytes.len() != 34 { return std::result::Result::Err(format!( "initialize_pausable_config requires exactly 34 bytes; received {}", bytes.len() )); } std::result::Result::Ok(ParsedInstruction { code: "initialize_pausable_config", tag, parameters: serde_json::json!({ "extension": "pausable", "subDiscriminant": sub, "pauseAuthority": bs58::encode(&bytes[2..34]).into_string(), }), consumed: 34, }) }, 1 | 2 => { if bytes.len() != 2 { return std::result::Result::Err(format!( "{} requires exactly 2 bytes; received {}", if sub == 1 { "pause_token_mint" } else { "resume_token_mint" }, bytes.len() )); } std::result::Result::Ok(ParsedInstruction { code: if sub == 1 { "pause_token_mint" } else { "resume_token_mint" }, tag, parameters: serde_json::json!({ "extension": "pausable", "subDiscriminant": sub, "paused": sub == 1, }), consumed: 2, }) }, value => std::result::Result::Err(format!("unknown pausable sub-discriminant {value}")), }; } fn parse_permissioned_burn_instruction( bytes: &[u8], tag: u8, ) -> std::result::Result { let sub = match bytes.get(1).copied() { std::option::Option::Some(value) => value, std::option::Option::None => { return std::result::Result::Err( "permissioned burn instruction requires a sub-discriminant".to_string(), ); }, }; return match sub { 0 => { if bytes.len() != 34 { return std::result::Result::Err(format!( "initialize_permissioned_burn requires exactly 34 bytes; received {}", bytes.len() )); } std::result::Result::Ok(ParsedInstruction { code: "initialize_permissioned_burn", tag, parameters: serde_json::json!({ "extension": "permissioned_burn", "subDiscriminant": sub, "permissionedBurnAuthority": bs58::encode(&bytes[2..34]).into_string(), }), consumed: 34, }) }, 1 => { if bytes.len() != 10 { return std::result::Result::Err(format!( "permissioned_burn requires exactly 10 bytes; received {}", bytes.len() )); } let amount = match read_u64_at(bytes, 2) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; std::result::Result::Ok(ParsedInstruction { code: "permissioned_burn", tag, parameters: serde_json::json!({ "extension": "permissioned_burn", "subDiscriminant": sub, "amountRaw": amount.to_string(), }), consumed: 10, }) }, 2 => { if bytes.len() != 11 { return std::result::Result::Err(format!( "permissioned_burn_checked requires exactly 11 bytes; received {}", bytes.len() )); } let amount = match read_u64_at(bytes, 2) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; std::result::Result::Ok(ParsedInstruction { code: "permissioned_burn_checked", tag, parameters: serde_json::json!({ "extension": "permissioned_burn", "subDiscriminant": sub, "amountRaw": amount.to_string(), "decimals": bytes[10], }), consumed: 11, }) }, 3 => { if bytes.len() != 169 { return std::result::Result::Err(format!( "permissioned_confidential_burn requires exactly 169 bytes; received {}", bytes.len() )); } std::result::Result::Ok(ParsedInstruction { code: "permissioned_confidential_burn", tag, parameters: serde_json::json!({ "extension":"permissioned_burn", "subDiscriminant":sub, "newDecryptableAvailableBalanceBase64":base64::engine::general_purpose::STANDARD.encode(&bytes[2..38]), "burnAmountAuditorCiphertextLoBase64":base64::engine::general_purpose::STANDARD.encode(&bytes[38..102]), "burnAmountAuditorCiphertextHiBase64":base64::engine::general_purpose::STANDARD.encode(&bytes[102..166]), "equalityProofInstructionOffset":i8::from_le_bytes([bytes[166]]), "ciphertextValidityProofInstructionOffset":i8::from_le_bytes([bytes[167]]), "rangeProofInstructionOffset":i8::from_le_bytes([bytes[168]]), }), consumed: 169, }) }, value => { std::result::Result::Err(format!("unknown permissioned burn sub-discriminant {value}")) }, }; } fn finite_f64_decimal(bits: u64) -> serde_json::Value { let value = f64::from_bits(bits); if value.is_finite() { return serde_json::Value::String(value.to_string()); } return serde_json::Value::Null; } fn parse_interest_bearing_instruction( bytes: &[u8], tag: u8, ) -> std::result::Result { let sub = match bytes.get(1).copied() { std::option::Option::Some(value) => value, std::option::Option::None => { return std::result::Result::Err( "interest-bearing instruction requires a sub-discriminant".to_string(), ); }, }; return match sub { 0 => { if bytes.len() != 36 { return std::result::Result::Err(format!( "initialize_interest_bearing_mint requires exactly 36 bytes; received {}", bytes.len() )); } let authority_bytes = &bytes[2..34]; let authority = if authority_bytes.iter().all(|value| return *value == 0) { serde_json::Value::Null } else { serde_json::Value::String(bs58::encode(authority_bytes).into_string()) }; let rate = i16::from_le_bytes([bytes[34], bytes[35]]); std::result::Result::Ok(ParsedInstruction { code: "initialize_interest_bearing_mint", tag, parameters: serde_json::json!({ "subDiscriminant": sub, "rateAuthority": authority, "rateBasisPoints": rate, }), consumed: 36, }) }, 1 => { if bytes.len() != 4 { return std::result::Result::Err(format!( "update_interest_bearing_rate requires exactly 4 bytes; received {}", bytes.len() )); } let rate = i16::from_le_bytes([bytes[2], bytes[3]]); std::result::Result::Ok(ParsedInstruction { code: "update_interest_bearing_rate", tag, parameters: serde_json::json!({ "subDiscriminant": sub, "rateBasisPoints": rate, }), consumed: 4, }) }, _ => std::result::Result::Err(format!("unknown interest-bearing sub-discriminant {sub}")), }; } fn parse_transfer_fee_instruction( bytes: &[u8], tag: u8, ) -> std::result::Result { let sub = match bytes.get(1).copied() { std::option::Option::Some(value) => value, std::option::Option::None => { return std::result::Result::Err( "transfer fee instruction requires a sub-discriminant".to_string(), ); }, }; let parsed = match sub { 0 => { let (config_authority, first_length) = match pubkey_option(&bytes[2..]) { std::result::Result::Ok(value) => value, std::result::Result::Err(message) => return std::result::Result::Err(message), }; let second_offset = 2_usize.saturating_add(first_length); let (withdraw_authority, second_length) = match pubkey_option(&bytes[second_offset..]) { std::result::Result::Ok(value) => value, std::result::Result::Err(message) => return std::result::Result::Err(message), }; let basis_offset = second_offset.saturating_add(second_length); let basis_points = match read_u16(bytes, basis_offset) { std::result::Result::Ok(value) => value, std::result::Result::Err(message) => return std::result::Result::Err(message), }; let maximum_fee = match read_u64_at(bytes, basis_offset.saturating_add(2)) { std::result::Result::Ok(value) => value, std::result::Result::Err(message) => return std::result::Result::Err(message), }; ParsedInstruction { code: "initialize_transfer_fee_config", tag, parameters: serde_json::json!({ "extension": "transfer_fee", "subDiscriminant": sub, "transferFeeConfigAuthority": config_authority, "withdrawWithheldAuthority": withdraw_authority, "transferFeeBasisPoints": basis_points, "maximumFeeRaw": maximum_fee.to_string(), }), consumed: basis_offset.saturating_add(10), } }, 1 => { let amount = match read_u64_at(bytes, 2) { std::result::Result::Ok(value) => value, std::result::Result::Err(message) => return std::result::Result::Err(message), }; let decimals = match bytes.get(10).copied() { std::option::Option::Some(value) => value, std::option::Option::None => { return std::result::Result::Err( "transfer_checked_with_fee requires decimals".to_string(), ); }, }; let fee = match read_u64_at(bytes, 11) { std::result::Result::Ok(value) => value, std::result::Result::Err(message) => return std::result::Result::Err(message), }; ParsedInstruction { code: "transfer_checked_with_fee", tag, parameters: serde_json::json!({ "extension": "transfer_fee", "subDiscriminant": sub, "amountRaw": amount.to_string(), "decimals": decimals, "feeRaw": fee.to_string(), }), consumed: 19, } }, 2 => ParsedInstruction { code: "withdraw_withheld_tokens_from_mint", tag, parameters: serde_json::json!({"extension":"transfer_fee","subDiscriminant":sub}), consumed: 2, }, 3 => { let count = match bytes.get(2).copied() { std::option::Option::Some(value) => value, std::option::Option::None => { return std::result::Result::Err( "withdraw_withheld_tokens_from_accounts requires a count".to_string(), ); }, }; ParsedInstruction { code: "withdraw_withheld_tokens_from_accounts", tag, parameters: serde_json::json!({ "extension": "transfer_fee", "subDiscriminant": sub, "tokenAccountCount": count, }), consumed: 3, } }, 4 => ParsedInstruction { code: "harvest_withheld_tokens_to_mint", tag, parameters: serde_json::json!({"extension":"transfer_fee","subDiscriminant":sub}), consumed: 2, }, 5 => { let basis_points = match read_u16(bytes, 2) { std::result::Result::Ok(value) => value, std::result::Result::Err(message) => return std::result::Result::Err(message), }; let maximum_fee = match read_u64_at(bytes, 4) { std::result::Result::Ok(value) => value, std::result::Result::Err(message) => return std::result::Result::Err(message), }; ParsedInstruction { code: "set_transfer_fee", tag, parameters: serde_json::json!({ "extension": "transfer_fee", "subDiscriminant": sub, "transferFeeBasisPoints": basis_points, "maximumFeeRaw": maximum_fee.to_string(), }), consumed: 12, } }, value => { return std::result::Result::Err(format!( "unknown transfer fee sub-discriminant {value}" )); }, }; if bytes.len() < parsed.consumed { return std::result::Result::Err(format!("{} payload is truncated", parsed.code)); } return std::result::Result::Ok(parsed); } fn parse_default_account_state_instruction( bytes: &[u8], tag: u8, ) -> std::result::Result { if bytes.len() < 3 { return std::result::Result::Err( "default account state instruction requires 3 bytes".to_string(), ); } let code = match bytes[1] { 0 => "initialize_default_account_state", 1 => "update_default_account_state", value => { return std::result::Result::Err(format!( "unknown default account state sub-discriminant {value}" )); }, }; let state = match bytes[2] { 0 => "uninitialized", 1 => "initialized", 2 => "frozen", value => return std::result::Result::Err(format!("unknown token account state {value}")), }; if bytes.len() != 3 { return std::result::Result::Err(format!("{code} requires exactly 3 bytes")); } return std::result::Result::Ok(ParsedInstruction { code, tag, parameters: serde_json::json!({ "extension": "default_account_state", "subDiscriminant": bytes[1], "accountState": state, "accountStateCode": bytes[2], }), consumed: 3, }); } fn parse_toggle_extension_instruction( bytes: &[u8], tag: u8, extension: &str, enable_code: &'static str, disable_code: &'static str, ) -> std::result::Result { if bytes.len() != 2 { return std::result::Result::Err(format!( "{extension} instruction requires exactly 2 bytes" )); } let code = match bytes[1] { 0 => enable_code, 1 => disable_code, value => { return std::result::Result::Err(format!( "unknown {extension} sub-discriminant {value}" )); }, }; return std::result::Result::Ok(ParsedInstruction { code, tag, parameters: serde_json::json!({ "extension": extension, "subDiscriminant": bytes[1], "enabled": bytes[1] == 0, }), consumed: 2, }); } fn parse_pointer_extension_instruction( bytes: &[u8], tag: u8, extension: &str, initialize_code: &'static str, update_code: &'static str, authority_field: &str, address_field: &str, ) -> std::result::Result { let sub = match bytes.get(1).copied() { std::option::Option::Some(value) => value, std::option::Option::None => { return std::result::Result::Err(format!( "{extension} instruction requires a sub-discriminant" )); }, }; let (code, authority, address, consumed) = match sub { 0 => { if bytes.len() != 66 { return std::result::Result::Err(format!( "{initialize_code} requires exactly 66 bytes; received {}", bytes.len() )); } ( initialize_code, optional_nonzero_pubkey(&bytes[2..34]), optional_nonzero_pubkey(&bytes[34..66]), 66_usize, ) }, 1 => { if bytes.len() != 34 { return std::result::Result::Err(format!( "{update_code} requires exactly 34 bytes; received {}", bytes.len() )); } ( update_code, serde_json::Value::Null, optional_nonzero_pubkey(&bytes[2..34]), 34_usize, ) }, value => { return std::result::Result::Err(format!( "unknown {extension} sub-discriminant {value}" )); }, }; let mut parameters = serde_json::Map::new(); parameters.insert("extension".to_string(), serde_json::Value::String(extension.to_string())); parameters.insert("subDiscriminant".to_string(), serde_json::json!(sub)); parameters.insert(address_field.to_string(), address); if sub == 0 { parameters.insert(authority_field.to_string(), authority); } return std::result::Result::Ok(ParsedInstruction { code, tag, parameters: serde_json::Value::Object(parameters), consumed, }); } fn optional_nonzero_pubkey(bytes: &[u8]) -> serde_json::Value { if bytes.len() != 32 || bytes.iter().all(|value| return *value == 0) { return serde_json::Value::Null; } return serde_json::Value::String(bs58::encode(bytes).into_string()); } fn parse_opaque_extension_envelope( bytes: &[u8], code: &'static str, tag: u8, ) -> std::result::Result { let sub_discriminant = match bytes.get(1).copied() { std::option::Option::Some(value) => value, std::option::Option::None => { return std::result::Result::Err(format!( "{code} requires an extension sub-discriminant" )); }, }; let opaque = &bytes[2..]; return std::result::Result::Ok(ParsedInstruction { code, tag, parameters: serde_json::json!({ "subDiscriminant": sub_discriminant, "opaquePayloadLength": opaque.len(), "opaquePayloadPrefixHex": hex_prefix(opaque), "semanticStatus": "opaque_until_extension_layout_audit" }), consumed: bytes.len(), }); } fn parse_initialize_mint( bytes: &[u8], code: &'static str, tag: u8, ) -> std::result::Result { if bytes.len() < 35 { return std::result::Result::Err(format!("{code} requires at least 35 bytes")); } let mint_authority = pubkey_text(&bytes[2..34]); let (freeze_authority, option_length) = match pubkey_option(&bytes[34..]) { std::result::Result::Ok(value) => value, std::result::Result::Err(message) => return std::result::Result::Err(message), }; return std::result::Result::Ok(ParsedInstruction { code, tag, parameters: serde_json::json!({ "decimals": bytes[1], "mintAuthority": mint_authority, "freezeAuthority": freeze_authority, }), consumed: 34 + option_length, }); } fn parse_one_u8( bytes: &[u8], code: &'static str, tag: u8, field: &str, ) -> std::result::Result { if bytes.len() < 2 { return std::result::Result::Err(format!("{code} requires 2 bytes")); } return std::result::Result::Ok(ParsedInstruction { code, tag, parameters: one_parameter(field, serde_json::Value::from(bytes[1])), consumed: 2, }); } fn parse_one_u64( bytes: &[u8], code: &'static str, tag: u8, field: &str, ) -> std::result::Result { let amount = match read_u64_at(bytes, 1) { std::result::Result::Ok(value) => value, std::result::Result::Err(message) => return std::result::Result::Err(message), }; return std::result::Result::Ok(ParsedInstruction { code, tag, parameters: one_parameter(field, serde_json::Value::String(amount.to_string())), consumed: 9, }); } fn parse_set_authority( bytes: &[u8], code: &'static str, tag: u8, ) -> std::result::Result { if bytes.len() < 3 { return std::result::Result::Err("set_authority requires at least 3 bytes".to_string()); } let authority_type = match bytes[1] { 0 => "mint_tokens", 1 => "freeze_account", 2 => "account_owner", 3 => "close_account", value => return std::result::Result::Err(format!("invalid authority type {value}")), }; let (new_authority, option_length) = match pubkey_option(&bytes[2..]) { std::result::Result::Ok(value) => value, std::result::Result::Err(message) => return std::result::Result::Err(message), }; return std::result::Result::Ok(ParsedInstruction { code, tag, parameters: serde_json::json!({ "authorityType": authority_type, "newAuthority": new_authority, }), consumed: 2 + option_length, }); } fn parse_amount_decimals( bytes: &[u8], code: &'static str, tag: u8, ) -> std::result::Result { if bytes.len() < 10 { return std::result::Result::Err(format!("{code} requires 10 bytes")); } let amount = match read_u64_at(bytes, 1) { std::result::Result::Ok(value) => value, std::result::Result::Err(message) => return std::result::Result::Err(message), }; return std::result::Result::Ok(ParsedInstruction { code, tag, parameters: serde_json::json!({ "amountRaw": amount.to_string(), "decimals": bytes[9], }), consumed: 10, }); } fn parse_pubkey_parameter( bytes: &[u8], code: &'static str, tag: u8, field: &str, ) -> std::result::Result { if bytes.len() < 33 { return std::result::Result::Err(format!("{code} requires 33 bytes")); } return std::result::Result::Ok(ParsedInstruction { code, tag, parameters: one_parameter(field, serde_json::Value::String(pubkey_text(&bytes[1..33]))), consumed: 33, }); } fn parse_ui_amount( bytes: &[u8], code: &'static str, tag: u8, ) -> std::result::Result { let text = match std::str::from_utf8(&bytes[1..]) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { return std::result::Result::Err(format!( "ui amount is not UTF-8 at byte {}", error.valid_up_to() )); }, }; return std::result::Result::Ok(ParsedInstruction { code, tag, parameters: serde_json::json!({"uiAmount": text}), consumed: bytes.len(), }); } fn parse_unwrap_lamports( bytes: &[u8], code: &'static str, tag: u8, ) -> std::result::Result { if bytes.len() < 2 { return std::result::Result::Err("unwrap_lamports requires an option tag".to_string()); } let (amount, consumed) = match bytes[1] { 0 => (serde_json::Value::Null, 2), 1 => { let value = match read_u64_at(bytes, 2) { std::result::Result::Ok(value) => value, std::result::Result::Err(message) => return std::result::Result::Err(message), }; (serde_json::Value::String(value.to_string()), 10) }, value => return std::result::Result::Err(format!("invalid unwrap amount option {value}")), }; return std::result::Result::Ok(ParsedInstruction { code, tag, parameters: serde_json::json!({"amountLamports": amount}), consumed, }); } fn read_u16(bytes: &[u8], offset: usize) -> std::result::Result { let slice = match bytes.get(offset..offset.saturating_add(2)) { std::option::Option::Some(value) if value.len() == 2 => value, _ => return std::result::Result::Err("truncated little-endian u16".to_string()), }; return std::result::Result::Ok(u16::from_le_bytes([slice[0], slice[1]])); } fn read_u64_at(bytes: &[u8], offset: usize) -> std::result::Result { let slice = match bytes.get(offset..offset.saturating_add(8)) { std::option::Option::Some(value) if value.len() == 8 => value, _ => return std::result::Result::Err("truncated little-endian u64".to_string()), }; let array = match <[u8; 8]>::try_from(slice) { std::result::Result::Ok(value) => value, std::result::Result::Err(_error) => { return std::result::Result::Err("invalid little-endian u64".to_string()); }, }; return std::result::Result::Ok(u64::from_le_bytes(array)); } fn pubkey_option( bytes: &[u8], ) -> std::result::Result<(serde_json::Value, usize), std::string::String> { return match bytes.first().copied() { std::option::Option::Some(0) => std::result::Result::Ok((serde_json::Value::Null, 1)), std::option::Option::Some(1) if bytes.len() >= 33 => { std::result::Result::Ok((serde_json::Value::String(pubkey_text(&bytes[1..33])), 33)) }, std::option::Option::Some(1) => { std::result::Result::Err("truncated optional pubkey".to_string()) }, std::option::Option::Some(value) => { std::result::Result::Err(format!("invalid optional pubkey tag {value}")) }, std::option::Option::None => { std::result::Result::Err("missing optional pubkey tag".to_string()) }, }; } fn pubkey_text(bytes: &[u8]) -> std::string::String { return bs58::encode(bytes).into_string(); } fn one_parameter(field: &str, value: serde_json::Value) -> serde_json::Value { let mut parameters = serde_json::Map::new(); parameters.insert(field.to_string(), value); return serde_json::Value::Object(parameters); } fn resolve_accounts( input: &kb_store_core::CoreInstructionReplayInput, ) -> kb_core::Result> { let instruction_accounts = match input.instruction_accounts_json.as_array() { std::option::Option::Some(value) => value, std::option::Option::None => { return std::result::Result::Err(kb_core::Error::invalid_state( "Token-2022 instruction accounts must be a JSON array", )); }, }; let account_keys = match input.account_keys_json.as_array() { std::option::Option::Some(value) => value, std::option::Option::None => { return std::result::Result::Err(kb_core::Error::invalid_state( "Token-2022 transaction account keys must be a JSON array", )); }, }; let mut output = std::vec::Vec::with_capacity(instruction_accounts.len()); for (position, account) in instruction_accounts.iter().enumerate() { let account_index = match account.get("accountIndex").and_then(serde_json::Value::as_u64) { std::option::Option::Some(value) => value, std::option::Option::None => { return std::result::Result::Err(kb_core::Error::invalid_state(format!( "Token-2022 instruction account {position} has no accountIndex" ))); }, }; let account_key = match account.get("accountKey").and_then(serde_json::Value::as_str) { std::option::Option::Some(value) if !value.trim().is_empty() => value, _ => { return std::result::Result::Err(kb_core::Error::invalid_state(format!( "Token-2022 instruction account {position} has no accountKey" ))); }, }; let resolved = account_keys.iter().find(|candidate| { return candidate.get("accountIndex").and_then(serde_json::Value::as_u64) == std::option::Option::Some(account_index); }); let resolved = match resolved { std::option::Option::Some(value) => value, std::option::Option::None => { return std::result::Result::Err(kb_core::Error::invalid_state(format!( "Token-2022 account index {account_index} is not resolved" ))); }, }; if resolved.get("accountKey").and_then(serde_json::Value::as_str) != std::option::Option::Some(account_key) { return std::result::Result::Err(kb_core::Error::invalid_state(format!( "Token-2022 account index {account_index} resolves to a different key" ))); } let signer = match resolved.get("signer").and_then(serde_json::Value::as_bool) { std::option::Option::Some(value) => value, std::option::Option::None => { return std::result::Result::Err(kb_core::Error::invalid_state(format!( "Token-2022 account index {account_index} has no signer flag" ))); }, }; let writable = match resolved.get("writable").and_then(serde_json::Value::as_bool) { std::option::Option::Some(value) => value, std::option::Option::None => { return std::result::Result::Err(kb_core::Error::invalid_state(format!( "Token-2022 account index {account_index} has no writable flag" ))); }, }; output.push(ResolvedAccount { position, account_index, account_key: account_key.to_string(), signer, writable, source: resolved.get("source").cloned().unwrap_or(serde_json::Value::Null), }); } return std::result::Result::Ok(output); } fn project_accounts(code: &str, accounts: &[ResolvedAccount]) -> serde_json::Value { let authority_index = authority_index(code); let values = accounts .iter() .enumerate() .map(|(role_position, account)| { return serde_json::json!({ "position": account.position, "rolePosition": role_position, "accountIndex": account.account_index, "accountKey": account.account_key, "signer": account.signer, "writable": account.writable, "source": account.source, "role": account_role(code, role_position, authority_index), }); }) .collect::>(); return serde_json::Value::Array(values); } fn authority_projection(code: &str, accounts: &[ResolvedAccount]) -> serde_json::Value { let authority_index = authority_index(code); let index = match authority_index { std::option::Option::Some(value) => value, std::option::Option::None => { return serde_json::json!({ "form": "not_applicable", "declaredAuthority": serde_json::Value::Null, "providedSigners": [], "statefulMultisigValidation": "not_applicable", }); }, }; let authority = accounts.get(index); let provided_signers = accounts .iter() .skip(index.saturating_add(1)) .filter(|account| return account.signer) .map(|account| return account.account_key.clone()) .collect::>(); let form = if accounts.len() > index.saturating_add(1) { "multisig" } else { "single" }; return serde_json::json!({ "form": form, "declaredAuthority": authority.map(|value| return value.account_key.clone()), "declaredAuthoritySigner": authority.map(|value| return value.signer), "providedSigners": provided_signers, "providedSignerCount": provided_signers.len(), "statefulMultisigValidation": if form == "multisig" { "requires_multisig_account_snapshot" } else { "not_applicable" }, "membersClaimed": false, }); } fn authority_index(code: &str) -> std::option::Option { return match code { "transfer" | "approve" | "mint_to" | "burn" | "close_account" | "freeze_account" | "thaw_account" | "withdraw_excess_lamports" | "unwrap_lamports" => std::option::Option::Some(2), "revoke" | "set_authority" => std::option::Option::Some(1), "transfer_checked" | "approve_checked" => std::option::Option::Some(3), "mint_to_checked" | "burn_checked" => std::option::Option::Some(2), "transfer_checked_with_fee" => std::option::Option::Some(3), "withdraw_withheld_tokens_from_mint" | "withdraw_withheld_tokens_from_accounts" => { std::option::Option::Some(2) }, "set_transfer_fee" | "update_default_account_state" => std::option::Option::Some(1), "enable_required_transfer_memos" | "disable_required_transfer_memos" | "enable_cpi_guard" | "disable_cpi_guard" => std::option::Option::Some(1), "update_token_metadata_field" | "remove_token_metadata_key" | "update_token_metadata_authority" | "update_token_group_max_size" | "update_token_group_authority" => std::option::Option::Some(1), _ => std::option::Option::None, }; } fn account_role( code: &str, position: usize, authority_index: std::option::Option, ) -> std::string::String { if authority_index.is_some_and(|index| return position > index) { return "multisig_signer".to_string(); } let role = match (code, position) { ("initialize_mint", 0) | ("initialize_mint2", 0) => "mint", ("initialize_mint", 1) => "rent_sysvar", ("initialize_account", 0) | ("initialize_account2", 0) | ("initialize_account3", 0) => { "token_account" }, ("initialize_account", 1) | ("initialize_account2", 1) | ("initialize_account3", 1) => { "mint" }, ("initialize_account", 2) => "owner", ("initialize_account", 3) | ("initialize_account2", 2) => "rent_sysvar", ("initialize_multisig", 0) | ("initialize_multisig2", 0) => "multisig_account", ("initialize_multisig", 1) => "rent_sysvar", ("initialize_multisig", _) | ("initialize_multisig2", _) => "multisig_member", ("transfer", 0) | ("transfer_checked", 0) => "source_token_account", ("transfer", 1) => "destination_token_account", ("transfer_checked", 1) => "mint", ("transfer_checked", 2) => "destination_token_account", ("approve", 0) | ("approve_checked", 0) | ("revoke", 0) => "source_token_account", ("approve", 1) | ("approve_checked", 2) => "delegate", ("approve_checked", 1) => "mint", ("mint_to", 0) | ("mint_to_checked", 0) => "mint", ("mint_to", 1) | ("mint_to_checked", 1) => "destination_token_account", ("burn", 0) | ("burn_checked", 0) => "source_token_account", ("burn", 1) | ("burn_checked", 1) => "mint", ("close_account", 0) | ("withdraw_excess_lamports", 0) | ("unwrap_lamports", 0) => { "source_token_account" }, ("close_account", 1) | ("withdraw_excess_lamports", 1) | ("unwrap_lamports", 1) => { "destination_account" }, ("freeze_account", 0) | ("thaw_account", 0) => "token_account", ("freeze_account", 1) | ("thaw_account", 1) => "mint", ("set_authority", 0) => "authority_target", ("sync_native", 0) | ("initialize_immutable_owner", 0) => "token_account", ("sync_native", 1) => "rent_sysvar", ("get_account_data_size", 0) | ("amount_to_ui_amount", 0) | ("ui_amount_to_amount", 0) => { "mint" }, ("initialize_transfer_fee_config", 0) | ("initialize_default_account_state", 0) | ("update_default_account_state", 0) | ("set_transfer_fee", 0) | ("harvest_withheld_tokens_to_mint", 0) => "mint", ("transfer_checked_with_fee", 0) => "source_token_account", ("transfer_checked_with_fee", 1) => "mint", ("transfer_checked_with_fee", 2) => "destination_token_account", ("withdraw_withheld_tokens_from_mint", 0) => "mint", ("withdraw_withheld_tokens_from_mint", 1) => "destination_token_account", ("withdraw_withheld_tokens_from_accounts", 0) => "mint", ("withdraw_withheld_tokens_from_accounts", 1) => "destination_token_account", ("withdraw_withheld_tokens_from_accounts", _) => "source_or_multisig_account", ("harvest_withheld_tokens_to_mint", _) => "source_token_account", ("enable_required_transfer_memos", 0) | ("disable_required_transfer_memos", 0) | ("enable_cpi_guard", 0) | ("disable_cpi_guard", 0) => "token_account", ("initialize_token_metadata", 0) | ("update_token_metadata_field", 0) | ("remove_token_metadata_key", 0) | ("update_token_metadata_authority", 0) | ("emit_token_metadata", 0) => "metadata", ("initialize_token_metadata", 1) => "update_authority", ("initialize_token_metadata", 2) => "mint", ("initialize_token_metadata", 3) => "mint_authority", ("initialize_token_group", 0) | ("update_token_group_max_size", 0) | ("update_token_group_authority", 0) | ("initialize_token_group_member", 3) => "group", ("initialize_token_group", 1) => "mint", ("initialize_token_group", 2) => "mint_authority", ("initialize_token_group_member", 0) => "member", ("initialize_token_group_member", 1) => "member_mint", ("initialize_token_group_member", 2) => "member_mint_authority", ("initialize_token_group_member", 4) => "group_update_authority", (_, value) if authority_index == std::option::Option::Some(value) => "authority", _ => "unresolved", }; return role.to_string(); } fn semantic_diagnostics( code: &str, parameters: serde_json::Value, accounts: &[ResolvedAccount], suffix_length: usize, ) -> serde_json::Value { let mut values = std::vec::Vec::new(); if suffix_length > 0 { values.push(serde_json::json!({ "code": "ignored_wire_suffix", "lengthBytes": suffix_length, "runtimeSemantics": "official_interface_unpack_ignores_suffix_for_this_variant", })); } let valid_count = valid_account_count(code, accounts.len()); if !valid_count { values.push(serde_json::json!({ "code": "unexpected_account_count", "provided": accounts.len(), })); } if matches!(code, "initialize_multisig" | "initialize_multisig2") { let member_start = if code == "initialize_multisig" { 2 } else { 1 }; let n = accounts.len().saturating_sub(member_start); let m = parameters.get("m").and_then(serde_json::Value::as_u64).unwrap_or(0); if !(1..=11).contains(&n) || m == 0 || m > n as u64 { values.push(serde_json::json!({ "code": "invalid_multisig_threshold", "m": m, "n": n, "minSigners": 1, "maxSigners": 11, })); } } let authority_index = authority_index(code); if let std::option::Option::Some(index) = authority_index { if let std::option::Option::Some(authority) = accounts.get(index) { let multisig_form = accounts.len() > index.saturating_add(1); if !multisig_form && !authority.signer { values.push(serde_json::json!({"code":"missing_single_authority_signature"})); } if multisig_form { let invalid_signers = accounts .iter() .skip(index.saturating_add(1)) .filter(|account| return !account.signer) .map(|account| return account.position) .collect::>(); if !invalid_signers.is_empty() { values.push(serde_json::json!({ "code": "multisig_meta_missing_signature", "positions": invalid_signers, })); } } } } return serde_json::Value::Array(values); } fn valid_account_count(code: &str, count: usize) -> bool { return match code { "initialize_mint" => count == 2, "initialize_mint2" => count == 1, "initialize_account" => count == 4, "initialize_account2" => count == 3, "initialize_account3" => count == 2, "initialize_multisig" => (3..=13).contains(&count), "initialize_multisig2" => (2..=12).contains(&count), "transfer" | "approve" | "mint_to" | "burn" | "close_account" | "freeze_account" | "thaw_account" | "withdraw_excess_lamports" | "unwrap_lamports" => count >= 3, "revoke" | "set_authority" => count >= 2, "transfer_checked" | "approve_checked" => count >= 4, "mint_to_checked" | "burn_checked" => count >= 3, "sync_native" => count == 1 || count == 2, "get_account_data_size" | "initialize_immutable_owner" | "amount_to_ui_amount" | "ui_amount_to_amount" => count == 1, "initialize_transfer_fee_config" | "initialize_default_account_state" => count == 1, "transfer_checked_with_fee" => count >= 4, "withdraw_withheld_tokens_from_mint" => count >= 3, "withdraw_withheld_tokens_from_accounts" => count >= 4, "harvest_withheld_tokens_to_mint" => count >= 2, "set_transfer_fee" | "update_default_account_state" | "enable_required_transfer_memos" | "disable_required_transfer_memos" | "enable_cpi_guard" | "disable_cpi_guard" => count >= 2, "batch" => true, _ => false, }; } #[allow(clippy::too_many_arguments)] fn observation( input: &kb_store_core::CoreInstructionReplayInput, code: &str, tag: u8, parameters: serde_json::Value, accounts: serde_json::Value, authority: serde_json::Value, semantic_diagnostics: serde_json::Value, wire: &[u8], suffix_length: usize, committed: bool, event_key: std::string::String, instruction_path: std::string::String, family: kb_model::EventFamily, ) -> kb_decoder_api::DecodedObservation { let wire_hash = hash(wire); let event = kb_model::DecodedProtocolEvent { signature: kb_model::Signature(input.signature.clone()), slot: kb_model::Slot(input.slot), instruction_path: kb_model::InstructionPath(instruction_path.clone()), program_id: kb_model::ProgramId(input.program_id.clone()), protocol_code: kb_model::ProtocolCode(crate::SURFACE_CODE.to_string()), surface_code: kb_model::SurfaceCode(crate::SURFACE_CODE.to_string()), event_code: kb_model::EventCode(format!("{}.{}", crate::SURFACE_CODE, code)), event_name: kb_model::EventName(code.to_string()), event_family: family, source_kind: if input.instruction_path.contains('/') { kb_model::EventSourceKind::InnerInstruction } else { kb_model::EventSourceKind::Instruction }, confidence: kb_model::DecoderConfidence::ManualExact, }; return kb_decoder_api::DecodedObservation { event_key, event, payload_json: serde_json::json!({ "eventVersion": crate::TOKEN_EVENT_VERSION, "programId": input.program_id, "instruction": code, "wireTag": tag, "wireTagHex": format!("{tag:02x}"), "instructionPath": instruction_path, "instructionLocation": if input.instruction_path.contains('/') { "inner" } else { "outer" }, "transactionSucceeded": !input.transaction_failed, "committed": committed, "parameters": parameters, "accounts": accounts, "authority": authority, "semanticDiagnostics": semantic_diagnostics, "wire": { "lengthBytes": wire.len(), "sha256": wire_hash, "prefixHex": hex_prefix(wire), "suffixLengthBytes": suffix_length, "complete": true, }, "inference": { "mintFromCoreBalanceChanges": serde_json::Value::Null, "mintInvented": false, "rpcAccountReadUsed": false, }, "returnData": { "availableInCoreReplayInput": false, "validated": false, }, }), transaction_failed: input.transaction_failed, transaction_error: input.transaction_err_json.clone(), observation_committed: committed, proof: kb_decoder_api::DecoderProof { kind: kb_decoder_api::DecoderProofKind::Manual, confidence: kb_model::DecoderConfidence::ManualExact, evidence: std::vec![ "spl_token_interface_3_0_0_wire_audit".to_string(), format!("wire_tag:{tag}"), format!("wire_sha256:{wire_hash}"), ], }, }; } fn batch_observations( input: &kb_store_core::CoreInstructionReplayInput, bytes: &[u8], accounts: &[ResolvedAccount], ) -> std::result::Result< (std::vec::Vec, serde_json::Value), std::string::String, > { if bytes.len() == 1 { return std::result::Result::Err("Batch requires at least one sub-instruction".to_string()); } let mut cursor = 1_usize; let mut account_cursor = 0_usize; let mut index = 0_usize; let mut observations = std::vec::Vec::new(); let mut manifest = std::vec::Vec::new(); while cursor < bytes.len() { if index >= crate::MAX_BATCH_INSTRUCTIONS { return std::result::Result::Err(format!( "Batch exceeds {} sub-instructions", crate::MAX_BATCH_INSTRUCTIONS )); } let account_count = match bytes.get(cursor).copied() { std::option::Option::Some(value) => value as usize, std::option::Option::None => { return std::result::Result::Err(format!( "Batch entry {index} has no account count" )); }, }; let data_length = match bytes.get(cursor.saturating_add(1)).copied() { std::option::Option::Some(value) => value as usize, std::option::Option::None => { return std::result::Result::Err(format!("Batch entry {index} has no data length")); }, }; if data_length == 0 { return std::result::Result::Err(format!("Batch entry {index} has empty data")); } let data_start = cursor.saturating_add(2); let data_end = data_start.saturating_add(data_length); let child_wire = match bytes.get(data_start..data_end) { std::option::Option::Some(value) => value, std::option::Option::None => { return std::result::Result::Err(format!( "Batch entry {index} data length exceeds payload" )); }, }; let account_end = account_cursor.saturating_add(account_count); if account_end > crate::MAX_BATCH_ACCOUNTS || account_end > accounts.len() { return std::result::Result::Err(format!( "Batch entry {index} account slice exceeds provided accounts or bound" )); } let parsed = match parse_instruction(child_wire, true) { std::result::Result::Ok(value) => value, std::result::Result::Err(message) => { return std::result::Result::Err(format!("Batch entry {index}: {message}")); }, }; let child_accounts = &accounts[account_cursor..account_end]; let suffix_length = child_wire.len().saturating_sub(parsed.consumed); observations.push(observation( input, parsed.code, parsed.tag, parsed.parameters.clone(), project_accounts(parsed.code, child_accounts), authority_projection(parsed.code, child_accounts), semantic_diagnostics(parsed.code, parsed.parameters, child_accounts, suffix_length), child_wire, suffix_length, !input.transaction_failed, format!("token_2022:batch:{index}"), format!("{}/batch/{index}", input.instruction_path), event_family(parsed.code), )); manifest.push(serde_json::json!({ "position": index, "instruction": parsed.code, "wireTag": parsed.tag, "accountStart": account_cursor, "accountCount": account_count, "dataOffset": data_start, "dataLength": data_length, "derivedInstructionPath": format!("{}/batch/{index}", input.instruction_path), })); cursor = data_end; account_cursor = account_end; index = index.saturating_add(1); } if account_cursor != accounts.len() { return std::result::Result::Err(format!( "Batch consumed {account_cursor} of {} provided accounts", accounts.len() )); } return std::result::Result::Ok(( observations, serde_json::json!({ "subInstructionCount": manifest.len(), "subInstructions": manifest, "nestedBatchAllowed": false, }), )); } fn event_family(code: &str) -> kb_model::EventFamily { return match code { "set_authority" | "initialize_multisig" | "initialize_multisig2" | "initialize_permanent_delegate" | "initialize_interest_bearing_mint" | "update_interest_bearing_rate" | "update_token_metadata_authority" | "update_token_group_authority" | "update_token_group_max_size" => kb_model::EventFamily::Admin, "initialize_mint" | "initialize_mint2" | "mint_to" | "mint_to_checked" | "initialize_non_transferable_mint" => kb_model::EventFamily::TokenMint, "burn" | "burn_checked" => kb_model::EventFamily::TokenBurn, "initialize_transfer_fee_config" | "set_transfer_fee" | "withdraw_withheld_tokens_from_mint" | "withdraw_withheld_tokens_from_accounts" | "harvest_withheld_tokens_to_mint" | "transfer_checked_with_fee" | "initialize_confidential_transfer_fee_config" | "withdraw_confidential_withheld_tokens_from_mint" | "withdraw_confidential_withheld_tokens_from_accounts" | "harvest_confidential_withheld_tokens_to_mint" | "enable_confidential_harvest_to_mint" | "disable_confidential_harvest_to_mint" | "transfer_confidential_tokens_with_fee" => kb_model::EventFamily::Fee, "initialize_default_account_state" | "update_default_account_state" | "enable_required_transfer_memos" | "disable_required_transfer_memos" | "enable_cpi_guard" | "disable_cpi_guard" => kb_model::EventFamily::Audit, "initialize_token_metadata" | "update_token_metadata_field" | "remove_token_metadata_key" | "emit_token_metadata" | "initialize_token_group" | "initialize_token_group_member" => kb_model::EventFamily::Metadata, "batch" | "get_account_data_size" | "amount_to_ui_amount" | "ui_amount_to_amount" => { kb_model::EventFamily::Audit }, _ => kb_model::EventFamily::TokenAccount, }; } fn hash(bytes: &[u8]) -> std::string::String { let digest = sha2::Sha256::digest(bytes); let mut output = std::string::String::with_capacity(64); for byte in digest { output.push_str(format!("{byte:02x}").as_str()); } return output; } fn hex_prefix(bytes: &[u8]) -> std::string::String { let length = std::cmp::min(bytes.len(), crate::DIAGNOSTIC_PREFIX_BYTES); let mut output = std::string::String::with_capacity(length.saturating_mul(2)); for byte in &bytes[..length] { output.push_str(format!("{byte:02x}").as_str()); } return output; } fn failed( entry_code: std::option::Option<&str>, code: &str, message: std::string::String, ) -> kb_decoder_api::DecoderExecutionResult { return kb_decoder_api::DecoderExecutionResult { status: kb_decoder_api::DecoderOutcomeStatus::Failed, recognized_entry_code: entry_code.map(str::to_string), observations: std::vec::Vec::new(), diagnostics: std::vec![kb_decoder_api::DecoderDiagnostic { code: code.to_string(), message, retriable: false, }], }; }