This commit is contained in:
2026-07-23 16:37:12 +02:00
parent 99c345f2f2
commit 0da75c1311
2159 changed files with 230833 additions and 0 deletions

View File

@@ -0,0 +1,21 @@
# file: kb_materializer_transaction_annotations/Cargo.toml
# version: 1
[package]
name = "kb_materializer_transaction_annotations"
version.workspace = true
edition.workspace = true
license.workspace = true
publish.workspace = true
[dependencies]
kb_core = { path = "../kb_core" }
kb_decoder_api = { path = "../kb_decoder_api" }
kb_materializer_api = { path = "../kb_materializer_api" }
kb_model = { path = "../kb_model" }
kb_program_ids = { path = "../kb_program_ids" }
serde_json.workspace = true
tracing.workspace = true
[lints]
workspace = true

View File

@@ -0,0 +1,47 @@
<!-- file: kb_materializer_transaction_annotations/README.md -->
<!-- version: 4 -->
# kb_materializer_transaction_annotations
Ce crate fournit une projection réutilisable dannotations de transaction. La première surface active est SPL Memo v1, v3 et v4 ; le domaine nest volontairement pas fusionné avec metadata, admin, lifecycle ou compliance audit.
## Projection Memo
Le matérialiseur accepte uniquement les observations `spl_memo` dont le Program ID, la surface et lentrée correspondent exactement. `add_memo`, `memo_intent` et `invalid_memo_attempt` sont reconnus afin que la politique commune puisse enregistrer un refus explicite, mais seule une observation `add_memo` réussie et commitée produit une sortie.
La sortie `TransactionAnnotation` conserve :
- signature, slot et chemin dinstruction outer/inner ;
- génération et Program ID exacts ;
- texte UTF8 complet dans la borne de 4 096 octets ;
- longueur et SHA-256 du payload ;
- signataires exigés, observés et réellement vérifiés par la génération ;
- état committed, version de projection et provenance du matérialiseur ;
- clé didempotence déterministe fondée sur signature, chemin, Program ID et hash.
Pour v1, les comptes signers restent observables mais la liste `verified` est vide, car le runtime historique ne les vérifie pas. Pour v3/v4, une projection commitée exige légalité ordonnée entre signataires requis et observés, doublons compris.
## Persistance et replay
La sortie utilise le store commun `kb_sol_mat_events` avec la famille `transaction_annotation`. Aucune migration SQL supplémentaire nest nécessaire et les événements decode/core ne sont pas dupliqués. Le ledger commun assure :
- replay identique et même version : skip ;
- force replay : remplacement atomique des sorties appartenant à la version ciblée ;
- changement de version : identité de processor distincte et déterministe.
Les transactions échouées, observations non commitées, UTF8 invalides et validations de signataires invalides restent uniquement dans `kb_sol_decode_events`. Les comptes complets, flags writable, préfixes diagnostics, erreurs UTF8 et preuves détaillées restent également dans lévénement décodé ; la projection ne conserve que les champs utiles à la consultation dannotations.
## Consultation
La fenêtre `demo_decode_replay` affiche les sorties comme un journal dannotations, et non comme une série OHLC : slot, signature, instruction path, génération, texte, longueur, signataires vérifiés, hash et provenance. La lecture passe par le contrat borné `DecodePipelineStore::list_materialized_events`, filtré exactement sur le processor `transaction_annotations` et la famille `transaction_annotation` ; aucun SQL libre ni payload JSON arbitraire ne traverse lUI.
La démo dexécution Memo v4 réutilise ce même contrat de lecture pour vérifier la projection après confirmation. Trois transactions Devnet réelles ont chacune produit exactement une annotation ; le second replay a été `skipped` sans sortie supplémentaire ni refus. Cette projection reste un journal consultable et corrélable, pas une agrégation temporelle de type OHLC.
Le parcours Mainnet du 14 juillet 2026 a acquis puis extrait 300 transactions réelles, 100 par Program ID. Le replay a décodé 102 instructions v1, 439 v3 et 100 v4 sans unmatched ni échec de décodage. La projection a produit respectivement 69, 422 et 100 annotations ; les 33 refus v1 et 17 refus v3 correspondaient aux intentions issues de transactions échouées. Un second replay v3 non forcé a sélectionné zéro entrée, validant le skip idempotent du ledger. Les campagnes forcées v3/v4 ont également reproduit les mêmes nombres de sorties.
## Règles locales
- `SuccessfulCommittedOnly` est appliqué avant toute sortie.
- Un payload décodé incohérent échoue fermé avec un diagnostic stable.
- Le target tracing canonique est `kb_materializer_transaction_annotations`.
- Aucun log ne contient de clé privée ni de texte Memo complet.

