2604 lines
102 KiB
Rust
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,
|
|
}],
|
|
};
|
|
}
|