0.1.0
This commit is contained in:
576
kb-lib/src/decoder/api/contracts.rs
Normal file
576
kb-lib/src/decoder/api/contracts.rs
Normal file
@@ -0,0 +1,576 @@
|
||||
// file: kb-lib/src/decoder/api/contracts.rs
|
||||
// version: 7
|
||||
|
||||
//! Backend-neutral contextual instruction decoder contracts.
|
||||
|
||||
use sha2::Digest; // rust-rules: trait-import
|
||||
|
||||
/// Current contextual core instruction input contract version.
|
||||
pub const CORE_INSTRUCTION_INPUT_CONTRACT_VERSION: u32 = crate::CORE_REPLAY_INPUT_CONTRACT_VERSION;
|
||||
|
||||
/// Stable identity of one contextual instruction decoder.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
pub struct DecoderIdentity {
|
||||
/// Stable lower snake case processor name.
|
||||
pub name: std::string::String,
|
||||
/// Semantic or deterministic implementation version.
|
||||
pub version: std::string::String,
|
||||
}
|
||||
|
||||
impl DecoderIdentity {
|
||||
/// Builds a validated decoder identity.
|
||||
pub fn new(
|
||||
name: impl std::convert::Into<std::string::String>,
|
||||
version: impl std::convert::Into<std::string::String>,
|
||||
) -> kb_core::Result<Self> {
|
||||
let value = Self {
|
||||
name: name.into(),
|
||||
version: version.into(),
|
||||
};
|
||||
if value.name.trim().is_empty() || value.version.trim().is_empty() {
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(
|
||||
"decoder identity fields must not be empty",
|
||||
));
|
||||
}
|
||||
return std::result::Result::Ok(value);
|
||||
}
|
||||
}
|
||||
|
||||
/// One statically declared Solana surface supported by a decoder.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct DecoderSurface {
|
||||
/// Exact Solana program identifier.
|
||||
pub program_id: &'static str,
|
||||
/// Stable surface code.
|
||||
pub surface_code: &'static str,
|
||||
/// Explicit dispatch priority, where larger values win.
|
||||
pub priority: u16,
|
||||
}
|
||||
|
||||
/// Declared decoder coverage entry kind.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
pub enum DecoderCoverageEntryKind {
|
||||
/// Program instruction entry.
|
||||
Instruction,
|
||||
/// Program event entry.
|
||||
Event,
|
||||
/// Raw discriminator entry.
|
||||
Discriminator,
|
||||
}
|
||||
|
||||
/// Machine-readable decoder coverage declaration.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
pub struct DecoderCoverageDeclaration {
|
||||
/// Solana program identifier.
|
||||
pub program_id: std::string::String,
|
||||
/// Optional stable surface code.
|
||||
pub surface_code: std::option::Option<std::string::String>,
|
||||
/// Entry kind.
|
||||
pub entry_kind: DecoderCoverageEntryKind,
|
||||
/// Stable instruction, event or discriminator code.
|
||||
pub entry_code: std::string::String,
|
||||
/// Optional normalized lowercase hexadecimal discriminator.
|
||||
pub discriminator_hex: std::option::Option<std::string::String>,
|
||||
/// Whether the entry is historical or deprecated but still supported.
|
||||
pub historical: bool,
|
||||
}
|
||||
|
||||
impl DecoderCoverageDeclaration {
|
||||
/// Builds a validated coverage declaration.
|
||||
pub fn new(
|
||||
program_id: impl std::convert::Into<std::string::String>,
|
||||
surface_code: std::option::Option<std::string::String>,
|
||||
entry_kind: DecoderCoverageEntryKind,
|
||||
entry_code: impl std::convert::Into<std::string::String>,
|
||||
discriminator_hex: std::option::Option<std::string::String>,
|
||||
historical: bool,
|
||||
) -> kb_core::Result<Self> {
|
||||
let value = Self {
|
||||
program_id: program_id.into(),
|
||||
surface_code,
|
||||
entry_kind,
|
||||
entry_code: entry_code.into(),
|
||||
discriminator_hex,
|
||||
historical,
|
||||
};
|
||||
if value.program_id.trim().is_empty() || value.entry_code.trim().is_empty() {
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(
|
||||
"decoder coverage program id and entry code must not be empty",
|
||||
));
|
||||
}
|
||||
if value
|
||||
.surface_code
|
||||
.as_deref()
|
||||
.is_some_and(|surface| return surface.trim().is_empty())
|
||||
{
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(
|
||||
"decoder coverage surface code must not be empty when present",
|
||||
));
|
||||
}
|
||||
if value
|
||||
.discriminator_hex
|
||||
.as_deref()
|
||||
.is_some_and(|discriminator| return discriminator.trim().is_empty())
|
||||
{
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(
|
||||
"decoder coverage discriminator must not be empty when present",
|
||||
));
|
||||
}
|
||||
return std::result::Result::Ok(value);
|
||||
}
|
||||
}
|
||||
|
||||
/// Deterministic recognition result used by dispatch.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
pub struct DecoderRecognition {
|
||||
/// Whether the decoder is compatible with the input.
|
||||
pub compatible: bool,
|
||||
/// Whether recognition is exact rather than program-only.
|
||||
pub exact: bool,
|
||||
/// Explicit priority used after program and surface matching.
|
||||
pub priority: u16,
|
||||
/// Optional recognized surface code.
|
||||
pub surface_code: std::option::Option<std::string::String>,
|
||||
/// Optional recognized instruction or discriminator code.
|
||||
pub entry_code: std::option::Option<std::string::String>,
|
||||
/// Optional normalized lowercase hexadecimal discriminator.
|
||||
pub discriminator_hex: std::option::Option<std::string::String>,
|
||||
}
|
||||
|
||||
impl DecoderRecognition {
|
||||
/// Builds a non-compatible recognition result.
|
||||
pub fn incompatible() -> Self {
|
||||
return Self {
|
||||
compatible: false,
|
||||
exact: false,
|
||||
priority: 0,
|
||||
surface_code: std::option::Option::None,
|
||||
entry_code: std::option::Option::None,
|
||||
discriminator_hex: std::option::Option::None,
|
||||
};
|
||||
}
|
||||
|
||||
/// Builds a compatible recognition result.
|
||||
pub fn compatible(
|
||||
exact: bool,
|
||||
priority: u16,
|
||||
surface_code: std::option::Option<std::string::String>,
|
||||
entry_code: std::option::Option<std::string::String>,
|
||||
discriminator_hex: std::option::Option<std::string::String>,
|
||||
) -> Self {
|
||||
return Self {
|
||||
compatible: true,
|
||||
exact,
|
||||
priority,
|
||||
surface_code,
|
||||
entry_code,
|
||||
discriminator_hex,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Decoder terminal result status.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
pub enum DecoderOutcomeStatus {
|
||||
/// At least one stable decoded observation was produced.
|
||||
Decoded,
|
||||
/// The decoder intentionally ignored a recognized input.
|
||||
Ignored,
|
||||
/// The input belongs to the program but its entry is unsupported.
|
||||
Unsupported,
|
||||
/// Decoding failed and diagnostics were produced.
|
||||
Failed,
|
||||
}
|
||||
|
||||
/// Proof category attached to one decoded observation.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
pub enum DecoderProofKind {
|
||||
/// Exact instruction discriminator match.
|
||||
ExactDiscriminator,
|
||||
/// Exact binary layout decode.
|
||||
ExactLayout,
|
||||
/// Exact IDL-based decode.
|
||||
Idl,
|
||||
/// Manually verified exact decode.
|
||||
Manual,
|
||||
/// Correlation with runtime logs.
|
||||
LogCorrelation,
|
||||
/// Balance delta evidence.
|
||||
BalanceDelta,
|
||||
/// Heuristic evidence that must not be treated as exact.
|
||||
Heuristic,
|
||||
/// Audit-only classification.
|
||||
Audit,
|
||||
/// Unknown or unclassified evidence.
|
||||
Unknown,
|
||||
}
|
||||
|
||||
/// Explicit proof attached to one decoded observation.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
pub struct DecoderProof {
|
||||
/// Proof category.
|
||||
pub kind: DecoderProofKind,
|
||||
/// Decoder confidence.
|
||||
pub confidence: crate::DecoderConfidence,
|
||||
/// Stable evidence strings, hashes or classifier codes.
|
||||
pub evidence: std::vec::Vec<std::string::String>,
|
||||
}
|
||||
|
||||
/// Structured decoder diagnostic without third-party error wrappers.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
pub struct DecoderDiagnostic {
|
||||
/// Stable machine-readable diagnostic code.
|
||||
pub code: std::string::String,
|
||||
/// Human-readable diagnostic message.
|
||||
pub message: std::string::String,
|
||||
/// Whether retrying the same version and input may succeed.
|
||||
pub retriable: bool,
|
||||
}
|
||||
|
||||
impl DecoderDiagnostic {
|
||||
/// Builds a validated decoder diagnostic.
|
||||
pub fn new(
|
||||
code: impl std::convert::Into<std::string::String>,
|
||||
message: impl std::convert::Into<std::string::String>,
|
||||
retriable: bool,
|
||||
) -> kb_core::Result<Self> {
|
||||
let value = Self {
|
||||
code: code.into(),
|
||||
message: message.into(),
|
||||
retriable,
|
||||
};
|
||||
if value.code.trim().is_empty() || value.message.trim().is_empty() {
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(
|
||||
"decoder diagnostic code and message must not be empty",
|
||||
));
|
||||
}
|
||||
return std::result::Result::Ok(value);
|
||||
}
|
||||
}
|
||||
|
||||
/// Stable contextual decoded observation persisted by the decode pipeline.
|
||||
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
pub struct DecodedObservation {
|
||||
/// Stable processor-owned event key within one input.
|
||||
pub event_key: std::string::String,
|
||||
/// Shared typed protocol event identity.
|
||||
pub event: crate::DecodedProtocolEvent,
|
||||
/// Typed event payload serialized as deterministic JSON.
|
||||
pub payload_json: serde_json::Value,
|
||||
/// Whether the source transaction failed on-chain.
|
||||
pub transaction_failed: bool,
|
||||
/// Optional transaction error JSON.
|
||||
pub transaction_error: std::option::Option<serde_json::Value>,
|
||||
/// Whether the observed state mutation was committed on-chain.
|
||||
pub observation_committed: bool,
|
||||
/// Explicit decoding proof.
|
||||
pub proof: DecoderProof,
|
||||
}
|
||||
|
||||
impl DecodedObservation {
|
||||
/// Validates failed transaction commit semantics and stable identity fields.
|
||||
pub fn validate(&self) -> kb_core::Result<()> {
|
||||
if self.event_key.trim().is_empty() {
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(
|
||||
"decoded observation event key must not be empty",
|
||||
));
|
||||
}
|
||||
if self.transaction_failed && self.observation_committed {
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(
|
||||
"failed transaction observations must not be marked committed",
|
||||
));
|
||||
}
|
||||
if self.event.signature.0.trim().is_empty()
|
||||
|| self.event.instruction_path.0.trim().is_empty()
|
||||
|| self.event.program_id.0.trim().is_empty()
|
||||
|| self.event.event_code.0.trim().is_empty()
|
||||
{
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(
|
||||
"decoded observation event identity fields must not be empty",
|
||||
));
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
/// Complete explicit result produced by one decoder invocation.
|
||||
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
pub struct DecoderExecutionResult {
|
||||
/// Terminal decoder status.
|
||||
pub status: DecoderOutcomeStatus,
|
||||
/// Optional recognized instruction or discriminator code.
|
||||
pub recognized_entry_code: std::option::Option<std::string::String>,
|
||||
/// Decoded observations.
|
||||
pub observations: std::vec::Vec<DecodedObservation>,
|
||||
/// Structured diagnostics.
|
||||
pub diagnostics: std::vec::Vec<DecoderDiagnostic>,
|
||||
}
|
||||
|
||||
impl DecoderExecutionResult {
|
||||
/// Builds an unsupported result with no false decoded observation.
|
||||
pub fn unsupported(entry_code: std::option::Option<std::string::String>) -> Self {
|
||||
return Self {
|
||||
status: DecoderOutcomeStatus::Unsupported,
|
||||
recognized_entry_code: entry_code,
|
||||
observations: std::vec::Vec::new(),
|
||||
diagnostics: std::vec::Vec::new(),
|
||||
};
|
||||
}
|
||||
|
||||
/// Builds an ignored result.
|
||||
pub fn ignored(entry_code: std::option::Option<std::string::String>) -> Self {
|
||||
return Self {
|
||||
status: DecoderOutcomeStatus::Ignored,
|
||||
recognized_entry_code: entry_code,
|
||||
observations: std::vec::Vec::new(),
|
||||
diagnostics: std::vec::Vec::new(),
|
||||
};
|
||||
}
|
||||
|
||||
/// Validates status and observation consistency.
|
||||
pub fn validate(&self) -> kb_core::Result<()> {
|
||||
if self.status == DecoderOutcomeStatus::Decoded && self.observations.is_empty() {
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(
|
||||
"decoded status requires at least one observation",
|
||||
));
|
||||
}
|
||||
if self.status != DecoderOutcomeStatus::Decoded && !self.observations.is_empty() {
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(
|
||||
"non-decoded status must not contain observations",
|
||||
));
|
||||
}
|
||||
if self.status == DecoderOutcomeStatus::Failed && self.diagnostics.is_empty() {
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(
|
||||
"failed decoder status requires at least one diagnostic",
|
||||
));
|
||||
}
|
||||
for observation in &self.observations {
|
||||
let validation_result = observation.validate();
|
||||
if let std::result::Result::Err(error) = validation_result {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
/// Contextual instruction decoder contract used by the common pipeline.
|
||||
pub trait InstructionDecoder: std::marker::Send + std::marker::Sync {
|
||||
/// Returns the stable decoder identity.
|
||||
fn identity(&self) -> DecoderIdentity;
|
||||
/// Returns every exact Solana surface supported by the decoder.
|
||||
fn surfaces(&self) -> &'static [DecoderSurface];
|
||||
/// Returns the machine-readable declared coverage matrix.
|
||||
fn coverage(&self) -> std::vec::Vec<DecoderCoverageDeclaration>;
|
||||
/// Recognizes one contextual core instruction for deterministic dispatch.
|
||||
fn recognize(&self, input: &crate::CoreInstructionReplayInput) -> DecoderRecognition;
|
||||
/// Decodes one recognized contextual core instruction.
|
||||
fn decode(&self, input: &crate::CoreInstructionReplayInput) -> DecoderExecutionResult;
|
||||
}
|
||||
|
||||
/// Returns true when a decoder declares the exact program id.
|
||||
pub fn decoder_handles_program_id(decoder: &dyn InstructionDecoder, program_id: &str) -> bool {
|
||||
for surface in decoder.surfaces() {
|
||||
if surface.program_id == program_id {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Computes a deterministic lowercase SHA-256 hash for one contextual input.
|
||||
pub fn contextual_input_hash(
|
||||
input: &crate::CoreInstructionReplayInput,
|
||||
) -> kb_core::Result<std::string::String> {
|
||||
let serialization_result = serde_json::to_value(input);
|
||||
let mut value = match serialization_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(kb_core::Error::json(format!(
|
||||
"cannot serialize contextual decoder input: {error}"
|
||||
)));
|
||||
},
|
||||
};
|
||||
return crate::deterministic_json_hash(&mut value);
|
||||
}
|
||||
|
||||
/// Computes a deterministic lowercase SHA-256 hash for one JSON value.
|
||||
pub fn deterministic_json_hash(
|
||||
value: &mut serde_json::Value,
|
||||
) -> kb_core::Result<std::string::String> {
|
||||
sort_json_value(value);
|
||||
let bytes_result = serde_json::to_vec(value);
|
||||
let bytes = match bytes_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(kb_core::Error::json(format!(
|
||||
"cannot serialize deterministic JSON bytes: {error}"
|
||||
)));
|
||||
},
|
||||
};
|
||||
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 std::result::Result::Ok(output);
|
||||
}
|
||||
|
||||
/// Extracts the first eight payload bytes as normalized lowercase hexadecimal when available.
|
||||
pub fn discriminator_8_hex(
|
||||
input: &crate::CoreInstructionReplayInput,
|
||||
) -> std::option::Option<std::string::String> {
|
||||
let payload = match &input.instruction_payload_json {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::option::Option::None,
|
||||
};
|
||||
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::option::Option::None,
|
||||
};
|
||||
let decoded_result =
|
||||
base64::Engine::decode(&base64::engine::general_purpose::STANDARD, encoded.as_bytes());
|
||||
let decoded = match decoded_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_error) => return std::option::Option::None,
|
||||
};
|
||||
if decoded.len() < 8 {
|
||||
return std::option::Option::None;
|
||||
}
|
||||
let mut output = std::string::String::with_capacity(16);
|
||||
for byte in &decoded[..8] {
|
||||
output.push_str(format!("{byte:02x}").as_str());
|
||||
}
|
||||
return std::option::Option::Some(output);
|
||||
}
|
||||
|
||||
fn sort_json_value(value: &mut serde_json::Value) {
|
||||
match value {
|
||||
serde_json::Value::Object(map) => {
|
||||
let mut entries = std::mem::take(map).into_iter().collect::<std::vec::Vec<_>>();
|
||||
entries.sort_by(|left, right| return left.0.cmp(&right.0));
|
||||
for (key, mut child) in entries {
|
||||
sort_json_value(&mut child);
|
||||
map.insert(key, child);
|
||||
}
|
||||
},
|
||||
serde_json::Value::Array(values) => {
|
||||
for child in values {
|
||||
sort_json_value(child);
|
||||
}
|
||||
},
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
fn replay_input() -> crate::CoreInstructionReplayInput {
|
||||
let result = crate::CoreInstructionReplayInput::new(
|
||||
"signature:0",
|
||||
"signature",
|
||||
42,
|
||||
"0",
|
||||
"program",
|
||||
false,
|
||||
std::option::Option::None,
|
||||
serde_json::json!([{"accountIndex": 0, "accountKey": "account"}]),
|
||||
serde_json::json!([{"accountIndex": 0, "accountKey": "account"}]),
|
||||
std::option::Option::Some(serde_json::json!({"dataBase64": "AQIDBAUGBwg="})),
|
||||
std::option::Option::Some("payload-hash".to_string()),
|
||||
serde_json::json!([{
|
||||
"instructionIndex": 0,
|
||||
"instructionPath": "0",
|
||||
"programId": "program",
|
||||
"payloadJson": {"dataBase64": "AQIDBAUGBwg="},
|
||||
"payloadHash": "payload-hash"
|
||||
}]),
|
||||
serde_json::json!([]),
|
||||
serde_json::json!([]),
|
||||
serde_json::json!([]),
|
||||
);
|
||||
return match result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("unexpected replay input error: {error}"),
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn contextual_input_hash_is_stable() {
|
||||
let first = crate::contextual_input_hash(&replay_input());
|
||||
let second = crate::contextual_input_hash(&replay_input());
|
||||
assert!(first.is_ok());
|
||||
assert_eq!(first, second);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn contextual_input_hash_changes_when_outer_context_changes() {
|
||||
let first = replay_input();
|
||||
let mut second = replay_input();
|
||||
second.outer_instructions_json = serde_json::json!([{
|
||||
"instructionIndex": 0,
|
||||
"instructionPath": "0",
|
||||
"programId": "program",
|
||||
"payloadJson": {"dataBase64": "CAcGBQQDAgE="},
|
||||
"payloadHash": "different-payload-hash"
|
||||
}]);
|
||||
let first_hash = crate::contextual_input_hash(&first);
|
||||
let second_hash = crate::contextual_input_hash(&second);
|
||||
assert!(first_hash.is_ok());
|
||||
assert!(second_hash.is_ok());
|
||||
assert_ne!(first_hash, second_hash);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn contextual_input_serialization_is_deterministic_with_outer_context() {
|
||||
let first_result = serde_json::to_string(&replay_input());
|
||||
let first = match first_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("unexpected serialization error: {error}"),
|
||||
};
|
||||
let second_result = serde_json::to_string(&replay_input());
|
||||
let second = match second_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("unexpected serialization error: {error}"),
|
||||
};
|
||||
assert_eq!(first, second);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn discriminator_uses_first_eight_bytes() {
|
||||
let value = crate::discriminator_8_hex(&replay_input());
|
||||
assert_eq!(value.as_deref(), std::option::Option::Some("0102030405060708"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_observation_cannot_be_committed() {
|
||||
let observation = crate::DecodedObservation {
|
||||
event_key: "event".to_string(),
|
||||
event: crate::DecodedProtocolEvent {
|
||||
signature: crate::Signature("signature".to_string()),
|
||||
slot: crate::Slot(42),
|
||||
instruction_path: crate::InstructionPath("0".to_string()),
|
||||
program_id: crate::ProgramId("program".to_string()),
|
||||
protocol_code: crate::ProtocolCode("solana".to_string()),
|
||||
surface_code: crate::SurfaceCode("system_program".to_string()),
|
||||
event_code: crate::EventCode("system_program.attempt".to_string()),
|
||||
event_name: crate::EventName("attempt".to_string()),
|
||||
event_family: crate::EventFamily::Audit,
|
||||
source_kind: crate::EventSourceKind::Instruction,
|
||||
confidence: crate::DecoderConfidence::Exact,
|
||||
},
|
||||
payload_json: serde_json::json!({}),
|
||||
transaction_failed: true,
|
||||
transaction_error: std::option::Option::Some(
|
||||
serde_json::json!({"InstructionError": [0, "Custom"]}),
|
||||
),
|
||||
observation_committed: true,
|
||||
proof: crate::DecoderProof {
|
||||
kind: crate::DecoderProofKind::ExactLayout,
|
||||
confidence: crate::DecoderConfidence::Exact,
|
||||
evidence: std::vec!["layout".to_string()],
|
||||
},
|
||||
};
|
||||
assert!(observation.validate().is_err());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user