View File

@@ -0,0 +1,13 @@
// file: kb_materializer_transaction_annotations/src/constants.rs
// version: 1
//! Local constants for the transaction annotation materializer.
/// Canonical tracing target for this crate.
pub(crate) const TRACING_TARGET: &str = "kb_materializer_transaction_annotations";
/// Stable projection contract version.
pub(crate) const PROJECTION_VERSION: u32 = 1;
/// Stable materializer processor name.
pub(crate) const PROCESSOR_NAME: &str = "transaction_annotations";
/// Maximum Memo payload accepted from the decoded contract.
pub(crate) const MAX_MEMO_PAYLOAD_BYTES: u64 = 4_096;

View File

@@ -0,0 +1,22 @@
// file: kb_materializer_transaction_annotations/src/lib.rs
// version: 4
//! Reusable transaction annotation materializer.
#![warn(missing_docs)]
#![deny(unreachable_pub)]
#![forbid(unsafe_code)]
mod constants;
mod materializer;
/// Maximum Memo payload accepted from the decoded contract.
pub(crate) use crate::constants::MAX_MEMO_PAYLOAD_BYTES;
/// Stable materializer processor name.
pub(crate) use crate::constants::PROCESSOR_NAME;
/// Stable projection contract version.
pub(crate) use crate::constants::PROJECTION_VERSION;
/// Canonical tracing target for this crate.
pub(crate) use crate::constants::TRACING_TARGET;
/// Stable transaction annotation materializer for committed SPL Memo observations.
pub use crate::materializer::TransactionAnnotationMaterializer;

View File

