v0.1.0-pre.006
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
// file: kb-lib/src/decoder.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
//! Consolidated decoder modules.
|
||||
|
||||
@@ -49,8 +49,32 @@ pub(crate) use self::spl::SPL_MEMO_V1_SURFACE_CODE;
|
||||
pub(crate) use self::spl::SPL_MEMO_V3_SURFACE_CODE;
|
||||
/// Stable Memo v4 surface code.
|
||||
pub(crate) use self::spl::SPL_MEMO_V4_SURFACE_CODE;
|
||||
/// Maximum prefix retained in bounded classic SPL Token diagnostics.
|
||||
pub(crate) use self::spl::SPL_TOKEN_DIAGNOSTIC_PREFIX_BYTES;
|
||||
/// Current stable classic SPL Token decoded event contract version.
|
||||
pub(crate) use self::spl::SPL_TOKEN_EVENT_VERSION;
|
||||
/// One exact classic SPL Token instruction entry published by the official interface.
|
||||
pub(crate) use self::spl::SPL_TOKEN_INSTRUCTION_ENTRIES;
|
||||
/// Maximum total account metas consumed by one classic SPL Token batch.
|
||||
pub(crate) use self::spl::SPL_TOKEN_MAX_BATCH_ACCOUNTS;
|
||||
/// Maximum decoded sub-instructions in one classic SPL Token batch.
|
||||
pub(crate) use self::spl::SPL_TOKEN_MAX_BATCH_INSTRUCTIONS;
|
||||
/// Maximum retained classic SPL Token instruction payload size.
|
||||
pub(crate) use self::spl::SPL_TOKEN_MAX_INSTRUCTION_BYTES;
|
||||
/// Stable protocol and surface code for the classic SPL Token program.
|
||||
pub(crate) use self::spl::SPL_TOKEN_SURFACE_CODE;
|
||||
/// Canonical tracing target for the classic SPL Token decoder.
|
||||
pub(crate) use self::spl::SPL_TOKEN_TRACING_TARGET;
|
||||
/// Decodes one bounded SPL Memo instruction.
|
||||
pub(crate) use self::spl::spl_memo_decode;
|
||||
/// Decodes one bounded classic SPL Token instruction.
|
||||
pub(crate) use self::spl::spl_token_decode;
|
||||
/// Returns the published entry matching one classic SPL Token tag.
|
||||
pub(crate) use self::spl::spl_token_entry_for_tag;
|
||||
/// Returns the first byte of one retained classic SPL Token payload.
|
||||
pub(crate) use self::spl::spl_token_payload_tag;
|
||||
|
||||
/// Exact decoder for the three registered SPL Memo generations.
|
||||
pub use self::spl::SplMemoDecoder;
|
||||
/// Exact decoder for the classic SPL Token program.
|
||||
pub use self::spl::SplTokenDecoder;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: kb-lib/src/decoder/spl.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
//! `spl` decoder family.
|
||||
|
||||
@@ -10,7 +10,7 @@ pub mod memo;
|
||||
pub mod noop;
|
||||
pub mod single_pool;
|
||||
pub mod stake_pool;
|
||||
pub mod token;
|
||||
mod token;
|
||||
pub mod token_2022;
|
||||
|
||||
/// Maximum payload prefix retained for bounded diagnostics.
|
||||
@@ -31,6 +31,30 @@ pub(crate) use self::memo::SPL_MEMO_V3_SURFACE_CODE;
|
||||
pub(crate) use self::memo::SPL_MEMO_V4_SURFACE_CODE;
|
||||
/// Decodes one bounded SPL Memo instruction.
|
||||
pub(crate) use self::memo::spl_memo_decode;
|
||||
/// Maximum prefix retained in bounded classic SPL Token diagnostics.
|
||||
pub(crate) use self::token::SPL_TOKEN_DIAGNOSTIC_PREFIX_BYTES;
|
||||
/// Current stable classic SPL Token decoded event contract version.
|
||||
pub(crate) use self::token::SPL_TOKEN_EVENT_VERSION;
|
||||
/// One exact classic SPL Token instruction entry published by the official interface.
|
||||
pub(crate) use self::token::SPL_TOKEN_INSTRUCTION_ENTRIES;
|
||||
/// Maximum total account metas consumed by one classic SPL Token batch.
|
||||
pub(crate) use self::token::SPL_TOKEN_MAX_BATCH_ACCOUNTS;
|
||||
/// Maximum decoded sub-instructions in one classic SPL Token batch.
|
||||
pub(crate) use self::token::SPL_TOKEN_MAX_BATCH_INSTRUCTIONS;
|
||||
/// Maximum retained classic SPL Token instruction payload size.
|
||||
pub(crate) use self::token::SPL_TOKEN_MAX_INSTRUCTION_BYTES;
|
||||
/// Stable protocol and surface code for the classic SPL Token program.
|
||||
pub(crate) use self::token::SPL_TOKEN_SURFACE_CODE;
|
||||
/// Canonical tracing target for the classic SPL Token decoder.
|
||||
pub(crate) use self::token::SPL_TOKEN_TRACING_TARGET;
|
||||
/// Decodes one bounded classic SPL Token instruction.
|
||||
pub(crate) use self::token::spl_token_decode;
|
||||
/// Returns the published entry matching one classic SPL Token tag.
|
||||
pub(crate) use self::token::spl_token_entry_for_tag;
|
||||
/// Returns the first byte of one retained classic SPL Token payload.
|
||||
pub(crate) use self::token::spl_token_payload_tag;
|
||||
|
||||
/// Exact decoder for the three registered SPL Memo generations.
|
||||
pub use self::memo::SplMemoDecoder;
|
||||
/// Exact decoder for the classic SPL Token program.
|
||||
pub use self::token::SplTokenDecoder;
|
||||
|
||||
@@ -1,10 +1,34 @@
|
||||
// file: kb-lib/src/decoder/spl/token.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! Migration boundary for legacy crate `kb_decoder_spl_token`.
|
||||
//! Exact classic SPL Token decoder component.
|
||||
|
||||
/// Legacy crate name retained for migration and compatibility tracking.
|
||||
pub const LEGACY_CRATE: &str = "kb_decoder_spl_token";
|
||||
mod constants;
|
||||
mod decoder;
|
||||
mod wire;
|
||||
|
||||
/// Current porting status.
|
||||
pub const MIGRATION_STATUS: &str = "source-preserved-pending-port";
|
||||
/// Maximum prefix retained in bounded diagnostics.
|
||||
pub(crate) use self::constants::SPL_TOKEN_DIAGNOSTIC_PREFIX_BYTES;
|
||||
/// Current stable decoded event contract version.
|
||||
pub(crate) use self::constants::SPL_TOKEN_EVENT_VERSION;
|
||||
/// One exact instruction entry published by `spl-token-interface` 3.0.0.
|
||||
pub(crate) use self::constants::SPL_TOKEN_INSTRUCTION_ENTRIES;
|
||||
/// Maximum total account metas consumed by one batch.
|
||||
pub(crate) use self::constants::SPL_TOKEN_MAX_BATCH_ACCOUNTS;
|
||||
/// Maximum decoded sub-instructions in one batch.
|
||||
pub(crate) use self::constants::SPL_TOKEN_MAX_BATCH_INSTRUCTIONS;
|
||||
/// Maximum retained instruction payload size.
|
||||
pub(crate) use self::constants::SPL_TOKEN_MAX_INSTRUCTION_BYTES;
|
||||
/// Stable protocol and surface code for the classic SPL Token program.
|
||||
pub(crate) use self::constants::SPL_TOKEN_SURFACE_CODE;
|
||||
/// Canonical tracing target for the classic SPL Token decoder.
|
||||
pub(crate) use self::constants::SPL_TOKEN_TRACING_TARGET;
|
||||
/// Decodes one bounded classic SPL Token instruction.
|
||||
pub(crate) use self::wire::decode as spl_token_decode;
|
||||
/// Returns the published entry matching one classic SPL Token tag.
|
||||
pub(crate) use self::wire::entry_for_tag as spl_token_entry_for_tag;
|
||||
/// Returns the first byte of one retained classic SPL Token payload.
|
||||
pub(crate) use self::wire::payload_tag as spl_token_payload_tag;
|
||||
|
||||
/// Exact decoder for the classic SPL Token program.
|
||||
pub use self::decoder::SplTokenDecoder;
|
||||
|
||||
50
kb-lib/src/decoder/spl/token/constants.rs
Normal file
50
kb-lib/src/decoder/spl/token/constants.rs
Normal file
@@ -0,0 +1,50 @@
|
||||
// file: kb-lib/src/decoder/spl/token/constants.rs
|
||||
// version: 1
|
||||
|
||||
//! Local constants for the `kb-lib` SPL Token component. Program identifiers live in `kb_program_ids`.
|
||||
|
||||
/// Stable protocol and surface code for the classic SPL Token program.
|
||||
pub(crate) const SPL_TOKEN_SURFACE_CODE: &str = "spl_token";
|
||||
/// Current stable decoded event contract version.
|
||||
pub(crate) const SPL_TOKEN_EVENT_VERSION: u32 = 1;
|
||||
/// Maximum retained instruction payload size.
|
||||
pub(crate) const SPL_TOKEN_MAX_INSTRUCTION_BYTES: usize = 16_384;
|
||||
/// Maximum prefix retained in diagnostics.
|
||||
pub(crate) const SPL_TOKEN_DIAGNOSTIC_PREFIX_BYTES: usize = 32;
|
||||
/// Maximum decoded sub-instructions in one batch.
|
||||
pub(crate) const SPL_TOKEN_MAX_BATCH_INSTRUCTIONS: usize = 64;
|
||||
/// Maximum total account metas consumed by one batch.
|
||||
pub(crate) const SPL_TOKEN_MAX_BATCH_ACCOUNTS: usize = 512;
|
||||
/// One exact instruction entry published by `spl-token-interface` 3.0.0.
|
||||
pub(crate) const SPL_TOKEN_INSTRUCTION_ENTRIES: &[(u8, &str, bool)] = &[
|
||||
(0, "initialize_mint", true),
|
||||
(1, "initialize_account", true),
|
||||
(2, "initialize_multisig", true),
|
||||
(3, "transfer", true),
|
||||
(4, "approve", true),
|
||||
(5, "revoke", true),
|
||||
(6, "set_authority", true),
|
||||
(7, "mint_to", true),
|
||||
(8, "burn", true),
|
||||
(9, "close_account", true),
|
||||
(10, "freeze_account", true),
|
||||
(11, "thaw_account", true),
|
||||
(12, "transfer_checked", false),
|
||||
(13, "approve_checked", false),
|
||||
(14, "mint_to_checked", false),
|
||||
(15, "burn_checked", false),
|
||||
(16, "initialize_account2", true),
|
||||
(17, "sync_native", false),
|
||||
(18, "initialize_account3", false),
|
||||
(19, "initialize_multisig2", false),
|
||||
(20, "initialize_mint2", false),
|
||||
(21, "get_account_data_size", false),
|
||||
(22, "initialize_immutable_owner", false),
|
||||
(23, "amount_to_ui_amount", false),
|
||||
(24, "ui_amount_to_amount", false),
|
||||
(38, "withdraw_excess_lamports", false),
|
||||
(45, "unwrap_lamports", false),
|
||||
(255, "batch", false),
|
||||
];
|
||||
/// Canonical tracing target for this crate.
|
||||
pub(crate) const SPL_TOKEN_TRACING_TARGET: &str = "kb-lib.decoder.spl.token";
|
||||
500
kb-lib/src/decoder/spl/token/decoder.rs
Normal file
500
kb-lib/src/decoder/spl/token/decoder.rs
Normal file
@@ -0,0 +1,500 @@
|
||||
// file: kb-lib/src/decoder/spl/token/decoder.rs
|
||||
// version: 1
|
||||
|
||||
//! Exact classic SPL Token dispatch for the common decode pipeline.
|
||||
|
||||
const TOKEN_SURFACES: &[crate::DecoderSurface] = &[crate::DecoderSurface {
|
||||
program_id: kb_program_ids::SPL_TOKEN_PROGRAM_ID,
|
||||
surface_code: crate::SPL_TOKEN_SURFACE_CODE,
|
||||
priority: 100,
|
||||
}];
|
||||
|
||||
const PROGRAM_IDS: &[&str] = &[kb_program_ids::SPL_TOKEN_PROGRAM_ID];
|
||||
|
||||
/// Exact decoder for the classic SPL Token program.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct SplTokenDecoder;
|
||||
|
||||
impl crate::ProtocolDecoder for crate::SplTokenDecoder {
|
||||
fn decoder_name(&self) -> &'static str {
|
||||
return "kb_decoder_spl_token";
|
||||
}
|
||||
|
||||
fn decoder_version(&self) -> &'static str {
|
||||
return env!("CARGO_PKG_VERSION");
|
||||
}
|
||||
|
||||
fn program_ids(&self) -> &'static [&'static str] {
|
||||
return PROGRAM_IDS;
|
||||
}
|
||||
|
||||
fn supports_observation(
|
||||
&self,
|
||||
observation: &crate::ProgramObservation,
|
||||
) -> crate::DecoderSupport {
|
||||
return if crate::ProtocolDecoder::handles_program_id(self, &observation.program_id) {
|
||||
crate::DecoderSupport::Yes
|
||||
} else {
|
||||
crate::DecoderSupport::No
|
||||
};
|
||||
}
|
||||
|
||||
fn decode_observation(
|
||||
&self,
|
||||
_observation: &crate::ProgramObservation,
|
||||
) -> kb_core::Result<std::vec::Vec<crate::DecodedProtocolEvent>> {
|
||||
return std::result::Result::Ok(std::vec::Vec::new());
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::InstructionDecoder for crate::SplTokenDecoder {
|
||||
fn identity(&self) -> crate::DecoderIdentity {
|
||||
return crate::DecoderIdentity {
|
||||
name: "spl_token".to_string(),
|
||||
version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
fn surfaces(&self) -> &'static [crate::DecoderSurface] {
|
||||
return TOKEN_SURFACES;
|
||||
}
|
||||
|
||||
fn coverage(&self) -> std::vec::Vec<crate::DecoderCoverageDeclaration> {
|
||||
return crate::SPL_TOKEN_INSTRUCTION_ENTRIES
|
||||
.iter()
|
||||
.map(|(tag, code, historical)| {
|
||||
return crate::DecoderCoverageDeclaration {
|
||||
program_id: kb_program_ids::SPL_TOKEN_PROGRAM_ID.to_string(),
|
||||
surface_code: std::option::Option::Some(
|
||||
crate::SPL_TOKEN_SURFACE_CODE.to_string(),
|
||||
),
|
||||
entry_kind: crate::DecoderCoverageEntryKind::Instruction,
|
||||
entry_code: (*code).to_string(),
|
||||
discriminator_hex: std::option::Option::Some(format!("{tag:02x}")),
|
||||
historical: *historical,
|
||||
};
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
|
||||
fn recognize(&self, input: &crate::CoreInstructionReplayInput) -> crate::DecoderRecognition {
|
||||
if input.program_id != kb_program_ids::SPL_TOKEN_PROGRAM_ID {
|
||||
return crate::DecoderRecognition::incompatible();
|
||||
}
|
||||
let tag = crate::spl_token_payload_tag(input);
|
||||
let entry = tag.and_then(crate::spl_token_entry_for_tag);
|
||||
return crate::DecoderRecognition::compatible(
|
||||
entry.is_some(),
|
||||
100,
|
||||
std::option::Option::Some(crate::SPL_TOKEN_SURFACE_CODE.to_string()),
|
||||
entry.map(|(_, code, _)| return code.to_string()),
|
||||
tag.map(|value| return format!("{value:02x}")),
|
||||
);
|
||||
}
|
||||
|
||||
fn decode(&self, input: &crate::CoreInstructionReplayInput) -> crate::DecoderExecutionResult {
|
||||
if input.program_id != kb_program_ids::SPL_TOKEN_PROGRAM_ID {
|
||||
return crate::DecoderExecutionResult::unsupported(std::option::Option::None);
|
||||
}
|
||||
let result = crate::spl_token_decode(input);
|
||||
if matches!(
|
||||
result.status,
|
||||
crate::DecoderOutcomeStatus::Failed | crate::DecoderOutcomeStatus::Unsupported
|
||||
) {
|
||||
tracing::error!(
|
||||
target: crate::SPL_TOKEN_TRACING_TARGET,
|
||||
action = "decode_failure",
|
||||
signature = %input.signature,
|
||||
slot = input.slot,
|
||||
instruction_path = %input.instruction_path,
|
||||
program_id = %input.program_id,
|
||||
processor_name = "spl_token",
|
||||
processor_version = env!("CARGO_PKG_VERSION"),
|
||||
input_key = %input.replay_input_key,
|
||||
payload_hash = ?input.instruction_payload_hash,
|
||||
result_status = ?result.status,
|
||||
diagnostics = ?result.diagnostics,
|
||||
"classic SPL Token instruction was not decoded successfully"
|
||||
);
|
||||
}
|
||||
tracing::debug!(
|
||||
target: crate::SPL_TOKEN_TRACING_TARGET,
|
||||
action = "decode",
|
||||
signature = %input.signature,
|
||||
instruction_path = %input.instruction_path,
|
||||
transaction_failed = input.transaction_failed,
|
||||
result_status = ?result.status,
|
||||
observation_count = result.observations.len(),
|
||||
"classic SPL Token instruction decode completed"
|
||||
);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use base64::Engine; // rust-rules: trait-import
|
||||
|
||||
fn input(
|
||||
program_id: &str,
|
||||
payload: &[u8],
|
||||
accounts: serde_json::Value,
|
||||
keys: serde_json::Value,
|
||||
failed: bool,
|
||||
path: &str,
|
||||
) -> crate::CoreInstructionReplayInput {
|
||||
let result = crate::CoreInstructionReplayInput::new(
|
||||
format!("signature:{path}"),
|
||||
"signature",
|
||||
42,
|
||||
path,
|
||||
program_id,
|
||||
failed,
|
||||
if failed {
|
||||
std::option::Option::Some(serde_json::json!({"InstructionError":[0,"Custom"]}))
|
||||
} else {
|
||||
std::option::Option::None
|
||||
},
|
||||
keys,
|
||||
accounts,
|
||||
std::option::Option::Some(serde_json::json!({
|
||||
"dataBase64": base64::engine::general_purpose::STANDARD.encode(payload)
|
||||
})),
|
||||
std::option::Option::None,
|
||||
serde_json::json!([]),
|
||||
serde_json::json!([]),
|
||||
serde_json::json!([]),
|
||||
serde_json::json!([]),
|
||||
);
|
||||
return match result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("Token replay input failed: {error}"),
|
||||
};
|
||||
}
|
||||
|
||||
fn minimal_wire(tag: u8) -> std::vec::Vec<u8> {
|
||||
return match tag {
|
||||
0 | 20 => {
|
||||
let mut value = std::vec![tag, 9];
|
||||
value.extend_from_slice(&[1; 32]);
|
||||
value.push(0);
|
||||
value
|
||||
},
|
||||
2 | 19 => std::vec![tag, 1],
|
||||
3 | 4 | 7 | 8 | 23 => {
|
||||
let mut value = std::vec![tag];
|
||||
value.extend_from_slice(&1_u64.to_le_bytes());
|
||||
value
|
||||
},
|
||||
6 => std::vec![tag, 0, 0],
|
||||
12 | 13 | 14 | 15 => {
|
||||
let mut value = std::vec![tag];
|
||||
value.extend_from_slice(&1_u64.to_le_bytes());
|
||||
value.push(9);
|
||||
value
|
||||
},
|
||||
16 | 18 => {
|
||||
let mut value = std::vec![tag];
|
||||
value.extend_from_slice(&[2; 32]);
|
||||
value
|
||||
},
|
||||
24 => std::vec![tag, b'1'],
|
||||
45 => std::vec![tag, 0],
|
||||
255 => std::vec![tag, 0, 1, 1],
|
||||
_ => std::vec![tag],
|
||||
};
|
||||
}
|
||||
|
||||
fn account(position: usize, signer: bool, writable: bool) -> serde_json::Value {
|
||||
return serde_json::json!({
|
||||
"position": position,
|
||||
"accountIndex": position,
|
||||
"accountKey": format!("account{position}"),
|
||||
"signer": signer,
|
||||
"writable": writable,
|
||||
"source": "static",
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exact_support_program_id_and_interface_match() {
|
||||
assert_eq!(spl_token_interface::ID.to_string(), kb_program_ids::SPL_TOKEN_PROGRAM_ID);
|
||||
let decoder = crate::SplTokenDecoder;
|
||||
assert_eq!(crate::InstructionDecoder::surfaces(&decoder).len(), 1);
|
||||
let observation = crate::ProgramObservation {
|
||||
signature: crate::Signature("signature".to_string()),
|
||||
slot: crate::Slot(1),
|
||||
instruction_path: crate::InstructionPath("0".to_string()),
|
||||
program_id: crate::ProgramId(kb_program_ids::SPL_TOKEN_PROGRAM_ID.to_string()),
|
||||
discriminator_8: std::option::Option::None,
|
||||
data_len: 1,
|
||||
accounts_len: 0,
|
||||
failed: false,
|
||||
};
|
||||
assert_eq!(
|
||||
crate::ProtocolDecoder::supports_observation(&decoder, &observation),
|
||||
crate::DecoderSupport::Yes
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn coverage_matrix_and_compiled_entries_are_equal() {
|
||||
let matrix: serde_json::Value =
|
||||
match serde_json::from_str(include_str!("../../../../../docs/SPL_TOKEN_MATRIX.json")) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("SPL Token matrix is invalid: {error}"),
|
||||
};
|
||||
let rows = match matrix.get("instructions").and_then(serde_json::Value::as_array) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => panic!("SPL Token matrix has no instructions"),
|
||||
};
|
||||
let matrix_entries = rows
|
||||
.iter()
|
||||
.map(|row| {
|
||||
return (
|
||||
row.get("tag").and_then(serde_json::Value::as_u64).unwrap_or(u64::MAX) as u8,
|
||||
row.get("name").and_then(serde_json::Value::as_str).unwrap_or(""),
|
||||
);
|
||||
})
|
||||
.collect::<std::vec::Vec<_>>();
|
||||
let compiled_entries = crate::SPL_TOKEN_INSTRUCTION_ENTRIES
|
||||
.iter()
|
||||
.map(|(tag, code, _)| return (*tag, *code))
|
||||
.collect::<std::vec::Vec<_>>();
|
||||
assert_eq!(matrix_entries, compiled_entries);
|
||||
assert_eq!(crate::InstructionDecoder::coverage(&crate::SplTokenDecoder).len(), 28);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cluster_evidence_scopes_recent_instruction_deployment_exactly() {
|
||||
let matrix: serde_json::Value =
|
||||
match serde_json::from_str(include_str!("../../../../../docs/SPL_TOKEN_MATRIX.json")) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("SPL Token matrix is invalid: {error}"),
|
||||
};
|
||||
assert_eq!(
|
||||
matrix["deploymentAudit"]["mainnet"]["status"],
|
||||
"real_corpus_decode_and_materialization_validated"
|
||||
);
|
||||
assert_eq!(matrix["corpusAudit"]["mainnet"]["invariants"]["uncommittedOutputs"], 0);
|
||||
assert_eq!(matrix["corpusAudit"]["mainnet"]["invariants"]["secondReplaySelected"], 0);
|
||||
assert_eq!(matrix["corpusAudit"]["devnet"]["recentInstructionProbe"]["submission"], false);
|
||||
assert_eq!(
|
||||
matrix["corpusAudit"]["devnet"]["recentInstructionProbe"]["batch"]["success"],
|
||||
true
|
||||
);
|
||||
assert_eq!(
|
||||
matrix["corpusAudit"]["devnet"]["recentInstructionProbe"]["batch"]["computeUnits"],
|
||||
270
|
||||
);
|
||||
assert_eq!(
|
||||
matrix["corpusAudit"]["devnet"]["recentInstructionProbe"]["unwrapLamports"]["success"],
|
||||
true
|
||||
);
|
||||
assert_eq!(
|
||||
matrix["corpusAudit"]["devnet"]["recentInstructionProbe"]["unwrapLamports"]["computeUnits"],
|
||||
140
|
||||
);
|
||||
|
||||
let instructions = match matrix.get("instructions").and_then(serde_json::Value::as_array) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => panic!("SPL Token matrix has no instructions"),
|
||||
};
|
||||
for recent_name in ["unwrap_lamports", "batch"] {
|
||||
let recent = match instructions.iter().find(|row| {
|
||||
return row.get("name").and_then(serde_json::Value::as_str)
|
||||
== std::option::Option::Some(recent_name);
|
||||
}) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
panic!("recent SPL Token instruction {recent_name} is absent")
|
||||
},
|
||||
};
|
||||
assert_eq!(recent["executorSupport"]["constructibility"], "official_builder_proven");
|
||||
assert_eq!(
|
||||
recent["executorSupport"]["clusterDeployment"],
|
||||
"devnet_simulation_succeeded_2026-07-16"
|
||||
);
|
||||
assert_eq!(recent["clusterStatus"]["localnet"], "not_tested");
|
||||
assert_eq!(recent["clusterStatus"]["devnet"], "simulation_succeeded_2026-07-16");
|
||||
assert_eq!(recent["clusterStatus"]["mainnet"], "not_tested");
|
||||
assert_eq!(recent["realSignatures"].as_array().map(std::vec::Vec::len), Some(0));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_published_tag_decodes_and_matches_official_unpack() {
|
||||
for (tag, code, _) in crate::SPL_TOKEN_INSTRUCTION_ENTRIES {
|
||||
let wire = minimal_wire(*tag);
|
||||
let official = spl_token_interface::instruction::TokenInstruction::unpack(&wire);
|
||||
assert!(official.is_ok(), "official unpack rejected tag {tag}");
|
||||
let input = input(
|
||||
kb_program_ids::SPL_TOKEN_PROGRAM_ID,
|
||||
wire.as_slice(),
|
||||
serde_json::json!([]),
|
||||
serde_json::json!([]),
|
||||
false,
|
||||
"0",
|
||||
);
|
||||
let result = crate::InstructionDecoder::decode(&crate::SplTokenDecoder, &input);
|
||||
assert_eq!(result.status, crate::DecoderOutcomeStatus::Decoded);
|
||||
assert_eq!(result.recognized_entry_code.as_deref(), std::option::Option::Some(*code));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_transfer_is_an_exact_uncommitted_inner_intent_without_invented_mint() {
|
||||
let mut wire = std::vec![3];
|
||||
wire.extend_from_slice(&u64::MAX.to_le_bytes());
|
||||
let accounts = serde_json::json!([
|
||||
{"position":0,"accountIndex":0,"accountKey":"source"},
|
||||
{"position":1,"accountIndex":1,"accountKey":"destination"},
|
||||
{"position":2,"accountIndex":2,"accountKey":"authority"}
|
||||
]);
|
||||
let keys = serde_json::json!([
|
||||
{"accountIndex":0,"accountKey":"source","signer":false,"writable":true,"source":"static"},
|
||||
{"accountIndex":1,"accountKey":"destination","signer":false,"writable":true,"source":"static"},
|
||||
{"accountIndex":2,"accountKey":"authority","signer":true,"writable":false,"source":"static"}
|
||||
]);
|
||||
let input = input(
|
||||
kb_program_ids::SPL_TOKEN_PROGRAM_ID,
|
||||
wire.as_slice(),
|
||||
accounts,
|
||||
keys,
|
||||
true,
|
||||
"2/1",
|
||||
);
|
||||
let result = crate::InstructionDecoder::decode(&crate::SplTokenDecoder, &input);
|
||||
assert_eq!(result.status, crate::DecoderOutcomeStatus::Decoded);
|
||||
assert!(!result.observations[0].observation_committed);
|
||||
assert_eq!(
|
||||
result.observations[0].payload_json["parameters"]["amountRaw"],
|
||||
u64::MAX.to_string()
|
||||
);
|
||||
assert_eq!(result.observations[0].payload_json["inference"]["mintInvented"], false);
|
||||
assert_eq!(
|
||||
result.observations[0].event.source_kind,
|
||||
crate::EventSourceKind::InnerInstruction
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn suffix_unknown_empty_truncated_and_invalid_utf8_are_bounded() {
|
||||
let mut transfer = minimal_wire(3);
|
||||
transfer.extend_from_slice(&[0xaa, 0xbb]);
|
||||
let suffix = input(
|
||||
kb_program_ids::SPL_TOKEN_PROGRAM_ID,
|
||||
transfer.as_slice(),
|
||||
serde_json::json!([]),
|
||||
serde_json::json!([]),
|
||||
false,
|
||||
"0",
|
||||
);
|
||||
let suffix_result = crate::InstructionDecoder::decode(&crate::SplTokenDecoder, &suffix);
|
||||
assert_eq!(suffix_result.status, crate::DecoderOutcomeStatus::Decoded);
|
||||
assert_eq!(suffix_result.observations[0].payload_json["wire"]["suffixLengthBytes"], 2);
|
||||
for wire in [std::vec::Vec::new(), std::vec![3, 1], std::vec![24, 0xff], std::vec![255]] {
|
||||
let input = input(
|
||||
kb_program_ids::SPL_TOKEN_PROGRAM_ID,
|
||||
wire.as_slice(),
|
||||
serde_json::json!([]),
|
||||
serde_json::json!([]),
|
||||
false,
|
||||
"0",
|
||||
);
|
||||
let result = crate::InstructionDecoder::decode(&crate::SplTokenDecoder, &input);
|
||||
assert_eq!(result.status, crate::DecoderOutcomeStatus::Failed);
|
||||
assert!(result.observations.is_empty());
|
||||
}
|
||||
let unknown = input(
|
||||
kb_program_ids::SPL_TOKEN_PROGRAM_ID,
|
||||
&[44],
|
||||
serde_json::json!([]),
|
||||
serde_json::json!([]),
|
||||
false,
|
||||
"0",
|
||||
);
|
||||
let result = crate::InstructionDecoder::decode(&crate::SplTokenDecoder, &unknown);
|
||||
assert_eq!(result.status, crate::DecoderOutcomeStatus::Unsupported);
|
||||
assert!(!result.diagnostics.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_preserves_order_slices_paths_and_rejects_nested_batch() {
|
||||
let mut batch = std::vec![255, 0, 9, 3];
|
||||
batch.extend_from_slice(&1_u64.to_le_bytes());
|
||||
batch.extend_from_slice(&[0, 1, 17]);
|
||||
let cinput = input(
|
||||
kb_program_ids::SPL_TOKEN_PROGRAM_ID,
|
||||
batch.as_slice(),
|
||||
serde_json::json!([]),
|
||||
serde_json::json!([]),
|
||||
false,
|
||||
"4/2",
|
||||
);
|
||||
let result = crate::InstructionDecoder::decode(&crate::SplTokenDecoder, &cinput);
|
||||
assert_eq!(result.status, crate::DecoderOutcomeStatus::Decoded);
|
||||
assert_eq!(result.observations.len(), 3);
|
||||
assert_eq!(result.observations[1].event.instruction_path.0, "4/2/batch/0");
|
||||
assert_eq!(result.observations[2].event.instruction_path.0, "4/2/batch/1");
|
||||
assert_eq!(result.observations[0].payload_json["parameters"]["subInstructionCount"], 2);
|
||||
let nested = input(
|
||||
kb_program_ids::SPL_TOKEN_PROGRAM_ID,
|
||||
&[255, 0, 1, 255],
|
||||
serde_json::json!([]),
|
||||
serde_json::json!([]),
|
||||
false,
|
||||
"0",
|
||||
);
|
||||
let nested_result = crate::InstructionDecoder::decode(&crate::SplTokenDecoder, &nested);
|
||||
assert_eq!(nested_result.status, crate::DecoderOutcomeStatus::Failed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authority_forms_and_multisig_threshold_diagnostics_are_explicit() {
|
||||
let wire = minimal_wire(3);
|
||||
let simple_accounts = serde_json::json!([
|
||||
{"position":0,"accountIndex":0,"accountKey":"account0"},
|
||||
{"position":1,"accountIndex":1,"accountKey":"account1"},
|
||||
{"position":2,"accountIndex":2,"accountKey":"account2"}
|
||||
]);
|
||||
let simple_keys = serde_json::json!([
|
||||
account(0, false, true),
|
||||
account(1, false, true),
|
||||
account(2, true, false)
|
||||
]);
|
||||
let simple = input(
|
||||
kb_program_ids::SPL_TOKEN_PROGRAM_ID,
|
||||
wire.as_slice(),
|
||||
simple_accounts.clone(),
|
||||
simple_keys,
|
||||
false,
|
||||
"0",
|
||||
);
|
||||
let simple_result = crate::InstructionDecoder::decode(&crate::SplTokenDecoder, &simple);
|
||||
assert_eq!(simple_result.observations[0].payload_json["authority"]["form"], "single");
|
||||
let mut multisig_accounts = simple_accounts.as_array().cloned().unwrap_or_default();
|
||||
multisig_accounts
|
||||
.push(serde_json::json!({"position":3,"accountIndex":3,"accountKey":"account3"}));
|
||||
let multisig_keys = serde_json::json!([
|
||||
account(0, false, true),
|
||||
account(1, false, true),
|
||||
account(2, false, false),
|
||||
account(3, true, false)
|
||||
]);
|
||||
let multisig = input(
|
||||
kb_program_ids::SPL_TOKEN_PROGRAM_ID,
|
||||
wire.as_slice(),
|
||||
serde_json::Value::Array(multisig_accounts),
|
||||
multisig_keys,
|
||||
false,
|
||||
"0",
|
||||
);
|
||||
let multisig_result = crate::InstructionDecoder::decode(&crate::SplTokenDecoder, &multisig);
|
||||
assert_eq!(multisig_result.observations[0].payload_json["authority"]["form"], "multisig");
|
||||
assert_eq!(
|
||||
multisig_result.observations[0].payload_json["authority"]["statefulMultisigValidation"],
|
||||
"requires_multisig_account_snapshot"
|
||||
);
|
||||
}
|
||||
}
|
||||
999
kb-lib/src/decoder/spl/token/wire.rs
Normal file
999
kb-lib/src/decoder/spl/token/wire.rs
Normal file
@@ -0,0 +1,999 @@
|
||||
// file: kb-lib/src/decoder/spl/token/wire.rs
|
||||
// version: 1
|
||||
|
||||
//! Bounded classic SPL Token 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::SPL_TOKEN_INSTRUCTION_ENTRIES
|
||||
.iter()
|
||||
.copied()
|
||||
.find(|(candidate, _, _)| return *candidate == tag);
|
||||
}
|
||||
|
||||
pub(crate) fn payload_tag(input: &crate::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: &crate::CoreInstructionReplayInput) -> crate::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",
|
||||
"classic SPL Token instruction payload is empty".to_string(),
|
||||
);
|
||||
},
|
||||
};
|
||||
let entry = match crate::spl_token_entry_for_tag(tag) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return crate::DecoderExecutionResult {
|
||||
status: crate::DecoderOutcomeStatus::Unsupported,
|
||||
recognized_entry_code: std::option::Option::None,
|
||||
observations: std::vec::Vec::new(),
|
||||
diagnostics: std::vec![crate::DecoderDiagnostic {
|
||||
code: "unknown_token_instruction_tag".to_string(),
|
||||
message: format!(
|
||||
"unknown classic SPL Token tag {tag}; 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(
|
||||
std::option::Option::Some(entry.1),
|
||||
"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:{}:0", parsed.code),
|
||||
input.instruction_path.clone(),
|
||||
event_family(parsed.code),
|
||||
);
|
||||
let mut observations = std::vec![parent];
|
||||
observations.extend(batch_children);
|
||||
return crate::DecoderExecutionResult {
|
||||
status: crate::DecoderOutcomeStatus::Decoded,
|
||||
recognized_entry_code: std::option::Option::Some(parsed.code.to_string()),
|
||||
observations,
|
||||
diagnostics: std::vec::Vec::new(),
|
||||
};
|
||||
}
|
||||
|
||||
fn decode_payload(input: &crate::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(
|
||||
"classic SPL Token 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(
|
||||
"classic SPL Token payload does not contain dataBase64",
|
||||
));
|
||||
},
|
||||
};
|
||||
let maximum_encoded = crate::SPL_TOKEN_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!(
|
||||
"classic SPL Token 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!(
|
||||
"classic SPL Token payload is not valid base64: {error}"
|
||||
)));
|
||||
},
|
||||
};
|
||||
if decoded.len() > crate::SPL_TOKEN_MAX_INSTRUCTION_BYTES {
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(format!(
|
||||
"classic SPL Token payload exceeds {} decoded bytes",
|
||||
crate::SPL_TOKEN_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> {
|
||||
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::spl_token_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),
|
||||
45 => parse_unwrap_lamports(bytes, code, tag),
|
||||
_ => std::result::Result::Err(format!("unsupported instruction tag {tag}")),
|
||||
};
|
||||
return result;
|
||||
}
|
||||
|
||||
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(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(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(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_u64(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: &crate::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(
|
||||
"classic SPL Token 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(
|
||||
"classic SPL Token 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!(
|
||||
"classic SPL Token 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!(
|
||||
"classic SPL Token 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!(
|
||||
"classic SPL Token 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!(
|
||||
"classic SPL Token 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!(
|
||||
"classic SPL Token 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!(
|
||||
"classic SPL Token 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),
|
||||
_ => 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"
|
||||
},
|
||||
(_, 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,
|
||||
"batch" => true,
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn observation(
|
||||
input: &crate::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: crate::EventFamily,
|
||||
) -> crate::DecodedObservation {
|
||||
let wire_hash = hash(wire);
|
||||
let event = crate::DecodedProtocolEvent {
|
||||
signature: crate::Signature(input.signature.clone()),
|
||||
slot: crate::Slot(input.slot),
|
||||
instruction_path: crate::InstructionPath(instruction_path.clone()),
|
||||
program_id: crate::ProgramId(input.program_id.clone()),
|
||||
protocol_code: crate::ProtocolCode(crate::SPL_TOKEN_SURFACE_CODE.to_string()),
|
||||
surface_code: crate::SurfaceCode(crate::SPL_TOKEN_SURFACE_CODE.to_string()),
|
||||
event_code: crate::EventCode(format!("{}.{}", crate::SPL_TOKEN_SURFACE_CODE, code)),
|
||||
event_name: crate::EventName(code.to_string()),
|
||||
event_family: family,
|
||||
source_kind: if input.instruction_path.contains('/') {
|
||||
crate::EventSourceKind::InnerInstruction
|
||||
} else {
|
||||
crate::EventSourceKind::Instruction
|
||||
},
|
||||
confidence: crate::DecoderConfidence::ManualExact,
|
||||
};
|
||||
return crate::DecodedObservation {
|
||||
event_key,
|
||||
event,
|
||||
payload_json: serde_json::json!({
|
||||
"eventVersion": crate::SPL_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: crate::DecoderProof {
|
||||
kind: crate::DecoderProofKind::Manual,
|
||||
confidence: crate::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: &crate::CoreInstructionReplayInput,
|
||||
bytes: &[u8],
|
||||
accounts: &[ResolvedAccount],
|
||||
) -> std::result::Result<
|
||||
(std::vec::Vec<crate::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::SPL_TOKEN_MAX_BATCH_INSTRUCTIONS {
|
||||
return std::result::Result::Err(format!(
|
||||
"Batch exceeds {} sub-instructions",
|
||||
crate::SPL_TOKEN_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::SPL_TOKEN_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: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) -> crate::EventFamily {
|
||||
return match code {
|
||||
"set_authority" | "initialize_multisig" | "initialize_multisig2" => {
|
||||
crate::EventFamily::Admin
|
||||
},
|
||||
"initialize_mint" | "initialize_mint2" | "mint_to" | "mint_to_checked" => {
|
||||
crate::EventFamily::TokenMint
|
||||
},
|
||||
"burn" | "burn_checked" => crate::EventFamily::TokenBurn,
|
||||
"batch" | "get_account_data_size" | "amount_to_ui_amount" | "ui_amount_to_amount" => {
|
||||
crate::EventFamily::Audit
|
||||
},
|
||||
_ => crate::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::SPL_TOKEN_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,
|
||||
) -> crate::DecoderExecutionResult {
|
||||
return crate::DecoderExecutionResult {
|
||||
status: crate::DecoderOutcomeStatus::Failed,
|
||||
recognized_entry_code: entry_code.map(str::to_string),
|
||||
observations: std::vec::Vec::new(),
|
||||
diagnostics: std::vec![crate::DecoderDiagnostic {
|
||||
code: code.to_string(),
|
||||
message,
|
||||
retriable: false,
|
||||
}],
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: kb-lib/src/lib.rs
|
||||
// version: 7
|
||||
// version: 8
|
||||
|
||||
//! Consolidated decoder, executor, materializer and shared model library.
|
||||
#![warn(missing_docs)]
|
||||
@@ -27,6 +27,22 @@ pub(crate) use crate::decoder::SPL_MEMO_V1_SURFACE_CODE;
|
||||
pub(crate) use crate::decoder::SPL_MEMO_V3_SURFACE_CODE;
|
||||
/// Stable Memo v4 surface code.
|
||||
pub(crate) use crate::decoder::SPL_MEMO_V4_SURFACE_CODE;
|
||||
/// Maximum prefix retained in bounded classic SPL Token diagnostics.
|
||||
pub(crate) use crate::decoder::SPL_TOKEN_DIAGNOSTIC_PREFIX_BYTES;
|
||||
/// Current stable classic SPL Token decoded event contract version.
|
||||
pub(crate) use crate::decoder::SPL_TOKEN_EVENT_VERSION;
|
||||
/// One exact classic SPL Token instruction entry published by the official interface.
|
||||
pub(crate) use crate::decoder::SPL_TOKEN_INSTRUCTION_ENTRIES;
|
||||
/// Maximum total account metas consumed by one classic SPL Token batch.
|
||||
pub(crate) use crate::decoder::SPL_TOKEN_MAX_BATCH_ACCOUNTS;
|
||||
/// Maximum decoded sub-instructions in one classic SPL Token batch.
|
||||
pub(crate) use crate::decoder::SPL_TOKEN_MAX_BATCH_INSTRUCTIONS;
|
||||
/// Maximum retained classic SPL Token instruction payload size.
|
||||
pub(crate) use crate::decoder::SPL_TOKEN_MAX_INSTRUCTION_BYTES;
|
||||
/// Stable protocol and surface code for the classic SPL Token program.
|
||||
pub(crate) use crate::decoder::SPL_TOKEN_SURFACE_CODE;
|
||||
/// Canonical tracing target for the classic SPL Token decoder.
|
||||
pub(crate) use crate::decoder::SPL_TOKEN_TRACING_TARGET;
|
||||
/// Stable Address Lookup Table surface code.
|
||||
pub(crate) use crate::decoder::solana::SOLANA_CORE_ADDRESS_LOOKUP_TABLE_SURFACE_CODE;
|
||||
/// Stable deprecated immutable BPF Loader surface code.
|
||||
@@ -79,6 +95,12 @@ pub(crate) use crate::decoder::solana::solana_core_config_recognize;
|
||||
pub(crate) use crate::decoder::solana::solana_core_resolve_accounts;
|
||||
/// Decodes one bounded SPL Memo instruction.
|
||||
pub(crate) use crate::decoder::spl_memo_decode;
|
||||
/// Decodes one bounded classic SPL Token instruction.
|
||||
pub(crate) use crate::decoder::spl_token_decode;
|
||||
/// Returns the published entry matching one classic SPL Token tag.
|
||||
pub(crate) use crate::decoder::spl_token_entry_for_tag;
|
||||
/// Returns the first byte of one retained classic SPL Token payload.
|
||||
pub(crate) use crate::decoder::spl_token_payload_tag;
|
||||
|
||||
/// Stable protocol code shared by native Solana events.
|
||||
pub(crate) use crate::decoder::solana::SOLANA_CORE_PROTOCOL_CODE;
|
||||
@@ -204,6 +226,8 @@ pub(crate) use crate::decoder::solana::solana_core_zk_token_proof_recognize;
|
||||
|
||||
/// Exact decoder for the three registered SPL Memo generations.
|
||||
pub use crate::decoder::SplMemoDecoder;
|
||||
/// Exact decoder for the classic SPL Token program.
|
||||
pub use crate::decoder::SplTokenDecoder;
|
||||
/// Current contextual core instruction input contract version.
|
||||
pub use crate::decoder::api::contracts::CORE_INSTRUCTION_INPUT_CONTRACT_VERSION;
|
||||
/// Stable contextual decoded observation.
|
||||
|
||||
Reference in New Issue
Block a user