Files
khadhroony-bot3/migration/khadhroony-bot2-reference/kb_decoder_spl_token_2022/src/token.rs
2026-07-23 16:37:12 +02:00

2604 lines
102 KiB
Rust

// 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<u8> {
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<std::vec::Vec<u8>> {
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<ParsedInstruction, std::string::String> {
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<ParsedInstruction, std::string::String> {
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<u8, std::string::String> {
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<u64, std::string::String> {
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<std::string::String, std::string::String> {
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<serde_json::Value, std::string::String> {
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<serde_json::Value, std::string::String> {
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<ParsedInstruction, std::string::String> {
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<ParsedInstruction, std::string::String> {
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<ParsedInstruction, std::string::String> {
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<ParsedInstruction, std::string::String> {
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<ParsedInstruction, std::string::String> {
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<ParsedInstruction, std::string::String> {
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<ParsedInstruction, std::string::String> {
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<ParsedInstruction, std::string::String> {
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<ParsedInstruction, std::string::String> {
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<ParsedInstruction, std::string::String> {
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<ParsedInstruction, std::string::String> {
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<ParsedInstruction, std::string::String> {
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<ParsedInstruction, std::string::String> {
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<ParsedInstruction, std::string::String> {
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<ParsedInstruction, std::string::String> {
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<ParsedInstruction, std::string::String> {
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<ParsedInstruction, std::string::String> {
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<ParsedInstruction, std::string::String> {
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<ParsedInstruction, std::string::String> {
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<ParsedInstruction, std::string::String> {
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<ParsedInstruction, std::string::String> {
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<ParsedInstruction, std::string::String> {
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<u16, std::string::String> {
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<u64, std::string::String> {
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<std::vec::Vec<ResolvedAccount>> {
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::<std::vec::Vec<_>>();
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::<std::vec::Vec<_>>();
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<usize> {
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<usize>,
) -> 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::<std::vec::Vec<_>>();
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<kb_decoder_api::DecodedObservation>, 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,
}],
};
}