@@ -0,0 +1,618 @@
// file: kb_materializer_transaction_annotations/src/materializer.rs
// version: 6
//! Exact committed SPL Memo transaction annotation projection.
const ACCEPTED_FAMILIES: &[kb_model::EventFamily] = &[kb_model::EventFamily::Audit];
const MEMO_ENTRY_NAMES: &[&str] = &["add_memo", "memo_intent", "invalid_memo_attempt"];
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum MemoGeneration {
V1,
V3,
V4,
}
impl MemoGeneration {
fn from_event(event: &kb_model::DecodedProtocolEvent) -> std::option::Option<Self> {
if event.program_id.0 == kb_program_ids::SPL_MEMO_V1_PROGRAM_ID
&& event.surface_code.0 == "spl_memo_v1"
{
return std::option::Option::Some(Self::V1);
}
if event.program_id.0 == kb_program_ids::SPL_MEMO_V3_PROGRAM_ID
&& event.surface_code.0 == "spl_memo_v3"
{
return std::option::Option::Some(Self::V3);
}
if event.program_id.0 == kb_program_ids::SPL_MEMO_V4_PROGRAM_ID
&& event.surface_code.0 == "spl_memo_v4"
{
return std::option::Option::Some(Self::V4);
}
return std::option::Option::None;
}
fn code(self) -> &'static str {
return match self {
Self::V1 => "v1",
Self::V3 => "v3",
Self::V4 => "v4",
};
}
fn signer_status(self) -> &'static str {
return match self {
Self::V1 => "not_applicable",
Self::V3 | Self::V4 => "valid",
};
}
}
struct MemoProjection {
generation: MemoGeneration,
memo_text: std::string::String,
payload_length_bytes: u64,
payload_sha256: std::string::String,
required_signers: std::vec::Vec<std::string::String>,
observed_signers: std::vec::Vec<std::string::String>,
verified_signers: std::vec::Vec<std::string::String>,
signer_validation_status: std::string::String,
}
/// Stable transaction annotation materializer.
#[derive(Clone, Debug, Default)]
pub struct TransactionAnnotationMaterializer;
impl kb_materializer_api::Materializer for crate::TransactionAnnotationMaterializer {
fn materializer_name(&self) -> &'static str {
return "kb_materializer_transaction_annotations";
}
fn materializer_version(&self) -> &'static str {
return env!("CARGO_PKG_VERSION");
}
fn accepts_event(&self, event: &kb_model::DecodedProtocolEvent) -> bool {
return accepts_event(event);
}
fn materialize_event(
&self,
_event: &kb_model::DecodedProtocolEvent,
) -> kb_core::Result<std::vec::Vec<kb_model::MaterializedEvent>> {
return std::result::Result::Ok(std::vec::Vec::new());
}
}
impl kb_materializer_api::EventMaterializer for crate::TransactionAnnotationMaterializer {
fn identity(&self) -> kb_materializer_api::MaterializerIdentity {
return kb_materializer_api::MaterializerIdentity {
name: crate::PROCESSOR_NAME.to_string(),
version: env!("CARGO_PKG_VERSION").to_string(),
};
}
fn accepted_families(&self) -> &'static [kb_model::EventFamily] {
return ACCEPTED_FAMILIES;
}
fn accepts_observation(&self, observation: &kb_decoder_api::DecodedObservation) -> bool {
return accepts_event(&observation.event);
}
fn transaction_policy(
&self,
_family: kb_model::EventFamily,
) -> kb_materializer_api::MaterializationTransactionPolicy {
return kb_materializer_api::MaterializationTransactionPolicy::SuccessfulCommittedOnly;
}
fn materialize(
&self,
observation: &kb_decoder_api::DecodedObservation,
) -> kb_materializer_api::MaterializerExecutionResult {
if !accepts_event(&observation.event) {
return kb_materializer_api::MaterializerExecutionResult::ignored();
}
if observation.transaction_failed || !observation.observation_committed {
return kb_materializer_api::MaterializerExecutionResult::refused(
"uncommitted_transaction_annotation_refused",
"failed or uncommitted Memo observations cannot create transaction annotations",
);
}
if observation.event.event_name.0 != "add_memo" {
return kb_materializer_api::MaterializerExecutionResult::refused(
"invalid_transaction_annotation_source_refused",
"only a committed add_memo observation can create a transaction annotation",
);
}
let projection_result = parse_projection(observation);
let projection = match projection_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(message) => {
return failed("memo_annotation_payload_invalid", message);
},
};
let idempotence_key = format!(
"spl_memo:{}:{}:{}:{}",
observation.event.signature.0,
observation.event.instruction_path.0,
observation.event.program_id.0,
projection.payload_sha256
);
let output = kb_materializer_api::MaterializedOutput {
output_key: "transaction_annotation:spl_memo:0".to_string(),
family: kb_model::MaterializedEventFamily::TransactionAnnotation,
payload_json: serde_json::json!({
"projectionVersion": crate::PROJECTION_VERSION,
"domain": "transaction_annotation",
"annotationKind": "spl_memo",
"idempotenceKey": idempotence_key,
"signature": observation.event.signature.0.clone(),
"slot": observation.event.slot.0,
"instructionPath": observation.event.instruction_path.0.clone(),
"sourceKind": match observation.event.source_kind {
kb_model::EventSourceKind::InnerInstruction => "inner_instruction",
_ => "instruction"
},
"generation": projection.generation.code(),
"programId": observation.event.program_id.0.clone(),
"text": projection.memo_text,
"payloadLengthBytes": projection.payload_length_bytes,
"payloadSha256": projection.payload_sha256,
"signers": {
"validationStatus": projection.signer_validation_status,
"required": projection.required_signers,
"observed": projection.observed_signers,
"verified": projection.verified_signers
},
"committed": true,
"provenance": {
"processorName": crate::PROCESSOR_NAME,
"processorVersion": env!("CARGO_PKG_VERSION"),
"sourceProtocol": observation.event.protocol_code.0.clone(),
"sourceSurface": observation.event.surface_code.0.clone(),
"sourceEventCode": observation.event.event_code.0.clone(),
"sourceEventKey": observation.event_key.clone()
}
}),
};
tracing::debug!(
target: crate::TRACING_TARGET,
action = "materialize_transaction_annotation",
signature = %observation.event.signature.0,
instruction_path = %observation.event.instruction_path.0,
program_id = %observation.event.program_id.0,
generation = projection.generation.code(),
payload_length_bytes = projection.payload_length_bytes,
output_count = 1_usize,
"materialize committed SPL Memo transaction annotation"
);
return kb_materializer_api::MaterializerExecutionResult {
status: kb_materializer_api::MaterializerOutcomeStatus::Inserted,
outputs: std::vec![output],
diagnostics: std::vec::Vec::new(),
};
}
}
fn accepts_event(event: &kb_model::DecodedProtocolEvent) -> bool {
return event.event_family == kb_model::EventFamily::Audit
&& event.protocol_code.0 == "spl_memo"
&& (event.source_kind == kb_model::EventSourceKind::Instruction
|| event.source_kind == kb_model::EventSourceKind::InnerInstruction)
&& MemoGeneration::from_event(event).is_some()
&& MEMO_ENTRY_NAMES.contains(&event.event_name.0.as_str());
}
fn parse_projection(
observation: &kb_decoder_api::DecodedObservation,
) -> std::result::Result<MemoProjection, std::string::String> {
let generation = match MemoGeneration::from_event(&observation.event) {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(
"Memo generation and Program ID do not match".to_string(),
);
},
};
let payload = &observation.payload_json;
if payload.get("committed").and_then(serde_json::Value::as_bool)
!= std::option::Option::Some(true)
{
return std::result::Result::Err("Memo payload is not marked committed".to_string());
}
if payload.get("transactionSucceeded").and_then(serde_json::Value::as_bool)
!= std::option::Option::Some(true)
|| payload.get("payloadComplete").and_then(serde_json::Value::as_bool)
!= std::option::Option::Some(true)
|| string_field(payload, "runtimeValidation")
!= std::option::Option::Some("proven_by_successful_transaction")
|| payload
.get("utf8Validation")
.and_then(|value| return string_field(value, "status"))
!= std::option::Option::Some("valid")
{
return std::result::Result::Err(
"Memo successful runtime validation proof is incomplete".to_string(),
);
}
if string_field(payload, "generation") != std::option::Option::Some(generation.code()) {
return std::result::Result::Err(
"Memo payload generation does not match its surface".to_string(),
);
}
if string_field(payload, "programId")
!= std::option::Option::Some(observation.event.program_id.0.as_str())
{
return std::result::Result::Err(
"Memo payload Program ID does not match its event".to_string(),
);
}
if string_field(payload, "instructionPath")
!= std::option::Option::Some(observation.event.instruction_path.0.as_str())
{
return std::result::Result::Err(
"Memo payload instruction path does not match its event".to_string(),
);
}
let memo_text = match string_field(payload, "memoText") {
std::option::Option::Some(value) => value.to_string(),
std::option::Option::None => {
return std::result::Result::Err("committed Memo has no UTF-8 text".to_string());
},
};
let payload_length_bytes =
match payload.get("payloadLengthBytes").and_then(serde_json::Value::as_u64) {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err("committed Memo has no byte length".to_string());
},
};
let measured_length = match u64::try_from(memo_text.len()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(format!(
"Memo text length conversion failed: {error}"
));
},
};
if measured_length != payload_length_bytes {
return std::result::Result::Err(
"Memo byte length does not match its UTF-8 text".to_string(),
);
}
if payload_length_bytes > crate::MAX_MEMO_PAYLOAD_BYTES {
return std::result::Result::Err(
"Memo byte length exceeds the projection limit".to_string(),
);
}
let payload_sha256 = match string_field(payload, "payloadSha256") {
std::option::Option::Some(value) if is_sha256(value) => value.to_string(),
_ => return std::result::Result::Err("Memo payload SHA-256 is invalid".to_string()),
};
let signer_validation = match payload.get("signerValidation") {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err("Memo signer validation is absent".to_string());
},
};
let signer_validation_status = match string_field(signer_validation, "status") {
std::option::Option::Some(value) if value == generation.signer_status() => {
value.to_string()
},
_ => {
return std::result::Result::Err(
"Memo signer validation status is inconsistent".to_string(),
);
},
};
let missing_signers = match string_array(signer_validation, "missingRequiredSigners") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(message) => return std::result::Result::Err(message),
};
if !missing_signers.is_empty() {
return std::result::Result::Err("committed Memo reports missing signers".to_string());
}
let required_signers = match string_array(signer_validation, "orderedRequiredSigners") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(message) => return std::result::Result::Err(message),
};
let observed_signers = match string_array(signer_validation, "orderedObservedSigners") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(message) => return std::result::Result::Err(message),
};
let verified_signers = if generation == MemoGeneration::V1 {
if !required_signers.is_empty() {
return std::result::Result::Err("Memo v1 cannot require signers".to_string());
}
std::vec::Vec::new()
} else {
if required_signers != observed_signers {
return std::result::Result::Err(
"committed Memo required and observed signers differ".to_string(),
);
}
observed_signers.clone()
};
return std::result::Result::Ok(MemoProjection {
generation,
memo_text,
payload_length_bytes,
payload_sha256,
required_signers,
observed_signers,
verified_signers,
signer_validation_status,
});
}
fn string_field<'a>(value: &'a serde_json::Value, key: &str) -> std::option::Option<&'a str> {
return value.get(key).and_then(serde_json::Value::as_str);
}
fn string_array(
value: &serde_json::Value,
key: &str,
) -> std::result::Result<std::vec::Vec<std::string::String>, std::string::String> {
let values = match value.get(key).and_then(serde_json::Value::as_array) {
std::option::Option::Some(values) => values,
std::option::Option::None => {
return std::result::Result::Err(format!("Memo signer field {key} is not an array"));
},
};
let mut output = std::vec::Vec::with_capacity(values.len());
for item in values {
let text = match item.as_str() {
std::option::Option::Some(text) if !text.trim().is_empty() => text,
_ => {
return std::result::Result::Err(format!(
"Memo signer field {key} contains an invalid pubkey"
));
},
};
output.push(text.to_string());
}
return std::result::Result::Ok(output);
}
fn is_sha256(value: &str) -> bool {
return value.len() == 64
&& value
.as_bytes()
.iter()
.all(|byte| return byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase());
}
fn failed(
code: &str,
message: std::string::String,
) -> kb_materializer_api::MaterializerExecutionResult {
return kb_materializer_api::MaterializerExecutionResult {
status: kb_materializer_api::MaterializerOutcomeStatus::Failed,
outputs: std::vec::Vec::new(),
diagnostics: std::vec![kb_materializer_api::MaterializerDiagnostic {
code: code.to_string(),
message,
retriable: false,
}],
};
}
#[cfg(test)]
mod tests {
fn observation(
program_id: &str,
surface_code: &str,
generation: &str,
event_name: &str,
failed: bool,
committed: bool,
required_signers: serde_json::Value,
observed_signers: serde_json::Value,
) -> kb_decoder_api::DecodedObservation {
return kb_decoder_api::DecodedObservation {
event_key: "memo:0".to_string(),
event: kb_model::DecodedProtocolEvent {
signature: kb_model::Signature("signature".to_string()),
slot: kb_model::Slot(42),
instruction_path: kb_model::InstructionPath("1/0".to_string()),
program_id: kb_model::ProgramId(program_id.to_string()),
protocol_code: kb_model::ProtocolCode("spl_memo".to_string()),
surface_code: kb_model::SurfaceCode(surface_code.to_string()),
event_code: kb_model::EventCode(format!("{surface_code}.{event_name}")),
event_name: kb_model::EventName(event_name.to_string()),
event_family: kb_model::EventFamily::Audit,
source_kind: kb_model::EventSourceKind::InnerInstruction,
confidence: kb_model::DecoderConfidence::ManualExact,
},
payload_json: serde_json::json!({
"generation": generation,
"programId": program_id,
"instructionPath": "1/0",
"transactionSucceeded": !failed,
"committed": committed,
"memoText": "memo",
"payloadLengthBytes": 4,
"payloadSha256": "1111111111111111111111111111111111111111111111111111111111111111",
"payloadComplete": true,
"utf8Validation": {"status":"valid","invalidFromByte":null},
"signerValidation": {
"status": if generation == "v1" { "not_applicable" } else { "valid" },
"orderedRequiredSigners": required_signers,
"orderedObservedSigners": observed_signers,
"missingRequiredSigners": []
},
"runtimeValidation": if failed {
"not_proven_transaction_failed"
} else {
"proven_by_successful_transaction"
}
}),
transaction_failed: failed,
transaction_error: if failed {
std::option::Option::Some(serde_json::json!({"InstructionError":[0,"Custom"]}))
} else {
std::option::Option::None
},
observation_committed: committed,
proof: kb_decoder_api::DecoderProof {
kind: kb_decoder_api::DecoderProofKind::Manual,
confidence: kb_model::DecoderConfidence::ManualExact,
evidence: std::vec!["memo".to_string()],
},
};
}
fn v4() -> kb_decoder_api::DecodedObservation {
return observation(
kb_program_ids::SPL_MEMO_V4_PROGRAM_ID,
"spl_memo_v4",
"v4",
"add_memo",
false,
true,
serde_json::json!(["signer", "signer"]),
serde_json::json!(["signer", "signer"]),
);
}
#[test]
fn committed_v4_memo_becomes_deterministic_transaction_annotation() {
let materializer = crate::TransactionAnnotationMaterializer;
let first = kb_materializer_api::EventMaterializer::materialize(&materializer, &v4());
let second = kb_materializer_api::EventMaterializer::materialize(&materializer, &v4());
assert_eq!(first, second);
assert_eq!(first.status, kb_materializer_api::MaterializerOutcomeStatus::Inserted);
assert_eq!(first.outputs.len(), 1);
assert_eq!(
first.outputs[0].family,
kb_model::MaterializedEventFamily::TransactionAnnotation
);
assert_eq!(
first.outputs[0].payload_json["signers"]["verified"],
serde_json::json!(["signer", "signer"])
);
assert_eq!(first.outputs[0].payload_json["committed"], true);
assert!(first.validate().is_ok());
}
#[test]
fn exact_v1_v3_and_v4_surfaces_are_accepted() {
for (program_id, surface, generation) in [
(kb_program_ids::SPL_MEMO_V1_PROGRAM_ID, "spl_memo_v1", "v1"),
(kb_program_ids::SPL_MEMO_V3_PROGRAM_ID, "spl_memo_v3", "v3"),
(kb_program_ids::SPL_MEMO_V4_PROGRAM_ID, "spl_memo_v4", "v4"),
] {
let observation = observation(
program_id,
surface,
generation,
"add_memo",
false,
true,
serde_json::json!([]),
serde_json::json!([]),
);
assert!(kb_materializer_api::EventMaterializer::accepts_observation(
&crate::TransactionAnnotationMaterializer,
&observation,
));
}
}
#[test]
fn v1_observed_accounts_are_not_claimed_as_runtime_verified() {
let observation = observation(
kb_program_ids::SPL_MEMO_V1_PROGRAM_ID,
"spl_memo_v1",
"v1",
"add_memo",
false,
true,
serde_json::json!([]),
serde_json::json!(["observed_only"]),
);
let result = kb_materializer_api::EventMaterializer::materialize(
&crate::TransactionAnnotationMaterializer,
&observation,
);
assert_eq!(result.outputs[0].payload_json["signers"]["verified"], serde_json::json!([]));
assert_eq!(
result.outputs[0].payload_json["signers"]["observed"],
serde_json::json!(["observed_only"])
);
}
#[test]
fn failed_or_uncommitted_memo_is_refused_without_output() {
let observation = observation(
kb_program_ids::SPL_MEMO_V4_PROGRAM_ID,
"spl_memo_v4",
"v4",
"memo_intent",
true,
false,
serde_json::json!([]),
serde_json::json!([]),
);
let materializer = crate::TransactionAnnotationMaterializer;
let policy =
kb_materializer_api::validate_materialization_policy(&materializer, &observation);
assert!(policy.is_err());
let result =
kb_materializer_api::EventMaterializer::materialize(&materializer, &observation);
assert_eq!(result.status, kb_materializer_api::MaterializerOutcomeStatus::Refused);
assert!(result.outputs.is_empty());
}
#[test]
fn malformed_committed_payload_fails_closed() {
let mut observation = v4();
observation.payload_json["payloadSha256"] = serde_json::Value::String("bad".to_string());
let result = kb_materializer_api::EventMaterializer::materialize(
&crate::TransactionAnnotationMaterializer,
&observation,
);
assert_eq!(result.status, kb_materializer_api::MaterializerOutcomeStatus::Failed);
assert!(result.outputs.is_empty());
assert_eq!(result.diagnostics[0].code, "memo_annotation_payload_invalid");
}
#[test]
fn foreign_audit_observation_is_not_accepted() {
let mut observation = v4();
observation.event.protocol_code = kb_model::ProtocolCode("foreign".to_string());
assert!(!kb_materializer_api::EventMaterializer::accepts_observation(
&crate::TransactionAnnotationMaterializer,
&observation,
));
let result = kb_materializer_api::EventMaterializer::materialize(
&crate::TransactionAnnotationMaterializer,
&observation,
);
assert_eq!(result.status, kb_materializer_api::MaterializerOutcomeStatus::Ignored);
}
#[test]
fn machine_readable_matrix_declares_transaction_annotation_policy() {
let parsed = serde_json::from_str::<serde_json::Value>(include_str!(
"../../docs/SPL_MEMO_MATRIX.json"
));
let matrix = match parsed {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("memo matrix is invalid JSON: {error}"),
};
assert_eq!(matrix["matrixVersion"], 4);
assert_eq!(
matrix["materializationContract"]["crate"],
"kb_materializer_transaction_annotations"
);
assert_eq!(
matrix["materializationContract"]["transactionPolicy"],
"SuccessfulCommittedOnly"
);
assert_eq!(matrix["materializationContract"]["failedOrUncommittedOutput"], false);
}
}