612 lines
24 KiB
Rust
612 lines
24 KiB
Rust
// file: kb-lib/src/materializer/transaction/annotations.rs
|
|
// version: 4
|
|
|
|
//! Exact committed SPL Memo transaction annotation projection.
|
|
|
|
const ACCEPTED_FAMILIES: &[crate::MdEventFamily] = &[crate::MdEventFamily::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: &crate::MdDecodedProtocolEvent) -> 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 MtTransactionAnnotationMaterializer;
|
|
|
|
impl crate::MtMaterializer for crate::MtTransactionAnnotationMaterializer {
|
|
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: &crate::MdDecodedProtocolEvent) -> bool {
|
|
return accepts_event(event);
|
|
}
|
|
|
|
fn materialize_event(
|
|
&self,
|
|
_event: &crate::MdDecodedProtocolEvent,
|
|
) -> kb_core::Result<std::vec::Vec<crate::MdMaterializedEvent>> {
|
|
return std::result::Result::Ok(std::vec::Vec::new());
|
|
}
|
|
}
|
|
|
|
impl crate::MtApiEventMaterializer for crate::MtTransactionAnnotationMaterializer {
|
|
fn identity(&self) -> crate::MtApiMaterializerIdentity {
|
|
return crate::MtApiMaterializerIdentity {
|
|
name: crate::MT_TRANSACTION_ANNOTATIONS_PROCESSOR_NAME.to_string(),
|
|
version: env!("CARGO_PKG_VERSION").to_string(),
|
|
};
|
|
}
|
|
|
|
fn accepted_families(&self) -> &'static [crate::MdEventFamily] {
|
|
return ACCEPTED_FAMILIES;
|
|
}
|
|
|
|
fn accepts_observation(&self, observation: &crate::DcApiDecodedObservation) -> bool {
|
|
return accepts_event(&observation.event);
|
|
}
|
|
|
|
fn transaction_policy(
|
|
&self,
|
|
_family: crate::MdEventFamily,
|
|
) -> crate::MtApiMaterializationTransactionPolicy {
|
|
return crate::MtApiMaterializationTransactionPolicy::SuccessfulCommittedOnly;
|
|
}
|
|
|
|
fn materialize(
|
|
&self,
|
|
observation: &crate::DcApiDecodedObservation,
|
|
) -> crate::MtApiMaterializerExecutionResult {
|
|
if !accepts_event(&observation.event) {
|
|
return crate::MtApiMaterializerExecutionResult::ignored();
|
|
}
|
|
if observation.transaction_failed || !observation.observation_committed {
|
|
return crate::MtApiMaterializerExecutionResult::refused(
|
|
"uncommitted_transaction_annotation_refused",
|
|
"failed or uncommitted Memo observations cannot create transaction annotations",
|
|
);
|
|
}
|
|
if observation.event.event_name.0 != "add_memo" {
|
|
return crate::MtApiMaterializerExecutionResult::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 = crate::MtApiMaterializedOutput {
|
|
output_key: "transaction_annotation:spl_memo:0".to_string(),
|
|
family: crate::MdMaterializedEventFamily::TransactionAnnotation,
|
|
payload_json: serde_json::json!({
|
|
"projectionVersion": crate::MT_TRANSACTION_ANNOTATIONS_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 {
|
|
crate::MdEventSourceKind::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::MT_TRANSACTION_ANNOTATIONS_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_MATERIALIZER_TRANSACTION_ANNOTATIONS,
|
|
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 crate::MtApiMaterializerExecutionResult {
|
|
status: crate::MtApiMaterializerOutcomeStatus::Inserted,
|
|
outputs: std::vec![output],
|
|
diagnostics: std::vec::Vec::new(),
|
|
};
|
|
}
|
|
}
|
|
|
|
fn accepts_event(event: &crate::MdDecodedProtocolEvent) -> bool {
|
|
return event.event_family == crate::MdEventFamily::Audit
|
|
&& event.protocol_code.0 == "spl_memo"
|
|
&& (event.source_kind == crate::MdEventSourceKind::Instruction
|
|
|| event.source_kind == crate::MdEventSourceKind::InnerInstruction)
|
|
&& MemoGeneration::from_event(event).is_some()
|
|
&& MEMO_ENTRY_NAMES.contains(&event.event_name.0.as_str());
|
|
}
|
|
|
|
fn parse_projection(
|
|
observation: &crate::DcApiDecodedObservation,
|
|
) -> 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::MT_TRANSACTION_ANNOTATIONS_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) -> crate::MtApiMaterializerExecutionResult {
|
|
return crate::MtApiMaterializerExecutionResult {
|
|
status: crate::MtApiMaterializerOutcomeStatus::Failed,
|
|
outputs: std::vec::Vec::new(),
|
|
diagnostics: std::vec![crate::MtApiMaterializerDiagnostic {
|
|
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,
|
|
) -> crate::DcApiDecodedObservation {
|
|
return crate::DcApiDecodedObservation {
|
|
event_key: "memo:0".to_string(),
|
|
event: crate::MdDecodedProtocolEvent {
|
|
signature: crate::MdSignature("signature".to_string()),
|
|
slot: crate::MdSlot(42),
|
|
instruction_path: crate::MdInstructionPath("1/0".to_string()),
|
|
program_id: crate::MdProgramId(program_id.to_string()),
|
|
protocol_code: crate::MdProtocolCode("spl_memo".to_string()),
|
|
surface_code: crate::MdSurfaceCode(surface_code.to_string()),
|
|
event_code: crate::MdEventCode(format!("{surface_code}.{event_name}")),
|
|
event_name: crate::MdEventName(event_name.to_string()),
|
|
event_family: crate::MdEventFamily::Audit,
|
|
source_kind: crate::MdEventSourceKind::InnerInstruction,
|
|
confidence: crate::MdDecoderConfidence::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: crate::DcApiDecoderProof {
|
|
kind: crate::DcApiDecoderProofKind::Manual,
|
|
confidence: crate::MdDecoderConfidence::ManualExact,
|
|
evidence: std::vec!["memo".to_string()],
|
|
},
|
|
};
|
|
}
|
|
|
|
fn v4() -> crate::DcApiDecodedObservation {
|
|
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::MtTransactionAnnotationMaterializer;
|
|
let first = crate::MtApiEventMaterializer::materialize(&materializer, &v4());
|
|
let second = crate::MtApiEventMaterializer::materialize(&materializer, &v4());
|
|
assert_eq!(first, second);
|
|
assert_eq!(first.status, crate::MtApiMaterializerOutcomeStatus::Inserted);
|
|
assert_eq!(first.outputs.len(), 1);
|
|
assert_eq!(
|
|
first.outputs[0].family,
|
|
crate::MdMaterializedEventFamily::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!(crate::MtApiEventMaterializer::accepts_observation(
|
|
&crate::MtTransactionAnnotationMaterializer,
|
|
&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 = crate::MtApiEventMaterializer::materialize(
|
|
&crate::MtTransactionAnnotationMaterializer,
|
|
&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::MtTransactionAnnotationMaterializer;
|
|
let policy =
|
|
crate::materializer_api_validate_materialization_policy(&materializer, &observation);
|
|
assert!(policy.is_err());
|
|
let result = crate::MtApiEventMaterializer::materialize(&materializer, &observation);
|
|
assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::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 = crate::MtApiEventMaterializer::materialize(
|
|
&crate::MtTransactionAnnotationMaterializer,
|
|
&observation,
|
|
);
|
|
assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::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 = crate::MdProtocolCode("foreign".to_string());
|
|
assert!(!crate::MtApiEventMaterializer::accepts_observation(
|
|
&crate::MtTransactionAnnotationMaterializer,
|
|
&observation,
|
|
));
|
|
let result = crate::MtApiEventMaterializer::materialize(
|
|
&crate::MtTransactionAnnotationMaterializer,
|
|
&observation,
|
|
);
|
|
assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::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-lib");
|
|
assert_eq!(
|
|
matrix["materializationContract"]["transactionPolicy"],
|
|
"SuccessfulCommittedOnly"
|
|
);
|
|
assert_eq!(matrix["materializationContract"]["failedOrUncommittedOutput"], false);
|
|
}
|
|
}
|