v0.1.0-pre.011
This commit is contained in:
@@ -1,17 +1,19 @@
|
||||
// file: kb-lib/src/materializer/admin.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
//! `admin` materializer family.
|
||||
|
||||
mod constants;
|
||||
pub mod core;
|
||||
mod core;
|
||||
|
||||
pub(crate) use crate::materializer::admin::constants::PROCESSOR_NAME;
|
||||
pub(crate) use crate::materializer::admin::constants::TRACING_TARGET as MATERIALIZER_ADMIN_TRACING_TARGET;
|
||||
/// Canonical processor name for the native and SPL administration materializer.
|
||||
pub(crate) use self::constants::MT_ADMIN_PROCESSOR_NAME;
|
||||
/// Canonical tracing target for the native and SPL administration materializer.
|
||||
pub(crate) use self::constants::MT_ADMIN_TRACING_TARGET;
|
||||
|
||||
/// Stable native and SPL administration materializer.
|
||||
pub use crate::materializer::admin::core::AdminMaterializer;
|
||||
/// Materializes one exact SPL ElGamal registry account snapshot.
|
||||
pub use crate::materializer::admin::core::materialize_elgamal_registry_state_snapshot;
|
||||
pub use self::core::MtAdminMaterializer;
|
||||
/// Materializes exact Token-2022 administration account snapshots.
|
||||
pub use crate::materializer::admin::core::materialize_token_2022_admin_state_snapshots;
|
||||
pub use self::core::materialize_token2022_admin_state_snapshots;
|
||||
/// Materializes one exact SPL ElGamal registry account snapshot.
|
||||
pub use self::core::materializer_admin_materialize_elgamal_registry_state_snapshot;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
//! Local constants for the native and SPL administration materializer.
|
||||
|
||||
/// Canonical processor name for this component.
|
||||
pub(crate) const PROCESSOR_NAME: &str = "solana_native_admin";
|
||||
pub(crate) const MT_ADMIN_PROCESSOR_NAME: &str = "solana_native_admin";
|
||||
|
||||
/// Canonical tracing target for this component.
|
||||
pub(crate) const TRACING_TARGET: &str = "kb-lib.materializer.admin";
|
||||
pub(crate) const MT_ADMIN_TRACING_TARGET: &str = "kb-lib.materializer.admin";
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
|
||||
//! Stable administrative projections derived from exact committed observations.
|
||||
|
||||
const ACCEPTED_FAMILIES: &[crate::EventFamily] =
|
||||
&[crate::EventFamily::Admin, crate::EventFamily::TokenAccount];
|
||||
const ACCEPTED_FAMILIES: &[crate::MdEventFamily] =
|
||||
&[crate::MdEventFamily::Admin, crate::MdEventFamily::TokenAccount];
|
||||
const SYSTEM_SURFACE: &str = "solana_native_system";
|
||||
const SYSTEM_ENTRIES: &[&str] = &["assign", "assign_with_seed"];
|
||||
const CONFIG_SURFACE: &str = "solana_native_config";
|
||||
@@ -14,7 +14,7 @@ const BPF_LOADER_UPGRADEABLE_ENTRIES: &[&str] = &["set_authority", "set_authorit
|
||||
const LOADER_V4_SURFACE: &str = "solana_native_loader_v4";
|
||||
const LOADER_V4_ENTRIES: &[&str] = &["transfer_authority"];
|
||||
const SPL_TOKEN_SURFACE: &str = "spl_token";
|
||||
const SPL_TOKEN_2022_SURFACE: &str = "spl_token_2022";
|
||||
const SPL_TOKEN2022_SURFACE: &str = "spl_token2022";
|
||||
const SPL_ELGAMAL_REGISTRY_SURFACE: &str = "spl_elgamal_registry";
|
||||
const SPL_ELGAMAL_REGISTRY_ENTRIES: &[&str] = &["create_registry", "update_registry"];
|
||||
const SPL_TOKEN_AUTHORITY_ENTRIES: &[&str] = &[
|
||||
@@ -24,7 +24,7 @@ const SPL_TOKEN_AUTHORITY_ENTRIES: &[&str] = &[
|
||||
"update_interest_bearing_rate",
|
||||
];
|
||||
const SPL_TOKEN_MULTISIG_ENTRIES: &[&str] = &["initialize_multisig", "initialize_multisig2"];
|
||||
const SPL_TOKEN_2022_EXTENSION_ADMIN_ENTRIES: &[&str] = &[
|
||||
const SPL_TOKEN2022_EXTENSION_ADMIN_ENTRIES: &[&str] = &[
|
||||
"initialize_transfer_hook",
|
||||
"update_transfer_hook_program_id",
|
||||
"initialize_metadata_pointer",
|
||||
@@ -57,7 +57,7 @@ impl ProjectionKind {
|
||||
Self::SplTokenAuthority => "spl_token_authority",
|
||||
Self::SplTokenMultisig => "spl_token_multisig",
|
||||
Self::SplElgamalRegistry => "spl_elgamal_registry",
|
||||
Self::SplToken2022ExtensionAdmin => "spl_token_2022_extension_admin",
|
||||
Self::SplToken2022ExtensionAdmin => "spl_token2022_extension_admin",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ impl ProjectionKind {
|
||||
Self::SplTokenMultisig => format!("spl_token_multisig:{operation}:0"),
|
||||
Self::SplElgamalRegistry => format!("spl_elgamal_registry:{operation}:0"),
|
||||
Self::SplToken2022ExtensionAdmin => {
|
||||
format!("spl_token_2022_extension_admin:{operation}:0")
|
||||
format!("spl_token2022_extension_admin:{operation}:0")
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -87,9 +87,9 @@ impl ProjectionKind {
|
||||
|
||||
/// Stable administrative materializer for supported native mutations.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct AdminMaterializer;
|
||||
pub struct MtAdminMaterializer;
|
||||
|
||||
impl crate::Materializer for crate::AdminMaterializer {
|
||||
impl crate::MtMaterializer for crate::MtAdminMaterializer {
|
||||
fn materializer_name(&self) -> &'static str {
|
||||
return "kb_materializer_admin";
|
||||
}
|
||||
@@ -98,7 +98,7 @@ impl crate::Materializer for crate::AdminMaterializer {
|
||||
return env!("CARGO_PKG_VERSION");
|
||||
}
|
||||
|
||||
fn accepts_event(&self, event: &crate::DecodedProtocolEvent) -> bool {
|
||||
fn accepts_event(&self, event: &crate::MdDecodedProtocolEvent) -> bool {
|
||||
return ACCEPTED_FAMILIES.contains(&event.event_family)
|
||||
&& program_matches_surface(event)
|
||||
&& projection(event.surface_code.0.as_str(), event.event_name.0.as_str()).is_some();
|
||||
@@ -106,28 +106,28 @@ impl crate::Materializer for crate::AdminMaterializer {
|
||||
|
||||
fn materialize_event(
|
||||
&self,
|
||||
event: &crate::DecodedProtocolEvent,
|
||||
) -> kb_core::Result<std::vec::Vec<crate::MaterializedEvent>> {
|
||||
if !crate::Materializer::accepts_event(self, event) {
|
||||
event: &crate::MdDecodedProtocolEvent,
|
||||
) -> kb_core::Result<std::vec::Vec<crate::MdMaterializedEvent>> {
|
||||
if !crate::MtMaterializer::accepts_event(self, event) {
|
||||
return std::result::Result::Ok(std::vec::Vec::new());
|
||||
}
|
||||
return std::result::Result::Ok(std::vec::Vec::new());
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::EventMaterializer for crate::AdminMaterializer {
|
||||
fn identity(&self) -> crate::MaterializerIdentity {
|
||||
return crate::MaterializerIdentity {
|
||||
impl crate::MtApiEventMaterializer for crate::MtAdminMaterializer {
|
||||
fn identity(&self) -> crate::MtApiMaterializerIdentity {
|
||||
return crate::MtApiMaterializerIdentity {
|
||||
name: "solana_native_admin".to_string(),
|
||||
version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
fn accepted_families(&self) -> &'static [crate::EventFamily] {
|
||||
fn accepted_families(&self) -> &'static [crate::MdEventFamily] {
|
||||
return ACCEPTED_FAMILIES;
|
||||
}
|
||||
|
||||
fn accepts_observation(&self, observation: &crate::DecodedObservation) -> bool {
|
||||
fn accepts_observation(&self, observation: &crate::DcApiDecodedObservation) -> bool {
|
||||
return ACCEPTED_FAMILIES.contains(&observation.event.event_family)
|
||||
&& program_matches_surface(&observation.event)
|
||||
&& projection(
|
||||
@@ -139,15 +139,15 @@ impl crate::EventMaterializer for crate::AdminMaterializer {
|
||||
|
||||
fn transaction_policy(
|
||||
&self,
|
||||
_family: crate::EventFamily,
|
||||
) -> crate::MaterializationTransactionPolicy {
|
||||
return crate::MaterializationTransactionPolicy::SuccessfulCommittedOnly;
|
||||
_family: crate::MdEventFamily,
|
||||
) -> crate::MtApiMaterializationTransactionPolicy {
|
||||
return crate::MtApiMaterializationTransactionPolicy::SuccessfulCommittedOnly;
|
||||
}
|
||||
|
||||
fn materialize(
|
||||
&self,
|
||||
observation: &crate::DecodedObservation,
|
||||
) -> crate::MaterializerExecutionResult {
|
||||
observation: &crate::DcApiDecodedObservation,
|
||||
) -> crate::MtApiMaterializerExecutionResult {
|
||||
let projection = match projection(
|
||||
observation.event.surface_code.0.as_str(),
|
||||
observation.event.event_name.0.as_str(),
|
||||
@@ -155,25 +155,25 @@ impl crate::EventMaterializer for crate::AdminMaterializer {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
tracing::debug!(
|
||||
target: crate::MATERIALIZER_ADMIN_TRACING_TARGET,
|
||||
target: crate::MT_ADMIN_TRACING_TARGET,
|
||||
action = "materialize_admin",
|
||||
surface_code = %observation.event.surface_code.0.as_str(),
|
||||
entry_code = %observation.event.event_name.0.as_str(),
|
||||
accepted = false,
|
||||
"ignore observation outside supported native administrative projections"
|
||||
);
|
||||
return crate::MaterializerExecutionResult::ignored();
|
||||
return crate::MtApiMaterializerExecutionResult::ignored();
|
||||
},
|
||||
};
|
||||
if !ACCEPTED_FAMILIES.contains(&observation.event.event_family) {
|
||||
return crate::MaterializerExecutionResult::ignored();
|
||||
return crate::MtApiMaterializerExecutionResult::ignored();
|
||||
}
|
||||
if !program_matches_surface(&observation.event) {
|
||||
return crate::MaterializerExecutionResult::ignored();
|
||||
return crate::MtApiMaterializerExecutionResult::ignored();
|
||||
}
|
||||
if observation.transaction_failed || !observation.observation_committed {
|
||||
tracing::debug!(
|
||||
target: crate::MATERIALIZER_ADMIN_TRACING_TARGET,
|
||||
target: crate::MT_ADMIN_TRACING_TARGET,
|
||||
action = "materialize_admin",
|
||||
surface_code = %observation.event.surface_code.0.as_str(),
|
||||
entry_code = %observation.event.event_name.0.as_str(),
|
||||
@@ -181,7 +181,7 @@ impl crate::EventMaterializer for crate::AdminMaterializer {
|
||||
transaction_failed = observation.transaction_failed,
|
||||
"refuse uncommitted native administrative observation"
|
||||
);
|
||||
return crate::MaterializerExecutionResult::refused(
|
||||
return crate::MtApiMaterializerExecutionResult::refused(
|
||||
"failed_transaction_admin_refused",
|
||||
"failed or uncommitted native observations cannot create administrative mutation outputs",
|
||||
);
|
||||
@@ -197,10 +197,10 @@ impl crate::EventMaterializer for crate::AdminMaterializer {
|
||||
.cloned()
|
||||
.unwrap_or_else(|| return observation.payload_json.clone());
|
||||
let operation = observation.event.event_name.0.clone();
|
||||
let output = crate::MaterializedOutput {
|
||||
let output = crate::MtApiMaterializedOutput {
|
||||
output_key: projection
|
||||
.output_key(observation.event.surface_code.0.as_str(), operation.as_str()),
|
||||
family: crate::MaterializedEventFamily::Admin,
|
||||
family: crate::MdMaterializedEventFamily::Admin,
|
||||
payload_json: serde_json::json!({
|
||||
"projectionVersion": 1,
|
||||
"projectionSemantics": "committed_instruction_admin_event",
|
||||
@@ -223,7 +223,7 @@ impl crate::EventMaterializer for crate::AdminMaterializer {
|
||||
}),
|
||||
};
|
||||
tracing::debug!(
|
||||
target: crate::MATERIALIZER_ADMIN_TRACING_TARGET,
|
||||
target: crate::MT_ADMIN_TRACING_TARGET,
|
||||
action = "materialize_admin",
|
||||
surface_code = %observation.event.surface_code.0.as_str(),
|
||||
entry_code = %observation.event.event_name.0.as_str(),
|
||||
@@ -231,8 +231,8 @@ impl crate::EventMaterializer for crate::AdminMaterializer {
|
||||
output_count = 1_usize,
|
||||
"materialize committed native administrative observation"
|
||||
);
|
||||
return crate::MaterializerExecutionResult {
|
||||
status: crate::MaterializerOutcomeStatus::Inserted,
|
||||
return crate::MtApiMaterializerExecutionResult {
|
||||
status: crate::MtApiMaterializerOutcomeStatus::Inserted,
|
||||
outputs: std::vec![output],
|
||||
diagnostics: std::vec::Vec::new(),
|
||||
};
|
||||
@@ -258,18 +258,18 @@ fn projection(surface_code: &str, entry_code: &str) -> std::option::Option<Proje
|
||||
LOADER_V4_SURFACE,
|
||||
));
|
||||
}
|
||||
if matches!(surface_code, SPL_TOKEN_SURFACE | SPL_TOKEN_2022_SURFACE)
|
||||
if matches!(surface_code, SPL_TOKEN_SURFACE | SPL_TOKEN2022_SURFACE)
|
||||
&& contains(SPL_TOKEN_AUTHORITY_ENTRIES, entry_code)
|
||||
{
|
||||
return std::option::Option::Some(ProjectionKind::SplTokenAuthority);
|
||||
}
|
||||
if matches!(surface_code, SPL_TOKEN_SURFACE | SPL_TOKEN_2022_SURFACE)
|
||||
if matches!(surface_code, SPL_TOKEN_SURFACE | SPL_TOKEN2022_SURFACE)
|
||||
&& contains(SPL_TOKEN_MULTISIG_ENTRIES, entry_code)
|
||||
{
|
||||
return std::option::Option::Some(ProjectionKind::SplTokenMultisig);
|
||||
}
|
||||
if surface_code == SPL_TOKEN_2022_SURFACE
|
||||
&& contains(SPL_TOKEN_2022_EXTENSION_ADMIN_ENTRIES, entry_code)
|
||||
if surface_code == SPL_TOKEN2022_SURFACE
|
||||
&& contains(SPL_TOKEN2022_EXTENSION_ADMIN_ENTRIES, entry_code)
|
||||
{
|
||||
return std::option::Option::Some(ProjectionKind::SplToken2022ExtensionAdmin);
|
||||
}
|
||||
@@ -281,15 +281,15 @@ fn projection(surface_code: &str, entry_code: &str) -> std::option::Option<Proje
|
||||
return std::option::Option::None;
|
||||
}
|
||||
|
||||
fn program_matches_surface(event: &crate::DecodedProtocolEvent) -> bool {
|
||||
fn program_matches_surface(event: &crate::MdDecodedProtocolEvent) -> bool {
|
||||
if event.surface_code.0 == SPL_TOKEN_SURFACE {
|
||||
return event.program_id.0 == kb_program_ids::SPL_TOKEN_PROGRAM_ID;
|
||||
}
|
||||
if event.surface_code.0 == SPL_TOKEN_2022_SURFACE {
|
||||
return event.program_id.0 == kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID;
|
||||
if event.surface_code.0 == SPL_TOKEN2022_SURFACE {
|
||||
return event.program_id.0 == kb_program_ids::SPL_TOKEN2022_PROGRAM_ID;
|
||||
}
|
||||
if event.surface_code.0 == SPL_ELGAMAL_REGISTRY_SURFACE {
|
||||
return event.program_id.0 == kb_program_ids::SPL_TOKEN_2022_ELGAMAL_REGISTRY_PROGRAM_ID;
|
||||
return event.program_id.0 == kb_program_ids::SPL_TOKEN2022_ELGAMAL_REGISTRY_PROGRAM_ID;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -497,21 +497,21 @@ mod tests {
|
||||
transaction_failed: bool,
|
||||
accounts: serde_json::Value,
|
||||
parameters: serde_json::Value,
|
||||
) -> crate::DecodedObservation {
|
||||
return crate::DecodedObservation {
|
||||
) -> crate::DcApiDecodedObservation {
|
||||
return crate::DcApiDecodedObservation {
|
||||
event_key: format!("{entry_code}:0"),
|
||||
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-id".to_string()),
|
||||
protocol_code: crate::ProtocolCode("solana_native".to_string()),
|
||||
surface_code: crate::SurfaceCode(surface_code.to_string()),
|
||||
event_code: crate::EventCode(format!("{surface_code}.{entry_code}")),
|
||||
event_name: crate::EventName(entry_code.to_string()),
|
||||
event_family: crate::EventFamily::Admin,
|
||||
source_kind: crate::EventSourceKind::Instruction,
|
||||
confidence: crate::DecoderConfidence::ManualExact,
|
||||
event: crate::MdDecodedProtocolEvent {
|
||||
signature: crate::MdSignature("signature".to_string()),
|
||||
slot: crate::MdSlot(42),
|
||||
instruction_path: crate::MdInstructionPath("0".to_string()),
|
||||
program_id: crate::MdProgramId("program-id".to_string()),
|
||||
protocol_code: crate::MdProtocolCode("solana_native".to_string()),
|
||||
surface_code: crate::MdSurfaceCode(surface_code.to_string()),
|
||||
event_code: crate::MdEventCode(format!("{surface_code}.{entry_code}")),
|
||||
event_name: crate::MdEventName(entry_code.to_string()),
|
||||
event_family: crate::MdEventFamily::Admin,
|
||||
source_kind: crate::MdEventSourceKind::Instruction,
|
||||
confidence: crate::MdDecoderConfidence::ManualExact,
|
||||
},
|
||||
payload_json: serde_json::json!({"accounts": accounts, "parameters": parameters}),
|
||||
transaction_failed,
|
||||
@@ -521,9 +521,9 @@ mod tests {
|
||||
std::option::Option::None
|
||||
},
|
||||
observation_committed: !transaction_failed,
|
||||
proof: crate::DecoderProof {
|
||||
kind: crate::DecoderProofKind::Manual,
|
||||
confidence: crate::DecoderConfidence::ManualExact,
|
||||
proof: crate::DcApiDecoderProof {
|
||||
kind: crate::DcApiDecoderProofKind::Manual,
|
||||
confidence: crate::MdDecoderConfidence::ManualExact,
|
||||
evidence: std::vec!["fixture".to_string()],
|
||||
},
|
||||
};
|
||||
@@ -535,7 +535,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn system_assign_creates_owner_projection() {
|
||||
let materializer = crate::AdminMaterializer;
|
||||
let materializer = crate::MtAdminMaterializer;
|
||||
let observation = observation(
|
||||
super::SYSTEM_SURFACE,
|
||||
"assign",
|
||||
@@ -543,16 +543,16 @@ mod tests {
|
||||
serde_json::json!([account("assigned_account", "account111",)]),
|
||||
serde_json::json!({"owner": "owner111"}),
|
||||
);
|
||||
let result = crate::EventMaterializer::materialize(&materializer, &observation);
|
||||
assert_eq!(result.status, crate::MaterializerOutcomeStatus::Inserted);
|
||||
assert_eq!(result.outputs[0].family, crate::MaterializedEventFamily::Admin);
|
||||
let result = crate::MtApiEventMaterializer::materialize(&materializer, &observation);
|
||||
assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Inserted);
|
||||
assert_eq!(result.outputs[0].family, crate::MdMaterializedEventFamily::Admin);
|
||||
assert_eq!(result.outputs[0].output_key, "system_account_ownership:assign:0");
|
||||
assert_eq!(result.outputs[0].payload_json["projection"]["newOwner"], "owner111");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seeded_assign_preserves_base_seed_and_owner() {
|
||||
let materializer = crate::AdminMaterializer;
|
||||
let materializer = crate::MtAdminMaterializer;
|
||||
let observation = observation(
|
||||
super::SYSTEM_SURFACE,
|
||||
"assign_with_seed",
|
||||
@@ -563,7 +563,7 @@ mod tests {
|
||||
]),
|
||||
serde_json::json!({"owner": "owner111", "seed": "seed"}),
|
||||
);
|
||||
let result = crate::EventMaterializer::materialize(&materializer, &observation);
|
||||
let result = crate::MtApiEventMaterializer::materialize(&materializer, &observation);
|
||||
assert_eq!(result.outputs[0].payload_json["projection"]["targetAccount"], "derived111");
|
||||
assert_eq!(result.outputs[0].payload_json["projection"]["baseAccount"], "base111");
|
||||
assert_eq!(result.outputs[0].payload_json["projection"]["seed"], "seed");
|
||||
@@ -571,7 +571,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn config_store_preserves_bounded_opaque_audit_fields() {
|
||||
let materializer = crate::AdminMaterializer;
|
||||
let materializer = crate::MtAdminMaterializer;
|
||||
let observation = observation(
|
||||
super::CONFIG_SURFACE,
|
||||
"store",
|
||||
@@ -589,7 +589,7 @@ mod tests {
|
||||
"configDataPrefixHex": "0102",
|
||||
}),
|
||||
);
|
||||
let result = crate::EventMaterializer::materialize(&materializer, &observation);
|
||||
let result = crate::MtApiEventMaterializer::materialize(&materializer, &observation);
|
||||
assert_eq!(result.outputs[0].output_key, "config_account_update:store:0");
|
||||
assert_eq!(result.outputs[0].payload_json["projection"]["targetAccount"], "config111");
|
||||
assert_eq!(result.outputs[0].payload_json["projection"]["configDataSha256"], "abc");
|
||||
@@ -598,7 +598,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn loader_authority_variants_create_exact_admin_outputs() {
|
||||
let materializer = crate::AdminMaterializer;
|
||||
let materializer = crate::MtAdminMaterializer;
|
||||
for (surface, entry, target_role) in [
|
||||
(super::BPF_LOADER_UPGRADEABLE_SURFACE, "set_authority", "authority_target"),
|
||||
(
|
||||
@@ -619,8 +619,8 @@ mod tests {
|
||||
]),
|
||||
serde_json::json!({}),
|
||||
);
|
||||
let result = crate::EventMaterializer::materialize(&materializer, &observation);
|
||||
assert_eq!(result.status, crate::MaterializerOutcomeStatus::Inserted);
|
||||
let result = crate::MtApiEventMaterializer::materialize(&materializer, &observation);
|
||||
assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Inserted);
|
||||
assert_eq!(result.outputs[0].payload_json["projection"]["targetAccount"], "target111");
|
||||
assert_eq!(result.outputs[0].payload_json["projection"]["newAuthority"], "new111");
|
||||
}
|
||||
@@ -639,9 +639,9 @@ mod tests {
|
||||
serde_json::json!({"authorityType":"mint_tokens","newAuthority":null}),
|
||||
);
|
||||
authority.event.program_id =
|
||||
crate::ProgramId(kb_program_ids::SPL_TOKEN_PROGRAM_ID.to_string());
|
||||
crate::MdProgramId(kb_program_ids::SPL_TOKEN_PROGRAM_ID.to_string());
|
||||
let authority_result =
|
||||
crate::EventMaterializer::materialize(&crate::AdminMaterializer, &authority);
|
||||
crate::MtApiEventMaterializer::materialize(&crate::MtAdminMaterializer, &authority);
|
||||
assert_eq!(authority_result.outputs[0].output_key, "spl_token_authority:set_authority:0");
|
||||
assert_eq!(
|
||||
authority_result.outputs[0].payload_json["projection"]["stateTransition"],
|
||||
@@ -660,9 +660,9 @@ mod tests {
|
||||
serde_json::json!({"m":2}),
|
||||
);
|
||||
multisig.event.program_id =
|
||||
crate::ProgramId(kb_program_ids::SPL_TOKEN_PROGRAM_ID.to_string());
|
||||
crate::MdProgramId(kb_program_ids::SPL_TOKEN_PROGRAM_ID.to_string());
|
||||
let multisig_result =
|
||||
crate::EventMaterializer::materialize(&crate::AdminMaterializer, &multisig);
|
||||
crate::MtApiEventMaterializer::materialize(&crate::MtAdminMaterializer, &multisig);
|
||||
assert_eq!(
|
||||
multisig_result.outputs[0].output_key,
|
||||
"spl_token_multisig:initialize_multisig2:0"
|
||||
@@ -676,9 +676,9 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn token_2022_authority_change_uses_the_admin_owner_once() {
|
||||
fn token2022_authority_change_uses_the_admin_owner_once() {
|
||||
let mut observation = observation(
|
||||
super::SPL_TOKEN_2022_SURFACE,
|
||||
super::SPL_TOKEN2022_SURFACE,
|
||||
"set_authority",
|
||||
false,
|
||||
serde_json::json!([
|
||||
@@ -688,19 +688,20 @@ mod tests {
|
||||
serde_json::json!({"authorityType":"mint_tokens","newAuthority":null}),
|
||||
);
|
||||
observation.event.program_id =
|
||||
crate::ProgramId(kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID.to_string());
|
||||
let result = crate::EventMaterializer::materialize(&crate::AdminMaterializer, &observation);
|
||||
assert_eq!(result.status, crate::MaterializerOutcomeStatus::Inserted);
|
||||
crate::MdProgramId(kb_program_ids::SPL_TOKEN2022_PROGRAM_ID.to_string());
|
||||
let result =
|
||||
crate::MtApiEventMaterializer::materialize(&crate::MtAdminMaterializer, &observation);
|
||||
assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Inserted);
|
||||
assert_eq!(result.outputs.len(), 1);
|
||||
assert_eq!(
|
||||
result.outputs[0].payload_json["programId"],
|
||||
kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID
|
||||
kb_program_ids::SPL_TOKEN2022_PROGRAM_ID
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_admin_mutation_is_refused() {
|
||||
let materializer = crate::AdminMaterializer;
|
||||
let materializer = crate::MtAdminMaterializer;
|
||||
let observation = observation(
|
||||
super::SYSTEM_SURFACE,
|
||||
"assign",
|
||||
@@ -708,8 +709,8 @@ mod tests {
|
||||
serde_json::json!([account("assigned_account", "account111",)]),
|
||||
serde_json::json!({"owner": "owner111"}),
|
||||
);
|
||||
let result = crate::EventMaterializer::materialize(&materializer, &observation);
|
||||
assert_eq!(result.status, crate::MaterializerOutcomeStatus::Refused);
|
||||
let result = crate::MtApiEventMaterializer::materialize(&materializer, &observation);
|
||||
assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Refused);
|
||||
assert!(result.outputs.is_empty());
|
||||
}
|
||||
|
||||
@@ -723,20 +724,21 @@ mod tests {
|
||||
serde_json::json!({}),
|
||||
);
|
||||
observation.event.program_id =
|
||||
crate::ProgramId(kb_program_ids::ASSOCIATED_TOKEN_PROGRAM_ID.to_string());
|
||||
observation.event.event_family = crate::EventFamily::Lifecycle;
|
||||
assert!(!crate::EventMaterializer::accepts_observation(
|
||||
&crate::AdminMaterializer,
|
||||
crate::MdProgramId(kb_program_ids::ASSOCIATED_TOKEN_PROGRAM_ID.to_string());
|
||||
observation.event.event_family = crate::MdEventFamily::Lifecycle;
|
||||
assert!(!crate::MtApiEventMaterializer::accepts_observation(
|
||||
&crate::MtAdminMaterializer,
|
||||
&observation,
|
||||
));
|
||||
let result = crate::EventMaterializer::materialize(&crate::AdminMaterializer, &observation);
|
||||
assert_eq!(result.status, crate::MaterializerOutcomeStatus::Ignored);
|
||||
let result =
|
||||
crate::MtApiEventMaterializer::materialize(&crate::MtAdminMaterializer, &observation);
|
||||
assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Ignored);
|
||||
assert!(result.outputs.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unrelated_admin_observation_is_ignored() {
|
||||
let materializer = crate::AdminMaterializer;
|
||||
let materializer = crate::MtAdminMaterializer;
|
||||
let observation = observation(
|
||||
"other_surface",
|
||||
"set_authority",
|
||||
@@ -744,13 +746,13 @@ mod tests {
|
||||
serde_json::json!([]),
|
||||
serde_json::json!({}),
|
||||
);
|
||||
let result = crate::EventMaterializer::materialize(&materializer, &observation);
|
||||
assert_eq!(result.status, crate::MaterializerOutcomeStatus::Ignored);
|
||||
let result = crate::MtApiEventMaterializer::materialize(&materializer, &observation);
|
||||
assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Ignored);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn output_serialization_is_deterministic() {
|
||||
let materializer = crate::AdminMaterializer;
|
||||
let materializer = crate::MtAdminMaterializer;
|
||||
let observation = observation(
|
||||
super::CONFIG_SURFACE,
|
||||
"store",
|
||||
@@ -765,8 +767,8 @@ mod tests {
|
||||
"configDataPrefixHex": "01",
|
||||
}),
|
||||
);
|
||||
let first = crate::EventMaterializer::materialize(&materializer, &observation);
|
||||
let second = crate::EventMaterializer::materialize(&materializer, &observation);
|
||||
let first = crate::MtApiEventMaterializer::materialize(&materializer, &observation);
|
||||
let second = crate::MtApiEventMaterializer::materialize(&materializer, &observation);
|
||||
let first_json = match serde_json::to_string(&first) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("first admin serialization failed: {error}"),
|
||||
@@ -780,7 +782,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn elgamal_registry_create_and_update_have_one_admin_owner() {
|
||||
let materializer = crate::AdminMaterializer;
|
||||
let materializer = crate::MtAdminMaterializer;
|
||||
for (operation, offset, owner_position, transition) in [
|
||||
("create_registry", 1_i64, 1_u64, "elgamal_registry_created"),
|
||||
("update_registry", 0_i64, 2_u64, "elgamal_registry_key_updated"),
|
||||
@@ -797,8 +799,8 @@ mod tests {
|
||||
]),
|
||||
serde_json::json!({}),
|
||||
);
|
||||
observation.event.program_id = crate::ProgramId(
|
||||
kb_program_ids::SPL_TOKEN_2022_ELGAMAL_REGISTRY_PROGRAM_ID.to_string(),
|
||||
observation.event.program_id = crate::MdProgramId(
|
||||
kb_program_ids::SPL_TOKEN2022_ELGAMAL_REGISTRY_PROGRAM_ID.to_string(),
|
||||
);
|
||||
observation.payload_json = serde_json::json!({
|
||||
"accounts": observation.payload_json["accounts"].clone(),
|
||||
@@ -809,8 +811,8 @@ mod tests {
|
||||
"relative_instruction_offset"
|
||||
},
|
||||
});
|
||||
let result = crate::EventMaterializer::materialize(&materializer, &observation);
|
||||
assert_eq!(result.status, crate::MaterializerOutcomeStatus::Inserted);
|
||||
let result = crate::MtApiEventMaterializer::materialize(&materializer, &observation);
|
||||
assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Inserted);
|
||||
assert_eq!(result.outputs.len(), 1);
|
||||
assert_eq!(
|
||||
result.outputs[0].payload_json["projection"]["registryAccount"],
|
||||
@@ -826,7 +828,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn elgamal_registry_foreign_program_and_failed_transaction_are_refused() {
|
||||
let materializer = crate::AdminMaterializer;
|
||||
let materializer = crate::MtAdminMaterializer;
|
||||
let mut foreign = observation(
|
||||
super::SPL_ELGAMAL_REGISTRY_SURFACE,
|
||||
"create_registry",
|
||||
@@ -835,20 +837,20 @@ mod tests {
|
||||
serde_json::json!({}),
|
||||
);
|
||||
foreign.event.program_id =
|
||||
crate::ProgramId(kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID.to_string());
|
||||
assert!(!crate::EventMaterializer::accepts_observation(&materializer, &foreign,));
|
||||
crate::MdProgramId(kb_program_ids::SPL_TOKEN2022_PROGRAM_ID.to_string());
|
||||
assert!(!crate::MtApiEventMaterializer::accepts_observation(&materializer, &foreign,));
|
||||
let mut failed = foreign.clone();
|
||||
failed.event.program_id = crate::ProgramId(
|
||||
kb_program_ids::SPL_TOKEN_2022_ELGAMAL_REGISTRY_PROGRAM_ID.to_string(),
|
||||
failed.event.program_id = crate::MdProgramId(
|
||||
kb_program_ids::SPL_TOKEN2022_ELGAMAL_REGISTRY_PROGRAM_ID.to_string(),
|
||||
);
|
||||
failed.transaction_failed = true;
|
||||
failed.observation_committed = false;
|
||||
let result = crate::EventMaterializer::materialize(&materializer, &failed);
|
||||
assert_eq!(result.status, crate::MaterializerOutcomeStatus::Refused);
|
||||
let result = crate::MtApiEventMaterializer::materialize(&materializer, &failed);
|
||||
assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Refused);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn token_2022_pointer_and_hook_admin_events_have_one_owner() {
|
||||
fn token2022_pointer_and_hook_admin_events_have_one_owner() {
|
||||
for (operation, field, value) in [
|
||||
("initialize_transfer_hook", "programId", "hook111"),
|
||||
("update_metadata_pointer", "metadataAddress", "metadata111"),
|
||||
@@ -856,18 +858,20 @@ mod tests {
|
||||
("update_group_member_pointer", "memberAddress", "member111"),
|
||||
] {
|
||||
let mut observation = observation(
|
||||
super::SPL_TOKEN_2022_SURFACE,
|
||||
super::SPL_TOKEN2022_SURFACE,
|
||||
operation,
|
||||
false,
|
||||
serde_json::json!([{"position":0,"accountKey":"mint111"}]),
|
||||
serde_json::json!({(field):value,"authority":"authority111"}),
|
||||
);
|
||||
observation.event.event_family = crate::EventFamily::TokenAccount;
|
||||
observation.event.event_family = crate::MdEventFamily::TokenAccount;
|
||||
observation.event.program_id =
|
||||
crate::ProgramId(kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID.to_string());
|
||||
let result =
|
||||
crate::EventMaterializer::materialize(&crate::AdminMaterializer, &observation);
|
||||
assert_eq!(result.status, crate::MaterializerOutcomeStatus::Inserted);
|
||||
crate::MdProgramId(kb_program_ids::SPL_TOKEN2022_PROGRAM_ID.to_string());
|
||||
let result = crate::MtApiEventMaterializer::materialize(
|
||||
&crate::MtAdminMaterializer,
|
||||
&observation,
|
||||
);
|
||||
assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Inserted);
|
||||
assert_eq!(result.outputs.len(), 1);
|
||||
assert_eq!(result.outputs[0].payload_json["projection"][field], value);
|
||||
assert_eq!(result.outputs[0].payload_json["projection"]["stateReadRequired"], true);
|
||||
@@ -875,44 +879,45 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn token_2022_extension_admin_rejects_foreign_program_and_failed_transaction() {
|
||||
fn token2022_extension_admin_rejects_foreign_program_and_failed_transaction() {
|
||||
let mut observation = observation(
|
||||
super::SPL_TOKEN_2022_SURFACE,
|
||||
super::SPL_TOKEN2022_SURFACE,
|
||||
"initialize_metadata_pointer",
|
||||
false,
|
||||
serde_json::json!([]),
|
||||
serde_json::json!({"metadataAddress":"metadata111"}),
|
||||
);
|
||||
observation.event.event_family = crate::EventFamily::TokenAccount;
|
||||
observation.event.event_family = crate::MdEventFamily::TokenAccount;
|
||||
observation.event.program_id =
|
||||
crate::ProgramId(kb_program_ids::SPL_TOKEN_PROGRAM_ID.to_string());
|
||||
assert!(!crate::EventMaterializer::accepts_observation(
|
||||
&crate::AdminMaterializer,
|
||||
crate::MdProgramId(kb_program_ids::SPL_TOKEN_PROGRAM_ID.to_string());
|
||||
assert!(!crate::MtApiEventMaterializer::accepts_observation(
|
||||
&crate::MtAdminMaterializer,
|
||||
&observation,
|
||||
));
|
||||
observation.event.program_id =
|
||||
crate::ProgramId(kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID.to_string());
|
||||
crate::MdProgramId(kb_program_ids::SPL_TOKEN2022_PROGRAM_ID.to_string());
|
||||
observation.transaction_failed = true;
|
||||
observation.observation_committed = false;
|
||||
let result = crate::EventMaterializer::materialize(&crate::AdminMaterializer, &observation);
|
||||
assert_eq!(result.status, crate::MaterializerOutcomeStatus::Refused);
|
||||
let result =
|
||||
crate::MtApiEventMaterializer::materialize(&crate::MtAdminMaterializer, &observation);
|
||||
assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Refused);
|
||||
}
|
||||
}
|
||||
|
||||
/// Materialize authoritative Token-2022 administrative extension snapshots.
|
||||
pub fn materialize_token_2022_admin_state_snapshots(
|
||||
pub fn materialize_token2022_admin_state_snapshots(
|
||||
account_key: &str,
|
||||
slot: u64,
|
||||
state: &crate::Token2022State,
|
||||
) -> std::result::Result<std::vec::Vec<crate::MaterializedOutput>, String> {
|
||||
state: &crate::DcToken2022State,
|
||||
) -> std::result::Result<std::vec::Vec<crate::MtApiMaterializedOutput>, String> {
|
||||
if account_key.is_empty() {
|
||||
return std::result::Result::Err(
|
||||
"Token-2022 admin state snapshot requires a non-empty account key".to_string(),
|
||||
);
|
||||
}
|
||||
if state.kind != crate::Token2022StateKind::Mint {
|
||||
if state.kind != crate::DcToken2022StateKind::Mint {
|
||||
for entry in &state.extensions {
|
||||
if is_token_2022_admin_mint_extension(entry.extension_name) {
|
||||
if is_token2022_admin_mint_extension(entry.extension_name) {
|
||||
return std::result::Result::Err(format!(
|
||||
"Token-2022 {} extension requires mint state",
|
||||
entry.extension_name
|
||||
@@ -923,22 +928,22 @@ pub fn materialize_token_2022_admin_state_snapshots(
|
||||
}
|
||||
let mut outputs = std::vec::Vec::new();
|
||||
for entry in &state.extensions {
|
||||
if !is_token_2022_admin_mint_extension(entry.extension_name) {
|
||||
if !is_token2022_admin_mint_extension(entry.extension_name) {
|
||||
continue;
|
||||
}
|
||||
let output_key =
|
||||
format!("token_2022_admin_state:{}:{account_key}:{slot}", entry.extension_name);
|
||||
outputs.push(crate::MaterializedOutput {
|
||||
format!("token2022_admin_state:{}:{account_key}:{slot}", entry.extension_name);
|
||||
outputs.push(crate::MtApiMaterializedOutput {
|
||||
output_key: output_key.clone(),
|
||||
family: crate::MaterializedEventFamily::Admin,
|
||||
family: crate::MdMaterializedEventFamily::Admin,
|
||||
payload_json: serde_json::json!({
|
||||
"projectionVersion": 1,
|
||||
"projectionSemantics": "authoritative_bounded_admin_state_snapshot",
|
||||
"domain": "token_2022_extension_admin_state",
|
||||
"domain": "token2022_extension_admin_state",
|
||||
"idempotenceKey": output_key,
|
||||
"accountKey": account_key,
|
||||
"slot": slot,
|
||||
"programId": kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID,
|
||||
"programId": kb_program_ids::SPL_TOKEN2022_PROGRAM_ID,
|
||||
"stateKind": "mint",
|
||||
"extensionType": entry.extension_type,
|
||||
"extensionName": entry.extension_name,
|
||||
@@ -946,9 +951,9 @@ pub fn materialize_token_2022_admin_state_snapshots(
|
||||
"valueFields": entry.value_fields,
|
||||
"finalAccountStateCaptured": true,
|
||||
"provenance": {
|
||||
"processorName": crate::MATERIALIZER_ADMIN_PROCESSOR_NAME,
|
||||
"processorName": crate::MT_ADMIN_PROCESSOR_NAME,
|
||||
"processorVersion": env!("CARGO_PKG_VERSION"),
|
||||
"source": "exact_token_2022_tlv_state_parser"
|
||||
"source": "exact_token2022_tlv_state_parser"
|
||||
}
|
||||
}),
|
||||
});
|
||||
@@ -956,7 +961,7 @@ pub fn materialize_token_2022_admin_state_snapshots(
|
||||
return std::result::Result::Ok(outputs);
|
||||
}
|
||||
|
||||
fn is_token_2022_admin_mint_extension(extension_name: &str) -> bool {
|
||||
fn is_token2022_admin_mint_extension(extension_name: &str) -> bool {
|
||||
return matches!(
|
||||
extension_name,
|
||||
"mint_close_authority"
|
||||
@@ -974,27 +979,27 @@ fn is_token_2022_admin_mint_extension(extension_name: &str) -> bool {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod token_2022_admin_state_tests {
|
||||
fn mint_state() -> crate::Token2022State {
|
||||
return crate::Token2022State {
|
||||
kind: crate::Token2022StateKind::Mint,
|
||||
mod token2022_admin_state_tests {
|
||||
fn mint_state() -> crate::DcToken2022State {
|
||||
return crate::DcToken2022State {
|
||||
kind: crate::DcToken2022StateKind::Mint,
|
||||
base_fields: serde_json::json!({}),
|
||||
base_hex: String::new(),
|
||||
account_type: std::option::Option::Some(1),
|
||||
extensions: vec![
|
||||
crate::Token2022TlvEntry {
|
||||
crate::DcToken2022TlvEntry {
|
||||
extension_type: 6,
|
||||
extension_name: "default_account_state",
|
||||
value_hex: "02".to_string(),
|
||||
value_fields: serde_json::json!({"state": 2}),
|
||||
},
|
||||
crate::Token2022TlvEntry {
|
||||
crate::DcToken2022TlvEntry {
|
||||
extension_type: 26,
|
||||
extension_name: "pausable",
|
||||
value_hex: "00".repeat(33),
|
||||
value_fields: serde_json::json!({"paused": false}),
|
||||
},
|
||||
crate::Token2022TlvEntry {
|
||||
crate::DcToken2022TlvEntry {
|
||||
extension_type: 1,
|
||||
extension_name: "transfer_fee_config",
|
||||
value_hex: "00".to_string(),
|
||||
@@ -1007,8 +1012,8 @@ mod token_2022_admin_state_tests {
|
||||
#[test]
|
||||
fn admin_snapshots_are_owned_once_filtered_and_idempotent() {
|
||||
let state = mint_state();
|
||||
let first = crate::materialize_token_2022_admin_state_snapshots("mint111", 44, &state);
|
||||
let second = crate::materialize_token_2022_admin_state_snapshots("mint111", 44, &state);
|
||||
let first = crate::materialize_token2022_admin_state_snapshots("mint111", 44, &state);
|
||||
let second = crate::materialize_token2022_admin_state_snapshots("mint111", 44, &state);
|
||||
assert_eq!(first, second);
|
||||
assert_eq!(first.as_ref().map(std::vec::Vec::len), std::result::Result::Ok(2));
|
||||
assert_eq!(
|
||||
@@ -1028,35 +1033,35 @@ mod token_2022_admin_state_tests {
|
||||
#[test]
|
||||
fn admin_snapshots_reject_empty_identity_and_wrong_state_kind() {
|
||||
let state = mint_state();
|
||||
assert!(crate::materialize_token_2022_admin_state_snapshots("", 44, &state).is_err());
|
||||
let account_state = crate::Token2022State {
|
||||
kind: crate::Token2022StateKind::Account,
|
||||
assert!(crate::materialize_token2022_admin_state_snapshots("", 44, &state).is_err());
|
||||
let account_state = crate::DcToken2022State {
|
||||
kind: crate::DcToken2022StateKind::Account,
|
||||
base_fields: serde_json::json!({}),
|
||||
base_hex: String::new(),
|
||||
account_type: std::option::Option::Some(2),
|
||||
extensions: state.extensions[..1].to_vec(),
|
||||
};
|
||||
assert!(
|
||||
crate::materialize_token_2022_admin_state_snapshots("account111", 44, &account_state,)
|
||||
crate::materialize_token2022_admin_state_snapshots("account111", 44, &account_state,)
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Materialize one authoritative SPL ElGamal registry account snapshot.
|
||||
pub fn materialize_elgamal_registry_state_snapshot(
|
||||
pub fn materializer_admin_materialize_elgamal_registry_state_snapshot(
|
||||
registry_account_key: &str,
|
||||
slot: u64,
|
||||
state: &crate::ElGamalRegistryState,
|
||||
) -> std::result::Result<crate::MaterializedOutput, String> {
|
||||
state: &crate::DcSplElGamalRegistryState,
|
||||
) -> std::result::Result<crate::MtApiMaterializedOutput, String> {
|
||||
if registry_account_key.is_empty() {
|
||||
return std::result::Result::Err(
|
||||
"SPL ElGamal registry snapshot requires a non-empty account key".to_string(),
|
||||
);
|
||||
}
|
||||
return std::result::Result::Ok(crate::MaterializedOutput {
|
||||
return std::result::Result::Ok(crate::MtApiMaterializedOutput {
|
||||
output_key: format!("spl_elgamal_registry_state:{registry_account_key}:{slot}"),
|
||||
family: crate::MaterializedEventFamily::Admin,
|
||||
family: crate::MdMaterializedEventFamily::Admin,
|
||||
payload_json: serde_json::json!({
|
||||
"projectionVersion": 1,
|
||||
"domain": "spl_elgamal_registry_account_state",
|
||||
@@ -1064,7 +1069,7 @@ pub fn materialize_elgamal_registry_state_snapshot(
|
||||
"idempotenceKey": format!("spl_elgamal_registry:{registry_account_key}:{slot}"),
|
||||
"registryAccountKey": registry_account_key,
|
||||
"slot": slot,
|
||||
"programId": kb_program_ids::SPL_TOKEN_2022_ELGAMAL_REGISTRY_PROGRAM_ID,
|
||||
"programId": kb_program_ids::SPL_TOKEN2022_ELGAMAL_REGISTRY_PROGRAM_ID,
|
||||
"owner": state.owner,
|
||||
"elgamalPubkeyBase64": state.elgamal_pubkey_base64,
|
||||
"wireHex": state.wire_hex,
|
||||
@@ -1072,7 +1077,7 @@ pub fn materialize_elgamal_registry_state_snapshot(
|
||||
"confidentialValuesDecrypted": false,
|
||||
"finalAccountStateCaptured": true,
|
||||
"provenance": {
|
||||
"processorName": crate::MATERIALIZER_ADMIN_PROCESSOR_NAME,
|
||||
"processorName": crate::MT_ADMIN_PROCESSOR_NAME,
|
||||
"processorVersion": env!("CARGO_PKG_VERSION"),
|
||||
"source": "exact_spl_elgamal_registry_state_parser"
|
||||
}
|
||||
@@ -1084,12 +1089,16 @@ pub fn materialize_elgamal_registry_state_snapshot(
|
||||
mod elgamal_registry_state_tests {
|
||||
#[test]
|
||||
fn exact_registry_snapshot_preserves_public_state_without_claiming_proof_validation() {
|
||||
let state = crate::ElGamalRegistryState {
|
||||
let state = crate::DcSplElGamalRegistryState {
|
||||
owner: "owner111".to_string(),
|
||||
elgamal_pubkey_base64: "AQID".to_string(),
|
||||
wire_hex: "00".repeat(64),
|
||||
};
|
||||
let output = crate::materialize_elgamal_registry_state_snapshot("registry111", 11, &state);
|
||||
let output = crate::materializer_admin_materialize_elgamal_registry_state_snapshot(
|
||||
"registry111",
|
||||
11,
|
||||
&state,
|
||||
);
|
||||
assert_eq!(
|
||||
output.as_ref().map(|value| return value.payload_json["owner"].clone()),
|
||||
std::result::Result::Ok(serde_json::json!("owner111"))
|
||||
@@ -1111,14 +1120,25 @@ mod elgamal_registry_state_tests {
|
||||
|
||||
#[test]
|
||||
fn registry_snapshot_is_idempotent_and_requires_account_identity() {
|
||||
let state = crate::ElGamalRegistryState {
|
||||
let state = crate::DcSplElGamalRegistryState {
|
||||
owner: "owner111".to_string(),
|
||||
elgamal_pubkey_base64: "AQID".to_string(),
|
||||
wire_hex: "00".repeat(64),
|
||||
};
|
||||
let first = crate::materialize_elgamal_registry_state_snapshot("registry111", 11, &state);
|
||||
let second = crate::materialize_elgamal_registry_state_snapshot("registry111", 11, &state);
|
||||
let first = crate::materializer_admin_materialize_elgamal_registry_state_snapshot(
|
||||
"registry111",
|
||||
11,
|
||||
&state,
|
||||
);
|
||||
let second = crate::materializer_admin_materialize_elgamal_registry_state_snapshot(
|
||||
"registry111",
|
||||
11,
|
||||
&state,
|
||||
);
|
||||
assert_eq!(first, second);
|
||||
assert!(crate::materialize_elgamal_registry_state_snapshot("", 11, &state).is_err());
|
||||
assert!(
|
||||
crate::materializer_admin_materialize_elgamal_registry_state_snapshot("", 11, &state)
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,31 +1,32 @@
|
||||
// file: kb-lib/src/materializer/api.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
//! Materializer contracts consolidated from `kb_materializer_api`.
|
||||
|
||||
pub mod contracts;
|
||||
pub mod materializer;
|
||||
mod contracts;
|
||||
mod materializer;
|
||||
|
||||
/// Stable decoded observation materializer contract used by the common pipeline.
|
||||
pub use crate::materializer::api::contracts::EventMaterializer;
|
||||
pub use self::contracts::MtApiEventMaterializer;
|
||||
/// Explicit policy applied to successful and failed source transactions.
|
||||
pub use crate::materializer::api::contracts::MaterializationTransactionPolicy;
|
||||
pub use self::contracts::MtApiMaterializationTransactionPolicy;
|
||||
/// Stable processor-owned business output derived from one decoded observation.
|
||||
pub use crate::materializer::api::contracts::MaterializedOutput;
|
||||
pub use self::contracts::MtApiMaterializedOutput;
|
||||
/// Structured materializer diagnostic without generic error wrappers.
|
||||
pub use crate::materializer::api::contracts::MaterializerDiagnostic;
|
||||
pub use self::contracts::MtApiMaterializerDiagnostic;
|
||||
/// Complete explicit result produced by one materializer invocation.
|
||||
pub use crate::materializer::api::contracts::MaterializerExecutionResult;
|
||||
pub use self::contracts::MtApiMaterializerExecutionResult;
|
||||
/// Stable identity of one decoded event materializer.
|
||||
pub use crate::materializer::api::contracts::MaterializerIdentity;
|
||||
pub use self::contracts::MtApiMaterializerIdentity;
|
||||
/// Terminal materializer result status.
|
||||
pub use crate::materializer::api::contracts::MaterializerOutcomeStatus;
|
||||
pub use self::contracts::MtApiMaterializerOutcomeStatus;
|
||||
/// Returns true when a materializer explicitly accepts one decoded family.
|
||||
pub use crate::materializer::api::contracts::materializer_accepts_family;
|
||||
pub use self::contracts::materializer_api_accepts_family;
|
||||
/// Returns true when a materializer accepts one exact decoded observation.
|
||||
pub use crate::materializer::api::contracts::materializer_accepts_observation;
|
||||
pub use self::contracts::materializer_api_accepts_observation;
|
||||
/// Applies the mandatory source transaction policy before materialization.
|
||||
pub use crate::materializer::api::contracts::validate_materialization_policy;
|
||||
pub use self::contracts::materializer_api_validate_materialization_policy;
|
||||
/// Rejects mutable business outputs derived from failed or uncommitted observations.
|
||||
pub use crate::materializer::api::contracts::validate_materialized_output_policy;
|
||||
pub use self::contracts::materializer_api_validate_materialized_output_policy;
|
||||
/// Exposes the legacy common business materializer trait.
|
||||
pub use crate::materializer::api::materializer::Materializer;
|
||||
pub use self::materializer::MtMaterializer;
|
||||
|
||||
@@ -5,14 +5,14 @@
|
||||
|
||||
/// Stable identity of one decoded event materializer.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
pub struct MaterializerIdentity {
|
||||
pub struct MtApiMaterializerIdentity {
|
||||
/// Stable lower snake case processor name.
|
||||
pub name: std::string::String,
|
||||
/// Semantic or deterministic implementation version.
|
||||
pub version: std::string::String,
|
||||
}
|
||||
|
||||
impl MaterializerIdentity {
|
||||
impl MtApiMaterializerIdentity {
|
||||
/// Builds a validated materializer identity.
|
||||
pub fn new(
|
||||
name: impl std::convert::Into<std::string::String>,
|
||||
@@ -33,7 +33,7 @@ impl MaterializerIdentity {
|
||||
|
||||
/// Explicit policy applied to successful and failed source transactions.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
pub enum MaterializationTransactionPolicy {
|
||||
pub enum MtApiMaterializationTransactionPolicy {
|
||||
/// Accept committed observations from successful transactions only.
|
||||
SuccessfulCommittedOnly,
|
||||
/// Accept successful observations and failed audit-only observations.
|
||||
@@ -44,7 +44,7 @@ pub enum MaterializationTransactionPolicy {
|
||||
|
||||
/// Terminal materializer result status.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
pub enum MaterializerOutcomeStatus {
|
||||
pub enum MtApiMaterializerOutcomeStatus {
|
||||
/// One or more new stable outputs were inserted.
|
||||
Inserted,
|
||||
/// One or more processor-owned outputs were deterministically replaced.
|
||||
@@ -59,7 +59,7 @@ pub enum MaterializerOutcomeStatus {
|
||||
|
||||
/// Structured materializer diagnostic without generic error wrappers.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
pub struct MaterializerDiagnostic {
|
||||
pub struct MtApiMaterializerDiagnostic {
|
||||
/// Stable machine-readable diagnostic code.
|
||||
pub code: std::string::String,
|
||||
/// Human-readable diagnostic message.
|
||||
@@ -68,7 +68,7 @@ pub struct MaterializerDiagnostic {
|
||||
pub retriable: bool,
|
||||
}
|
||||
|
||||
impl MaterializerDiagnostic {
|
||||
impl MtApiMaterializerDiagnostic {
|
||||
/// Builds a validated materializer diagnostic.
|
||||
pub fn new(
|
||||
code: impl std::convert::Into<std::string::String>,
|
||||
@@ -91,16 +91,16 @@ impl MaterializerDiagnostic {
|
||||
|
||||
/// Stable processor-owned business output derived from one decoded observation.
|
||||
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
pub struct MaterializedOutput {
|
||||
pub struct MtApiMaterializedOutput {
|
||||
/// Stable processor-owned output key within one decoded observation.
|
||||
pub output_key: std::string::String,
|
||||
/// Business output family.
|
||||
pub family: crate::MaterializedEventFamily,
|
||||
pub family: crate::MdMaterializedEventFamily,
|
||||
/// Typed business payload serialized as deterministic JSON.
|
||||
pub payload_json: serde_json::Value,
|
||||
}
|
||||
|
||||
impl MaterializedOutput {
|
||||
impl MtApiMaterializedOutput {
|
||||
/// Validates stable output identity.
|
||||
pub fn validate(&self) -> kb_core::Result<()> {
|
||||
if self.output_key.trim().is_empty() {
|
||||
@@ -114,22 +114,22 @@ impl MaterializedOutput {
|
||||
|
||||
/// Complete explicit result produced by one materializer invocation.
|
||||
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
pub struct MaterializerExecutionResult {
|
||||
pub struct MtApiMaterializerExecutionResult {
|
||||
/// Terminal materializer status.
|
||||
pub status: MaterializerOutcomeStatus,
|
||||
pub status: MtApiMaterializerOutcomeStatus,
|
||||
/// Stable business outputs.
|
||||
pub outputs: std::vec::Vec<MaterializedOutput>,
|
||||
pub outputs: std::vec::Vec<MtApiMaterializedOutput>,
|
||||
/// Structured diagnostics.
|
||||
pub diagnostics: std::vec::Vec<MaterializerDiagnostic>,
|
||||
pub diagnostics: std::vec::Vec<MtApiMaterializerDiagnostic>,
|
||||
}
|
||||
|
||||
impl MaterializerExecutionResult {
|
||||
impl MtApiMaterializerExecutionResult {
|
||||
/// Builds a policy refusal result.
|
||||
pub fn refused(code: &str, message: &str) -> Self {
|
||||
return Self {
|
||||
status: MaterializerOutcomeStatus::Refused,
|
||||
status: MtApiMaterializerOutcomeStatus::Refused,
|
||||
outputs: std::vec::Vec::new(),
|
||||
diagnostics: std::vec![MaterializerDiagnostic {
|
||||
diagnostics: std::vec![MtApiMaterializerDiagnostic {
|
||||
code: code.to_string(),
|
||||
message: message.to_string(),
|
||||
retriable: false,
|
||||
@@ -140,7 +140,7 @@ impl MaterializerExecutionResult {
|
||||
/// Builds an ignored result.
|
||||
pub fn ignored() -> Self {
|
||||
return Self {
|
||||
status: MaterializerOutcomeStatus::Ignored,
|
||||
status: MtApiMaterializerOutcomeStatus::Ignored,
|
||||
outputs: std::vec::Vec::new(),
|
||||
diagnostics: std::vec::Vec::new(),
|
||||
};
|
||||
@@ -148,8 +148,8 @@ impl MaterializerExecutionResult {
|
||||
|
||||
/// Validates status and output consistency.
|
||||
pub fn validate(&self) -> kb_core::Result<()> {
|
||||
let output_status = self.status == MaterializerOutcomeStatus::Inserted
|
||||
|| self.status == MaterializerOutcomeStatus::Replaced;
|
||||
let output_status = self.status == MtApiMaterializerOutcomeStatus::Inserted
|
||||
|| self.status == MtApiMaterializerOutcomeStatus::Replaced;
|
||||
if output_status && self.outputs.is_empty() {
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(
|
||||
"inserted or replaced materializer status requires outputs",
|
||||
@@ -160,7 +160,7 @@ impl MaterializerExecutionResult {
|
||||
"ignored, refused or failed materializer status must not contain outputs",
|
||||
));
|
||||
}
|
||||
if self.status == MaterializerOutcomeStatus::Failed && self.diagnostics.is_empty() {
|
||||
if self.status == MtApiMaterializerOutcomeStatus::Failed && self.diagnostics.is_empty() {
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(
|
||||
"failed materializer status requires at least one diagnostic",
|
||||
));
|
||||
@@ -176,26 +176,32 @@ impl MaterializerExecutionResult {
|
||||
}
|
||||
|
||||
/// Stable decoded observation materializer contract used by the common pipeline.
|
||||
pub trait EventMaterializer: std::marker::Send + std::marker::Sync {
|
||||
pub trait MtApiEventMaterializer: std::marker::Send + std::marker::Sync {
|
||||
/// Returns the stable materializer identity.
|
||||
fn identity(&self) -> MaterializerIdentity;
|
||||
fn identity(&self) -> MtApiMaterializerIdentity;
|
||||
/// Returns every decoded event family explicitly accepted by this materializer.
|
||||
fn accepted_families(&self) -> &'static [crate::EventFamily];
|
||||
fn accepted_families(&self) -> &'static [crate::MdEventFamily];
|
||||
/// Returns whether this materializer accepts one exact decoded observation.
|
||||
fn accepts_observation(&self, observation: &crate::DecodedObservation) -> bool {
|
||||
fn accepts_observation(&self, observation: &crate::DcApiDecodedObservation) -> bool {
|
||||
let family = observation.event.event_family;
|
||||
return self.accepted_families().iter().any(|accepted| return *accepted == family);
|
||||
}
|
||||
/// Returns the failed/successful transaction policy for one accepted family.
|
||||
fn transaction_policy(&self, family: crate::EventFamily) -> MaterializationTransactionPolicy;
|
||||
fn transaction_policy(
|
||||
&self,
|
||||
family: crate::MdEventFamily,
|
||||
) -> MtApiMaterializationTransactionPolicy;
|
||||
/// Materializes one validated decoded observation.
|
||||
fn materialize(&self, observation: &crate::DecodedObservation) -> MaterializerExecutionResult;
|
||||
fn materialize(
|
||||
&self,
|
||||
observation: &crate::DcApiDecodedObservation,
|
||||
) -> MtApiMaterializerExecutionResult;
|
||||
}
|
||||
|
||||
/// Returns true when a materializer explicitly accepts one decoded family.
|
||||
pub fn materializer_accepts_family(
|
||||
materializer: &dyn EventMaterializer,
|
||||
family: crate::EventFamily,
|
||||
pub fn materializer_api_accepts_family(
|
||||
materializer: &dyn MtApiEventMaterializer,
|
||||
family: crate::MdEventFamily,
|
||||
) -> bool {
|
||||
return materializer
|
||||
.accepted_families()
|
||||
@@ -204,48 +210,48 @@ pub fn materializer_accepts_family(
|
||||
}
|
||||
|
||||
/// Returns true when a materializer accepts one exact decoded observation.
|
||||
pub fn materializer_accepts_observation(
|
||||
materializer: &dyn EventMaterializer,
|
||||
observation: &crate::DecodedObservation,
|
||||
pub fn materializer_api_accepts_observation(
|
||||
materializer: &dyn MtApiEventMaterializer,
|
||||
observation: &crate::DcApiDecodedObservation,
|
||||
) -> bool {
|
||||
return materializer.accepts_observation(observation);
|
||||
}
|
||||
|
||||
/// Applies the mandatory source transaction policy before materialization.
|
||||
pub fn validate_materialization_policy(
|
||||
materializer: &dyn EventMaterializer,
|
||||
observation: &crate::DecodedObservation,
|
||||
) -> std::result::Result<(), MaterializerExecutionResult> {
|
||||
pub fn materializer_api_validate_materialization_policy(
|
||||
materializer: &dyn MtApiEventMaterializer,
|
||||
observation: &crate::DcApiDecodedObservation,
|
||||
) -> std::result::Result<(), MtApiMaterializerExecutionResult> {
|
||||
let family = observation.event.event_family;
|
||||
if !crate::materializer_accepts_observation(materializer, observation) {
|
||||
return std::result::Result::Err(MaterializerExecutionResult::refused(
|
||||
if !crate::materializer_api_accepts_observation(materializer, observation) {
|
||||
return std::result::Result::Err(MtApiMaterializerExecutionResult::refused(
|
||||
"unsupported_decoded_observation",
|
||||
"materializer does not accept the decoded observation surface and entry",
|
||||
));
|
||||
}
|
||||
let policy = materializer.transaction_policy(family);
|
||||
if observation.transaction_failed || !observation.observation_committed {
|
||||
if family == crate::EventFamily::Trade
|
||||
|| family == crate::EventFamily::Liquidity
|
||||
|| family == crate::EventFamily::Lifecycle
|
||||
if family == crate::MdEventFamily::Trade
|
||||
|| family == crate::MdEventFamily::Liquidity
|
||||
|| family == crate::MdEventFamily::Lifecycle
|
||||
{
|
||||
return std::result::Result::Err(MaterializerExecutionResult::refused(
|
||||
return std::result::Result::Err(MtApiMaterializerExecutionResult::refused(
|
||||
"failed_transaction_mutation_refused",
|
||||
"failed or uncommitted observations cannot create successful mutable business outputs",
|
||||
));
|
||||
}
|
||||
if policy == MaterializationTransactionPolicy::SuccessfulCommittedOnly {
|
||||
return std::result::Result::Err(MaterializerExecutionResult::refused(
|
||||
if policy == MtApiMaterializationTransactionPolicy::SuccessfulCommittedOnly {
|
||||
return std::result::Result::Err(MtApiMaterializerExecutionResult::refused(
|
||||
"failed_transaction_policy_refused",
|
||||
"materializer accepts committed observations from successful transactions only",
|
||||
));
|
||||
}
|
||||
if policy == MaterializationTransactionPolicy::SuccessfulOrFailedAudit
|
||||
&& family != crate::EventFamily::Audit
|
||||
&& family != crate::EventFamily::ComplianceAudit
|
||||
&& family != crate::EventFamily::Risk
|
||||
if policy == MtApiMaterializationTransactionPolicy::SuccessfulOrFailedAudit
|
||||
&& family != crate::MdEventFamily::Audit
|
||||
&& family != crate::MdEventFamily::ComplianceAudit
|
||||
&& family != crate::MdEventFamily::Risk
|
||||
{
|
||||
return std::result::Result::Err(MaterializerExecutionResult::refused(
|
||||
return std::result::Result::Err(MtApiMaterializerExecutionResult::refused(
|
||||
"failed_transaction_non_audit_refused",
|
||||
"failed transaction materialization is limited to declared audit or risk families",
|
||||
));
|
||||
@@ -255,20 +261,20 @@ pub fn validate_materialization_policy(
|
||||
}
|
||||
|
||||
/// Rejects successful mutable business outputs derived from failed or uncommitted observations.
|
||||
pub fn validate_materialized_output_policy(
|
||||
observation: &crate::DecodedObservation,
|
||||
result: &MaterializerExecutionResult,
|
||||
) -> std::result::Result<(), MaterializerExecutionResult> {
|
||||
pub fn materializer_api_validate_materialized_output_policy(
|
||||
observation: &crate::DcApiDecodedObservation,
|
||||
result: &MtApiMaterializerExecutionResult,
|
||||
) -> std::result::Result<(), MtApiMaterializerExecutionResult> {
|
||||
if !observation.transaction_failed && observation.observation_committed {
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
let forbidden_output = result.outputs.iter().any(|output| {
|
||||
return output.family != crate::MaterializedEventFamily::ComplianceAudit
|
||||
&& output.family != crate::MaterializedEventFamily::TokenMetadataRisk
|
||||
&& output.family != crate::MaterializedEventFamily::Risk;
|
||||
return output.family != crate::MdMaterializedEventFamily::ComplianceAudit
|
||||
&& output.family != crate::MdMaterializedEventFamily::TokenMetadataRisk
|
||||
&& output.family != crate::MdMaterializedEventFamily::Risk;
|
||||
});
|
||||
if forbidden_output {
|
||||
return std::result::Result::Err(MaterializerExecutionResult::refused(
|
||||
return std::result::Result::Err(MtApiMaterializerExecutionResult::refused(
|
||||
"failed_transaction_output_refused",
|
||||
"failed or uncommitted observations may only produce audit or risk outputs",
|
||||
));
|
||||
@@ -280,48 +286,48 @@ pub fn validate_materialized_output_policy(
|
||||
mod tests {
|
||||
struct TradeMaterializer;
|
||||
|
||||
impl crate::EventMaterializer for TradeMaterializer {
|
||||
fn identity(&self) -> crate::MaterializerIdentity {
|
||||
return crate::MaterializerIdentity {
|
||||
impl crate::MtApiEventMaterializer for TradeMaterializer {
|
||||
fn identity(&self) -> crate::MtApiMaterializerIdentity {
|
||||
return crate::MtApiMaterializerIdentity {
|
||||
name: "trade_materializer".to_string(),
|
||||
version: "1".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
fn accepted_families(&self) -> &'static [crate::EventFamily] {
|
||||
return &[crate::EventFamily::Trade];
|
||||
fn accepted_families(&self) -> &'static [crate::MdEventFamily] {
|
||||
return &[crate::MdEventFamily::Trade];
|
||||
}
|
||||
|
||||
fn transaction_policy(
|
||||
&self,
|
||||
_family: crate::EventFamily,
|
||||
) -> crate::MaterializationTransactionPolicy {
|
||||
return crate::MaterializationTransactionPolicy::SuccessfulCommittedOnly;
|
||||
_family: crate::MdEventFamily,
|
||||
) -> crate::MtApiMaterializationTransactionPolicy {
|
||||
return crate::MtApiMaterializationTransactionPolicy::SuccessfulCommittedOnly;
|
||||
}
|
||||
|
||||
fn materialize(
|
||||
&self,
|
||||
_observation: &crate::DecodedObservation,
|
||||
) -> crate::MaterializerExecutionResult {
|
||||
return crate::MaterializerExecutionResult::ignored();
|
||||
_observation: &crate::DcApiDecodedObservation,
|
||||
) -> crate::MtApiMaterializerExecutionResult {
|
||||
return crate::MtApiMaterializerExecutionResult::ignored();
|
||||
}
|
||||
}
|
||||
|
||||
fn failed_trade_observation() -> crate::DecodedObservation {
|
||||
return crate::DecodedObservation {
|
||||
fn failed_trade_observation() -> crate::DcApiDecodedObservation {
|
||||
return crate::DcApiDecodedObservation {
|
||||
event_key: "trade".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("protocol".to_string()),
|
||||
surface_code: crate::SurfaceCode("surface".to_string()),
|
||||
event_code: crate::EventCode("trade".to_string()),
|
||||
event_name: crate::EventName("trade".to_string()),
|
||||
event_family: crate::EventFamily::Trade,
|
||||
source_kind: crate::EventSourceKind::Instruction,
|
||||
confidence: crate::DecoderConfidence::Exact,
|
||||
event: crate::MdDecodedProtocolEvent {
|
||||
signature: crate::MdSignature("signature".to_string()),
|
||||
slot: crate::MdSlot(42),
|
||||
instruction_path: crate::MdInstructionPath("0".to_string()),
|
||||
program_id: crate::MdProgramId("program".to_string()),
|
||||
protocol_code: crate::MdProtocolCode("protocol".to_string()),
|
||||
surface_code: crate::MdSurfaceCode("surface".to_string()),
|
||||
event_code: crate::MdEventCode("trade".to_string()),
|
||||
event_name: crate::MdEventName("trade".to_string()),
|
||||
event_family: crate::MdEventFamily::Trade,
|
||||
source_kind: crate::MdEventSourceKind::Instruction,
|
||||
confidence: crate::MdDecoderConfidence::Exact,
|
||||
},
|
||||
payload_json: serde_json::json!({}),
|
||||
transaction_failed: true,
|
||||
@@ -329,9 +335,9 @@ mod tests {
|
||||
serde_json::json!({"InstructionError": [0, "Custom"]}),
|
||||
),
|
||||
observation_committed: false,
|
||||
proof: crate::DecoderProof {
|
||||
kind: crate::DecoderProofKind::ExactLayout,
|
||||
confidence: crate::DecoderConfidence::Exact,
|
||||
proof: crate::DcApiDecoderProof {
|
||||
kind: crate::DcApiDecoderProofKind::ExactLayout,
|
||||
confidence: crate::MdDecoderConfidence::Exact,
|
||||
evidence: std::vec!["layout".to_string()],
|
||||
},
|
||||
};
|
||||
@@ -339,47 +345,49 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn failed_trade_is_refused_before_materialization() {
|
||||
let result =
|
||||
crate::validate_materialization_policy(&TradeMaterializer, &failed_trade_observation());
|
||||
let result = crate::materializer_api_validate_materialization_policy(
|
||||
&TradeMaterializer,
|
||||
&failed_trade_observation(),
|
||||
);
|
||||
assert!(result.is_err());
|
||||
let refusal = match result {
|
||||
std::result::Result::Ok(()) => panic!("failed trade unexpectedly accepted"),
|
||||
std::result::Result::Err(value) => value,
|
||||
};
|
||||
assert_eq!(refusal.status, crate::MaterializerOutcomeStatus::Refused);
|
||||
assert_eq!(refusal.status, crate::MtApiMaterializerOutcomeStatus::Refused);
|
||||
assert_eq!(refusal.diagnostics[0].code, "failed_transaction_mutation_refused");
|
||||
}
|
||||
|
||||
struct FailedAuditToTradeMaterializer;
|
||||
|
||||
impl crate::EventMaterializer for FailedAuditToTradeMaterializer {
|
||||
fn identity(&self) -> crate::MaterializerIdentity {
|
||||
return crate::MaterializerIdentity {
|
||||
impl crate::MtApiEventMaterializer for FailedAuditToTradeMaterializer {
|
||||
fn identity(&self) -> crate::MtApiMaterializerIdentity {
|
||||
return crate::MtApiMaterializerIdentity {
|
||||
name: "failed_audit_to_trade".to_string(),
|
||||
version: "1".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
fn accepted_families(&self) -> &'static [crate::EventFamily] {
|
||||
return &[crate::EventFamily::Audit];
|
||||
fn accepted_families(&self) -> &'static [crate::MdEventFamily] {
|
||||
return &[crate::MdEventFamily::Audit];
|
||||
}
|
||||
|
||||
fn transaction_policy(
|
||||
&self,
|
||||
_family: crate::EventFamily,
|
||||
) -> crate::MaterializationTransactionPolicy {
|
||||
return crate::MaterializationTransactionPolicy::SuccessfulOrFailedAudit;
|
||||
_family: crate::MdEventFamily,
|
||||
) -> crate::MtApiMaterializationTransactionPolicy {
|
||||
return crate::MtApiMaterializationTransactionPolicy::SuccessfulOrFailedAudit;
|
||||
}
|
||||
|
||||
fn materialize(
|
||||
&self,
|
||||
_observation: &crate::DecodedObservation,
|
||||
) -> crate::MaterializerExecutionResult {
|
||||
return crate::MaterializerExecutionResult {
|
||||
status: crate::MaterializerOutcomeStatus::Inserted,
|
||||
outputs: std::vec![crate::MaterializedOutput {
|
||||
_observation: &crate::DcApiDecodedObservation,
|
||||
) -> crate::MtApiMaterializerExecutionResult {
|
||||
return crate::MtApiMaterializerExecutionResult {
|
||||
status: crate::MtApiMaterializerOutcomeStatus::Inserted,
|
||||
outputs: std::vec![crate::MtApiMaterializedOutput {
|
||||
output_key: "trade".to_string(),
|
||||
family: crate::MaterializedEventFamily::Trade,
|
||||
family: crate::MdMaterializedEventFamily::Trade,
|
||||
payload_json: serde_json::json!({}),
|
||||
}],
|
||||
diagnostics: std::vec::Vec::new(),
|
||||
@@ -387,9 +395,9 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn failed_audit_observation() -> crate::DecodedObservation {
|
||||
fn failed_audit_observation() -> crate::DcApiDecodedObservation {
|
||||
let mut observation = failed_trade_observation();
|
||||
observation.event.event_family = crate::EventFamily::Audit;
|
||||
observation.event.event_family = crate::MdEventFamily::Audit;
|
||||
return observation;
|
||||
}
|
||||
|
||||
@@ -397,16 +405,20 @@ mod tests {
|
||||
fn failed_audit_cannot_emit_successful_trade_output() {
|
||||
let materializer = FailedAuditToTradeMaterializer;
|
||||
let observation = failed_audit_observation();
|
||||
let policy_result = crate::validate_materialization_policy(&materializer, &observation);
|
||||
let policy_result =
|
||||
crate::materializer_api_validate_materialization_policy(&materializer, &observation);
|
||||
assert!(policy_result.is_ok());
|
||||
let materialized = crate::EventMaterializer::materialize(&materializer, &observation);
|
||||
let output_result = crate::validate_materialized_output_policy(&observation, &materialized);
|
||||
let materialized = crate::MtApiEventMaterializer::materialize(&materializer, &observation);
|
||||
let output_result = crate::materializer_api_validate_materialized_output_policy(
|
||||
&observation,
|
||||
&materialized,
|
||||
);
|
||||
assert!(output_result.is_err());
|
||||
let refusal = match output_result {
|
||||
std::result::Result::Ok(()) => panic!("failed audit unexpectedly emitted a trade"),
|
||||
std::result::Result::Err(value) => value,
|
||||
};
|
||||
assert_eq!(refusal.status, crate::MaterializerOutcomeStatus::Refused);
|
||||
assert_eq!(refusal.status, crate::MtApiMaterializerOutcomeStatus::Refused);
|
||||
assert_eq!(refusal.diagnostics[0].code, "failed_transaction_output_refused");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
//! Materializer API contract used by business materializer crates.
|
||||
|
||||
/// Common contract implemented by all business materializer crates.
|
||||
pub trait Materializer {
|
||||
pub trait MtMaterializer {
|
||||
/// Returns the stable materializer name.
|
||||
fn materializer_name(&self) -> &'static str;
|
||||
|
||||
@@ -12,11 +12,11 @@ pub trait Materializer {
|
||||
fn materializer_version(&self) -> &'static str;
|
||||
|
||||
/// Tests whether this materializer accepts a decoded event.
|
||||
fn accepts_event(&self, event: &crate::DecodedProtocolEvent) -> bool;
|
||||
fn accepts_event(&self, event: &crate::MdDecodedProtocolEvent) -> bool;
|
||||
|
||||
/// Materializes a decoded event into zero or more business events.
|
||||
fn materialize_event(
|
||||
&self,
|
||||
event: &crate::DecodedProtocolEvent,
|
||||
) -> kb_core::Result<std::vec::Vec<crate::MaterializedEvent>>;
|
||||
event: &crate::MdDecodedProtocolEvent,
|
||||
) -> kb_core::Result<std::vec::Vec<crate::MdMaterializedEvent>>;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// file: kb-lib/src/materializer/bridge.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! `bridge` materializer family.
|
||||
|
||||
pub mod core;
|
||||
mod core;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// file: kb-lib/src/materializer/bridge/core.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! Migration boundary for legacy crate `kb_materializer_bridge`.
|
||||
|
||||
/// Legacy crate name retained for migration and compatibility tracking.
|
||||
pub const LEGACY_CRATE: &str = "kb_materializer_bridge";
|
||||
const LEGACY_CRATE: &str = "kb_materializer_bridge";
|
||||
|
||||
/// Current porting status.
|
||||
pub const MIGRATION_STATUS: &str = "source-preserved-pending-port";
|
||||
const MIGRATION_STATUS: &str = "source-preserved-pending-port";
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
// file: kb-lib/src/materializer/compliance.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
//! `compliance` materializer family.
|
||||
|
||||
pub mod audit;
|
||||
mod audit;
|
||||
mod constants;
|
||||
|
||||
pub(crate) use crate::materializer::compliance::constants::TRACING_TARGET as MATERIALIZER_COMPLIANCE_AUDIT_TRACING_TARGET;
|
||||
/// Canonical tracing target for the native compliance audit materializer.
|
||||
pub(crate) use self::constants::MT_COMPLIANCE_AUDIT_TRACING_TARGET;
|
||||
|
||||
/// Stable native compliance audit materializer.
|
||||
pub use crate::materializer::compliance::audit::ComplianceAuditMaterializer;
|
||||
pub use self::audit::MtComplianceAuditMaterializer;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
//! Stable compliance-audit projections for committed native bytecode mutations and execution profiles.
|
||||
|
||||
const ACCEPTED_FAMILIES: &[crate::EventFamily] = &[crate::EventFamily::Audit];
|
||||
const ACCEPTED_FAMILIES: &[crate::MdEventFamily] = &[crate::MdEventFamily::Audit];
|
||||
const COMPUTE_BUDGET_SURFACE: &str = "solana_native_compute_budget";
|
||||
const COMPUTE_BUDGET_ENTRIES: &[&str] = &[
|
||||
"request_units_deprecated",
|
||||
@@ -47,9 +47,9 @@ impl ProjectionKind {
|
||||
|
||||
/// Stable compliance-audit materializer for native bytecode mutations and execution profiles.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct ComplianceAuditMaterializer;
|
||||
pub struct MtComplianceAuditMaterializer;
|
||||
|
||||
impl crate::Materializer for crate::ComplianceAuditMaterializer {
|
||||
impl crate::MtMaterializer for crate::MtComplianceAuditMaterializer {
|
||||
fn materializer_name(&self) -> &'static str {
|
||||
return "kb_materializer_compliance_audit";
|
||||
}
|
||||
@@ -58,36 +58,36 @@ impl crate::Materializer for crate::ComplianceAuditMaterializer {
|
||||
return env!("CARGO_PKG_VERSION");
|
||||
}
|
||||
|
||||
fn accepts_event(&self, event: &crate::DecodedProtocolEvent) -> bool {
|
||||
return event.event_family == crate::EventFamily::Audit
|
||||
fn accepts_event(&self, event: &crate::MdDecodedProtocolEvent) -> bool {
|
||||
return event.event_family == crate::MdEventFamily::Audit
|
||||
&& projection(event.surface_code.0.as_str(), event.event_name.0.as_str()).is_some();
|
||||
}
|
||||
|
||||
fn materialize_event(
|
||||
&self,
|
||||
event: &crate::DecodedProtocolEvent,
|
||||
) -> kb_core::Result<std::vec::Vec<crate::MaterializedEvent>> {
|
||||
if !crate::Materializer::accepts_event(self, event) {
|
||||
event: &crate::MdDecodedProtocolEvent,
|
||||
) -> kb_core::Result<std::vec::Vec<crate::MdMaterializedEvent>> {
|
||||
if !crate::MtMaterializer::accepts_event(self, event) {
|
||||
return std::result::Result::Ok(std::vec::Vec::new());
|
||||
}
|
||||
return std::result::Result::Ok(std::vec::Vec::new());
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::EventMaterializer for crate::ComplianceAuditMaterializer {
|
||||
fn identity(&self) -> crate::MaterializerIdentity {
|
||||
return crate::MaterializerIdentity {
|
||||
impl crate::MtApiEventMaterializer for crate::MtComplianceAuditMaterializer {
|
||||
fn identity(&self) -> crate::MtApiMaterializerIdentity {
|
||||
return crate::MtApiMaterializerIdentity {
|
||||
name: "solana_native_compliance_audit".to_string(),
|
||||
version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
fn accepted_families(&self) -> &'static [crate::EventFamily] {
|
||||
fn accepted_families(&self) -> &'static [crate::MdEventFamily] {
|
||||
return ACCEPTED_FAMILIES;
|
||||
}
|
||||
|
||||
fn accepts_observation(&self, observation: &crate::DecodedObservation) -> bool {
|
||||
return observation.event.event_family == crate::EventFamily::Audit
|
||||
fn accepts_observation(&self, observation: &crate::DcApiDecodedObservation) -> bool {
|
||||
return observation.event.event_family == crate::MdEventFamily::Audit
|
||||
&& projection(
|
||||
observation.event.surface_code.0.as_str(),
|
||||
observation.event.event_name.0.as_str(),
|
||||
@@ -97,15 +97,15 @@ impl crate::EventMaterializer for crate::ComplianceAuditMaterializer {
|
||||
|
||||
fn transaction_policy(
|
||||
&self,
|
||||
_family: crate::EventFamily,
|
||||
) -> crate::MaterializationTransactionPolicy {
|
||||
return crate::MaterializationTransactionPolicy::SuccessfulOrFailedAudit;
|
||||
_family: crate::MdEventFamily,
|
||||
) -> crate::MtApiMaterializationTransactionPolicy {
|
||||
return crate::MtApiMaterializationTransactionPolicy::SuccessfulOrFailedAudit;
|
||||
}
|
||||
|
||||
fn materialize(
|
||||
&self,
|
||||
observation: &crate::DecodedObservation,
|
||||
) -> crate::MaterializerExecutionResult {
|
||||
observation: &crate::DcApiDecodedObservation,
|
||||
) -> crate::MtApiMaterializerExecutionResult {
|
||||
let projection = match projection(
|
||||
observation.event.surface_code.0.as_str(),
|
||||
observation.event.event_name.0.as_str(),
|
||||
@@ -113,24 +113,24 @@ impl crate::EventMaterializer for crate::ComplianceAuditMaterializer {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
tracing::debug!(
|
||||
target: crate::MATERIALIZER_COMPLIANCE_AUDIT_TRACING_TARGET,
|
||||
target: crate::MT_COMPLIANCE_AUDIT_TRACING_TARGET,
|
||||
action = "materialize_compliance_audit",
|
||||
surface_code = %observation.event.surface_code.0.as_str(),
|
||||
entry_code = %observation.event.event_name.0.as_str(),
|
||||
accepted = false,
|
||||
"ignore observation outside supported native compliance-audit projections"
|
||||
);
|
||||
return crate::MaterializerExecutionResult::ignored();
|
||||
return crate::MtApiMaterializerExecutionResult::ignored();
|
||||
},
|
||||
};
|
||||
if observation.event.event_family != crate::EventFamily::Audit {
|
||||
return crate::MaterializerExecutionResult::ignored();
|
||||
if observation.event.event_family != crate::MdEventFamily::Audit {
|
||||
return crate::MtApiMaterializerExecutionResult::ignored();
|
||||
}
|
||||
if projection != ProjectionKind::ComputeBudgetProfile
|
||||
&& (observation.transaction_failed || !observation.observation_committed)
|
||||
{
|
||||
tracing::debug!(
|
||||
target: crate::MATERIALIZER_COMPLIANCE_AUDIT_TRACING_TARGET,
|
||||
target: crate::MT_COMPLIANCE_AUDIT_TRACING_TARGET,
|
||||
action = "materialize_compliance_audit",
|
||||
surface_code = %observation.event.surface_code.0.as_str(),
|
||||
entry_code = %observation.event.event_name.0.as_str(),
|
||||
@@ -138,7 +138,7 @@ impl crate::EventMaterializer for crate::ComplianceAuditMaterializer {
|
||||
transaction_failed = observation.transaction_failed,
|
||||
"refuse uncommitted native bytecode mutation observation"
|
||||
);
|
||||
return crate::MaterializerExecutionResult::refused(
|
||||
return crate::MtApiMaterializerExecutionResult::refused(
|
||||
"failed_transaction_compliance_audit_refused",
|
||||
"failed or uncommitted native observations cannot create committed bytecode mutation outputs",
|
||||
);
|
||||
@@ -157,10 +157,10 @@ impl crate::EventMaterializer for crate::ComplianceAuditMaterializer {
|
||||
if projection == ProjectionKind::ComputeBudgetProfile {
|
||||
return materialize_compute_budget_profile(observation, parameters, operation);
|
||||
}
|
||||
let output = crate::MaterializedOutput {
|
||||
let output = crate::MtApiMaterializedOutput {
|
||||
output_key: projection
|
||||
.output_key(observation.event.surface_code.0.as_str(), operation.as_str()),
|
||||
family: crate::MaterializedEventFamily::ComplianceAudit,
|
||||
family: crate::MdMaterializedEventFamily::ComplianceAudit,
|
||||
payload_json: serde_json::json!({
|
||||
"projectionVersion": 1,
|
||||
"projectionSemantics": "committed_instruction_compliance_audit_event",
|
||||
@@ -178,7 +178,7 @@ impl crate::EventMaterializer for crate::ComplianceAuditMaterializer {
|
||||
}),
|
||||
};
|
||||
tracing::debug!(
|
||||
target: crate::MATERIALIZER_COMPLIANCE_AUDIT_TRACING_TARGET,
|
||||
target: crate::MT_COMPLIANCE_AUDIT_TRACING_TARGET,
|
||||
action = "materialize_compliance_audit",
|
||||
surface_code = %observation.event.surface_code.0.as_str(),
|
||||
entry_code = %observation.event.event_name.0.as_str(),
|
||||
@@ -186,8 +186,8 @@ impl crate::EventMaterializer for crate::ComplianceAuditMaterializer {
|
||||
output_count = 1_usize,
|
||||
"materialize committed native bytecode mutation observation"
|
||||
);
|
||||
return crate::MaterializerExecutionResult {
|
||||
status: crate::MaterializerOutcomeStatus::Inserted,
|
||||
return crate::MtApiMaterializerExecutionResult {
|
||||
status: crate::MtApiMaterializerOutcomeStatus::Inserted,
|
||||
outputs: std::vec![output],
|
||||
diagnostics: std::vec::Vec::new(),
|
||||
};
|
||||
@@ -195,17 +195,17 @@ impl crate::EventMaterializer for crate::ComplianceAuditMaterializer {
|
||||
}
|
||||
|
||||
fn materialize_compute_budget_profile(
|
||||
observation: &crate::DecodedObservation,
|
||||
observation: &crate::DcApiDecodedObservation,
|
||||
parameters: serde_json::Value,
|
||||
operation: std::string::String,
|
||||
) -> crate::MaterializerExecutionResult {
|
||||
) -> crate::MtApiMaterializerExecutionResult {
|
||||
let profile = parameters.get("transactionProfile").cloned().unwrap_or(serde_json::Value::Null);
|
||||
if !compute_budget_profile_emits(&profile) {
|
||||
return crate::MaterializerExecutionResult::ignored();
|
||||
return crate::MtApiMaterializerExecutionResult::ignored();
|
||||
}
|
||||
let output = crate::MaterializedOutput {
|
||||
let output = crate::MtApiMaterializedOutput {
|
||||
output_key: "transaction_compute_budget_profile:0".to_string(),
|
||||
family: crate::MaterializedEventFamily::ComplianceAudit,
|
||||
family: crate::MdMaterializedEventFamily::ComplianceAudit,
|
||||
payload_json: serde_json::json!({
|
||||
"projectionVersion": 1,
|
||||
"projectionSemantics": "transaction_compute_budget_profile",
|
||||
@@ -225,7 +225,7 @@ fn materialize_compute_budget_profile(
|
||||
}),
|
||||
};
|
||||
tracing::debug!(
|
||||
target: crate::MATERIALIZER_COMPLIANCE_AUDIT_TRACING_TARGET,
|
||||
target: crate::MT_COMPLIANCE_AUDIT_TRACING_TARGET,
|
||||
action = "materialize_compute_budget_profile",
|
||||
surface_code = %observation.event.surface_code.0.as_str(),
|
||||
entry_code = %observation.event.event_name.0.as_str(),
|
||||
@@ -233,8 +233,8 @@ fn materialize_compute_budget_profile(
|
||||
transaction_failed = observation.transaction_failed,
|
||||
"materialize native Compute Budget transaction profile"
|
||||
);
|
||||
return crate::MaterializerExecutionResult {
|
||||
status: crate::MaterializerOutcomeStatus::Inserted,
|
||||
return crate::MtApiMaterializerExecutionResult {
|
||||
status: crate::MtApiMaterializerOutcomeStatus::Inserted,
|
||||
outputs: std::vec![output],
|
||||
diagnostics: std::vec::Vec::new(),
|
||||
};
|
||||
@@ -349,21 +349,21 @@ mod tests {
|
||||
transaction_failed: bool,
|
||||
accounts: serde_json::Value,
|
||||
parameters: serde_json::Value,
|
||||
) -> crate::DecodedObservation {
|
||||
return crate::DecodedObservation {
|
||||
) -> crate::DcApiDecodedObservation {
|
||||
return crate::DcApiDecodedObservation {
|
||||
event_key: format!("{entry_code}:0"),
|
||||
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-id".to_string()),
|
||||
protocol_code: crate::ProtocolCode("solana_native".to_string()),
|
||||
surface_code: crate::SurfaceCode(surface_code.to_string()),
|
||||
event_code: crate::EventCode(format!("{surface_code}.{entry_code}")),
|
||||
event_name: crate::EventName(entry_code.to_string()),
|
||||
event_family: crate::EventFamily::Audit,
|
||||
source_kind: crate::EventSourceKind::Instruction,
|
||||
confidence: crate::DecoderConfidence::ManualExact,
|
||||
event: crate::MdDecodedProtocolEvent {
|
||||
signature: crate::MdSignature("signature".to_string()),
|
||||
slot: crate::MdSlot(42),
|
||||
instruction_path: crate::MdInstructionPath("0".to_string()),
|
||||
program_id: crate::MdProgramId("program-id".to_string()),
|
||||
protocol_code: crate::MdProtocolCode("solana_native".to_string()),
|
||||
surface_code: crate::MdSurfaceCode(surface_code.to_string()),
|
||||
event_code: crate::MdEventCode(format!("{surface_code}.{entry_code}")),
|
||||
event_name: crate::MdEventName(entry_code.to_string()),
|
||||
event_family: crate::MdEventFamily::Audit,
|
||||
source_kind: crate::MdEventSourceKind::Instruction,
|
||||
confidence: crate::MdDecoderConfidence::ManualExact,
|
||||
},
|
||||
payload_json: serde_json::json!({"accounts": accounts, "parameters": parameters}),
|
||||
transaction_failed,
|
||||
@@ -373,9 +373,9 @@ mod tests {
|
||||
std::option::Option::None
|
||||
},
|
||||
observation_committed: !transaction_failed,
|
||||
proof: crate::DecoderProof {
|
||||
kind: crate::DecoderProofKind::Manual,
|
||||
confidence: crate::DecoderConfidence::ManualExact,
|
||||
proof: crate::DcApiDecoderProof {
|
||||
kind: crate::DcApiDecoderProofKind::Manual,
|
||||
confidence: crate::MdDecoderConfidence::ManualExact,
|
||||
evidence: std::vec!["fixture".to_string()],
|
||||
},
|
||||
};
|
||||
@@ -387,7 +387,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn loader_write_variants_create_bounded_code_audits() {
|
||||
let materializer = crate::ComplianceAuditMaterializer;
|
||||
let materializer = crate::MtComplianceAuditMaterializer;
|
||||
for (surface, target_role, authority_role) in [
|
||||
(super::BPF_LOADER_DEPRECATED_SURFACE, "program_account", "missing"),
|
||||
(super::BPF_LOADER_SURFACE, "program_account", "missing"),
|
||||
@@ -409,9 +409,9 @@ mod tests {
|
||||
"bytesPrefixHex": "0102",
|
||||
}),
|
||||
);
|
||||
let result = crate::EventMaterializer::materialize(&materializer, &observation);
|
||||
assert_eq!(result.status, crate::MaterializerOutcomeStatus::Inserted);
|
||||
assert_eq!(result.outputs[0].family, crate::MaterializedEventFamily::ComplianceAudit);
|
||||
let result = crate::MtApiEventMaterializer::materialize(&materializer, &observation);
|
||||
assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Inserted);
|
||||
assert_eq!(result.outputs[0].family, crate::MdMaterializedEventFamily::ComplianceAudit);
|
||||
assert_eq!(result.outputs[0].payload_json["projection"]["targetAccount"], "target111");
|
||||
assert_eq!(result.outputs[0].payload_json["projection"]["bytesSha256"], "abc");
|
||||
assert_eq!(
|
||||
@@ -423,7 +423,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn loader_v4_copy_preserves_source_and_bounded_ranges() {
|
||||
let materializer = crate::ComplianceAuditMaterializer;
|
||||
let materializer = crate::MtComplianceAuditMaterializer;
|
||||
let observation = observation(
|
||||
super::LOADER_V4_SURFACE,
|
||||
"copy",
|
||||
@@ -435,7 +435,7 @@ mod tests {
|
||||
]),
|
||||
serde_json::json!({"destinationOffset": 8, "sourceOffset": 4, "length": 16}),
|
||||
);
|
||||
let result = crate::EventMaterializer::materialize(&materializer, &observation);
|
||||
let result = crate::MtApiEventMaterializer::materialize(&materializer, &observation);
|
||||
assert_eq!(
|
||||
result.outputs[0].output_key,
|
||||
"program_loader_code:solana_native_loader_v4:copy:0"
|
||||
@@ -449,7 +449,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn compute_budget_profile_is_emitted_only_by_profile_emitter() {
|
||||
let materializer = crate::ComplianceAuditMaterializer;
|
||||
let materializer = crate::MtComplianceAuditMaterializer;
|
||||
let emitter = observation(
|
||||
super::COMPUTE_BUDGET_SURFACE,
|
||||
"set_compute_unit_limit",
|
||||
@@ -469,10 +469,10 @@ mod tests {
|
||||
}
|
||||
}),
|
||||
);
|
||||
let emitted = crate::EventMaterializer::materialize(&materializer, &emitter);
|
||||
assert_eq!(emitted.status, crate::MaterializerOutcomeStatus::Inserted);
|
||||
let emitted = crate::MtApiEventMaterializer::materialize(&materializer, &emitter);
|
||||
assert_eq!(emitted.status, crate::MtApiMaterializerOutcomeStatus::Inserted);
|
||||
assert_eq!(emitted.outputs[0].output_key, "transaction_compute_budget_profile:0");
|
||||
assert_eq!(emitted.outputs[0].family, crate::MaterializedEventFamily::ComplianceAudit);
|
||||
assert_eq!(emitted.outputs[0].family, crate::MdMaterializedEventFamily::ComplianceAudit);
|
||||
assert_eq!(emitted.outputs[0].payload_json["profile"]["computeBudgetInstructionCount"], 2);
|
||||
let non_emitter = observation(
|
||||
super::COMPUTE_BUDGET_SURFACE,
|
||||
@@ -486,13 +486,13 @@ mod tests {
|
||||
}
|
||||
}),
|
||||
);
|
||||
let ignored = crate::EventMaterializer::materialize(&materializer, &non_emitter);
|
||||
assert_eq!(ignored.status, crate::MaterializerOutcomeStatus::Ignored);
|
||||
let ignored = crate::MtApiEventMaterializer::materialize(&materializer, &non_emitter);
|
||||
assert_eq!(ignored.status, crate::MtApiMaterializerOutcomeStatus::Ignored);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_compute_budget_profile_remains_audit_materializable() {
|
||||
let materializer = crate::ComplianceAuditMaterializer;
|
||||
let materializer = crate::MtComplianceAuditMaterializer;
|
||||
let observation = observation(
|
||||
super::COMPUTE_BUDGET_SURFACE,
|
||||
"set_compute_unit_price",
|
||||
@@ -506,14 +506,14 @@ mod tests {
|
||||
}
|
||||
}),
|
||||
);
|
||||
let result = crate::EventMaterializer::materialize(&materializer, &observation);
|
||||
assert_eq!(result.status, crate::MaterializerOutcomeStatus::Inserted);
|
||||
let result = crate::MtApiEventMaterializer::materialize(&materializer, &observation);
|
||||
assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Inserted);
|
||||
assert_eq!(result.outputs[0].payload_json["transactionSucceeded"], false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_code_mutation_is_refused() {
|
||||
let materializer = crate::ComplianceAuditMaterializer;
|
||||
let materializer = crate::MtComplianceAuditMaterializer;
|
||||
let observation = observation(
|
||||
super::LOADER_V4_SURFACE,
|
||||
"write",
|
||||
@@ -521,14 +521,14 @@ mod tests {
|
||||
serde_json::json!([account("program_account", "target111")]),
|
||||
serde_json::json!({"offset": 0, "byteLength": 1, "bytesSha256": "abc"}),
|
||||
);
|
||||
let result = crate::EventMaterializer::materialize(&materializer, &observation);
|
||||
assert_eq!(result.status, crate::MaterializerOutcomeStatus::Refused);
|
||||
let result = crate::MtApiEventMaterializer::materialize(&materializer, &observation);
|
||||
assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Refused);
|
||||
assert!(result.outputs.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unrelated_audit_observation_is_ignored() {
|
||||
let materializer = crate::ComplianceAuditMaterializer;
|
||||
let materializer = crate::MtComplianceAuditMaterializer;
|
||||
let observation = observation(
|
||||
"other_surface",
|
||||
"write",
|
||||
@@ -536,13 +536,13 @@ mod tests {
|
||||
serde_json::json!([]),
|
||||
serde_json::json!({}),
|
||||
);
|
||||
let result = crate::EventMaterializer::materialize(&materializer, &observation);
|
||||
assert_eq!(result.status, crate::MaterializerOutcomeStatus::Ignored);
|
||||
let result = crate::MtApiEventMaterializer::materialize(&materializer, &observation);
|
||||
assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Ignored);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn output_serialization_is_deterministic() {
|
||||
let materializer = crate::ComplianceAuditMaterializer;
|
||||
let materializer = crate::MtComplianceAuditMaterializer;
|
||||
let observation = observation(
|
||||
super::BPF_LOADER_UPGRADEABLE_SURFACE,
|
||||
"write",
|
||||
@@ -558,8 +558,8 @@ mod tests {
|
||||
"bytesPrefixHex": "0102",
|
||||
}),
|
||||
);
|
||||
let first = crate::EventMaterializer::materialize(&materializer, &observation);
|
||||
let second = crate::EventMaterializer::materialize(&materializer, &observation);
|
||||
let first = crate::MtApiEventMaterializer::materialize(&materializer, &observation);
|
||||
let second = crate::MtApiEventMaterializer::materialize(&materializer, &observation);
|
||||
let first_json = match serde_json::to_string(&first) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
|
||||
@@ -4,4 +4,4 @@
|
||||
//! Local constants for the native compliance audit materializer.
|
||||
|
||||
/// Canonical tracing target for this component.
|
||||
pub(crate) const TRACING_TARGET: &str = "kb-lib.materializer.compliance";
|
||||
pub(crate) const MT_COMPLIANCE_AUDIT_TRACING_TARGET: &str = "kb-lib.materializer.compliance";
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
// file: kb-lib/src/materializer/fees.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! `fees` materializer family.
|
||||
|
||||
pub mod core;
|
||||
mod core;
|
||||
|
||||
/// Fee materializer for committed Token-2022 fee observations.
|
||||
pub use self::core::MtFeesMaterializer;
|
||||
/// Materializes authoritative Token-2022 fee extension snapshots.
|
||||
pub use self::core::materialize_token2022_fee_state_snapshots;
|
||||
|
||||
@@ -1,10 +1,432 @@
|
||||
// file: kb-lib/src/materializer/fees/core.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! Migration boundary for legacy crate `kb_materializer_fees`.
|
||||
//! Stable fee projections derived from exact committed Token-2022 observations.
|
||||
|
||||
/// Legacy crate name retained for migration and compatibility tracking.
|
||||
pub const LEGACY_CRATE: &str = "kb_materializer_fees";
|
||||
const ACCEPTED_FAMILIES: &[crate::MdEventFamily] = &[crate::MdEventFamily::Fee];
|
||||
const SPL_TOKEN2022_SURFACE: &str = "spl_token2022";
|
||||
const TOKEN2022_FEE_ENTRIES: &[&str] = &[
|
||||
"initialize_transfer_fee_config",
|
||||
"set_transfer_fee",
|
||||
"withdraw_withheld_tokens_from_mint",
|
||||
"withdraw_withheld_tokens_from_accounts",
|
||||
"harvest_withheld_tokens_to_mint",
|
||||
"transfer_checked_with_fee",
|
||||
"initialize_confidential_transfer_fee_config",
|
||||
"withdraw_confidential_withheld_tokens_from_mint",
|
||||
"withdraw_confidential_withheld_tokens_from_accounts",
|
||||
"harvest_confidential_withheld_tokens_to_mint",
|
||||
"enable_confidential_harvest_to_mint",
|
||||
"disable_confidential_harvest_to_mint",
|
||||
"transfer_confidential_tokens_with_fee",
|
||||
];
|
||||
|
||||
/// Current porting status.
|
||||
pub const MIGRATION_STATUS: &str = "source-preserved-pending-port";
|
||||
/// Fee materializer for committed Token-2022 fee instructions.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct MtFeesMaterializer;
|
||||
|
||||
impl crate::MtMaterializer for crate::MtFeesMaterializer {
|
||||
fn materializer_name(&self) -> &'static str {
|
||||
return "kb_materializer_fees";
|
||||
}
|
||||
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::MtFeesMaterializer {
|
||||
fn identity(&self) -> crate::MtApiMaterializerIdentity {
|
||||
return crate::MtApiMaterializerIdentity {
|
||||
name: "fees".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(
|
||||
"failed_transaction_fee_refused",
|
||||
"failed or uncommitted Token-2022 observations cannot create fee outputs",
|
||||
);
|
||||
}
|
||||
let operation = observation.event.event_name.0.clone();
|
||||
let accounts = observation
|
||||
.payload_json
|
||||
.get("accounts")
|
||||
.cloned()
|
||||
.unwrap_or(serde_json::Value::Null);
|
||||
let parameters = observation
|
||||
.payload_json
|
||||
.get("parameters")
|
||||
.cloned()
|
||||
.unwrap_or_else(|| return observation.payload_json.clone());
|
||||
let output = crate::MtApiMaterializedOutput {
|
||||
output_key: format!("token2022_fee:{operation}:0"),
|
||||
family: crate::MdMaterializedEventFamily::Fee,
|
||||
payload_json: serde_json::json!({
|
||||
"projectionVersion": 1,
|
||||
"projectionSemantics": "committed_instruction_fee_event",
|
||||
"domain": fee_domain(observation.event.event_name.0.as_str()),
|
||||
"operation": operation,
|
||||
"programId": observation.event.program_id.0.clone(),
|
||||
"signature": observation.event.signature.0.clone(),
|
||||
"slot": observation.event.slot.0,
|
||||
"instructionPath": observation.event.instruction_path.0.clone(),
|
||||
"transactionSucceeded": true,
|
||||
"accounts": accounts,
|
||||
"parameters": parameters,
|
||||
"stateReadRequired": state_read_required(
|
||||
observation.event.event_name.0.as_str(),
|
||||
),
|
||||
"confidentialValuesDecrypted": false,
|
||||
}),
|
||||
};
|
||||
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::Fee
|
||||
&& event.surface_code.0 == SPL_TOKEN2022_SURFACE
|
||||
&& event.program_id.0 == kb_program_ids::SPL_TOKEN2022_PROGRAM_ID
|
||||
&& contains(TOKEN2022_FEE_ENTRIES, event.event_name.0.as_str());
|
||||
}
|
||||
|
||||
fn fee_domain(operation: &str) -> &'static str {
|
||||
if operation.contains("confidential") {
|
||||
return "token2022_confidential_transfer_fee";
|
||||
}
|
||||
return "token2022_transfer_fee";
|
||||
}
|
||||
|
||||
fn state_read_required(operation: &str) -> bool {
|
||||
return matches!(
|
||||
operation,
|
||||
"initialize_transfer_fee_config"
|
||||
| "set_transfer_fee"
|
||||
| "withdraw_withheld_tokens_from_mint"
|
||||
| "withdraw_withheld_tokens_from_accounts"
|
||||
| "harvest_withheld_tokens_to_mint"
|
||||
| "initialize_confidential_transfer_fee_config"
|
||||
| "withdraw_confidential_withheld_tokens_from_mint"
|
||||
| "withdraw_confidential_withheld_tokens_from_accounts"
|
||||
| "harvest_confidential_withheld_tokens_to_mint"
|
||||
| "enable_confidential_harvest_to_mint"
|
||||
| "disable_confidential_harvest_to_mint"
|
||||
);
|
||||
}
|
||||
|
||||
fn contains(entries: &[&str], entry_code: &str) -> bool {
|
||||
return entries.iter().any(|entry| return *entry == entry_code);
|
||||
}
|
||||
|
||||
/// Materialize authoritative Token-2022 fee extension snapshots without decrypting confidential values.
|
||||
pub fn materialize_token2022_fee_state_snapshots(
|
||||
account_key: &str,
|
||||
slot: u64,
|
||||
state: &crate::DcToken2022State,
|
||||
) -> std::result::Result<std::vec::Vec<crate::MtApiMaterializedOutput>, String> {
|
||||
if account_key.is_empty() {
|
||||
return std::result::Result::Err(
|
||||
"Token-2022 fee state snapshot requires a non-empty account key".to_string(),
|
||||
);
|
||||
}
|
||||
let state_kind = match state.kind {
|
||||
crate::DcToken2022StateKind::Mint => "mint",
|
||||
crate::DcToken2022StateKind::Account => "account",
|
||||
crate::DcToken2022StateKind::Multisig => {
|
||||
return std::result::Result::Err(
|
||||
"Token-2022 multisig state cannot contain fee extensions".to_string(),
|
||||
);
|
||||
},
|
||||
};
|
||||
let mut outputs = std::vec::Vec::new();
|
||||
for entry in &state.extensions {
|
||||
let confidential = match entry.extension_name {
|
||||
"transfer_fee_config" | "transfer_fee_amount" => false,
|
||||
"confidential_transfer_fee_config" | "confidential_transfer_fee_amount" => true,
|
||||
_ => continue,
|
||||
};
|
||||
let expected_kind = match entry.extension_name {
|
||||
"transfer_fee_config" | "confidential_transfer_fee_config" => "mint",
|
||||
"transfer_fee_amount" | "confidential_transfer_fee_amount" => "account",
|
||||
_ => continue,
|
||||
};
|
||||
if state_kind != expected_kind {
|
||||
return std::result::Result::Err(format!(
|
||||
"Token-2022 {} extension requires {expected_kind} state, got {state_kind}",
|
||||
entry.extension_name
|
||||
));
|
||||
}
|
||||
outputs.push(crate::MtApiMaterializedOutput {
|
||||
output_key: format!(
|
||||
"token2022_fee_state:{}:{account_key}:{slot}",
|
||||
entry.extension_name
|
||||
),
|
||||
family: crate::MdMaterializedEventFamily::Fee,
|
||||
payload_json: serde_json::json!({
|
||||
"projectionVersion": 1,
|
||||
"projectionSemantics": "authoritative_bounded_fee_state_snapshot",
|
||||
"domain": if confidential {
|
||||
"token2022_confidential_transfer_fee_state"
|
||||
} else {
|
||||
"token2022_transfer_fee_state"
|
||||
},
|
||||
"idempotenceKey": format!(
|
||||
"token2022_fee_state:{}:{account_key}:{slot}",
|
||||
entry.extension_name
|
||||
),
|
||||
"accountKey": account_key,
|
||||
"slot": slot,
|
||||
"programId": kb_program_ids::SPL_TOKEN2022_PROGRAM_ID,
|
||||
"stateKind": state_kind,
|
||||
"extensionType": entry.extension_type,
|
||||
"extensionName": entry.extension_name,
|
||||
"valueHex": entry.value_hex,
|
||||
"valueFields": entry.value_fields,
|
||||
"containsConfidentialState": confidential,
|
||||
"confidentialValuesDecrypted": false,
|
||||
"finalAccountStateCaptured": true,
|
||||
"provenance": {
|
||||
"processorName": "fees",
|
||||
"processorVersion": env!("CARGO_PKG_VERSION"),
|
||||
"source": "bounded_token2022_state_parser"
|
||||
}
|
||||
}),
|
||||
});
|
||||
}
|
||||
return std::result::Result::Ok(outputs);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
fn observation(
|
||||
surface_code: &str,
|
||||
program_id: &str,
|
||||
entry_code: &str,
|
||||
transaction_failed: bool,
|
||||
) -> crate::DcApiDecodedObservation {
|
||||
return crate::DcApiDecodedObservation {
|
||||
event_key: format!("{entry_code}:0"),
|
||||
event: crate::MdDecodedProtocolEvent {
|
||||
signature: crate::MdSignature("signature".to_string()),
|
||||
slot: crate::MdSlot(42),
|
||||
instruction_path: crate::MdInstructionPath("0".to_string()),
|
||||
program_id: crate::MdProgramId(program_id.to_string()),
|
||||
protocol_code: crate::MdProtocolCode("spl_token2022".to_string()),
|
||||
surface_code: crate::MdSurfaceCode(surface_code.to_string()),
|
||||
event_code: crate::MdEventCode(format!("{surface_code}.{entry_code}")),
|
||||
event_name: crate::MdEventName(entry_code.to_string()),
|
||||
event_family: crate::MdEventFamily::Fee,
|
||||
source_kind: crate::MdEventSourceKind::Instruction,
|
||||
confidence: crate::MdDecoderConfidence::ManualExact,
|
||||
},
|
||||
payload_json: serde_json::json!({
|
||||
"accounts": [{"position": 0, "role": "mint", "accountKey": "mint111"}],
|
||||
"parameters": {"extension": "transfer_fee", "basisPoints": 25},
|
||||
}),
|
||||
transaction_failed,
|
||||
transaction_error: if transaction_failed {
|
||||
std::option::Option::Some(serde_json::json!({"InstructionError": [0, "Custom"]}))
|
||||
} else {
|
||||
std::option::Option::None
|
||||
},
|
||||
observation_committed: !transaction_failed,
|
||||
proof: crate::DcApiDecoderProof {
|
||||
kind: crate::DcApiDecoderProofKind::Manual,
|
||||
confidence: crate::MdDecoderConfidence::ManualExact,
|
||||
evidence: std::vec!["fixture".to_string()],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn committed_token2022_fee_instruction_creates_one_stable_output() {
|
||||
let observation = observation(
|
||||
super::SPL_TOKEN2022_SURFACE,
|
||||
kb_program_ids::SPL_TOKEN2022_PROGRAM_ID,
|
||||
"set_transfer_fee",
|
||||
false,
|
||||
);
|
||||
let result =
|
||||
crate::MtApiEventMaterializer::materialize(&crate::MtFeesMaterializer, &observation);
|
||||
assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Inserted);
|
||||
assert_eq!(result.outputs.len(), 1);
|
||||
assert_eq!(result.outputs[0].family, crate::MdMaterializedEventFamily::Fee);
|
||||
assert_eq!(result.outputs[0].output_key, "token2022_fee:set_transfer_fee:0");
|
||||
assert_eq!(result.outputs[0].payload_json["stateReadRequired"], true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_token2022_fee_instruction_is_refused() {
|
||||
let observation = observation(
|
||||
super::SPL_TOKEN2022_SURFACE,
|
||||
kb_program_ids::SPL_TOKEN2022_PROGRAM_ID,
|
||||
"harvest_withheld_tokens_to_mint",
|
||||
true,
|
||||
);
|
||||
let result =
|
||||
crate::MtApiEventMaterializer::materialize(&crate::MtFeesMaterializer, &observation);
|
||||
assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Refused);
|
||||
assert!(result.outputs.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classic_token_and_program_surface_mismatches_are_ignored() {
|
||||
let classic = observation(
|
||||
"spl_token",
|
||||
kb_program_ids::SPL_TOKEN_PROGRAM_ID,
|
||||
"set_transfer_fee",
|
||||
false,
|
||||
);
|
||||
let wrong_program = observation(
|
||||
super::SPL_TOKEN2022_SURFACE,
|
||||
kb_program_ids::SPL_TOKEN_PROGRAM_ID,
|
||||
"set_transfer_fee",
|
||||
false,
|
||||
);
|
||||
assert!(!crate::MtApiEventMaterializer::accepts_observation(
|
||||
&crate::MtFeesMaterializer,
|
||||
&classic,
|
||||
));
|
||||
assert!(!crate::MtApiEventMaterializer::accepts_observation(
|
||||
&crate::MtFeesMaterializer,
|
||||
&wrong_program,
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transfer_checked_with_fee_is_self_contained_but_state_mutations_are_not() {
|
||||
assert!(!super::state_read_required("transfer_checked_with_fee"));
|
||||
assert!(super::state_read_required("withdraw_withheld_tokens_from_accounts"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn committed_confidential_fee_instruction_is_projected_without_decryption() {
|
||||
let observation = observation(
|
||||
super::SPL_TOKEN2022_SURFACE,
|
||||
kb_program_ids::SPL_TOKEN2022_PROGRAM_ID,
|
||||
"initialize_confidential_transfer_fee_config",
|
||||
false,
|
||||
);
|
||||
let result =
|
||||
crate::MtApiEventMaterializer::materialize(&crate::MtFeesMaterializer, &observation);
|
||||
assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Inserted);
|
||||
assert_eq!(result.outputs.len(), 1);
|
||||
assert_eq!(result.outputs[0].payload_json["domain"], "token2022_confidential_transfer_fee");
|
||||
assert_eq!(result.outputs[0].payload_json["stateReadRequired"], true);
|
||||
assert_eq!(result.outputs[0].payload_json["confidentialValuesDecrypted"], false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn confidential_transfer_with_fee_is_instructionally_self_contained() {
|
||||
assert!(!super::state_read_required("transfer_confidential_tokens_with_fee",));
|
||||
assert_eq!(
|
||||
super::fee_domain("transfer_confidential_tokens_with_fee"),
|
||||
"token2022_confidential_transfer_fee"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_fee_state_snapshots_are_owned_once_and_idempotent() {
|
||||
let mint = crate::DcToken2022State {
|
||||
kind: crate::DcToken2022StateKind::Mint,
|
||||
base_fields: serde_json::json!({}),
|
||||
base_hex: "00".to_string(),
|
||||
account_type: std::option::Option::Some(1),
|
||||
extensions: std::vec![crate::DcToken2022TlvEntry {
|
||||
extension_type: 1,
|
||||
extension_name: "transfer_fee_config",
|
||||
value_hex: "01".to_string(),
|
||||
value_fields: serde_json::json!({"withheldAmount":"7"}),
|
||||
}],
|
||||
};
|
||||
let first = crate::materialize_token2022_fee_state_snapshots("mint111", 42, &mint);
|
||||
let second = crate::materialize_token2022_fee_state_snapshots("mint111", 42, &mint);
|
||||
assert!(first.is_ok());
|
||||
assert_eq!(first, second);
|
||||
let outputs = match first {
|
||||
std::result::Result::Ok(outputs) => outputs,
|
||||
std::result::Result::Err(error) => panic!("unexpected error: {error}"),
|
||||
};
|
||||
assert_eq!(outputs.len(), 1);
|
||||
assert_eq!(outputs[0].payload_json["stateKind"], "mint");
|
||||
assert_eq!(outputs[0].payload_json["valueFields"]["withheldAmount"], "7");
|
||||
assert_eq!(outputs[0].payload_json["confidentialValuesDecrypted"], false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn confidential_fee_amount_preserves_ciphertext_without_decryption_claim() {
|
||||
let account = crate::DcToken2022State {
|
||||
kind: crate::DcToken2022StateKind::Account,
|
||||
base_fields: serde_json::json!({}),
|
||||
base_hex: "00".to_string(),
|
||||
account_type: std::option::Option::Some(2),
|
||||
extensions: std::vec![crate::DcToken2022TlvEntry {
|
||||
extension_type: 17,
|
||||
extension_name: "confidential_transfer_fee_amount",
|
||||
value_hex: "02".to_string(),
|
||||
value_fields: serde_json::json!({"withheldAmount":"ciphertext"}),
|
||||
}],
|
||||
};
|
||||
let result = crate::materialize_token2022_fee_state_snapshots("account111", 43, &account);
|
||||
assert!(result.is_ok());
|
||||
let outputs = match result {
|
||||
std::result::Result::Ok(outputs) => outputs,
|
||||
std::result::Result::Err(error) => panic!("unexpected error: {error}"),
|
||||
};
|
||||
assert_eq!(outputs[0].payload_json["containsConfidentialState"], true);
|
||||
assert_eq!(outputs[0].payload_json["confidentialValuesDecrypted"], false);
|
||||
assert_eq!(outputs[0].payload_json["valueFields"]["withheldAmount"], "ciphertext");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fee_state_snapshot_rejects_wrong_state_kind_and_empty_identity() {
|
||||
let wrong = crate::DcToken2022State {
|
||||
kind: crate::DcToken2022StateKind::Account,
|
||||
base_fields: serde_json::json!({}),
|
||||
base_hex: "00".to_string(),
|
||||
account_type: std::option::Option::Some(2),
|
||||
extensions: std::vec![crate::DcToken2022TlvEntry {
|
||||
extension_type: 1,
|
||||
extension_name: "transfer_fee_config",
|
||||
value_hex: "01".to_string(),
|
||||
value_fields: serde_json::json!({}),
|
||||
}],
|
||||
};
|
||||
assert!(
|
||||
crate::materialize_token2022_fee_state_snapshots("account111", 44, &wrong,).is_err()
|
||||
);
|
||||
assert!(crate::materialize_token2022_fee_state_snapshots("", 44, &wrong,).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// file: kb-lib/src/materializer/governance.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! `governance` materializer family.
|
||||
|
||||
pub mod core;
|
||||
mod core;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// file: kb-lib/src/materializer/governance/core.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! Migration boundary for legacy crate `kb_materializer_governance`.
|
||||
|
||||
/// Legacy crate name retained for migration and compatibility tracking.
|
||||
pub const LEGACY_CRATE: &str = "kb_materializer_governance";
|
||||
const LEGACY_CRATE: &str = "kb_materializer_governance";
|
||||
|
||||
/// Current porting status.
|
||||
pub const MIGRATION_STATUS: &str = "source-preserved-pending-port";
|
||||
const MIGRATION_STATUS: &str = "source-preserved-pending-port";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// file: kb-lib/src/materializer/lending.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! `lending` materializer family.
|
||||
|
||||
pub mod core;
|
||||
mod core;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// file: kb-lib/src/materializer/lending/core.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! Migration boundary for legacy crate `kb_materializer_lending`.
|
||||
|
||||
/// Legacy crate name retained for migration and compatibility tracking.
|
||||
pub const LEGACY_CRATE: &str = "kb_materializer_lending";
|
||||
const LEGACY_CRATE: &str = "kb_materializer_lending";
|
||||
|
||||
/// Current porting status.
|
||||
pub const MIGRATION_STATUS: &str = "source-preserved-pending-port";
|
||||
const MIGRATION_STATUS: &str = "source-preserved-pending-port";
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
// file: kb-lib/src/materializer/lifecycle.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
//! `lifecycle` materializer family.
|
||||
|
||||
mod constants;
|
||||
pub mod core;
|
||||
mod core;
|
||||
|
||||
pub(crate) use crate::materializer::lifecycle::constants::TRACING_TARGET as MATERIALIZER_LIFECYCLE_TRACING_TARGET;
|
||||
/// Canonical tracing target for the native lifecycle materializer.
|
||||
pub(crate) use self::constants::MT_LIFECYCLE_TRACING_TARGET;
|
||||
|
||||
/// Stable native lifecycle materializer.
|
||||
pub use crate::materializer::lifecycle::core::LifecycleMaterializer;
|
||||
pub use self::core::MtLifecycleMaterializer;
|
||||
|
||||
@@ -4,4 +4,4 @@
|
||||
//! Local constants for the native lifecycle materializer.
|
||||
|
||||
/// Canonical tracing target for this component.
|
||||
pub(crate) const TRACING_TARGET: &str = "kb-lib.materializer.lifecycle";
|
||||
pub(crate) const MT_LIFECYCLE_TRACING_TARGET: &str = "kb-lib.materializer.lifecycle";
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
|
||||
//! Stable lifecycle projections derived from exact decoded observations.
|
||||
|
||||
const ACCEPTED_FAMILIES: &[crate::EventFamily] = &[
|
||||
crate::EventFamily::Lifecycle,
|
||||
crate::EventFamily::Admin,
|
||||
crate::EventFamily::Audit,
|
||||
const ACCEPTED_FAMILIES: &[crate::MdEventFamily] = &[
|
||||
crate::MdEventFamily::Lifecycle,
|
||||
crate::MdEventFamily::Admin,
|
||||
crate::MdEventFamily::Audit,
|
||||
];
|
||||
const ADDRESS_LOOKUP_TABLE_SURFACE: &str = "solana_native_address_lookup_table";
|
||||
const ADDRESS_LOOKUP_TABLE_ENTRIES: &[&str] = &[
|
||||
@@ -121,9 +121,9 @@ impl ProjectionKind {
|
||||
|
||||
/// Stable lifecycle materializer for supported native lifecycle projections.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct LifecycleMaterializer;
|
||||
pub struct MtLifecycleMaterializer;
|
||||
|
||||
impl crate::Materializer for crate::LifecycleMaterializer {
|
||||
impl crate::MtMaterializer for crate::MtLifecycleMaterializer {
|
||||
fn materializer_name(&self) -> &'static str {
|
||||
return "kb_materializer_lifecycle";
|
||||
}
|
||||
@@ -132,7 +132,7 @@ impl crate::Materializer for crate::LifecycleMaterializer {
|
||||
return env!("CARGO_PKG_VERSION");
|
||||
}
|
||||
|
||||
fn accepts_event(&self, event: &crate::DecodedProtocolEvent) -> bool {
|
||||
fn accepts_event(&self, event: &crate::MdDecodedProtocolEvent) -> bool {
|
||||
return family_is_accepted(event.event_family)
|
||||
&& static_projection(event.surface_code.0.as_str(), event.event_name.0.as_str())
|
||||
.is_some();
|
||||
@@ -140,60 +140,60 @@ impl crate::Materializer for crate::LifecycleMaterializer {
|
||||
|
||||
fn materialize_event(
|
||||
&self,
|
||||
event: &crate::DecodedProtocolEvent,
|
||||
) -> kb_core::Result<std::vec::Vec<crate::MaterializedEvent>> {
|
||||
if !crate::Materializer::accepts_event(self, event) {
|
||||
event: &crate::MdDecodedProtocolEvent,
|
||||
) -> kb_core::Result<std::vec::Vec<crate::MdMaterializedEvent>> {
|
||||
if !crate::MtMaterializer::accepts_event(self, event) {
|
||||
return std::result::Result::Ok(std::vec::Vec::new());
|
||||
}
|
||||
return std::result::Result::Ok(std::vec::Vec::new());
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::EventMaterializer for crate::LifecycleMaterializer {
|
||||
fn identity(&self) -> crate::MaterializerIdentity {
|
||||
return crate::MaterializerIdentity {
|
||||
impl crate::MtApiEventMaterializer for crate::MtLifecycleMaterializer {
|
||||
fn identity(&self) -> crate::MtApiMaterializerIdentity {
|
||||
return crate::MtApiMaterializerIdentity {
|
||||
name: "solana_native_lifecycle".to_string(),
|
||||
version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
fn accepted_families(&self) -> &'static [crate::EventFamily] {
|
||||
fn accepted_families(&self) -> &'static [crate::MdEventFamily] {
|
||||
return ACCEPTED_FAMILIES;
|
||||
}
|
||||
|
||||
fn accepts_observation(&self, observation: &crate::DecodedObservation) -> bool {
|
||||
fn accepts_observation(&self, observation: &crate::DcApiDecodedObservation) -> bool {
|
||||
return family_is_accepted(observation.event.event_family)
|
||||
&& observation_projection(observation).is_some();
|
||||
}
|
||||
|
||||
fn transaction_policy(
|
||||
&self,
|
||||
_family: crate::EventFamily,
|
||||
) -> crate::MaterializationTransactionPolicy {
|
||||
return crate::MaterializationTransactionPolicy::SuccessfulCommittedOnly;
|
||||
_family: crate::MdEventFamily,
|
||||
) -> crate::MtApiMaterializationTransactionPolicy {
|
||||
return crate::MtApiMaterializationTransactionPolicy::SuccessfulCommittedOnly;
|
||||
}
|
||||
|
||||
fn materialize(
|
||||
&self,
|
||||
observation: &crate::DecodedObservation,
|
||||
) -> crate::MaterializerExecutionResult {
|
||||
observation: &crate::DcApiDecodedObservation,
|
||||
) -> crate::MtApiMaterializerExecutionResult {
|
||||
let projection = match observation_projection(observation) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
tracing::debug!(
|
||||
target: crate::MATERIALIZER_LIFECYCLE_TRACING_TARGET,
|
||||
target: crate::MT_LIFECYCLE_TRACING_TARGET,
|
||||
action = "materialize_lifecycle",
|
||||
surface_code = %observation.event.surface_code.0.as_str(),
|
||||
entry_code = %observation.event.event_name.0.as_str(),
|
||||
accepted = false,
|
||||
"ignore observation outside supported native lifecycle projections"
|
||||
);
|
||||
return crate::MaterializerExecutionResult::ignored();
|
||||
return crate::MtApiMaterializerExecutionResult::ignored();
|
||||
},
|
||||
};
|
||||
if observation.transaction_failed || !observation.observation_committed {
|
||||
tracing::debug!(
|
||||
target: crate::MATERIALIZER_LIFECYCLE_TRACING_TARGET,
|
||||
target: crate::MT_LIFECYCLE_TRACING_TARGET,
|
||||
action = "materialize_lifecycle",
|
||||
surface_code = %observation.event.surface_code.0.as_str(),
|
||||
entry_code = %observation.event.event_name.0.as_str(),
|
||||
@@ -202,7 +202,7 @@ impl crate::EventMaterializer for crate::LifecycleMaterializer {
|
||||
transaction_failed = observation.transaction_failed,
|
||||
"refuse uncommitted native lifecycle observation"
|
||||
);
|
||||
return crate::MaterializerExecutionResult::refused(
|
||||
return crate::MtApiMaterializerExecutionResult::refused(
|
||||
"failed_transaction_lifecycle_refused",
|
||||
"failed or uncommitted native lifecycle observations cannot create lifecycle outputs",
|
||||
);
|
||||
@@ -225,7 +225,7 @@ impl crate::EventMaterializer for crate::LifecycleMaterializer {
|
||||
let projection_details =
|
||||
projection_details(projection, &accounts, ¶meters, operation.as_str());
|
||||
tracing::debug!(
|
||||
target: crate::MATERIALIZER_LIFECYCLE_TRACING_TARGET,
|
||||
target: crate::MT_LIFECYCLE_TRACING_TARGET,
|
||||
action = "materialize_lifecycle",
|
||||
surface_code = %observation.event.surface_code.0.as_str(),
|
||||
entry_code = %observation.event.event_name.0.as_str(),
|
||||
@@ -235,9 +235,9 @@ impl crate::EventMaterializer for crate::LifecycleMaterializer {
|
||||
output_count = 1_usize,
|
||||
"materialize committed native lifecycle observation"
|
||||
);
|
||||
let output = crate::MaterializedOutput {
|
||||
let output = crate::MtApiMaterializedOutput {
|
||||
output_key,
|
||||
family: crate::MaterializedEventFamily::Lifecycle,
|
||||
family: crate::MdMaterializedEventFamily::Lifecycle,
|
||||
payload_json: serde_json::json!({
|
||||
"projectionVersion": 1,
|
||||
"projectionSemantics": "committed_instruction_lifecycle_event",
|
||||
@@ -254,20 +254,20 @@ impl crate::EventMaterializer for crate::LifecycleMaterializer {
|
||||
"projection": projection_details,
|
||||
}),
|
||||
};
|
||||
return crate::MaterializerExecutionResult {
|
||||
status: crate::MaterializerOutcomeStatus::Inserted,
|
||||
return crate::MtApiMaterializerExecutionResult {
|
||||
status: crate::MtApiMaterializerOutcomeStatus::Inserted,
|
||||
outputs: std::vec![output],
|
||||
diagnostics: std::vec::Vec::new(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn family_is_accepted(family: crate::EventFamily) -> bool {
|
||||
fn family_is_accepted(family: crate::MdEventFamily) -> bool {
|
||||
return ACCEPTED_FAMILIES.iter().any(|accepted| return *accepted == family);
|
||||
}
|
||||
|
||||
fn observation_projection(
|
||||
observation: &crate::DecodedObservation,
|
||||
observation: &crate::DcApiDecodedObservation,
|
||||
) -> std::option::Option<ProjectionKind> {
|
||||
let surface_code = observation.event.surface_code.0.as_str();
|
||||
let entry_code = observation.event.event_name.0.as_str();
|
||||
@@ -479,25 +479,25 @@ mod tests {
|
||||
fn observation_with_payload(
|
||||
surface_code: &str,
|
||||
entry_code: &str,
|
||||
event_family: crate::EventFamily,
|
||||
event_family: crate::MdEventFamily,
|
||||
transaction_failed: bool,
|
||||
accounts: serde_json::Value,
|
||||
parameters: serde_json::Value,
|
||||
) -> crate::DecodedObservation {
|
||||
return crate::DecodedObservation {
|
||||
) -> crate::DcApiDecodedObservation {
|
||||
return crate::DcApiDecodedObservation {
|
||||
event_key: format!("{entry_code}:0"),
|
||||
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-id".to_string()),
|
||||
protocol_code: crate::ProtocolCode("solana_native".to_string()),
|
||||
surface_code: crate::SurfaceCode(surface_code.to_string()),
|
||||
event_code: crate::EventCode(format!("{surface_code}.{entry_code}")),
|
||||
event_name: crate::EventName(entry_code.to_string()),
|
||||
event: crate::MdDecodedProtocolEvent {
|
||||
signature: crate::MdSignature("signature".to_string()),
|
||||
slot: crate::MdSlot(42),
|
||||
instruction_path: crate::MdInstructionPath("0".to_string()),
|
||||
program_id: crate::MdProgramId("program-id".to_string()),
|
||||
protocol_code: crate::MdProtocolCode("solana_native".to_string()),
|
||||
surface_code: crate::MdSurfaceCode(surface_code.to_string()),
|
||||
event_code: crate::MdEventCode(format!("{surface_code}.{entry_code}")),
|
||||
event_name: crate::MdEventName(entry_code.to_string()),
|
||||
event_family,
|
||||
source_kind: crate::EventSourceKind::Instruction,
|
||||
confidence: crate::DecoderConfidence::ManualExact,
|
||||
source_kind: crate::MdEventSourceKind::Instruction,
|
||||
confidence: crate::MdDecoderConfidence::ManualExact,
|
||||
},
|
||||
payload_json: serde_json::json!({
|
||||
"accounts": accounts,
|
||||
@@ -510,9 +510,9 @@ mod tests {
|
||||
std::option::Option::None
|
||||
},
|
||||
observation_committed: !transaction_failed,
|
||||
proof: crate::DecoderProof {
|
||||
kind: crate::DecoderProofKind::Manual,
|
||||
confidence: crate::DecoderConfidence::ManualExact,
|
||||
proof: crate::DcApiDecoderProof {
|
||||
kind: crate::DcApiDecoderProofKind::Manual,
|
||||
confidence: crate::MdDecoderConfidence::ManualExact,
|
||||
evidence: std::vec!["fixture".to_string()],
|
||||
},
|
||||
};
|
||||
@@ -522,11 +522,11 @@ mod tests {
|
||||
surface_code: &str,
|
||||
entry_code: &str,
|
||||
transaction_failed: bool,
|
||||
) -> crate::DecodedObservation {
|
||||
) -> crate::DcApiDecodedObservation {
|
||||
return observation_with_payload(
|
||||
surface_code,
|
||||
entry_code,
|
||||
crate::EventFamily::Lifecycle,
|
||||
crate::MdEventFamily::Lifecycle,
|
||||
transaction_failed,
|
||||
serde_json::json!([{"role": "target", "accountKey": "account"}]),
|
||||
serde_json::json!({"value": 41}),
|
||||
@@ -535,9 +535,9 @@ mod tests {
|
||||
|
||||
fn nonce_observation(
|
||||
entry_code: &str,
|
||||
family: crate::EventFamily,
|
||||
family: crate::MdEventFamily,
|
||||
transaction_failed: bool,
|
||||
) -> crate::DecodedObservation {
|
||||
) -> crate::DcApiDecodedObservation {
|
||||
return observation_with_payload(
|
||||
super::SYSTEM_SURFACE,
|
||||
entry_code,
|
||||
@@ -558,10 +558,10 @@ mod tests {
|
||||
fn slashing_observation(
|
||||
entry_code: &str,
|
||||
transaction_failed: bool,
|
||||
) -> crate::DecodedObservation {
|
||||
) -> crate::DcApiDecodedObservation {
|
||||
let (family, accounts) = if entry_code == "duplicate_block_proof" {
|
||||
(
|
||||
crate::EventFamily::Audit,
|
||||
crate::MdEventFamily::Audit,
|
||||
serde_json::json!([
|
||||
{"role": "proof_account", "accountKey": "proof111"},
|
||||
{"role": "violation_report", "accountKey": "report111"},
|
||||
@@ -571,7 +571,7 @@ mod tests {
|
||||
)
|
||||
} else {
|
||||
(
|
||||
crate::EventFamily::Lifecycle,
|
||||
crate::MdEventFamily::Lifecycle,
|
||||
serde_json::json!([
|
||||
{"role": "violation_report", "accountKey": "report111"},
|
||||
{"role": "destination", "accountKey": "destination111"}
|
||||
@@ -599,7 +599,7 @@ mod tests {
|
||||
entry_code: &str,
|
||||
context_state_requested: bool,
|
||||
transaction_failed: bool,
|
||||
) -> crate::DecodedObservation {
|
||||
) -> crate::DcApiDecodedObservation {
|
||||
let accounts = if entry_code == "close_context_state" {
|
||||
serde_json::json!([
|
||||
{"role": "proof_context_state", "accountKey": "context111"},
|
||||
@@ -615,9 +615,9 @@ mod tests {
|
||||
serde_json::json!([])
|
||||
};
|
||||
let family = if entry_code == "close_context_state" {
|
||||
crate::EventFamily::Lifecycle
|
||||
crate::MdEventFamily::Lifecycle
|
||||
} else {
|
||||
crate::EventFamily::Audit
|
||||
crate::MdEventFamily::Audit
|
||||
};
|
||||
return observation_with_payload(
|
||||
super::ZK_ELGAMAL_PROOF_SURFACE,
|
||||
@@ -634,12 +634,12 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn successful_alt_lifecycle_creates_one_stable_output() {
|
||||
let materializer = crate::LifecycleMaterializer;
|
||||
let result = crate::EventMaterializer::materialize(
|
||||
let materializer = crate::MtLifecycleMaterializer;
|
||||
let result = crate::MtApiEventMaterializer::materialize(
|
||||
&materializer,
|
||||
&observation(super::ADDRESS_LOOKUP_TABLE_SURFACE, "create_lookup_table", false),
|
||||
);
|
||||
assert_eq!(result.status, crate::MaterializerOutcomeStatus::Inserted);
|
||||
assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Inserted);
|
||||
assert_eq!(result.outputs.len(), 1);
|
||||
assert_eq!(result.outputs[0].output_key, "address_lookup_table:create_lookup_table:0");
|
||||
assert_eq!(result.outputs[0].payload_json["domain"], "address_lookup_table");
|
||||
@@ -648,12 +648,12 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn successful_loader_lifecycle_creates_program_projection() {
|
||||
let materializer = crate::LifecycleMaterializer;
|
||||
let result = crate::EventMaterializer::materialize(
|
||||
let materializer = crate::MtLifecycleMaterializer;
|
||||
let result = crate::MtApiEventMaterializer::materialize(
|
||||
&materializer,
|
||||
&observation(super::BPF_LOADER_UPGRADEABLE_SURFACE, "deploy_with_max_data_len", false),
|
||||
);
|
||||
assert_eq!(result.status, crate::MaterializerOutcomeStatus::Inserted);
|
||||
assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Inserted);
|
||||
assert_eq!(
|
||||
result.outputs[0].output_key,
|
||||
"program_loader:solana_native_bpf_loader_upgradeable:deploy_with_max_data_len:0"
|
||||
@@ -667,19 +667,19 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn successful_feature_revoke_creates_feature_gate_projection() {
|
||||
let materializer = crate::LifecycleMaterializer;
|
||||
let result = crate::EventMaterializer::materialize(
|
||||
let materializer = crate::MtLifecycleMaterializer;
|
||||
let result = crate::MtApiEventMaterializer::materialize(
|
||||
&materializer,
|
||||
&observation(super::FEATURE_SURFACE, "revoke_pending_activation", false),
|
||||
);
|
||||
assert_eq!(result.status, crate::MaterializerOutcomeStatus::Inserted);
|
||||
assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Inserted);
|
||||
assert_eq!(result.outputs[0].output_key, "feature_gate:revoke_pending_activation:0");
|
||||
assert_eq!(result.outputs[0].payload_json["domain"], "feature_gate");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_committed_system_account_operation_creates_a_lifecycle_projection() {
|
||||
let materializer = crate::LifecycleMaterializer;
|
||||
let materializer = crate::MtLifecycleMaterializer;
|
||||
for (operation, role, transition) in [
|
||||
("create_account", "new_account", "created_allocated_funded_and_assigned"),
|
||||
(
|
||||
@@ -698,7 +698,7 @@ mod tests {
|
||||
let observation = observation_with_payload(
|
||||
super::SYSTEM_SURFACE,
|
||||
operation,
|
||||
crate::EventFamily::Lifecycle,
|
||||
crate::MdEventFamily::Lifecycle,
|
||||
false,
|
||||
serde_json::json!([
|
||||
{"role": role, "accountKey": "target111"},
|
||||
@@ -712,8 +712,8 @@ mod tests {
|
||||
"seed": "seed",
|
||||
}),
|
||||
);
|
||||
let result = crate::EventMaterializer::materialize(&materializer, &observation);
|
||||
assert_eq!(result.status, crate::MaterializerOutcomeStatus::Inserted);
|
||||
let result = crate::MtApiEventMaterializer::materialize(&materializer, &observation);
|
||||
assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Inserted);
|
||||
assert_eq!(result.outputs[0].output_key, format!("system_account:{operation}:0"));
|
||||
assert_eq!(result.outputs[0].payload_json["projection"]["targetAccount"], "target111");
|
||||
assert_eq!(result.outputs[0].payload_json["projection"]["stateTransition"], transition);
|
||||
@@ -722,11 +722,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn system_account_creation_does_not_duplicate_balance_changes() {
|
||||
let materializer = crate::LifecycleMaterializer;
|
||||
let materializer = crate::MtLifecycleMaterializer;
|
||||
let observation = observation_with_payload(
|
||||
super::SYSTEM_SURFACE,
|
||||
"create_account",
|
||||
crate::EventFamily::Lifecycle,
|
||||
crate::MdEventFamily::Lifecycle,
|
||||
false,
|
||||
serde_json::json!([
|
||||
{"role": "funding_account", "accountKey": "funding111"},
|
||||
@@ -734,7 +734,7 @@ mod tests {
|
||||
]),
|
||||
serde_json::json!({"lamports": 55, "space": 64, "owner": "owner111"}),
|
||||
);
|
||||
let result = crate::EventMaterializer::materialize(&materializer, &observation);
|
||||
let result = crate::MtApiEventMaterializer::materialize(&materializer, &observation);
|
||||
assert_eq!(result.outputs[0].payload_json["projection"]["requestedLamports"], 55);
|
||||
assert_eq!(
|
||||
result.outputs[0].payload_json["projection"]["lamportTransferRepresentedByCoreBalanceChanges"],
|
||||
@@ -745,26 +745,29 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn every_committed_system_nonce_operation_creates_a_lifecycle_projection() {
|
||||
let materializer = crate::LifecycleMaterializer;
|
||||
let materializer = crate::MtLifecycleMaterializer;
|
||||
for (entry_code, family, transition) in [
|
||||
("initialize_nonce_account", crate::EventFamily::Lifecycle, "initialized"),
|
||||
("advance_nonce_account", crate::EventFamily::Lifecycle, "nonce_advanced"),
|
||||
("authorize_nonce_account", crate::EventFamily::Admin, "authority_changed"),
|
||||
("initialize_nonce_account", crate::MdEventFamily::Lifecycle, "initialized"),
|
||||
("advance_nonce_account", crate::MdEventFamily::Lifecycle, "nonce_advanced"),
|
||||
("authorize_nonce_account", crate::MdEventFamily::Admin, "authority_changed"),
|
||||
(
|
||||
"upgrade_nonce_account",
|
||||
crate::EventFamily::Lifecycle,
|
||||
crate::MdEventFamily::Lifecycle,
|
||||
"legacy_version_upgraded",
|
||||
),
|
||||
(
|
||||
"withdraw_nonce_account",
|
||||
crate::EventFamily::Lifecycle,
|
||||
crate::MdEventFamily::Lifecycle,
|
||||
"lamports_withdrawn_possible_closure",
|
||||
),
|
||||
] {
|
||||
let observation = nonce_observation(entry_code, family, false);
|
||||
assert!(crate::EventMaterializer::accepts_observation(&materializer, &observation,));
|
||||
let result = crate::EventMaterializer::materialize(&materializer, &observation);
|
||||
assert_eq!(result.status, crate::MaterializerOutcomeStatus::Inserted);
|
||||
assert!(crate::MtApiEventMaterializer::accepts_observation(
|
||||
&materializer,
|
||||
&observation,
|
||||
));
|
||||
let result = crate::MtApiEventMaterializer::materialize(&materializer, &observation);
|
||||
assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Inserted);
|
||||
assert_eq!(result.outputs[0].payload_json["domain"], "durable_nonce_account");
|
||||
assert_eq!(result.outputs[0].payload_json["projection"]["targetAccount"], "nonce111");
|
||||
assert_eq!(
|
||||
@@ -777,10 +780,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn nonce_withdrawal_preserves_conditional_closure_semantics() {
|
||||
let materializer = crate::LifecycleMaterializer;
|
||||
let materializer = crate::MtLifecycleMaterializer;
|
||||
let observation =
|
||||
nonce_observation("withdraw_nonce_account", crate::EventFamily::Lifecycle, false);
|
||||
let result = crate::EventMaterializer::materialize(&materializer, &observation);
|
||||
nonce_observation("withdraw_nonce_account", crate::MdEventFamily::Lifecycle, false);
|
||||
let result = crate::MtApiEventMaterializer::materialize(&materializer, &observation);
|
||||
assert_eq!(result.outputs[0].payload_json["projection"]["withdrawLamports"], 55);
|
||||
assert_eq!(
|
||||
result.outputs[0].payload_json["projection"]["lamportDestination"],
|
||||
@@ -799,13 +802,16 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn zk_context_is_materialized_only_when_requested() {
|
||||
let materializer = crate::LifecycleMaterializer;
|
||||
let materializer = crate::MtLifecycleMaterializer;
|
||||
let without_context = zk_observation("verify_zero_ciphertext", false, false);
|
||||
assert!(!crate::EventMaterializer::accepts_observation(&materializer, &without_context,));
|
||||
assert!(!crate::MtApiEventMaterializer::accepts_observation(
|
||||
&materializer,
|
||||
&without_context,
|
||||
));
|
||||
let with_context = zk_observation("verify_zero_ciphertext", true, false);
|
||||
assert!(crate::EventMaterializer::accepts_observation(&materializer, &with_context,));
|
||||
let result = crate::EventMaterializer::materialize(&materializer, &with_context);
|
||||
assert_eq!(result.status, crate::MaterializerOutcomeStatus::Inserted);
|
||||
assert!(crate::MtApiEventMaterializer::accepts_observation(&materializer, &with_context,));
|
||||
let result = crate::MtApiEventMaterializer::materialize(&materializer, &with_context);
|
||||
assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Inserted);
|
||||
assert_eq!(result.outputs[0].payload_json["domain"], "zk_proof_context");
|
||||
assert_eq!(result.outputs[0].payload_json["projection"]["targetAccount"], "context111");
|
||||
assert_eq!(
|
||||
@@ -820,10 +826,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn zk_context_close_creates_a_stable_lifecycle_projection() {
|
||||
let materializer = crate::LifecycleMaterializer;
|
||||
let materializer = crate::MtLifecycleMaterializer;
|
||||
let observation = zk_observation("close_context_state", false, false);
|
||||
let result = crate::EventMaterializer::materialize(&materializer, &observation);
|
||||
assert_eq!(result.status, crate::MaterializerOutcomeStatus::Inserted);
|
||||
let result = crate::MtApiEventMaterializer::materialize(&materializer, &observation);
|
||||
assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Inserted);
|
||||
assert_eq!(result.outputs[0].output_key, "zk_proof_context:close_context_state:0");
|
||||
assert_eq!(
|
||||
result.outputs[0].payload_json["projection"]["lamportDestination"],
|
||||
@@ -837,11 +843,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn successful_slashing_proof_initializes_violation_report_projection() {
|
||||
let materializer = crate::LifecycleMaterializer;
|
||||
let materializer = crate::MtLifecycleMaterializer;
|
||||
let observation = slashing_observation("duplicate_block_proof", false);
|
||||
assert!(crate::EventMaterializer::accepts_observation(&materializer, &observation,));
|
||||
let result = crate::EventMaterializer::materialize(&materializer, &observation);
|
||||
assert_eq!(result.status, crate::MaterializerOutcomeStatus::Inserted);
|
||||
assert!(crate::MtApiEventMaterializer::accepts_observation(&materializer, &observation,));
|
||||
let result = crate::MtApiEventMaterializer::materialize(&materializer, &observation);
|
||||
assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Inserted);
|
||||
assert_eq!(
|
||||
result.outputs[0].output_key,
|
||||
"slashing_violation_report:duplicate_block_proof:0"
|
||||
@@ -854,10 +860,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn successful_slashing_close_creates_stable_closure_projection() {
|
||||
let materializer = crate::LifecycleMaterializer;
|
||||
let materializer = crate::MtLifecycleMaterializer;
|
||||
let observation = slashing_observation("close_violation_report", false);
|
||||
let result = crate::EventMaterializer::materialize(&materializer, &observation);
|
||||
assert_eq!(result.status, crate::MaterializerOutcomeStatus::Inserted);
|
||||
let result = crate::MtApiEventMaterializer::materialize(&materializer, &observation);
|
||||
assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Inserted);
|
||||
assert_eq!(
|
||||
result.outputs[0].payload_json["projection"]["stateTransition"],
|
||||
"closed_after_minimum_retention_lamports_reclaimed_owner_reset_to_system_program"
|
||||
@@ -871,78 +877,86 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn historical_zk_token_proof_is_not_materialized() {
|
||||
let materializer = crate::LifecycleMaterializer;
|
||||
let materializer = crate::MtLifecycleMaterializer;
|
||||
let observation = observation_with_payload(
|
||||
"solana_native_zk_token_proof",
|
||||
"close_context_state",
|
||||
crate::EventFamily::Audit,
|
||||
crate::MdEventFamily::Audit,
|
||||
false,
|
||||
serde_json::json!([]),
|
||||
serde_json::json!({}),
|
||||
);
|
||||
assert!(!crate::EventMaterializer::accepts_observation(&materializer, &observation,));
|
||||
let result = crate::EventMaterializer::materialize(&materializer, &observation);
|
||||
assert_eq!(result.status, crate::MaterializerOutcomeStatus::Ignored);
|
||||
assert!(!crate::MtApiEventMaterializer::accepts_observation(&materializer, &observation,));
|
||||
let result = crate::MtApiEventMaterializer::materialize(&materializer, &observation);
|
||||
assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Ignored);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn loader_write_and_admin_events_are_not_lifecycle_projections() {
|
||||
let materializer = crate::LifecycleMaterializer;
|
||||
let materializer = crate::MtLifecycleMaterializer;
|
||||
for entry_code in ["write", "set_authority", "transfer_authority"] {
|
||||
let observation = observation(super::BPF_LOADER_UPGRADEABLE_SURFACE, entry_code, false);
|
||||
assert!(!crate::EventMaterializer::accepts_observation(&materializer, &observation,));
|
||||
assert!(!crate::MtApiEventMaterializer::accepts_observation(
|
||||
&materializer,
|
||||
&observation,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unrelated_lifecycle_is_ignored() {
|
||||
let materializer = crate::LifecycleMaterializer;
|
||||
let result = crate::EventMaterializer::materialize(
|
||||
let materializer = crate::MtLifecycleMaterializer;
|
||||
let result = crate::MtApiEventMaterializer::materialize(
|
||||
&materializer,
|
||||
&observation(super::SYSTEM_SURFACE, "transfer", false),
|
||||
);
|
||||
assert_eq!(result.status, crate::MaterializerOutcomeStatus::Ignored);
|
||||
assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Ignored);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ata_lifecycle_is_owned_by_token_accounts_materializer_only() {
|
||||
let mut observation = observation("spl_associated_token_account", "create", false);
|
||||
observation.event.program_id =
|
||||
crate::ProgramId(kb_program_ids::ASSOCIATED_TOKEN_PROGRAM_ID.to_string());
|
||||
assert!(!crate::EventMaterializer::accepts_observation(
|
||||
&crate::LifecycleMaterializer,
|
||||
crate::MdProgramId(kb_program_ids::ASSOCIATED_TOKEN_PROGRAM_ID.to_string());
|
||||
assert!(!crate::MtApiEventMaterializer::accepts_observation(
|
||||
&crate::MtLifecycleMaterializer,
|
||||
&observation,
|
||||
));
|
||||
let result =
|
||||
crate::EventMaterializer::materialize(&crate::LifecycleMaterializer, &observation);
|
||||
assert_eq!(result.status, crate::MaterializerOutcomeStatus::Ignored);
|
||||
let result = crate::MtApiEventMaterializer::materialize(
|
||||
&crate::MtLifecycleMaterializer,
|
||||
&observation,
|
||||
);
|
||||
assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Ignored);
|
||||
assert!(result.outputs.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_native_lifecycle_is_refused() {
|
||||
let materializer = crate::LifecycleMaterializer;
|
||||
let materializer = crate::MtLifecycleMaterializer;
|
||||
let observations = [
|
||||
observation(super::ADDRESS_LOOKUP_TABLE_SURFACE, "close_lookup_table", true),
|
||||
observation(super::LOADER_V4_SURFACE, "deploy", true),
|
||||
nonce_observation("authorize_nonce_account", crate::EventFamily::Admin, true),
|
||||
nonce_observation("authorize_nonce_account", crate::MdEventFamily::Admin, true),
|
||||
zk_observation("verify_zero_ciphertext", true, true),
|
||||
slashing_observation("duplicate_block_proof", true),
|
||||
];
|
||||
for observation in observations {
|
||||
let policy = crate::validate_materialization_policy(&materializer, &observation);
|
||||
let policy = crate::materializer_api_validate_materialization_policy(
|
||||
&materializer,
|
||||
&observation,
|
||||
);
|
||||
assert!(policy.is_err());
|
||||
let direct = crate::EventMaterializer::materialize(&materializer, &observation);
|
||||
assert_eq!(direct.status, crate::MaterializerOutcomeStatus::Refused);
|
||||
let direct = crate::MtApiEventMaterializer::materialize(&materializer, &observation);
|
||||
assert_eq!(direct.status, crate::MtApiMaterializerOutcomeStatus::Refused);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn output_serialization_is_deterministic() {
|
||||
let materializer = crate::LifecycleMaterializer;
|
||||
let materializer = crate::MtLifecycleMaterializer;
|
||||
let observation = zk_observation("verify_batched_range_proof_u64", true, false);
|
||||
let first = crate::EventMaterializer::materialize(&materializer, &observation);
|
||||
let second = crate::EventMaterializer::materialize(&materializer, &observation);
|
||||
let first = crate::MtApiEventMaterializer::materialize(&materializer, &observation);
|
||||
let second = crate::MtApiEventMaterializer::materialize(&materializer, &observation);
|
||||
let first_json = match serde_json::to_string(&first) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// file: kb-lib/src/materializer/liquidity.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! `liquidity` materializer family.
|
||||
|
||||
pub mod core;
|
||||
mod core;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// file: kb-lib/src/materializer/liquidity/core.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! Migration boundary for legacy crate `kb_materializer_liquidity`.
|
||||
|
||||
/// Legacy crate name retained for migration and compatibility tracking.
|
||||
pub const LEGACY_CRATE: &str = "kb_materializer_liquidity";
|
||||
const LEGACY_CRATE: &str = "kb_materializer_liquidity";
|
||||
|
||||
/// Current porting status.
|
||||
pub const MIGRATION_STATUS: &str = "source-preserved-pending-port";
|
||||
const MIGRATION_STATUS: &str = "source-preserved-pending-port";
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
// file: kb-lib/src/materializer/metadata.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! `metadata` materializer family.
|
||||
|
||||
pub mod core;
|
||||
mod core;
|
||||
|
||||
/// Token-2022 and Metaplex metadata snapshot materializer.
|
||||
pub use self::core::MtMetadataMaterializer;
|
||||
/// Materializes one canonical non-Metadata Metaplex account snapshot.
|
||||
pub use self::core::materialize_metaplex_account_snapshot;
|
||||
/// Materializes one canonical Metaplex Metadata account snapshot.
|
||||
pub use self::core::materialize_metaplex_metadata_snapshot;
|
||||
/// Materializes Token-2022 embedded metadata and group snapshots.
|
||||
pub use self::core::materialize_token2022_metadata_snapshot;
|
||||
|
||||
@@ -1,10 +1,565 @@
|
||||
// file: kb-lib/src/materializer/metadata/core.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! Migration boundary for legacy crate `kb_materializer_metadata`.
|
||||
//! Stable Token-2022 and Metaplex metadata snapshot materializer.
|
||||
|
||||
/// Legacy crate name retained for migration and compatibility tracking.
|
||||
pub const LEGACY_CRATE: &str = "kb_materializer_metadata";
|
||||
/// Initial materializer implementation placeholder.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct MtMetadataMaterializer;
|
||||
|
||||
/// Current porting status.
|
||||
pub const MIGRATION_STATUS: &str = "source-preserved-pending-port";
|
||||
impl crate::MtMaterializer for crate::MtMetadataMaterializer {
|
||||
fn materializer_name(&self) -> &'static str {
|
||||
return "kb_materializer_metadata";
|
||||
}
|
||||
|
||||
fn materializer_version(&self) -> &'static str {
|
||||
return env!("CARGO_PKG_VERSION");
|
||||
}
|
||||
|
||||
fn accepts_event(&self, _event: &crate::MdDecodedProtocolEvent) -> bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
fn materialize_event(
|
||||
&self,
|
||||
event: &crate::MdDecodedProtocolEvent,
|
||||
) -> kb_core::Result<std::vec::Vec<crate::MdMaterializedEvent>> {
|
||||
let materialized = crate::MdMaterializedEvent {
|
||||
signature: event.signature.clone(),
|
||||
slot: event.slot,
|
||||
decoded_family: event.event_family,
|
||||
materialized_family: crate::MdMaterializedEventFamily::Metadata,
|
||||
};
|
||||
return std::result::Result::Ok(std::vec![materialized]);
|
||||
}
|
||||
}
|
||||
|
||||
/// Materialize embedded Token-2022 metadata, group, and group-member extensions.
|
||||
pub fn materialize_token2022_metadata_snapshot(
|
||||
account_key: &str,
|
||||
slot: u64,
|
||||
state: &crate::DcToken2022State,
|
||||
) -> std::result::Result<std::vec::Vec<crate::MtApiMaterializedOutput>, String> {
|
||||
if account_key.is_empty() {
|
||||
return std::result::Result::Err(
|
||||
"Token-2022 metadata snapshot requires a non-empty account key".to_string(),
|
||||
);
|
||||
}
|
||||
if state.kind != crate::DcToken2022StateKind::Mint {
|
||||
return std::result::Result::Err(
|
||||
"Token-2022 metadata snapshot requires a parsed mint state".to_string(),
|
||||
);
|
||||
}
|
||||
let mut outputs = std::vec::Vec::new();
|
||||
for entry in &state.extensions {
|
||||
let projection_kind = match entry.extension_name {
|
||||
"token_metadata" => std::option::Option::Some("token_metadata"),
|
||||
"token_group" => std::option::Option::Some("token_group"),
|
||||
"token_group_member" => std::option::Option::Some("token_group_member"),
|
||||
_ => std::option::Option::None,
|
||||
};
|
||||
let kind = match projection_kind {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => continue,
|
||||
};
|
||||
outputs.push(crate::MtApiMaterializedOutput {
|
||||
output_key: format!("token2022_metadata_snapshot:{account_key}:{slot}:{kind}"),
|
||||
family: crate::MdMaterializedEventFamily::Metadata,
|
||||
payload_json: serde_json::json!({
|
||||
"projectionVersion": 1,
|
||||
"domain": "spl_token2022_embedded_metadata",
|
||||
"projectionSemantics": "authoritative_bounded_mint_extension_snapshot",
|
||||
"idempotenceKey": format!("token2022_metadata:{account_key}:{slot}:{kind}"),
|
||||
"accountKey": account_key,
|
||||
"slot": slot,
|
||||
"programId": kb_program_ids::SPL_TOKEN2022_PROGRAM_ID,
|
||||
"metadataSource": "token2022_embedded_tlv",
|
||||
"projectionKind": kind,
|
||||
"extensionType": entry.extension_type,
|
||||
"extensionName": entry.extension_name,
|
||||
"valueHex": entry.value_hex,
|
||||
"valueFields": entry.value_fields,
|
||||
"externalUriFetched": false,
|
||||
"identityAuthority": "mint_address",
|
||||
"finalAccountStateCaptured": true,
|
||||
"provenance": {
|
||||
"processorName": "kb_materializer_metadata",
|
||||
"processorVersion": env!("CARGO_PKG_VERSION"),
|
||||
"source": "bounded_token2022_state_parser"
|
||||
}
|
||||
}),
|
||||
});
|
||||
}
|
||||
return std::result::Result::Ok(outputs);
|
||||
}
|
||||
|
||||
/// Materialize one canonical Metaplex `MetadataV1` account snapshot.
|
||||
///
|
||||
/// Account snapshots are authoritative for current state. Instruction observations may describe
|
||||
/// mutations, but must not emit a second competing metadata-state projection.
|
||||
pub fn materialize_metaplex_metadata_snapshot(
|
||||
snapshot: &crate::DcMetadataMtmCanonicalMetadataAccountSnapshot,
|
||||
) -> std::result::Result<crate::MtApiMaterializedOutput, String> {
|
||||
if !snapshot.provenance.committed {
|
||||
return std::result::Result::Err(
|
||||
"Metaplex metadata state materialization requires committed provenance".to_string(),
|
||||
);
|
||||
}
|
||||
if snapshot.identity.materialization
|
||||
!= crate::DcMetadataMtmCanonicalMaterializationPolicy::ActiveState
|
||||
{
|
||||
return std::result::Result::Err(
|
||||
"Metaplex MetadataV1 materialization requires active-state policy".to_string(),
|
||||
);
|
||||
}
|
||||
let metadata = match &snapshot.state {
|
||||
crate::DcMetadataMtmCanonicalMetadataAccountState::Metadata(value) => value,
|
||||
_ => {
|
||||
return std::result::Result::Err(
|
||||
"Metaplex metadata materializer received another account layout".to_string(),
|
||||
);
|
||||
},
|
||||
};
|
||||
let slot = snapshot.provenance.slot.map(|value| return value.to_string());
|
||||
let write_version = snapshot.provenance.write_version.map(|value| return value.to_string());
|
||||
let payload = serde_json::json!({
|
||||
"projectionVersion": 1,
|
||||
"domain": "metaplex_token_metadata",
|
||||
"projectionSemantics": "authoritative_committed_account_snapshot",
|
||||
"stableId": snapshot.identity.stable_id,
|
||||
"account": metadata.account,
|
||||
"mint": metadata.mint,
|
||||
"updateAuthority": metadata.update_authority,
|
||||
"name": metadata.name,
|
||||
"symbol": metadata.symbol,
|
||||
"uri": metadata.uri,
|
||||
"sellerFeeBasisPoints": metadata.seller_fee_basis_points,
|
||||
"primarySaleHappened": metadata.primary_sale_happened,
|
||||
"isMutable": metadata.is_mutable,
|
||||
"layout": snapshot.identity.layout,
|
||||
"layoutDiscriminant": snapshot.identity.discriminant,
|
||||
"canonicalVersion": snapshot.identity.canonical_version,
|
||||
"lifecycle": "active",
|
||||
"materializationPolicy": "active_state",
|
||||
"metadataSource": "metaplex_owned_account",
|
||||
"accountState": metadata.payload_json,
|
||||
"externalUriFetched": false,
|
||||
"stateAuthority": "owned_account_snapshot",
|
||||
"instructionProjectionRole": "mutation_observation_only_no_duplicate_state",
|
||||
"provenance": {
|
||||
"source": format!("{:?}", snapshot.provenance.source),
|
||||
"slot": slot,
|
||||
"writeVersion": write_version,
|
||||
"signature": snapshot.provenance.signature,
|
||||
"instructionIndex": snapshot.provenance.instruction_index,
|
||||
"innerInstructionIndex": snapshot.provenance.inner_instruction_index,
|
||||
"committed": snapshot.provenance.committed,
|
||||
"processorName": "kb_materializer_metadata",
|
||||
"processorVersion": env!("CARGO_PKG_VERSION")
|
||||
}
|
||||
});
|
||||
return std::result::Result::Ok(crate::MtApiMaterializedOutput {
|
||||
output_key: format!("metaplex_metadata_state:{}", metadata.account),
|
||||
family: crate::MdMaterializedEventFamily::Metadata,
|
||||
payload_json: payload,
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests_metaplex_metadata_state {
|
||||
pub(super) fn snapshot(
|
||||
committed: bool,
|
||||
) -> crate::DcMetadataMtmCanonicalMetadataAccountSnapshot {
|
||||
let metadata = crate::DcMetadataMtmMetadataAccountSnapshot {
|
||||
account: "metadata111".to_string(),
|
||||
bump: 1,
|
||||
mint: "mint111".to_string(),
|
||||
update_authority: "authority111".to_string(),
|
||||
name: "Asset".to_string(),
|
||||
symbol: "AST".to_string(),
|
||||
uri: "https://example.invalid/asset.json".to_string(),
|
||||
seller_fee_basis_points: 500,
|
||||
primary_sale_happened: true,
|
||||
is_mutable: false,
|
||||
payload_json: serde_json::json!({
|
||||
"creators": [{"address":"creator111","verified":true,"share":100}],
|
||||
"collection": {"verified":true,"key":"collection111"},
|
||||
"uses": {"useMethod":"Single","remaining":"0","total":"1"},
|
||||
"tokenStandard": "ProgrammableNonFungible",
|
||||
"programmableConfig": {"ruleSet":"rules111"}
|
||||
}),
|
||||
};
|
||||
return crate::DcMetadataMtmCanonicalMetadataAccountSnapshot::from_metadata(
|
||||
metadata,
|
||||
crate::DcMetadataMtmCanonicalAccountProvenance {
|
||||
source: crate::DcMetadataMtmCanonicalObservationSource::Backfill,
|
||||
slot: std::option::Option::Some(42),
|
||||
write_version: std::option::Option::Some(7),
|
||||
signature: std::option::Option::Some("signature111".to_string()),
|
||||
instruction_index: std::option::Option::Some(3),
|
||||
inner_instruction_index: std::option::Option::None,
|
||||
committed,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn account_snapshot_is_one_stable_authoritative_metadata_projection() {
|
||||
let first = crate::materialize_metaplex_metadata_snapshot(&snapshot(true));
|
||||
let second = crate::materialize_metaplex_metadata_snapshot(&snapshot(true));
|
||||
assert_eq!(first, second);
|
||||
assert_eq!(
|
||||
first.as_ref().map(|value| return value.output_key.as_str()),
|
||||
std::result::Result::Ok("metaplex_metadata_state:metadata111")
|
||||
);
|
||||
assert_eq!(
|
||||
first.as_ref().map(|value| return value.payload_json["metadataSource"].clone()),
|
||||
std::result::Result::Ok(serde_json::json!("metaplex_owned_account"))
|
||||
);
|
||||
assert_eq!(
|
||||
first
|
||||
.as_ref()
|
||||
.map(|value| return value.payload_json["instructionProjectionRole"].clone()),
|
||||
std::result::Result::Ok(serde_json::json!(
|
||||
"mutation_observation_only_no_duplicate_state"
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
first
|
||||
.as_ref()
|
||||
.map(|value| return value.payload_json["accountState"]["creators"][0]["share"]
|
||||
.clone()),
|
||||
std::result::Result::Ok(serde_json::json!(100))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn uncommitted_or_non_metadata_state_is_rejected() {
|
||||
assert!(crate::materialize_metaplex_metadata_snapshot(&snapshot(false)).is_err());
|
||||
let mut wrong = snapshot(true);
|
||||
wrong.state = crate::DcMetadataMtmCanonicalMetadataAccountState::EditionMarker(
|
||||
crate::DcMetadataMtmEditionMarkerAccountSnapshot {
|
||||
account: "marker111".to_string(),
|
||||
bump: 1,
|
||||
mint: "mint111".to_string(),
|
||||
kind: crate::DcMetadataMtmEditionMarkerAccountKind::EditionMarkerV1,
|
||||
marker_group: std::option::Option::Some(0),
|
||||
edition: 1,
|
||||
byte_index: 0,
|
||||
bit_mask: 64,
|
||||
edition_taken: true,
|
||||
ledger: std::vec![64],
|
||||
payload_json: serde_json::json!({}),
|
||||
},
|
||||
);
|
||||
assert!(crate::materialize_metaplex_metadata_snapshot(&wrong).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
/// Materialize any canonical Metaplex account state other than `MetadataV1`.
|
||||
///
|
||||
/// Every account address maps to one stable state key. Historical layouts remain materializable
|
||||
/// with an explicit historical lifecycle, while execution policy remains independently disabled.
|
||||
pub fn materialize_metaplex_account_snapshot(
|
||||
snapshot: &crate::DcMetadataMtmCanonicalMetadataAccountSnapshot,
|
||||
) -> std::result::Result<crate::MtApiMaterializedOutput, String> {
|
||||
if !snapshot.provenance.committed {
|
||||
return std::result::Result::Err(
|
||||
"Metaplex account-state materialization requires committed provenance".to_string(),
|
||||
);
|
||||
}
|
||||
let (projection_kind, fact_owner, state_json) = match &snapshot.state {
|
||||
crate::DcMetadataMtmCanonicalMetadataAccountState::Metadata(_) => {
|
||||
return std::result::Result::Err(
|
||||
"MetadataV1 must use the dedicated metadata-state materializer".to_string(),
|
||||
);
|
||||
},
|
||||
crate::DcMetadataMtmCanonicalMetadataAccountState::Edition(value) => {
|
||||
("edition_state", "kb_materializer_metadata", value.payload_json.clone())
|
||||
},
|
||||
crate::DcMetadataMtmCanonicalMetadataAccountState::EditionMarker(value) => {
|
||||
("edition_marker_state", "kb_materializer_metadata", value.payload_json.clone())
|
||||
},
|
||||
crate::DcMetadataMtmCanonicalMetadataAccountState::TokenRecord(value) => (
|
||||
"programmable_token_state",
|
||||
"kb_materializer_lifecycle",
|
||||
value.payload_json.clone(),
|
||||
),
|
||||
crate::DcMetadataMtmCanonicalMetadataAccountState::DelegateRecord(value) => {
|
||||
("delegate_admin_state", "kb_materializer_admin", value.payload_json.clone())
|
||||
},
|
||||
crate::DcMetadataMtmCanonicalMetadataAccountState::AuthorityRecord(value) => {
|
||||
("authority_admin_state", "kb_materializer_admin", value.payload_json.clone())
|
||||
},
|
||||
crate::DcMetadataMtmCanonicalMetadataAccountState::TokenOwnedEscrow(value) => (
|
||||
"token_owned_escrow_state",
|
||||
"kb_materializer_lifecycle",
|
||||
value.payload_json.clone(),
|
||||
),
|
||||
crate::DcMetadataMtmCanonicalMetadataAccountState::ReservationList(value) => (
|
||||
"historical_reservation_state",
|
||||
"kb_materializer_metadata",
|
||||
value.payload_json.clone(),
|
||||
),
|
||||
};
|
||||
let lifecycle = match snapshot.identity.lifecycle {
|
||||
crate::DcMetadataMtmCanonicalAccountLifecycle::Active => "active",
|
||||
crate::DcMetadataMtmCanonicalAccountLifecycle::ActiveExperimental => "active_experimental",
|
||||
crate::DcMetadataMtmCanonicalAccountLifecycle::Historical => "historical",
|
||||
};
|
||||
let materialization_policy = match snapshot.identity.materialization {
|
||||
crate::DcMetadataMtmCanonicalMaterializationPolicy::ActiveState => "active_state",
|
||||
crate::DcMetadataMtmCanonicalMaterializationPolicy::HistoricalState => "historical_state",
|
||||
};
|
||||
let execution_policy = match snapshot.identity.execution {
|
||||
crate::DcMetadataMtmCanonicalExecutionPolicy::CurrentCandidate => "current_candidate",
|
||||
crate::DcMetadataMtmCanonicalExecutionPolicy::ExperimentalCandidate => {
|
||||
"experimental_candidate"
|
||||
},
|
||||
crate::DcMetadataMtmCanonicalExecutionPolicy::NeverHistorical => "never_historical",
|
||||
};
|
||||
return std::result::Result::Ok(crate::MtApiMaterializedOutput {
|
||||
output_key: format!("metaplex_account_state:{}", snapshot.identity.account),
|
||||
family: crate::MdMaterializedEventFamily::Metadata,
|
||||
payload_json: serde_json::json!({
|
||||
"projectionVersion": 1,
|
||||
"domain": "metaplex_token_metadata",
|
||||
"projectionKind": projection_kind,
|
||||
"projectionSemantics": "authoritative_committed_owned_account_snapshot",
|
||||
"stableId": snapshot.identity.stable_id,
|
||||
"account": snapshot.identity.account,
|
||||
"programId": snapshot.identity.program_id,
|
||||
"layout": snapshot.identity.layout,
|
||||
"layoutDiscriminant": snapshot.identity.discriminant,
|
||||
"canonicalVersion": snapshot.identity.canonical_version,
|
||||
"lifecycle": lifecycle,
|
||||
"materializationPolicy": materialization_policy,
|
||||
"executionPolicy": execution_policy,
|
||||
"factOwner": fact_owner,
|
||||
"metadataSource": "metaplex_owned_account",
|
||||
"accountState": state_json,
|
||||
"instructionProjectionRole": "mutation_observation_only_no_duplicate_state",
|
||||
"provenance": {
|
||||
"source": format!("{:?}", snapshot.provenance.source),
|
||||
"slot": snapshot.provenance.slot.map(|value: u64| return value.to_string()),
|
||||
"writeVersion": snapshot.provenance.write_version.map(|value: u64| return value.to_string()),
|
||||
"signature": snapshot.provenance.signature,
|
||||
"instructionIndex": snapshot.provenance.instruction_index,
|
||||
"innerInstructionIndex": snapshot.provenance.inner_instruction_index,
|
||||
"committed": snapshot.provenance.committed,
|
||||
"processorName": "kb_materializer_metadata",
|
||||
"processorVersion": env!("CARGO_PKG_VERSION")
|
||||
}
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests_metaplex_account_state {
|
||||
fn provenance() -> crate::DcMetadataMtmCanonicalAccountProvenance {
|
||||
return crate::DcMetadataMtmCanonicalAccountProvenance {
|
||||
source: crate::DcMetadataMtmCanonicalObservationSource::Backfill,
|
||||
slot: std::option::Option::Some(90),
|
||||
write_version: std::option::Option::Some(2),
|
||||
signature: std::option::Option::None,
|
||||
instruction_index: std::option::Option::None,
|
||||
inner_instruction_index: std::option::Option::None,
|
||||
committed: true,
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn historical_edition_and_reservation_are_stable_materializable_state() {
|
||||
let edition = crate::DcMetadataMtmCanonicalMetadataAccountSnapshot::from_edition(
|
||||
crate::DcMetadataMtmEditionAccountSnapshot {
|
||||
account: "master-v1".to_string(),
|
||||
bump: 1,
|
||||
mint: "mint-v1".to_string(),
|
||||
kind: crate::DcMetadataMtmEditionAccountKind::MasterEditionV1,
|
||||
supply: std::option::Option::Some(3),
|
||||
max_supply: std::option::Option::Some(10),
|
||||
parent: std::option::Option::None,
|
||||
edition: std::option::Option::None,
|
||||
printing_mint: std::option::Option::Some("printing".to_string()),
|
||||
one_time_printing_authorization_mint: std::option::Option::Some(
|
||||
"authorization".to_string(),
|
||||
),
|
||||
payload_json: serde_json::json!({"supply":"3","maxSupply":"10"}),
|
||||
},
|
||||
provenance(),
|
||||
);
|
||||
let reservation =
|
||||
crate::DcMetadataMtmCanonicalMetadataAccountSnapshot::from_reservation_list(
|
||||
crate::DcMetadataMtmReservationListAccountSnapshot {
|
||||
account: "reservation-v2".to_string(),
|
||||
bump: 2,
|
||||
kind: crate::DcMetadataMtmReservationListAccountKind::V2,
|
||||
master_edition: "master-v1".to_string(),
|
||||
resource: "resource".to_string(),
|
||||
supply_snapshot: std::option::Option::Some(3),
|
||||
reservations: std::vec::Vec::new(),
|
||||
total_reservation_spots: 0,
|
||||
current_reservation_spots: 0,
|
||||
payload_json: serde_json::json!({"reservations":[]}),
|
||||
},
|
||||
provenance(),
|
||||
);
|
||||
let first = crate::materialize_metaplex_account_snapshot(&edition);
|
||||
let second = crate::materialize_metaplex_account_snapshot(&reservation);
|
||||
assert_eq!(
|
||||
first.as_ref().map(|value| return value.payload_json["lifecycle"].clone()),
|
||||
std::result::Result::Ok(serde_json::json!("historical"))
|
||||
);
|
||||
assert_eq!(
|
||||
first.as_ref().map(|value| return value.payload_json["executionPolicy"].clone()),
|
||||
std::result::Result::Ok(serde_json::json!("never_historical"))
|
||||
);
|
||||
assert_eq!(
|
||||
second.as_ref().map(|value| return value.output_key.as_str()),
|
||||
std::result::Result::Ok("metaplex_account_state:reservation-v2")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn programmable_delegate_authority_and_escrow_keep_unique_fact_owners() {
|
||||
let token = crate::DcMetadataMtmCanonicalMetadataAccountSnapshot::from_token_record(
|
||||
crate::DcMetadataMtmTokenRecordAccountSnapshot {
|
||||
account: "token-record".to_string(),
|
||||
bump: 1,
|
||||
mint: "mint".to_string(),
|
||||
token: "token".to_string(),
|
||||
stored_bump: 1,
|
||||
state: "Locked".to_string(),
|
||||
rule_set_revision: std::option::Option::Some(4),
|
||||
delegate: std::option::Option::Some("delegate".to_string()),
|
||||
delegate_role: std::option::Option::Some("Transfer".to_string()),
|
||||
locked_transfer: std::option::Option::Some("destination".to_string()),
|
||||
locked: true,
|
||||
payload_json: serde_json::json!({"state":"Locked"}),
|
||||
},
|
||||
provenance(),
|
||||
);
|
||||
let output = crate::materialize_metaplex_account_snapshot(&token);
|
||||
assert_eq!(
|
||||
output.as_ref().map(|value| return value.payload_json["factOwner"].clone()),
|
||||
std::result::Result::Ok(serde_json::json!("kb_materializer_lifecycle"))
|
||||
);
|
||||
assert_eq!(
|
||||
output.as_ref().map(|value| return value.payload_json["projectionKind"].clone()),
|
||||
std::result::Result::Ok(serde_json::json!("programmable_token_state"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn uncommitted_and_metadata_snapshots_use_explicit_boundaries() {
|
||||
let mut marker = crate::DcMetadataMtmCanonicalMetadataAccountSnapshot::from_edition_marker(
|
||||
crate::DcMetadataMtmEditionMarkerAccountSnapshot {
|
||||
account: "marker".to_string(),
|
||||
bump: 1,
|
||||
mint: "mint".to_string(),
|
||||
kind: crate::DcMetadataMtmEditionMarkerAccountKind::EditionMarkerV2,
|
||||
marker_group: std::option::Option::None,
|
||||
edition: 8,
|
||||
byte_index: 1,
|
||||
bit_mask: 128,
|
||||
edition_taken: true,
|
||||
ledger: std::vec![0, 128],
|
||||
payload_json: serde_json::json!({"editionTaken":true}),
|
||||
},
|
||||
provenance(),
|
||||
);
|
||||
marker.provenance.committed = false;
|
||||
assert!(crate::materialize_metaplex_account_snapshot(&marker).is_err());
|
||||
assert!(
|
||||
crate::materialize_metaplex_account_snapshot(
|
||||
&super::tests_metaplex_metadata_state::snapshot(true)
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests_token2022_state {
|
||||
#[test]
|
||||
fn embedded_metadata_group_and_member_have_distinct_stable_outputs() {
|
||||
let state = crate::DcToken2022State {
|
||||
kind: crate::DcToken2022StateKind::Mint,
|
||||
base_fields: serde_json::json!({"supply":"1","decimals":0}),
|
||||
base_hex: "00".repeat(82),
|
||||
account_type: std::option::Option::Some(1),
|
||||
extensions: std::vec![
|
||||
crate::DcToken2022TlvEntry {
|
||||
extension_type: 19,
|
||||
extension_name: "token_metadata",
|
||||
value_hex: "01".to_string(),
|
||||
value_fields: serde_json::json!({"name":"Token","symbol":"TOK","uri":"https://example.invalid/token.json"}),
|
||||
},
|
||||
crate::DcToken2022TlvEntry {
|
||||
extension_type: 20,
|
||||
extension_name: "token_group",
|
||||
value_hex: "02".to_string(),
|
||||
value_fields: serde_json::json!({"mint":"mint111","size":"1","maxSize":"10"}),
|
||||
},
|
||||
crate::DcToken2022TlvEntry {
|
||||
extension_type: 21,
|
||||
extension_name: "token_group_member",
|
||||
value_hex: "03".to_string(),
|
||||
value_fields: serde_json::json!({"mint":"mint111","group":"group111","memberNumber":"1"}),
|
||||
},
|
||||
],
|
||||
};
|
||||
let outputs = crate::materialize_token2022_metadata_snapshot("mint111", 9, &state);
|
||||
assert_eq!(outputs.as_ref().map(|values| return values.len()), std::result::Result::Ok(3));
|
||||
assert_eq!(
|
||||
outputs
|
||||
.as_ref()
|
||||
.map(|values| return values[0].payload_json["metadataSource"].clone()),
|
||||
std::result::Result::Ok(serde_json::json!("token2022_embedded_tlv"))
|
||||
);
|
||||
assert_eq!(
|
||||
outputs
|
||||
.as_ref()
|
||||
.map(|values| return values[0].payload_json["externalUriFetched"].clone()),
|
||||
std::result::Result::Ok(serde_json::json!(false))
|
||||
);
|
||||
assert_eq!(
|
||||
outputs
|
||||
.as_ref()
|
||||
.map(|values| return values[1].payload_json["projectionKind"].clone()),
|
||||
std::result::Result::Ok(serde_json::json!("token_group"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_is_idempotent_filters_unrelated_extensions_and_requires_mint_identity() {
|
||||
let state = crate::DcToken2022State {
|
||||
kind: crate::DcToken2022StateKind::Mint,
|
||||
base_fields: serde_json::json!({}),
|
||||
base_hex: "00".repeat(82),
|
||||
account_type: std::option::Option::Some(1),
|
||||
extensions: std::vec![crate::DcToken2022TlvEntry {
|
||||
extension_type: 1,
|
||||
extension_name: "transfer_fee_config",
|
||||
value_hex: "00".to_string(),
|
||||
value_fields: serde_json::json!({}),
|
||||
}],
|
||||
};
|
||||
let first = crate::materialize_token2022_metadata_snapshot("mint111", 9, &state);
|
||||
let second = crate::materialize_token2022_metadata_snapshot("mint111", 9, &state);
|
||||
assert_eq!(first, second);
|
||||
assert_eq!(first.as_ref().map(|values| return values.len()), std::result::Result::Ok(0));
|
||||
assert!(crate::materialize_token2022_metadata_snapshot("", 9, &state).is_err());
|
||||
let account_state = crate::DcToken2022State {
|
||||
kind: crate::DcToken2022StateKind::Account,
|
||||
base_fields: serde_json::json!({}),
|
||||
base_hex: "00".repeat(165),
|
||||
account_type: std::option::Option::None,
|
||||
extensions: std::vec::Vec::new(),
|
||||
};
|
||||
assert!(
|
||||
crate::materialize_token2022_metadata_snapshot("account111", 9, &account_state)
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// file: kb-lib/src/materializer/nft.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! `nft` materializer family.
|
||||
|
||||
pub mod core;
|
||||
mod core;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// file: kb-lib/src/materializer/nft/core.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! Migration boundary for legacy crate `kb_materializer_nft`.
|
||||
|
||||
/// Legacy crate name retained for migration and compatibility tracking.
|
||||
pub const LEGACY_CRATE: &str = "kb_materializer_nft";
|
||||
const LEGACY_CRATE: &str = "kb_materializer_nft";
|
||||
|
||||
/// Current porting status.
|
||||
pub const MIGRATION_STATUS: &str = "source-preserved-pending-port";
|
||||
const MIGRATION_STATUS: &str = "source-preserved-pending-port";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// file: kb-lib/src/materializer/oracle.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! `oracle` materializer family.
|
||||
|
||||
pub mod core;
|
||||
mod core;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// file: kb-lib/src/materializer/oracle/core.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! Migration boundary for legacy crate `kb_materializer_oracle`.
|
||||
|
||||
/// Legacy crate name retained for migration and compatibility tracking.
|
||||
pub const LEGACY_CRATE: &str = "kb_materializer_oracle";
|
||||
const LEGACY_CRATE: &str = "kb_materializer_oracle";
|
||||
|
||||
/// Current porting status.
|
||||
pub const MIGRATION_STATUS: &str = "source-preserved-pending-port";
|
||||
const MIGRATION_STATUS: &str = "source-preserved-pending-port";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// file: kb-lib/src/materializer/orderbook.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! `orderbook` materializer family.
|
||||
|
||||
pub mod core;
|
||||
mod core;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// file: kb-lib/src/materializer/orderbook/core.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! Migration boundary for legacy crate `kb_materializer_orderbook`.
|
||||
|
||||
/// Legacy crate name retained for migration and compatibility tracking.
|
||||
pub const LEGACY_CRATE: &str = "kb_materializer_orderbook";
|
||||
const LEGACY_CRATE: &str = "kb_materializer_orderbook";
|
||||
|
||||
/// Current porting status.
|
||||
pub const MIGRATION_STATUS: &str = "source-preserved-pending-port";
|
||||
const MIGRATION_STATUS: &str = "source-preserved-pending-port";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// file: kb-lib/src/materializer/perpetuals.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! `perpetuals` materializer family.
|
||||
|
||||
pub mod core;
|
||||
mod core;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// file: kb-lib/src/materializer/perpetuals/core.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! Migration boundary for legacy crate `kb_materializer_perpetuals`.
|
||||
|
||||
/// Legacy crate name retained for migration and compatibility tracking.
|
||||
pub const LEGACY_CRATE: &str = "kb_materializer_perpetuals";
|
||||
const LEGACY_CRATE: &str = "kb_materializer_perpetuals";
|
||||
|
||||
/// Current porting status.
|
||||
pub const MIGRATION_STATUS: &str = "source-preserved-pending-port";
|
||||
const MIGRATION_STATUS: &str = "source-preserved-pending-port";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// file: kb-lib/src/materializer/pool.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! `pool` materializer family.
|
||||
|
||||
pub mod state;
|
||||
mod state;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// file: kb-lib/src/materializer/pool/state.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! Migration boundary for legacy crate `kb_materializer_pool_state`.
|
||||
|
||||
/// Legacy crate name retained for migration and compatibility tracking.
|
||||
pub const LEGACY_CRATE: &str = "kb_materializer_pool_state";
|
||||
const LEGACY_CRATE: &str = "kb_materializer_pool_state";
|
||||
|
||||
/// Current porting status.
|
||||
pub const MIGRATION_STATUS: &str = "source-preserved-pending-port";
|
||||
const MIGRATION_STATUS: &str = "source-preserved-pending-port";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// file: kb-lib/src/materializer/rewards.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! `rewards` materializer family.
|
||||
|
||||
pub mod core;
|
||||
mod core;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// file: kb-lib/src/materializer/rewards/core.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! Migration boundary for legacy crate `kb_materializer_rewards`.
|
||||
|
||||
/// Legacy crate name retained for migration and compatibility tracking.
|
||||
pub const LEGACY_CRATE: &str = "kb_materializer_rewards";
|
||||
const LEGACY_CRATE: &str = "kb_materializer_rewards";
|
||||
|
||||
/// Current porting status.
|
||||
pub const MIGRATION_STATUS: &str = "source-preserved-pending-port";
|
||||
const MIGRATION_STATUS: &str = "source-preserved-pending-port";
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
// file: kb-lib/src/materializer/risk.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! `risk` materializer family.
|
||||
|
||||
pub mod core;
|
||||
mod constants;
|
||||
mod core;
|
||||
|
||||
/// Stable processor name for SPL Token and ATA risk facts.
|
||||
pub(crate) use self::constants::MT_RISK_PROCESSOR_NAME;
|
||||
/// Stable projection contract version for SPL Token and ATA risk facts.
|
||||
pub(crate) use self::constants::MT_RISK_PROJECTION_VERSION;
|
||||
/// Canonical tracing target for SPL Token and ATA risk facts.
|
||||
pub(crate) use self::constants::MT_RISK_TRACING_TARGET;
|
||||
|
||||
/// Stable SPL Token and ATA risk materializer.
|
||||
pub use self::core::MtRiskMaterializer;
|
||||
|
||||
11
kb-lib/src/materializer/risk/constants.rs
Normal file
11
kb-lib/src/materializer/risk/constants.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
// file: kb-lib/src/materializer/risk/constants.rs
|
||||
// version: 1
|
||||
|
||||
//! Local constants for fact-only risk projections.
|
||||
|
||||
/// Stable processor name persisted by the replay ledger.
|
||||
pub(crate) const MT_RISK_PROCESSOR_NAME: &str = "spl_token_risk";
|
||||
/// Stable projection payload contract version.
|
||||
pub(crate) const MT_RISK_PROJECTION_VERSION: u32 = 2;
|
||||
/// Canonical tracing target for this component.
|
||||
pub(crate) const MT_RISK_TRACING_TARGET: &str = "kb-lib.materializer.risk";
|
||||
@@ -1,10 +1,641 @@
|
||||
// file: kb-lib/src/materializer/risk/core.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! Migration boundary for legacy crate `kb_materializer_risk`.
|
||||
//! Stable fact-only risk projections for committed SPL Token and ATA observations.
|
||||
|
||||
/// Legacy crate name retained for migration and compatibility tracking.
|
||||
pub const LEGACY_CRATE: &str = "kb_materializer_risk";
|
||||
const ACCEPTED_FAMILIES: &[crate::MdEventFamily] = &[
|
||||
crate::MdEventFamily::TokenAccount,
|
||||
crate::MdEventFamily::TokenMint,
|
||||
crate::MdEventFamily::Admin,
|
||||
crate::MdEventFamily::Lifecycle,
|
||||
crate::MdEventFamily::Audit,
|
||||
];
|
||||
const RISK_ENTRIES: &[&str] = &[
|
||||
"approve",
|
||||
"approve_checked",
|
||||
"revoke",
|
||||
"freeze_account",
|
||||
"thaw_account",
|
||||
"set_authority",
|
||||
"initialize_multisig",
|
||||
"initialize_multisig2",
|
||||
"initialize_default_account_state",
|
||||
"update_default_account_state",
|
||||
"enable_required_transfer_memos",
|
||||
"disable_required_transfer_memos",
|
||||
"enable_cpi_guard",
|
||||
"disable_cpi_guard",
|
||||
"initialize_non_transferable_mint",
|
||||
"initialize_pausable_config",
|
||||
"pause",
|
||||
"resume",
|
||||
"initialize_permissioned_burn",
|
||||
"enable_confidential_credits",
|
||||
"disable_confidential_credits",
|
||||
"enable_non_confidential_credits",
|
||||
"disable_non_confidential_credits",
|
||||
];
|
||||
|
||||
/// Current porting status.
|
||||
pub const MIGRATION_STATUS: &str = "source-preserved-pending-port";
|
||||
/// Stable fact-only classic SPL Token risk materializer.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct MtRiskMaterializer;
|
||||
|
||||
impl crate::MtMaterializer for crate::MtRiskMaterializer {
|
||||
fn materializer_name(&self) -> &'static str {
|
||||
return "kb_materializer_risk";
|
||||
}
|
||||
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::MtRiskMaterializer {
|
||||
fn identity(&self) -> crate::MtApiMaterializerIdentity {
|
||||
return crate::MtApiMaterializerIdentity {
|
||||
name: crate::MT_RISK_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_token_risk_fact_refused",
|
||||
"failed or uncommitted SPL Token or ATA observations cannot create risk facts",
|
||||
);
|
||||
}
|
||||
let ata = is_ata_recovery(&observation.event);
|
||||
if ata && !ata_addresses_valid(&observation.payload_json) {
|
||||
return crate::MtApiMaterializerExecutionResult::refused(
|
||||
"invalid_nested_ata_risk_fact_refused",
|
||||
"nested ATA risk fact requires all three canonical address validations",
|
||||
);
|
||||
}
|
||||
let operation = observation.event.event_name.0.as_str();
|
||||
let parameters = observation
|
||||
.payload_json
|
||||
.get("parameters")
|
||||
.cloned()
|
||||
.unwrap_or(serde_json::Value::Null);
|
||||
let accounts = observation
|
||||
.payload_json
|
||||
.get("accounts")
|
||||
.cloned()
|
||||
.unwrap_or(serde_json::Value::Null);
|
||||
let authority = observation
|
||||
.payload_json
|
||||
.get("authority")
|
||||
.cloned()
|
||||
.unwrap_or(serde_json::Value::Null);
|
||||
let addresses = observation
|
||||
.payload_json
|
||||
.get("addresses")
|
||||
.cloned()
|
||||
.unwrap_or(serde_json::Value::Null);
|
||||
let risk_kind = match risk_kind(operation, ¶meters, &accounts) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return crate::MtApiMaterializerExecutionResult::ignored();
|
||||
},
|
||||
};
|
||||
let output = crate::MtApiMaterializedOutput {
|
||||
output_key: if ata {
|
||||
format!("ata_risk_fact:{risk_kind}:0")
|
||||
} else {
|
||||
format!("spl_token_risk_fact:{risk_kind}:0")
|
||||
},
|
||||
family: crate::MdMaterializedEventFamily::Risk,
|
||||
payload_json: serde_json::json!({
|
||||
"projectionVersion": crate::MT_RISK_PROJECTION_VERSION,
|
||||
"domain": if ata { "spl_associated_token_account_risk_fact" } else { "spl_token_risk_fact" },
|
||||
"riskKind": risk_kind,
|
||||
"operation": operation,
|
||||
"signature": observation.event.signature.0.clone(),
|
||||
"slot": observation.event.slot.0,
|
||||
"instructionPath": observation.event.instruction_path.0.clone(),
|
||||
"programId": observation.event.program_id.0.clone(),
|
||||
"parameters": parameters,
|
||||
"accounts": accounts,
|
||||
"authority": authority,
|
||||
"addresses": addresses,
|
||||
"interpretation": {
|
||||
"score": serde_json::Value::Null,
|
||||
"severity": serde_json::Value::Null,
|
||||
"factOnly": true
|
||||
},
|
||||
"committed": true,
|
||||
"provenance": {
|
||||
"processorName": crate::MT_RISK_PROCESSOR_NAME,
|
||||
"processorVersion": env!("CARGO_PKG_VERSION"),
|
||||
"sourceEventKey": observation.event_key.clone()
|
||||
}
|
||||
}),
|
||||
};
|
||||
tracing::debug!(target: crate::MT_RISK_TRACING_TARGET, action = "materialize_token_risk_fact", risk_kind, operation, ata, "materialize committed SPL Token or ATA risk fact");
|
||||
return crate::MtApiMaterializerExecutionResult {
|
||||
status: crate::MtApiMaterializerOutcomeStatus::Inserted,
|
||||
outputs: std::vec![output],
|
||||
diagnostics: std::vec::Vec::new(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn accepts_event(event: &crate::MdDecodedProtocolEvent) -> bool {
|
||||
let token_program = (event.program_id.0 == kb_program_ids::SPL_TOKEN_PROGRAM_ID
|
||||
&& event.surface_code.0 == "spl_token")
|
||||
|| (event.program_id.0 == kb_program_ids::SPL_TOKEN2022_PROGRAM_ID
|
||||
&& event.surface_code.0 == "spl_token2022");
|
||||
let token_risk = token_program && RISK_ENTRIES.contains(&event.event_name.0.as_str());
|
||||
return ACCEPTED_FAMILIES.contains(&event.event_family)
|
||||
&& (token_risk || is_ata_recovery(event));
|
||||
}
|
||||
|
||||
fn is_ata_recovery(event: &crate::MdDecodedProtocolEvent) -> bool {
|
||||
return event.program_id.0 == kb_program_ids::ASSOCIATED_TOKEN_PROGRAM_ID
|
||||
&& event.surface_code.0 == "spl_associated_token_account"
|
||||
&& event.event_family == crate::MdEventFamily::Lifecycle
|
||||
&& event.event_name.0 == "recover_nested";
|
||||
}
|
||||
|
||||
fn ata_addresses_valid(payload: &serde_json::Value) -> bool {
|
||||
let addresses = match payload.get("addresses") {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return false,
|
||||
};
|
||||
let token_supported = addresses
|
||||
.get("tokenProgram")
|
||||
.and_then(|value| return value.get("supported"))
|
||||
.and_then(serde_json::Value::as_bool)
|
||||
== std::option::Option::Some(true);
|
||||
let token_program_id = addresses
|
||||
.get("tokenProgram")
|
||||
.and_then(|value| return value.get("programId"))
|
||||
.and_then(serde_json::Value::as_str);
|
||||
let token_program_exact = matches!(
|
||||
token_program_id,
|
||||
std::option::Option::Some(kb_program_ids::SPL_TOKEN_PROGRAM_ID)
|
||||
| std::option::Option::Some(kb_program_ids::SPL_TOKEN2022_PROGRAM_ID)
|
||||
);
|
||||
return token_supported
|
||||
&& token_program_exact
|
||||
&& [
|
||||
"ownerAssociatedTokenAccount",
|
||||
"nestedAssociatedTokenAccount",
|
||||
"walletNestedMintAssociatedTokenAccount",
|
||||
]
|
||||
.iter()
|
||||
.all(|field| {
|
||||
let address = match addresses.get(*field) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return false,
|
||||
};
|
||||
return address
|
||||
.get("observed")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.is_some_and(|value| return !value.is_empty())
|
||||
&& address.get("valid").and_then(serde_json::Value::as_bool)
|
||||
== std::option::Option::Some(true);
|
||||
});
|
||||
}
|
||||
|
||||
fn risk_kind(
|
||||
operation: &str,
|
||||
parameters: &serde_json::Value,
|
||||
accounts: &serde_json::Value,
|
||||
) -> std::option::Option<&'static str> {
|
||||
return match operation {
|
||||
"recover_nested" => std::option::Option::Some("nested_ata_anti_pattern_recovered"),
|
||||
"approve" | "approve_checked" => std::option::Option::Some("delegation_allowance_granted"),
|
||||
"revoke" => std::option::Option::Some("delegation_revoked"),
|
||||
"freeze_account" => std::option::Option::Some("account_frozen"),
|
||||
"thaw_account" => std::option::Option::Some("account_thawed"),
|
||||
"set_authority"
|
||||
if parameters.get("newAuthority").is_some_and(serde_json::Value::is_null) =>
|
||||
{
|
||||
std::option::Option::Some("authority_revoked")
|
||||
},
|
||||
"set_authority" => std::option::Option::Some("authority_changed"),
|
||||
"initialize_default_account_state" | "update_default_account_state" => {
|
||||
std::option::Option::Some("default_account_state_configured")
|
||||
},
|
||||
"enable_required_transfer_memos" => {
|
||||
std::option::Option::Some("required_transfer_memos_enabled")
|
||||
},
|
||||
"disable_required_transfer_memos" => {
|
||||
std::option::Option::Some("required_transfer_memos_disabled")
|
||||
},
|
||||
"enable_cpi_guard" => std::option::Option::Some("cpi_guard_enabled"),
|
||||
"disable_cpi_guard" => std::option::Option::Some("cpi_guard_disabled"),
|
||||
"initialize_non_transferable_mint" => {
|
||||
std::option::Option::Some("non_transferable_mint_configured")
|
||||
},
|
||||
"initialize_pausable_config" => std::option::Option::Some("pausable_mint_configured"),
|
||||
"pause" => std::option::Option::Some("mint_activity_paused"),
|
||||
"resume" => std::option::Option::Some("mint_activity_resumed"),
|
||||
"initialize_permissioned_burn" => std::option::Option::Some("permissioned_burn_configured"),
|
||||
"enable_confidential_credits" => std::option::Option::Some("confidential_credits_enabled"),
|
||||
"disable_confidential_credits" => {
|
||||
std::option::Option::Some("confidential_credits_disabled")
|
||||
},
|
||||
"enable_non_confidential_credits" => {
|
||||
std::option::Option::Some("non_confidential_credits_enabled")
|
||||
},
|
||||
"disable_non_confidential_credits" => {
|
||||
std::option::Option::Some("non_confidential_credits_disabled")
|
||||
},
|
||||
"initialize_multisig" | "initialize_multisig2" => {
|
||||
let m = parameters.get("m").and_then(serde_json::Value::as_u64).unwrap_or(0);
|
||||
let n = role_count(accounts, "multisig_member");
|
||||
if m == 1 && n > 1 {
|
||||
std::option::Option::Some("weak_multisig_threshold")
|
||||
} else {
|
||||
std::option::Option::None
|
||||
}
|
||||
},
|
||||
_ => std::option::Option::None,
|
||||
};
|
||||
}
|
||||
|
||||
fn role_count(accounts: &serde_json::Value, role: &str) -> usize {
|
||||
return accounts
|
||||
.as_array()
|
||||
.map(|rows| {
|
||||
return rows
|
||||
.iter()
|
||||
.filter(|row| {
|
||||
return row.get("role").and_then(serde_json::Value::as_str)
|
||||
== std::option::Option::Some(role);
|
||||
})
|
||||
.count();
|
||||
})
|
||||
.unwrap_or(0);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
fn observation(
|
||||
entry: &str,
|
||||
family: crate::MdEventFamily,
|
||||
parameters: serde_json::Value,
|
||||
failed: bool,
|
||||
) -> crate::DcApiDecodedObservation {
|
||||
return crate::DcApiDecodedObservation {
|
||||
event_key: format!("token:{entry}:0"),
|
||||
event: crate::MdDecodedProtocolEvent {
|
||||
signature: crate::MdSignature("signature".to_string()),
|
||||
slot: crate::MdSlot(1),
|
||||
instruction_path: crate::MdInstructionPath("0".to_string()),
|
||||
program_id: crate::MdProgramId(kb_program_ids::SPL_TOKEN_PROGRAM_ID.to_string()),
|
||||
protocol_code: crate::MdProtocolCode("spl_token".to_string()),
|
||||
surface_code: crate::MdSurfaceCode("spl_token".to_string()),
|
||||
event_code: crate::MdEventCode(format!("spl_token.{entry}")),
|
||||
event_name: crate::MdEventName(entry.to_string()),
|
||||
event_family: family,
|
||||
source_kind: crate::MdEventSourceKind::Instruction,
|
||||
confidence: crate::MdDecoderConfidence::ManualExact,
|
||||
},
|
||||
payload_json: serde_json::json!({"parameters":parameters,"accounts":[{"role":"multisig_member"},{"role":"multisig_member"}],"authority":{}}),
|
||||
transaction_failed: failed,
|
||||
transaction_error: std::option::Option::None,
|
||||
observation_committed: !failed,
|
||||
proof: crate::DcApiDecoderProof {
|
||||
kind: crate::DcApiDecoderProofKind::Manual,
|
||||
confidence: crate::MdDecoderConfidence::ManualExact,
|
||||
evidence: std::vec!["fixture".to_string()],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
fn token2022_observation(
|
||||
entry: &str,
|
||||
family: crate::MdEventFamily,
|
||||
parameters: serde_json::Value,
|
||||
failed: bool,
|
||||
) -> crate::DcApiDecodedObservation {
|
||||
let mut observation = observation(entry, family, parameters, failed);
|
||||
observation.event.program_id =
|
||||
crate::MdProgramId(kb_program_ids::SPL_TOKEN2022_PROGRAM_ID.to_string());
|
||||
observation.event.protocol_code = crate::MdProtocolCode("spl_token2022".to_string());
|
||||
observation.event.surface_code = crate::MdSurfaceCode("spl_token2022".to_string());
|
||||
observation.event.event_code = crate::MdEventCode(format!("spl_token2022.{entry}"));
|
||||
observation.event_key = format!("token2022:{entry}:0");
|
||||
return observation;
|
||||
}
|
||||
|
||||
fn nested_ata_observation(failed: bool, valid: bool) -> crate::DcApiDecodedObservation {
|
||||
return crate::DcApiDecodedObservation {
|
||||
event_key: "ata:recover_nested:0".to_string(),
|
||||
event: crate::MdDecodedProtocolEvent {
|
||||
signature: crate::MdSignature("signature".to_string()),
|
||||
slot: crate::MdSlot(2),
|
||||
instruction_path: crate::MdInstructionPath("1/0".to_string()),
|
||||
program_id: crate::MdProgramId(
|
||||
kb_program_ids::ASSOCIATED_TOKEN_PROGRAM_ID.to_string(),
|
||||
),
|
||||
protocol_code: crate::MdProtocolCode("spl_associated_token_account".to_string()),
|
||||
surface_code: crate::MdSurfaceCode("spl_associated_token_account".to_string()),
|
||||
event_code: crate::MdEventCode(
|
||||
"spl_associated_token_account.recover_nested".to_string(),
|
||||
),
|
||||
event_name: crate::MdEventName("recover_nested".to_string()),
|
||||
event_family: crate::MdEventFamily::Lifecycle,
|
||||
source_kind: crate::MdEventSourceKind::InnerInstruction,
|
||||
confidence: crate::MdDecoderConfidence::ManualExact,
|
||||
},
|
||||
payload_json: serde_json::json!({
|
||||
"addresses": {
|
||||
"tokenProgram": {
|
||||
"programId": kb_program_ids::SPL_TOKEN2022_PROGRAM_ID,
|
||||
"kind": "token2022",
|
||||
"supported": true
|
||||
},
|
||||
"ownerAssociatedTokenAccount": {"observed":"ownerAta","valid":valid},
|
||||
"nestedAssociatedTokenAccount": {"observed":"nestedAta","valid":valid},
|
||||
"walletNestedMintAssociatedTokenAccount": {"observed":"destinationAta","valid":valid}
|
||||
}
|
||||
}),
|
||||
transaction_failed: failed,
|
||||
transaction_error: std::option::Option::None,
|
||||
observation_committed: !failed,
|
||||
proof: crate::DcApiDecoderProof {
|
||||
kind: crate::DcApiDecoderProofKind::Manual,
|
||||
confidence: crate::MdDecoderConfidence::ManualExact,
|
||||
evidence: std::vec!["fixture".to_string()],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn approval_and_authority_revocation_are_fact_only_without_score() {
|
||||
for (entry, family, parameters, expected) in [
|
||||
(
|
||||
"approve_checked",
|
||||
crate::MdEventFamily::TokenAccount,
|
||||
serde_json::json!({"amountRaw":"10"}),
|
||||
"delegation_allowance_granted",
|
||||
),
|
||||
(
|
||||
"set_authority",
|
||||
crate::MdEventFamily::Admin,
|
||||
serde_json::json!({"newAuthority":null}),
|
||||
"authority_revoked",
|
||||
),
|
||||
] {
|
||||
let result = crate::MtApiEventMaterializer::materialize(
|
||||
&crate::MtRiskMaterializer,
|
||||
&observation(entry, family, parameters, false),
|
||||
);
|
||||
assert_eq!(result.outputs[0].payload_json["riskKind"], expected);
|
||||
assert!(result.outputs[0].payload_json["interpretation"]["score"].is_null());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn token2022_revocation_is_owned_once_without_an_invented_score() {
|
||||
let observation = token2022_observation(
|
||||
"set_authority",
|
||||
crate::MdEventFamily::Admin,
|
||||
serde_json::json!({"newAuthority":null}),
|
||||
false,
|
||||
);
|
||||
let result =
|
||||
crate::MtApiEventMaterializer::materialize(&crate::MtRiskMaterializer, &observation);
|
||||
assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Inserted);
|
||||
assert_eq!(result.outputs.len(), 1);
|
||||
assert_eq!(result.outputs[0].payload_json["riskKind"], "authority_revoked");
|
||||
assert!(result.outputs[0].payload_json["interpretation"]["score"].is_null());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_structurally_weak_multisig_creates_risk_fact() {
|
||||
let weak = observation(
|
||||
"initialize_multisig2",
|
||||
crate::MdEventFamily::Admin,
|
||||
serde_json::json!({"m":1}),
|
||||
false,
|
||||
);
|
||||
let strong = observation(
|
||||
"initialize_multisig2",
|
||||
crate::MdEventFamily::Admin,
|
||||
serde_json::json!({"m":2}),
|
||||
false,
|
||||
);
|
||||
assert_eq!(
|
||||
crate::MtApiEventMaterializer::materialize(&crate::MtRiskMaterializer, &weak).status,
|
||||
crate::MtApiMaterializerOutcomeStatus::Inserted
|
||||
);
|
||||
assert_eq!(
|
||||
crate::MtApiEventMaterializer::materialize(&crate::MtRiskMaterializer, &strong).status,
|
||||
crate::MtApiMaterializerOutcomeStatus::Ignored
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_risk_fact_is_refused() {
|
||||
let failed = observation(
|
||||
"freeze_account",
|
||||
crate::MdEventFamily::TokenAccount,
|
||||
serde_json::json!({}),
|
||||
true,
|
||||
);
|
||||
assert_eq!(
|
||||
crate::MtApiEventMaterializer::materialize(&crate::MtRiskMaterializer, &failed).status,
|
||||
crate::MtApiMaterializerOutcomeStatus::Refused
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn committed_nested_ata_recovery_creates_one_fact_without_score() {
|
||||
let result = crate::MtApiEventMaterializer::materialize(
|
||||
&crate::MtRiskMaterializer,
|
||||
&nested_ata_observation(false, true),
|
||||
);
|
||||
assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Inserted);
|
||||
assert_eq!(result.outputs.len(), 1);
|
||||
assert_eq!(
|
||||
result.outputs[0].output_key,
|
||||
"ata_risk_fact:nested_ata_anti_pattern_recovered:0"
|
||||
);
|
||||
assert_eq!(result.outputs[0].payload_json["riskKind"], "nested_ata_anti_pattern_recovered");
|
||||
assert_eq!(
|
||||
result.outputs[0].payload_json["domain"],
|
||||
"spl_associated_token_account_risk_fact"
|
||||
);
|
||||
assert!(result.outputs[0].payload_json["interpretation"]["score"].is_null());
|
||||
assert!(result.outputs[0].payload_json["interpretation"]["severity"].is_null());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_or_invalid_nested_ata_recovery_creates_no_risk_fact() {
|
||||
for observation in
|
||||
[nested_ata_observation(true, true), nested_ata_observation(false, false)]
|
||||
{
|
||||
let result = crate::MtApiEventMaterializer::materialize(
|
||||
&crate::MtRiskMaterializer,
|
||||
&observation,
|
||||
);
|
||||
assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Refused);
|
||||
assert!(result.outputs.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ata_matrix_declares_the_single_compiled_risk_fact() {
|
||||
let matrix: serde_json::Value = match serde_json::from_str(include_str!(
|
||||
"../../../../docs/SPL_ASSOCIATED_TOKEN_ACCOUNT_MATRIX.json"
|
||||
)) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("ATA matrix is invalid: {error}"),
|
||||
};
|
||||
let instructions = match matrix["instructions"].as_array() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => panic!("ATA matrix instructions are absent"),
|
||||
};
|
||||
let recover = match instructions.iter().find(|row| {
|
||||
return row.get("name").and_then(serde_json::Value::as_str)
|
||||
== std::option::Option::Some("recover_nested");
|
||||
}) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => panic!("RecoverNested matrix entry is absent"),
|
||||
};
|
||||
assert!(recover["authorizedProjections"].as_array().is_some_and(|values| {
|
||||
return values.iter().any(|value| {
|
||||
return value == "risk:nested_ata_anti_pattern_recovered";
|
||||
});
|
||||
}));
|
||||
assert_eq!(
|
||||
matrix["materializationContract"]["projectionOwners"][1]["facts"],
|
||||
serde_json::json!(["nested_ata_anti_pattern_recovered"])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn token2022_compliance_toggles_are_projected_as_fact_only_risk_rows() {
|
||||
for (entry, expected) in [
|
||||
("initialize_default_account_state", "default_account_state_configured"),
|
||||
("enable_required_transfer_memos", "required_transfer_memos_enabled"),
|
||||
("disable_required_transfer_memos", "required_transfer_memos_disabled"),
|
||||
("enable_cpi_guard", "cpi_guard_enabled"),
|
||||
("disable_cpi_guard", "cpi_guard_disabled"),
|
||||
] {
|
||||
let result = crate::MtApiEventMaterializer::materialize(
|
||||
&crate::MtRiskMaterializer,
|
||||
&token2022_observation(
|
||||
entry,
|
||||
crate::MdEventFamily::Audit,
|
||||
serde_json::json!({}),
|
||||
false,
|
||||
),
|
||||
);
|
||||
assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Inserted);
|
||||
assert_eq!(result.outputs[0].payload_json["riskKind"], expected);
|
||||
assert_eq!(result.outputs[0].payload_json["interpretation"]["factOnly"], true);
|
||||
assert!(result.outputs[0].payload_json["interpretation"]["score"].is_null());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn token2022_restriction_and_pause_facts_have_unique_stable_kinds() {
|
||||
for (entry, family, expected) in [
|
||||
(
|
||||
"initialize_non_transferable_mint",
|
||||
crate::MdEventFamily::TokenMint,
|
||||
"non_transferable_mint_configured",
|
||||
),
|
||||
(
|
||||
"initialize_pausable_config",
|
||||
crate::MdEventFamily::TokenAccount,
|
||||
"pausable_mint_configured",
|
||||
),
|
||||
("pause", crate::MdEventFamily::TokenAccount, "mint_activity_paused"),
|
||||
("resume", crate::MdEventFamily::TokenAccount, "mint_activity_resumed"),
|
||||
(
|
||||
"initialize_permissioned_burn",
|
||||
crate::MdEventFamily::TokenAccount,
|
||||
"permissioned_burn_configured",
|
||||
),
|
||||
] {
|
||||
let result = crate::MtApiEventMaterializer::materialize(
|
||||
&crate::MtRiskMaterializer,
|
||||
&token2022_observation(entry, family, serde_json::json!({}), false),
|
||||
);
|
||||
assert_eq!(result.outputs[0].payload_json["riskKind"], expected);
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn confidential_credit_toggles_are_fact_only_and_keep_wire_parameters() {
|
||||
for (entry, credit_kind, enabled, expected) in [
|
||||
(
|
||||
"enable_confidential_credits",
|
||||
"confidential",
|
||||
true,
|
||||
"confidential_credits_enabled",
|
||||
),
|
||||
(
|
||||
"disable_confidential_credits",
|
||||
"confidential",
|
||||
false,
|
||||
"confidential_credits_disabled",
|
||||
),
|
||||
(
|
||||
"enable_non_confidential_credits",
|
||||
"non_confidential",
|
||||
true,
|
||||
"non_confidential_credits_enabled",
|
||||
),
|
||||
(
|
||||
"disable_non_confidential_credits",
|
||||
"non_confidential",
|
||||
false,
|
||||
"non_confidential_credits_disabled",
|
||||
),
|
||||
] {
|
||||
let result = crate::MtApiEventMaterializer::materialize(
|
||||
&crate::MtRiskMaterializer,
|
||||
&token2022_observation(
|
||||
entry,
|
||||
crate::MdEventFamily::TokenAccount,
|
||||
serde_json::json!({
|
||||
"extension":"confidential_transfer",
|
||||
"creditKind":credit_kind,
|
||||
"enabled":enabled,
|
||||
}),
|
||||
false,
|
||||
),
|
||||
);
|
||||
assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Inserted);
|
||||
assert_eq!(result.outputs.len(), 1);
|
||||
assert_eq!(result.outputs[0].payload_json["riskKind"], expected);
|
||||
assert_eq!(result.outputs[0].payload_json["parameters"]["creditKind"], credit_kind);
|
||||
assert_eq!(result.outputs[0].payload_json["parameters"]["enabled"], enabled);
|
||||
assert_eq!(result.outputs[0].payload_json["interpretation"]["factOnly"], true);
|
||||
assert!(result.outputs[0].payload_json["interpretation"]["score"].is_null());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// file: kb-lib/src/materializer/routing.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! `routing` materializer family.
|
||||
|
||||
pub mod core;
|
||||
mod core;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// file: kb-lib/src/materializer/routing/core.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! Migration boundary for legacy crate `kb_materializer_routing`.
|
||||
|
||||
/// Legacy crate name retained for migration and compatibility tracking.
|
||||
pub const LEGACY_CRATE: &str = "kb_materializer_routing";
|
||||
const LEGACY_CRATE: &str = "kb_materializer_routing";
|
||||
|
||||
/// Current porting status.
|
||||
pub const MIGRATION_STATUS: &str = "source-preserved-pending-port";
|
||||
const MIGRATION_STATUS: &str = "source-preserved-pending-port";
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
// file: kb-lib/src/materializer/staking.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
//! `staking` materializer family.
|
||||
|
||||
mod constants;
|
||||
pub mod core;
|
||||
mod core;
|
||||
|
||||
pub(crate) use crate::materializer::staking::constants::TRACING_TARGET as MATERIALIZER_STAKING_TRACING_TARGET;
|
||||
/// Canonical tracing target for the native stake and vote materializer.
|
||||
pub(crate) use self::constants::MT_STAKING_TRACING_TARGET;
|
||||
|
||||
/// Stable native stake and vote materializer.
|
||||
pub use crate::materializer::staking::core::StakingMaterializer;
|
||||
pub use self::core::MtStakingMaterializer;
|
||||
|
||||
@@ -4,4 +4,4 @@
|
||||
//! Local constants for the native stake and vote materializer.
|
||||
|
||||
/// Canonical tracing target for this component.
|
||||
pub(crate) const TRACING_TARGET: &str = "kb-lib.materializer.staking";
|
||||
pub(crate) const MT_STAKING_TRACING_TARGET: &str = "kb-lib.materializer.staking";
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
|
||||
//! Instruction-level staking projections for native Stake and Vote observations.
|
||||
|
||||
const ACCEPTED_FAMILIES: &[crate::EventFamily] = &[
|
||||
crate::EventFamily::Lifecycle,
|
||||
crate::EventFamily::Admin,
|
||||
crate::EventFamily::Staking,
|
||||
crate::EventFamily::Reward,
|
||||
const ACCEPTED_FAMILIES: &[crate::MdEventFamily] = &[
|
||||
crate::MdEventFamily::Lifecycle,
|
||||
crate::MdEventFamily::Admin,
|
||||
crate::MdEventFamily::Staking,
|
||||
crate::MdEventFamily::Reward,
|
||||
];
|
||||
const STAKE_SURFACE: &str = "solana_native_stake";
|
||||
const VOTE_SURFACE: &str = "solana_native_vote";
|
||||
@@ -82,10 +82,10 @@ impl ProjectionKind {
|
||||
};
|
||||
}
|
||||
|
||||
fn family(self) -> crate::MaterializedEventFamily {
|
||||
fn family(self) -> crate::MdMaterializedEventFamily {
|
||||
return match self {
|
||||
Self::VoteReward => crate::MaterializedEventFamily::Reward,
|
||||
_ => crate::MaterializedEventFamily::Staking,
|
||||
Self::VoteReward => crate::MdMaterializedEventFamily::Reward,
|
||||
_ => crate::MdMaterializedEventFamily::Staking,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -96,9 +96,9 @@ impl ProjectionKind {
|
||||
|
||||
/// Stable staking materializer for committed native Stake and Vote observations.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct StakingMaterializer;
|
||||
pub struct MtStakingMaterializer;
|
||||
|
||||
impl crate::Materializer for crate::StakingMaterializer {
|
||||
impl crate::MtMaterializer for crate::MtStakingMaterializer {
|
||||
fn materializer_name(&self) -> &'static str {
|
||||
return "kb_materializer_staking";
|
||||
}
|
||||
@@ -107,35 +107,35 @@ impl crate::Materializer for crate::StakingMaterializer {
|
||||
return env!("CARGO_PKG_VERSION");
|
||||
}
|
||||
|
||||
fn accepts_event(&self, event: &crate::DecodedProtocolEvent) -> bool {
|
||||
fn accepts_event(&self, event: &crate::MdDecodedProtocolEvent) -> bool {
|
||||
return family_is_accepted(event.event_family)
|
||||
&& projection(event.surface_code.0.as_str(), event.event_name.0.as_str()).is_some();
|
||||
}
|
||||
|
||||
fn materialize_event(
|
||||
&self,
|
||||
event: &crate::DecodedProtocolEvent,
|
||||
) -> kb_core::Result<std::vec::Vec<crate::MaterializedEvent>> {
|
||||
if !crate::Materializer::accepts_event(self, event) {
|
||||
event: &crate::MdDecodedProtocolEvent,
|
||||
) -> kb_core::Result<std::vec::Vec<crate::MdMaterializedEvent>> {
|
||||
if !crate::MtMaterializer::accepts_event(self, event) {
|
||||
return std::result::Result::Ok(std::vec::Vec::new());
|
||||
}
|
||||
return std::result::Result::Ok(std::vec::Vec::new());
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::EventMaterializer for crate::StakingMaterializer {
|
||||
fn identity(&self) -> crate::MaterializerIdentity {
|
||||
return crate::MaterializerIdentity {
|
||||
impl crate::MtApiEventMaterializer for crate::MtStakingMaterializer {
|
||||
fn identity(&self) -> crate::MtApiMaterializerIdentity {
|
||||
return crate::MtApiMaterializerIdentity {
|
||||
name: "solana_native_staking".to_string(),
|
||||
version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
fn accepted_families(&self) -> &'static [crate::EventFamily] {
|
||||
fn accepted_families(&self) -> &'static [crate::MdEventFamily] {
|
||||
return ACCEPTED_FAMILIES;
|
||||
}
|
||||
|
||||
fn accepts_observation(&self, observation: &crate::DecodedObservation) -> bool {
|
||||
fn accepts_observation(&self, observation: &crate::DcApiDecodedObservation) -> bool {
|
||||
return family_is_accepted(observation.event.event_family)
|
||||
&& projection(
|
||||
observation.event.surface_code.0.as_str(),
|
||||
@@ -146,15 +146,15 @@ impl crate::EventMaterializer for crate::StakingMaterializer {
|
||||
|
||||
fn transaction_policy(
|
||||
&self,
|
||||
_family: crate::EventFamily,
|
||||
) -> crate::MaterializationTransactionPolicy {
|
||||
return crate::MaterializationTransactionPolicy::SuccessfulCommittedOnly;
|
||||
_family: crate::MdEventFamily,
|
||||
) -> crate::MtApiMaterializationTransactionPolicy {
|
||||
return crate::MtApiMaterializationTransactionPolicy::SuccessfulCommittedOnly;
|
||||
}
|
||||
|
||||
fn materialize(
|
||||
&self,
|
||||
observation: &crate::DecodedObservation,
|
||||
) -> crate::MaterializerExecutionResult {
|
||||
observation: &crate::DcApiDecodedObservation,
|
||||
) -> crate::MtApiMaterializerExecutionResult {
|
||||
let projection = match projection(
|
||||
observation.event.surface_code.0.as_str(),
|
||||
observation.event.event_name.0.as_str(),
|
||||
@@ -162,22 +162,22 @@ impl crate::EventMaterializer for crate::StakingMaterializer {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
tracing::debug!(
|
||||
target: crate::MATERIALIZER_STAKING_TRACING_TARGET,
|
||||
target: crate::MT_STAKING_TRACING_TARGET,
|
||||
action = "materialize_staking",
|
||||
surface_code = %observation.event.surface_code.0.as_str(),
|
||||
entry_code = %observation.event.event_name.0.as_str(),
|
||||
accepted = false,
|
||||
"ignore observation outside supported native staking projections"
|
||||
);
|
||||
return crate::MaterializerExecutionResult::ignored();
|
||||
return crate::MtApiMaterializerExecutionResult::ignored();
|
||||
},
|
||||
};
|
||||
if !family_is_accepted(observation.event.event_family) {
|
||||
return crate::MaterializerExecutionResult::ignored();
|
||||
return crate::MtApiMaterializerExecutionResult::ignored();
|
||||
}
|
||||
if observation.transaction_failed || !observation.observation_committed {
|
||||
tracing::debug!(
|
||||
target: crate::MATERIALIZER_STAKING_TRACING_TARGET,
|
||||
target: crate::MT_STAKING_TRACING_TARGET,
|
||||
action = "materialize_staking",
|
||||
surface_code = %observation.event.surface_code.0.as_str(),
|
||||
entry_code = %observation.event.event_name.0.as_str(),
|
||||
@@ -185,7 +185,7 @@ impl crate::EventMaterializer for crate::StakingMaterializer {
|
||||
transaction_failed = observation.transaction_failed,
|
||||
"refuse uncommitted native staking observation"
|
||||
);
|
||||
return crate::MaterializerExecutionResult::refused(
|
||||
return crate::MtApiMaterializerExecutionResult::refused(
|
||||
"failed_transaction_staking_refused",
|
||||
"failed or uncommitted native Stake/Vote observations cannot create staking outputs",
|
||||
);
|
||||
@@ -201,7 +201,7 @@ impl crate::EventMaterializer for crate::StakingMaterializer {
|
||||
.cloned()
|
||||
.unwrap_or(serde_json::Value::Null);
|
||||
let operation = observation.event.event_name.0.clone();
|
||||
let output = crate::MaterializedOutput {
|
||||
let output = crate::MtApiMaterializedOutput {
|
||||
output_key: projection.output_key(operation.as_str()),
|
||||
family: projection.family(),
|
||||
payload_json: serde_json::json!({
|
||||
@@ -225,7 +225,7 @@ impl crate::EventMaterializer for crate::StakingMaterializer {
|
||||
}),
|
||||
};
|
||||
tracing::debug!(
|
||||
target: crate::MATERIALIZER_STAKING_TRACING_TARGET,
|
||||
target: crate::MT_STAKING_TRACING_TARGET,
|
||||
action = "materialize_staking",
|
||||
surface_code = %observation.event.surface_code.0.as_str(),
|
||||
entry_code = %observation.event.event_name.0.as_str(),
|
||||
@@ -233,15 +233,15 @@ impl crate::EventMaterializer for crate::StakingMaterializer {
|
||||
output_count = 1_usize,
|
||||
"materialize committed native staking observation"
|
||||
);
|
||||
return crate::MaterializerExecutionResult {
|
||||
status: crate::MaterializerOutcomeStatus::Inserted,
|
||||
return crate::MtApiMaterializerExecutionResult {
|
||||
status: crate::MtApiMaterializerOutcomeStatus::Inserted,
|
||||
outputs: std::vec![output],
|
||||
diagnostics: std::vec::Vec::new(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn family_is_accepted(family: crate::EventFamily) -> bool {
|
||||
fn family_is_accepted(family: crate::MdEventFamily) -> bool {
|
||||
return ACCEPTED_FAMILIES.iter().any(|accepted| return *accepted == family);
|
||||
}
|
||||
|
||||
@@ -593,25 +593,25 @@ mod tests {
|
||||
fn observation(
|
||||
surface_code: &str,
|
||||
entry_code: &str,
|
||||
family: crate::EventFamily,
|
||||
family: crate::MdEventFamily,
|
||||
transaction_failed: bool,
|
||||
accounts: serde_json::Value,
|
||||
parameters: serde_json::Value,
|
||||
) -> crate::DecodedObservation {
|
||||
return crate::DecodedObservation {
|
||||
) -> crate::DcApiDecodedObservation {
|
||||
return crate::DcApiDecodedObservation {
|
||||
event_key: format!("{entry_code}:0"),
|
||||
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-id".to_string()),
|
||||
protocol_code: crate::ProtocolCode("solana_native".to_string()),
|
||||
surface_code: crate::SurfaceCode(surface_code.to_string()),
|
||||
event_code: crate::EventCode(format!("{surface_code}.{entry_code}")),
|
||||
event_name: crate::EventName(entry_code.to_string()),
|
||||
event: crate::MdDecodedProtocolEvent {
|
||||
signature: crate::MdSignature("signature".to_string()),
|
||||
slot: crate::MdSlot(42),
|
||||
instruction_path: crate::MdInstructionPath("0".to_string()),
|
||||
program_id: crate::MdProgramId("program-id".to_string()),
|
||||
protocol_code: crate::MdProtocolCode("solana_native".to_string()),
|
||||
surface_code: crate::MdSurfaceCode(surface_code.to_string()),
|
||||
event_code: crate::MdEventCode(format!("{surface_code}.{entry_code}")),
|
||||
event_name: crate::MdEventName(entry_code.to_string()),
|
||||
event_family: family,
|
||||
source_kind: crate::EventSourceKind::Instruction,
|
||||
confidence: crate::DecoderConfidence::ManualExact,
|
||||
source_kind: crate::MdEventSourceKind::Instruction,
|
||||
confidence: crate::MdDecoderConfidence::ManualExact,
|
||||
},
|
||||
payload_json: serde_json::json!({"accounts": accounts, "parameters": parameters}),
|
||||
transaction_failed,
|
||||
@@ -621,9 +621,9 @@ mod tests {
|
||||
std::option::Option::None
|
||||
},
|
||||
observation_committed: !transaction_failed,
|
||||
proof: crate::DecoderProof {
|
||||
kind: crate::DecoderProofKind::Manual,
|
||||
confidence: crate::DecoderConfidence::ManualExact,
|
||||
proof: crate::DcApiDecoderProof {
|
||||
kind: crate::DcApiDecoderProofKind::Manual,
|
||||
confidence: crate::MdDecoderConfidence::ManualExact,
|
||||
evidence: std::vec!["fixture".to_string()],
|
||||
},
|
||||
};
|
||||
@@ -635,18 +635,18 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn stake_initialize_creates_instructional_staking_projection() {
|
||||
let materializer = crate::StakingMaterializer;
|
||||
let materializer = crate::MtStakingMaterializer;
|
||||
let observation = observation(
|
||||
super::STAKE_SURFACE,
|
||||
"initialize",
|
||||
crate::EventFamily::Lifecycle,
|
||||
crate::MdEventFamily::Lifecycle,
|
||||
false,
|
||||
serde_json::json!([account("stake_account", "stake111")]),
|
||||
serde_json::json!({"authorized": {"staker": "staker111"}}),
|
||||
);
|
||||
let result = crate::EventMaterializer::materialize(&materializer, &observation);
|
||||
assert_eq!(result.status, crate::MaterializerOutcomeStatus::Inserted);
|
||||
assert_eq!(result.outputs[0].family, crate::MaterializedEventFamily::Staking);
|
||||
let result = crate::MtApiEventMaterializer::materialize(&materializer, &observation);
|
||||
assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Inserted);
|
||||
assert_eq!(result.outputs[0].family, crate::MdMaterializedEventFamily::Staking);
|
||||
assert_eq!(result.outputs[0].output_key, "stake_account:initialize:0");
|
||||
assert_eq!(result.outputs[0].payload_json["projection"]["stakeAccount"], "stake111");
|
||||
assert_eq!(
|
||||
@@ -657,11 +657,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn stake_authority_and_lockup_events_are_projected() {
|
||||
let materializer = crate::StakingMaterializer;
|
||||
let materializer = crate::MtStakingMaterializer;
|
||||
let authority = observation(
|
||||
super::STAKE_SURFACE,
|
||||
"authorize_with_seed",
|
||||
crate::EventFamily::Admin,
|
||||
crate::MdEventFamily::Admin,
|
||||
false,
|
||||
serde_json::json!([account("stake_account", "stake111")]),
|
||||
serde_json::json!({
|
||||
@@ -674,7 +674,7 @@ mod tests {
|
||||
let lockup = observation(
|
||||
super::STAKE_SURFACE,
|
||||
"set_lockup_checked",
|
||||
crate::EventFamily::Admin,
|
||||
crate::MdEventFamily::Admin,
|
||||
false,
|
||||
serde_json::json!([
|
||||
account("stake_account", "stake111"),
|
||||
@@ -682,8 +682,9 @@ mod tests {
|
||||
]),
|
||||
serde_json::json!({"epoch": 42}),
|
||||
);
|
||||
let authority_result = crate::EventMaterializer::materialize(&materializer, &authority);
|
||||
let lockup_result = crate::EventMaterializer::materialize(&materializer, &lockup);
|
||||
let authority_result =
|
||||
crate::MtApiEventMaterializer::materialize(&materializer, &authority);
|
||||
let lockup_result = crate::MtApiEventMaterializer::materialize(&materializer, &lockup);
|
||||
assert_eq!(authority_result.outputs[0].output_key, "stake_authority:authorize_with_seed:0");
|
||||
assert_eq!(
|
||||
authority_result.outputs[0].payload_json["projection"]["newAuthority"],
|
||||
@@ -698,11 +699,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn stake_value_projection_does_not_duplicate_lamport_changes() {
|
||||
let materializer = crate::StakingMaterializer;
|
||||
let materializer = crate::MtStakingMaterializer;
|
||||
let observation = observation(
|
||||
super::STAKE_SURFACE,
|
||||
"withdraw",
|
||||
crate::EventFamily::Staking,
|
||||
crate::MdEventFamily::Staking,
|
||||
false,
|
||||
serde_json::json!([
|
||||
account("stake_account", "stake111"),
|
||||
@@ -710,7 +711,7 @@ mod tests {
|
||||
]),
|
||||
serde_json::json!({"lamports": 99}),
|
||||
);
|
||||
let result = crate::EventMaterializer::materialize(&materializer, &observation);
|
||||
let result = crate::MtApiEventMaterializer::materialize(&materializer, &observation);
|
||||
assert_eq!(result.outputs[0].output_key, "stake_value:withdraw:0");
|
||||
assert_eq!(result.outputs[0].payload_json["projection"]["lamports"], 99);
|
||||
assert_eq!(
|
||||
@@ -721,11 +722,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn vote_account_and_authority_events_are_projected() {
|
||||
let materializer = crate::StakingMaterializer;
|
||||
let materializer = crate::MtStakingMaterializer;
|
||||
let initialize = observation(
|
||||
super::VOTE_SURFACE,
|
||||
"initialize_account_v2",
|
||||
crate::EventFamily::Lifecycle,
|
||||
crate::MdEventFamily::Lifecycle,
|
||||
false,
|
||||
serde_json::json!([
|
||||
account("vote_account", "vote111"),
|
||||
@@ -740,7 +741,7 @@ mod tests {
|
||||
let authorize = observation(
|
||||
super::VOTE_SURFACE,
|
||||
"authorize_checked",
|
||||
crate::EventFamily::Admin,
|
||||
crate::MdEventFamily::Admin,
|
||||
false,
|
||||
serde_json::json!([
|
||||
account("vote_account", "vote111"),
|
||||
@@ -748,8 +749,10 @@ mod tests {
|
||||
]),
|
||||
serde_json::json!({"authorizationType": {"kind": "voter"}, "newAuthorityFromAccount": true}),
|
||||
);
|
||||
let initialize_result = crate::EventMaterializer::materialize(&materializer, &initialize);
|
||||
let authorize_result = crate::EventMaterializer::materialize(&materializer, &authorize);
|
||||
let initialize_result =
|
||||
crate::MtApiEventMaterializer::materialize(&materializer, &initialize);
|
||||
let authorize_result =
|
||||
crate::MtApiEventMaterializer::materialize(&materializer, &authorize);
|
||||
assert_eq!(initialize_result.outputs[0].output_key, "vote_account:initialize_account_v2:0");
|
||||
assert_eq!(
|
||||
initialize_result.outputs[0].payload_json["projection"]["validatorIdentity"],
|
||||
@@ -764,11 +767,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn vote_state_and_reward_outputs_are_distinguished() {
|
||||
let materializer = crate::StakingMaterializer;
|
||||
let materializer = crate::MtStakingMaterializer;
|
||||
let vote_state = observation(
|
||||
super::VOTE_SURFACE,
|
||||
"tower_sync_switch",
|
||||
crate::EventFamily::Staking,
|
||||
crate::MdEventFamily::Staking,
|
||||
false,
|
||||
serde_json::json!([
|
||||
account("vote_account", "vote111"),
|
||||
@@ -779,7 +782,7 @@ mod tests {
|
||||
let reward = observation(
|
||||
super::VOTE_SURFACE,
|
||||
"deposit_delegator_rewards",
|
||||
crate::EventFamily::Reward,
|
||||
crate::MdEventFamily::Reward,
|
||||
false,
|
||||
serde_json::json!([
|
||||
account("vote_account", "vote111"),
|
||||
@@ -787,40 +790,40 @@ mod tests {
|
||||
]),
|
||||
serde_json::json!({"depositLamports": 123}),
|
||||
);
|
||||
let vote_result = crate::EventMaterializer::materialize(&materializer, &vote_state);
|
||||
let reward_result = crate::EventMaterializer::materialize(&materializer, &reward);
|
||||
let vote_result = crate::MtApiEventMaterializer::materialize(&materializer, &vote_state);
|
||||
let reward_result = crate::MtApiEventMaterializer::materialize(&materializer, &reward);
|
||||
assert_eq!(vote_result.outputs[0].output_key, "vote_state:tower_sync_switch:0");
|
||||
assert_eq!(
|
||||
vote_result.outputs[0].payload_json["projection"]["switchProofHash"],
|
||||
"switch111"
|
||||
);
|
||||
assert_eq!(reward_result.outputs[0].family, crate::MaterializedEventFamily::Reward);
|
||||
assert_eq!(reward_result.outputs[0].family, crate::MdMaterializedEventFamily::Reward);
|
||||
assert_eq!(reward_result.outputs[0].output_key, "vote_reward:deposit_delegator_rewards:0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_staking_mutation_is_refused() {
|
||||
let materializer = crate::StakingMaterializer;
|
||||
let materializer = crate::MtStakingMaterializer;
|
||||
let observation = observation(
|
||||
super::STAKE_SURFACE,
|
||||
"delegate_stake",
|
||||
crate::EventFamily::Staking,
|
||||
crate::MdEventFamily::Staking,
|
||||
true,
|
||||
serde_json::json!([account("stake_account", "stake111")]),
|
||||
serde_json::json!({}),
|
||||
);
|
||||
let result = crate::EventMaterializer::materialize(&materializer, &observation);
|
||||
assert_eq!(result.status, crate::MaterializerOutcomeStatus::Refused);
|
||||
let result = crate::MtApiEventMaterializer::materialize(&materializer, &observation);
|
||||
assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Refused);
|
||||
assert!(result.outputs.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unrelated_or_disabled_observations_are_ignored() {
|
||||
let materializer = crate::StakingMaterializer;
|
||||
let materializer = crate::MtStakingMaterializer;
|
||||
let unrelated = observation(
|
||||
"solana_native_system",
|
||||
"create_account",
|
||||
crate::EventFamily::Lifecycle,
|
||||
crate::MdEventFamily::Lifecycle,
|
||||
false,
|
||||
serde_json::json!([]),
|
||||
serde_json::json!({}),
|
||||
@@ -828,30 +831,32 @@ mod tests {
|
||||
let redelegate = observation(
|
||||
super::STAKE_SURFACE,
|
||||
"redelegate",
|
||||
crate::EventFamily::Staking,
|
||||
crate::MdEventFamily::Staking,
|
||||
false,
|
||||
serde_json::json!([]),
|
||||
serde_json::json!({"officiallyDisabled": true}),
|
||||
);
|
||||
let unrelated_result = crate::EventMaterializer::materialize(&materializer, &unrelated);
|
||||
let redelegate_result = crate::EventMaterializer::materialize(&materializer, &redelegate);
|
||||
assert_eq!(unrelated_result.status, crate::MaterializerOutcomeStatus::Ignored);
|
||||
assert_eq!(redelegate_result.status, crate::MaterializerOutcomeStatus::Ignored);
|
||||
let unrelated_result =
|
||||
crate::MtApiEventMaterializer::materialize(&materializer, &unrelated);
|
||||
let redelegate_result =
|
||||
crate::MtApiEventMaterializer::materialize(&materializer, &redelegate);
|
||||
assert_eq!(unrelated_result.status, crate::MtApiMaterializerOutcomeStatus::Ignored);
|
||||
assert_eq!(redelegate_result.status, crate::MtApiMaterializerOutcomeStatus::Ignored);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn output_serialization_is_deterministic() {
|
||||
let materializer = crate::StakingMaterializer;
|
||||
let materializer = crate::MtStakingMaterializer;
|
||||
let observation = observation(
|
||||
super::VOTE_SURFACE,
|
||||
"update_commission_bps",
|
||||
crate::EventFamily::Admin,
|
||||
crate::MdEventFamily::Admin,
|
||||
false,
|
||||
serde_json::json!([account("vote_account", "vote111")]),
|
||||
serde_json::json!({"commissionBasisPoints": 250, "commissionKind": "inflation_rewards"}),
|
||||
);
|
||||
let first = crate::EventMaterializer::materialize(&materializer, &observation);
|
||||
let second = crate::EventMaterializer::materialize(&materializer, &observation);
|
||||
let first = crate::MtApiEventMaterializer::materialize(&materializer, &observation);
|
||||
let second = crate::MtApiEventMaterializer::materialize(&materializer, &observation);
|
||||
let first_json_result = serde_json::to_string(&first.outputs[0].payload_json);
|
||||
let first_json = match first_json_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
|
||||
@@ -1,7 +1,20 @@
|
||||
// file: kb-lib/src/materializer/token.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! `token` materializer family.
|
||||
|
||||
pub mod accounts;
|
||||
pub mod metadata_risk;
|
||||
mod accounts;
|
||||
mod constants;
|
||||
mod metadata_risk;
|
||||
|
||||
/// Stable processor name for Token, ATA and Token-2022 account projections.
|
||||
pub(crate) use self::constants::MT_TOKEN_ACCOUNTS_PROCESSOR_NAME;
|
||||
/// Stable projection contract version for Token, ATA and Token-2022 account projections.
|
||||
pub(crate) use self::constants::MT_TOKEN_ACCOUNTS_PROJECTION_VERSION;
|
||||
/// Canonical tracing target for Token, ATA and Token-2022 account projections.
|
||||
pub(crate) use self::constants::MT_TOKEN_ACCOUNTS_TRACING_TARGET;
|
||||
|
||||
/// Stable Token, ATA and Token-2022 account materializer.
|
||||
pub use self::accounts::MtTokenAccountsMaterializer;
|
||||
/// Materializes one bounded Token-2022 account state snapshot.
|
||||
pub use self::accounts::materialize_token2022_state_snapshot;
|
||||
|
||||
@@ -1,10 +1,831 @@
|
||||
// file: kb-lib/src/materializer/token/accounts.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! Migration boundary for legacy crate `kb_materializer_token_accounts`.
|
||||
//! Stable committed SPL Token mutation and ATA lifecycle projections.
|
||||
|
||||
/// Legacy crate name retained for migration and compatibility tracking.
|
||||
pub const LEGACY_CRATE: &str = "kb_materializer_token_accounts";
|
||||
const ACCEPTED_FAMILIES: &[crate::MdEventFamily] = &[
|
||||
crate::MdEventFamily::TokenAccount,
|
||||
crate::MdEventFamily::TokenMint,
|
||||
crate::MdEventFamily::TokenBurn,
|
||||
crate::MdEventFamily::Lifecycle,
|
||||
];
|
||||
|
||||
/// Current porting status.
|
||||
pub const MIGRATION_STATUS: &str = "source-preserved-pending-port";
|
||||
const MUTATION_ENTRIES: &[&str] = &[
|
||||
"initialize_mint",
|
||||
"initialize_mint2",
|
||||
"initialize_account",
|
||||
"initialize_account2",
|
||||
"initialize_account3",
|
||||
"transfer",
|
||||
"transfer_checked",
|
||||
"approve",
|
||||
"approve_checked",
|
||||
"revoke",
|
||||
"mint_to",
|
||||
"mint_to_checked",
|
||||
"burn",
|
||||
"burn_checked",
|
||||
"close_account",
|
||||
"freeze_account",
|
||||
"thaw_account",
|
||||
"sync_native",
|
||||
"initialize_immutable_owner",
|
||||
"withdraw_excess_lamports",
|
||||
"unwrap_lamports",
|
||||
"initialize_non_transferable_mint",
|
||||
"reallocate",
|
||||
];
|
||||
const ATA_LIFECYCLE_ENTRIES: &[(&str, &str)] = &[
|
||||
("create", "ata_created"),
|
||||
("create_idempotent", "ata_created_or_reused_idempotently"),
|
||||
("recover_nested", "nested_ata_recovered"),
|
||||
];
|
||||
|
||||
/// Stable committed classic SPL Token account and mint mutation materializer.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct MtTokenAccountsMaterializer;
|
||||
|
||||
impl crate::MtMaterializer for crate::MtTokenAccountsMaterializer {
|
||||
fn materializer_name(&self) -> &'static str {
|
||||
return "kb_materializer_token_accounts";
|
||||
}
|
||||
|
||||
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::MtTokenAccountsMaterializer {
|
||||
fn identity(&self) -> crate::MtApiMaterializerIdentity {
|
||||
return crate::MtApiMaterializerIdentity {
|
||||
name: crate::MT_TOKEN_ACCOUNTS_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_token_account_projection_refused",
|
||||
"failed or uncommitted SPL Token or ATA observations cannot create mutable outputs",
|
||||
);
|
||||
}
|
||||
if is_ata_event(&observation.event) {
|
||||
return materialize_ata(observation);
|
||||
}
|
||||
let parameters = observation
|
||||
.payload_json
|
||||
.get("parameters")
|
||||
.cloned()
|
||||
.unwrap_or(serde_json::Value::Null);
|
||||
let accounts = observation
|
||||
.payload_json
|
||||
.get("accounts")
|
||||
.cloned()
|
||||
.unwrap_or(serde_json::Value::Null);
|
||||
let authority = observation
|
||||
.payload_json
|
||||
.get("authority")
|
||||
.cloned()
|
||||
.unwrap_or(serde_json::Value::Null);
|
||||
let operation = observation.event.event_name.0.clone();
|
||||
let output = crate::MtApiMaterializedOutput {
|
||||
output_key: format!("token_account_mutation:{operation}:0"),
|
||||
family: crate::MdMaterializedEventFamily::TokenAccount,
|
||||
payload_json: serde_json::json!({
|
||||
"projectionVersion": crate::MT_TOKEN_ACCOUNTS_PROJECTION_VERSION,
|
||||
"domain": "spl_token_account_mutation",
|
||||
"projectionSemantics": "committed_instruction_mutation_not_final_account_snapshot",
|
||||
"idempotenceKey": format!(
|
||||
"spl_token:{}:{}:{}",
|
||||
observation.event.signature.0,
|
||||
observation.event.instruction_path.0,
|
||||
operation
|
||||
),
|
||||
"operation": operation,
|
||||
"signature": observation.event.signature.0.clone(),
|
||||
"slot": observation.event.slot.0,
|
||||
"instructionPath": observation.event.instruction_path.0.clone(),
|
||||
"programId": observation.event.program_id.0.clone(),
|
||||
"mint": account_key_for_role(&accounts, "mint"),
|
||||
"amountRaw": parameters.get("amountRaw").cloned().unwrap_or(serde_json::Value::Null),
|
||||
"decimals": parameters.get("decimals").cloned().unwrap_or(serde_json::Value::Null),
|
||||
"parameters": parameters,
|
||||
"accounts": accounts,
|
||||
"authority": authority,
|
||||
"committed": true,
|
||||
"transactionFinalAccountStateCaptured": false,
|
||||
"coreBalanceChangesDuplicated": false,
|
||||
"provenance": {
|
||||
"processorName": crate::MT_TOKEN_ACCOUNTS_PROCESSOR_NAME,
|
||||
"processorVersion": env!("CARGO_PKG_VERSION"),
|
||||
"sourceSurface": observation.event.surface_code.0.clone(),
|
||||
"sourceEventCode": observation.event.event_code.0.clone(),
|
||||
"sourceEventKey": observation.event_key.clone()
|
||||
}
|
||||
}),
|
||||
};
|
||||
tracing::debug!(
|
||||
target: crate::MT_TOKEN_ACCOUNTS_TRACING_TARGET,
|
||||
action = "materialize_token_account_mutation",
|
||||
signature = %observation.event.signature.0,
|
||||
instruction_path = %observation.event.instruction_path.0,
|
||||
operation = %observation.event.event_name.0,
|
||||
"materialize committed classic SPL Token mutation"
|
||||
);
|
||||
return crate::MtApiMaterializerExecutionResult {
|
||||
status: crate::MtApiMaterializerOutcomeStatus::Inserted,
|
||||
outputs: std::vec![output],
|
||||
diagnostics: std::vec::Vec::new(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn accepts_event(event: &crate::MdDecodedProtocolEvent) -> bool {
|
||||
let token_program = (event.program_id.0 == kb_program_ids::SPL_TOKEN_PROGRAM_ID
|
||||
&& event.surface_code.0 == "spl_token")
|
||||
|| (event.program_id.0 == kb_program_ids::SPL_TOKEN2022_PROGRAM_ID
|
||||
&& event.surface_code.0 == "spl_token2022");
|
||||
let token_mutation = token_program && MUTATION_ENTRIES.contains(&event.event_name.0.as_str());
|
||||
let ata = is_ata_event(event);
|
||||
return ACCEPTED_FAMILIES.contains(&event.event_family) && (token_mutation || ata);
|
||||
}
|
||||
|
||||
fn is_ata_event(event: &crate::MdDecodedProtocolEvent) -> bool {
|
||||
return event.program_id.0 == kb_program_ids::ASSOCIATED_TOKEN_PROGRAM_ID
|
||||
&& event.surface_code.0 == "spl_associated_token_account"
|
||||
&& event.event_family == crate::MdEventFamily::Lifecycle
|
||||
&& ATA_LIFECYCLE_ENTRIES
|
||||
.iter()
|
||||
.any(|(entry, _)| return *entry == event.event_name.0);
|
||||
}
|
||||
|
||||
fn materialize_ata(
|
||||
observation: &crate::DcApiDecodedObservation,
|
||||
) -> crate::MtApiMaterializerExecutionResult {
|
||||
let operation = observation.event.event_name.0.as_str();
|
||||
let addresses = match observation.payload_json.get("addresses") {
|
||||
std::option::Option::Some(value) if value.is_object() => value,
|
||||
_ => {
|
||||
return crate::MtApiMaterializerExecutionResult::refused(
|
||||
"ata_addresses_missing",
|
||||
"committed ATA lifecycle projection requires the decoded address contract",
|
||||
);
|
||||
},
|
||||
};
|
||||
let validation = validate_ata_addresses(operation, addresses);
|
||||
if let std::result::Result::Err((code, message)) = validation {
|
||||
return crate::MtApiMaterializerExecutionResult::refused(code, message);
|
||||
}
|
||||
let lifecycle_kind =
|
||||
match ATA_LIFECYCLE_ENTRIES.iter().find(|(entry, _)| return *entry == operation) {
|
||||
std::option::Option::Some((_, value)) => *value,
|
||||
std::option::Option::None => {
|
||||
return crate::MtApiMaterializerExecutionResult::ignored();
|
||||
},
|
||||
};
|
||||
let associated_token_account = if operation == "recover_nested" {
|
||||
observed_address(addresses, "nestedAssociatedTokenAccount")
|
||||
} else {
|
||||
observed_address(addresses, "associatedTokenAccount")
|
||||
};
|
||||
let token_program = addresses
|
||||
.get("tokenProgram")
|
||||
.and_then(|value| return value.get("programId"))
|
||||
.cloned()
|
||||
.unwrap_or(serde_json::Value::Null);
|
||||
let wallet = addresses.get("walletOwner").cloned().unwrap_or(serde_json::Value::Null);
|
||||
let mint = if operation == "recover_nested" {
|
||||
addresses.get("nestedMint").cloned().unwrap_or(serde_json::Value::Null)
|
||||
} else {
|
||||
addresses.get("mint").cloned().unwrap_or(serde_json::Value::Null)
|
||||
};
|
||||
let output = crate::MtApiMaterializedOutput {
|
||||
output_key: format!("ata_lifecycle:{lifecycle_kind}:0"),
|
||||
family: crate::MdMaterializedEventFamily::TokenAccount,
|
||||
payload_json: serde_json::json!({
|
||||
"projectionVersion": crate::MT_TOKEN_ACCOUNTS_PROJECTION_VERSION,
|
||||
"domain": "spl_associated_token_account_lifecycle",
|
||||
"projectionSemantics": "committed_associated_address_lifecycle_not_token_state_snapshot",
|
||||
"idempotenceKey": format!(
|
||||
"ata:{}:{}:{}:{}:{}",
|
||||
observation.event.signature.0,
|
||||
observation.event.instruction_path.0,
|
||||
observation.event.program_id.0,
|
||||
lifecycle_kind,
|
||||
associated_token_account.as_str().unwrap_or("")
|
||||
),
|
||||
"operation": operation,
|
||||
"lifecycleKind": lifecycle_kind,
|
||||
"associatedTokenAccount": associated_token_account,
|
||||
"walletOwner": wallet,
|
||||
"mint": mint,
|
||||
"tokenProgramId": token_program,
|
||||
"fundingAccount": addresses.get("fundingAccount").cloned().unwrap_or(serde_json::Value::Null),
|
||||
"ownerMint": addresses.get("ownerMint").cloned().unwrap_or(serde_json::Value::Null),
|
||||
"nestedMint": addresses.get("nestedMint").cloned().unwrap_or(serde_json::Value::Null),
|
||||
"ownerAssociatedTokenAccount": observed_address(addresses, "ownerAssociatedTokenAccount"),
|
||||
"nestedAssociatedTokenAccount": observed_address(addresses, "nestedAssociatedTokenAccount"),
|
||||
"walletNestedMintAssociatedTokenAccount": observed_address(addresses, "walletNestedMintAssociatedTokenAccount"),
|
||||
"idempotentOutcome": if operation == "create_idempotent" {
|
||||
"created_or_reused_not_distinguishable_from_parent_instruction_alone"
|
||||
} else {
|
||||
"not_applicable"
|
||||
},
|
||||
"signature": observation.event.signature.0.clone(),
|
||||
"slot": observation.event.slot.0,
|
||||
"instructionPath": observation.event.instruction_path.0.clone(),
|
||||
"programId": observation.event.program_id.0.clone(),
|
||||
"committed": true,
|
||||
"transactionFinalAccountStateCaptured": false,
|
||||
"tokenBalanceReconstructed": false,
|
||||
"splTokenCpiMutationsDuplicated": false,
|
||||
"ownership": {
|
||||
"ataParent": "associated_address_lifecycle_only",
|
||||
"splTokenCpi": "initialize_transfer_and_close_mutations"
|
||||
},
|
||||
"provenance": {
|
||||
"processorName": crate::MT_TOKEN_ACCOUNTS_PROCESSOR_NAME,
|
||||
"processorVersion": env!("CARGO_PKG_VERSION"),
|
||||
"sourceSurface": observation.event.surface_code.0.clone(),
|
||||
"sourceEventCode": observation.event.event_code.0.clone(),
|
||||
"sourceEventKey": observation.event_key.clone()
|
||||
}
|
||||
}),
|
||||
};
|
||||
tracing::debug!(
|
||||
target: crate::MT_TOKEN_ACCOUNTS_TRACING_TARGET,
|
||||
action = "materialize_ata_lifecycle",
|
||||
signature = %observation.event.signature.0,
|
||||
instruction_path = %observation.event.instruction_path.0,
|
||||
operation,
|
||||
lifecycle_kind,
|
||||
"materialize committed ATA lifecycle without duplicating SPL Token CPI mutations"
|
||||
);
|
||||
return crate::MtApiMaterializerExecutionResult {
|
||||
status: crate::MtApiMaterializerOutcomeStatus::Inserted,
|
||||
outputs: std::vec![output],
|
||||
diagnostics: std::vec::Vec::new(),
|
||||
};
|
||||
}
|
||||
|
||||
fn validate_ata_addresses(
|
||||
operation: &str,
|
||||
addresses: &serde_json::Value,
|
||||
) -> std::result::Result<(), (&'static str, &'static str)> {
|
||||
let token_supported = addresses
|
||||
.get("tokenProgram")
|
||||
.and_then(|value| return value.get("supported"))
|
||||
.and_then(serde_json::Value::as_bool)
|
||||
== std::option::Option::Some(true);
|
||||
let token_program_id = addresses
|
||||
.get("tokenProgram")
|
||||
.and_then(|value| return value.get("programId"))
|
||||
.and_then(serde_json::Value::as_str);
|
||||
let token_program_exact = matches!(
|
||||
token_program_id,
|
||||
std::option::Option::Some(kb_program_ids::SPL_TOKEN_PROGRAM_ID)
|
||||
| std::option::Option::Some(kb_program_ids::SPL_TOKEN2022_PROGRAM_ID)
|
||||
);
|
||||
if !token_supported || !token_program_exact {
|
||||
return std::result::Result::Err((
|
||||
"ata_token_program_not_supported",
|
||||
"ATA lifecycle projection requires classic SPL Token or Token-2022",
|
||||
));
|
||||
}
|
||||
let fields: &[&str] = if operation == "recover_nested" {
|
||||
&[
|
||||
"ownerAssociatedTokenAccount",
|
||||
"nestedAssociatedTokenAccount",
|
||||
"walletNestedMintAssociatedTokenAccount",
|
||||
]
|
||||
} else {
|
||||
&["associatedTokenAccount"]
|
||||
};
|
||||
for field in fields {
|
||||
let valid = addresses
|
||||
.get(*field)
|
||||
.and_then(|value| return value.get("valid"))
|
||||
.and_then(serde_json::Value::as_bool)
|
||||
== std::option::Option::Some(true);
|
||||
let observed = observed_address(addresses, field);
|
||||
if !valid || observed.as_str().is_none_or(|value| return value.is_empty()) {
|
||||
return std::result::Result::Err((
|
||||
"ata_derived_address_invalid",
|
||||
"ATA lifecycle projection requires every canonical address validation to pass",
|
||||
));
|
||||
}
|
||||
}
|
||||
let identity_fields: &[&str] = if operation == "recover_nested" {
|
||||
&["walletOwner", "ownerMint", "nestedMint"]
|
||||
} else {
|
||||
&["walletOwner", "mint"]
|
||||
};
|
||||
if identity_fields.iter().any(|field| {
|
||||
return addresses
|
||||
.get(*field)
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.is_none_or(|value| return value.is_empty());
|
||||
}) {
|
||||
return std::result::Result::Err((
|
||||
"ata_identity_field_missing",
|
||||
"ATA lifecycle projection requires wallet and mint identities",
|
||||
));
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
fn observed_address(addresses: &serde_json::Value, field: &str) -> serde_json::Value {
|
||||
return addresses
|
||||
.get(field)
|
||||
.and_then(|value| return value.get("observed"))
|
||||
.cloned()
|
||||
.unwrap_or(serde_json::Value::Null);
|
||||
}
|
||||
|
||||
fn account_key_for_role(accounts: &serde_json::Value, role: &str) -> serde_json::Value {
|
||||
let value = accounts.as_array().and_then(|rows| {
|
||||
return rows.iter().find_map(|row| {
|
||||
if row.get("role").and_then(serde_json::Value::as_str)
|
||||
!= std::option::Option::Some(role)
|
||||
{
|
||||
return std::option::Option::None;
|
||||
}
|
||||
return row.get("accountKey").and_then(serde_json::Value::as_str);
|
||||
});
|
||||
});
|
||||
return match value {
|
||||
std::option::Option::Some(account_key) => {
|
||||
serde_json::Value::String(account_key.to_string())
|
||||
},
|
||||
std::option::Option::None => serde_json::Value::Null,
|
||||
};
|
||||
}
|
||||
|
||||
/// Materialize one already parsed Token-2022 account snapshot without inferring hidden state.
|
||||
pub fn materialize_token2022_state_snapshot(
|
||||
account_key: &str,
|
||||
slot: u64,
|
||||
state: &crate::DcToken2022State,
|
||||
) -> std::result::Result<crate::MtApiMaterializedOutput, String> {
|
||||
if account_key.is_empty() {
|
||||
return std::result::Result::Err(
|
||||
"Token-2022 state snapshot requires a non-empty account key".to_string(),
|
||||
);
|
||||
}
|
||||
let kind = match state.kind {
|
||||
crate::DcToken2022StateKind::Mint => "mint",
|
||||
crate::DcToken2022StateKind::Account => "account",
|
||||
crate::DcToken2022StateKind::Multisig => "multisig",
|
||||
};
|
||||
let extensions = state
|
||||
.extensions
|
||||
.iter()
|
||||
.map(|entry| {
|
||||
return serde_json::json!({
|
||||
"extensionType": entry.extension_type,
|
||||
"extensionName": entry.extension_name,
|
||||
"valueHex": entry.value_hex,
|
||||
"valueFields": entry.value_fields,
|
||||
});
|
||||
})
|
||||
.collect::<std::vec::Vec<_>>();
|
||||
let extension_names = state
|
||||
.extensions
|
||||
.iter()
|
||||
.map(|entry| return entry.extension_name)
|
||||
.collect::<std::vec::Vec<_>>();
|
||||
let contains_confidential_state = state.extensions.iter().any(|entry| {
|
||||
return entry.extension_name.starts_with("confidential_")
|
||||
|| entry.extension_name == "confidential_mint_burn";
|
||||
});
|
||||
return std::result::Result::Ok(crate::MtApiMaterializedOutput {
|
||||
output_key: format!("token2022_state_snapshot:{account_key}:{slot}"),
|
||||
family: crate::MdMaterializedEventFamily::TokenAccount,
|
||||
payload_json: serde_json::json!({
|
||||
"projectionVersion": crate::MT_TOKEN_ACCOUNTS_PROJECTION_VERSION,
|
||||
"domain": "spl_token2022_account_state",
|
||||
"projectionSemantics": "authoritative_bounded_account_snapshot",
|
||||
"idempotenceKey": format!("token2022_state:{account_key}:{slot}"),
|
||||
"accountKey": account_key,
|
||||
"slot": slot,
|
||||
"programId": kb_program_ids::SPL_TOKEN2022_PROGRAM_ID,
|
||||
"stateKind": kind,
|
||||
"baseFields": state.base_fields,
|
||||
"baseHex": state.base_hex,
|
||||
"accountType": state.account_type,
|
||||
"extensionCount": state.extensions.len(),
|
||||
"extensionNames": extension_names,
|
||||
"extensions": extensions,
|
||||
"containsConfidentialState": contains_confidential_state,
|
||||
"confidentialValuesDecrypted": false,
|
||||
"finalAccountStateCaptured": true,
|
||||
"provenance": {
|
||||
"processorName": crate::MT_TOKEN_ACCOUNTS_PROCESSOR_NAME,
|
||||
"processorVersion": env!("CARGO_PKG_VERSION"),
|
||||
"source": "bounded_token2022_state_parser"
|
||||
}
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
fn observation(
|
||||
entry: &str,
|
||||
family: crate::MdEventFamily,
|
||||
failed: bool,
|
||||
) -> crate::DcApiDecodedObservation {
|
||||
return crate::DcApiDecodedObservation {
|
||||
event_key: format!("token:{entry}:0"),
|
||||
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(kb_program_ids::SPL_TOKEN_PROGRAM_ID.to_string()),
|
||||
protocol_code: crate::MdProtocolCode("spl_token".to_string()),
|
||||
surface_code: crate::MdSurfaceCode("spl_token".to_string()),
|
||||
event_code: crate::MdEventCode(format!("spl_token.{entry}")),
|
||||
event_name: crate::MdEventName(entry.to_string()),
|
||||
event_family: family,
|
||||
source_kind: crate::MdEventSourceKind::InnerInstruction,
|
||||
confidence: crate::MdDecoderConfidence::ManualExact,
|
||||
},
|
||||
payload_json: serde_json::json!({
|
||||
"parameters": {"amountRaw":"18446744073709551615","decimals":9},
|
||||
"accounts": [{"role":"mint","accountKey":"mint111"}],
|
||||
"authority": {"form":"single"}
|
||||
}),
|
||||
transaction_failed: failed,
|
||||
transaction_error: if failed {
|
||||
std::option::Option::Some(serde_json::json!({"error":"failed"}))
|
||||
} else {
|
||||
std::option::Option::None
|
||||
},
|
||||
observation_committed: !failed,
|
||||
proof: crate::DcApiDecoderProof {
|
||||
kind: crate::DcApiDecoderProofKind::Manual,
|
||||
confidence: crate::MdDecoderConfidence::ManualExact,
|
||||
evidence: std::vec!["fixture".to_string()],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
fn token2022_observation(
|
||||
entry: &str,
|
||||
family: crate::MdEventFamily,
|
||||
failed: bool,
|
||||
) -> crate::DcApiDecodedObservation {
|
||||
let mut observation = observation(entry, family, failed);
|
||||
observation.event.program_id =
|
||||
crate::MdProgramId(kb_program_ids::SPL_TOKEN2022_PROGRAM_ID.to_string());
|
||||
observation.event.protocol_code = crate::MdProtocolCode("spl_token2022".to_string());
|
||||
observation.event.surface_code = crate::MdSurfaceCode("spl_token2022".to_string());
|
||||
observation.event.event_code = crate::MdEventCode(format!("spl_token2022.{entry}"));
|
||||
observation.event_key = format!("token2022:{entry}:0");
|
||||
return observation;
|
||||
}
|
||||
|
||||
fn ata_observation(entry: &str, failed: bool, valid: bool) -> crate::DcApiDecodedObservation {
|
||||
let addresses = if entry == "recover_nested" {
|
||||
serde_json::json!({
|
||||
"tokenProgram": {
|
||||
"programId": kb_program_ids::SPL_TOKEN_PROGRAM_ID,
|
||||
"kind": "spl_token_classic",
|
||||
"supported": true
|
||||
},
|
||||
"ownerAssociatedTokenAccount": {"expected":"ownerAta","observed":"ownerAta","valid":valid},
|
||||
"nestedAssociatedTokenAccount": {"expected":"nestedAta","observed":"nestedAta","valid":valid},
|
||||
"walletNestedMintAssociatedTokenAccount": {"expected":"destinationAta","observed":"destinationAta","valid":valid},
|
||||
"walletOwner": "wallet111",
|
||||
"ownerMint": "ownerMint111",
|
||||
"nestedMint": "nestedMint111"
|
||||
})
|
||||
} else {
|
||||
serde_json::json!({
|
||||
"tokenProgram": {
|
||||
"programId": kb_program_ids::SPL_TOKEN2022_PROGRAM_ID,
|
||||
"kind": "token2022",
|
||||
"supported": true
|
||||
},
|
||||
"associatedTokenAccount": {"expected":"ata111","observed":"ata111","valid":valid},
|
||||
"fundingAccount": "payer111",
|
||||
"walletOwner": "wallet111",
|
||||
"mint": "mint111"
|
||||
})
|
||||
};
|
||||
return crate::DcApiDecodedObservation {
|
||||
event_key: format!("ata:{entry}:0"),
|
||||
event: crate::MdDecodedProtocolEvent {
|
||||
signature: crate::MdSignature("signature".to_string()),
|
||||
slot: crate::MdSlot(42),
|
||||
instruction_path: crate::MdInstructionPath("0".to_string()),
|
||||
program_id: crate::MdProgramId(
|
||||
kb_program_ids::ASSOCIATED_TOKEN_PROGRAM_ID.to_string(),
|
||||
),
|
||||
protocol_code: crate::MdProtocolCode("spl_associated_token_account".to_string()),
|
||||
surface_code: crate::MdSurfaceCode("spl_associated_token_account".to_string()),
|
||||
event_code: crate::MdEventCode(format!("spl_associated_token_account.{entry}")),
|
||||
event_name: crate::MdEventName(entry.to_string()),
|
||||
event_family: crate::MdEventFamily::Lifecycle,
|
||||
source_kind: crate::MdEventSourceKind::Instruction,
|
||||
confidence: crate::MdDecoderConfidence::ManualExact,
|
||||
},
|
||||
payload_json: serde_json::json!({"addresses":addresses}),
|
||||
transaction_failed: failed,
|
||||
transaction_error: std::option::Option::None,
|
||||
observation_committed: !failed,
|
||||
proof: crate::DcApiDecoderProof {
|
||||
kind: crate::DcApiDecoderProofKind::Manual,
|
||||
confidence: crate::MdDecoderConfidence::ManualExact,
|
||||
evidence: std::vec!["fixture".to_string()],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn committed_checked_transfer_preserves_exact_amount_mint_and_path() {
|
||||
let observation =
|
||||
observation("transfer_checked", crate::MdEventFamily::TokenAccount, false);
|
||||
let result = crate::MtApiEventMaterializer::materialize(
|
||||
&crate::MtTokenAccountsMaterializer,
|
||||
&observation,
|
||||
);
|
||||
assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Inserted);
|
||||
assert_eq!(result.outputs[0].payload_json["amountRaw"], "18446744073709551615");
|
||||
assert_eq!(result.outputs[0].payload_json["mint"], "mint111");
|
||||
assert_eq!(result.outputs[0].payload_json["instructionPath"], "1/0");
|
||||
assert_eq!(result.outputs[0].payload_json["transactionFinalAccountStateCaptured"], false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn committed_token2022_transfer_has_the_same_single_projection_owner() {
|
||||
let observation =
|
||||
token2022_observation("transfer_checked", crate::MdEventFamily::TokenAccount, false);
|
||||
let result = crate::MtApiEventMaterializer::materialize(
|
||||
&crate::MtTokenAccountsMaterializer,
|
||||
&observation,
|
||||
);
|
||||
assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Inserted);
|
||||
assert_eq!(result.outputs.len(), 1);
|
||||
assert_eq!(result.outputs[0].payload_json["operation"], "transfer_checked");
|
||||
assert_eq!(
|
||||
result.outputs[0].payload_json["programId"],
|
||||
kb_program_ids::SPL_TOKEN2022_PROGRAM_ID
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_mutation_is_refused() {
|
||||
let observation = observation("burn", crate::MdEventFamily::TokenBurn, true);
|
||||
let result = crate::MtApiEventMaterializer::materialize(
|
||||
&crate::MtTokenAccountsMaterializer,
|
||||
&observation,
|
||||
);
|
||||
assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Refused);
|
||||
assert!(result.outputs.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_parent_admin_and_conversion_events_are_not_duplicated() {
|
||||
for (entry, family) in [
|
||||
("batch", crate::MdEventFamily::Audit),
|
||||
("set_authority", crate::MdEventFamily::Admin),
|
||||
("amount_to_ui_amount", crate::MdEventFamily::Audit),
|
||||
] {
|
||||
let observation = observation(entry, family, false);
|
||||
let result = crate::MtApiEventMaterializer::materialize(
|
||||
&crate::MtTokenAccountsMaterializer,
|
||||
&observation,
|
||||
);
|
||||
assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Ignored);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_ata_variant_has_one_exact_lifecycle_projection() {
|
||||
for (entry, expected_kind, expected_ata, expected_token_program) in [
|
||||
("create", "ata_created", "ata111", kb_program_ids::SPL_TOKEN2022_PROGRAM_ID),
|
||||
(
|
||||
"create_idempotent",
|
||||
"ata_created_or_reused_idempotently",
|
||||
"ata111",
|
||||
kb_program_ids::SPL_TOKEN2022_PROGRAM_ID,
|
||||
),
|
||||
(
|
||||
"recover_nested",
|
||||
"nested_ata_recovered",
|
||||
"nestedAta",
|
||||
kb_program_ids::SPL_TOKEN_PROGRAM_ID,
|
||||
),
|
||||
] {
|
||||
let result = crate::MtApiEventMaterializer::materialize(
|
||||
&crate::MtTokenAccountsMaterializer,
|
||||
&ata_observation(entry, false, true),
|
||||
);
|
||||
assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Inserted);
|
||||
assert_eq!(result.outputs.len(), 1);
|
||||
assert_eq!(result.outputs[0].payload_json["lifecycleKind"], expected_kind);
|
||||
assert_eq!(result.outputs[0].payload_json["associatedTokenAccount"], expected_ata);
|
||||
assert_eq!(result.outputs[0].payload_json["tokenProgramId"], expected_token_program);
|
||||
assert_eq!(result.outputs[0].payload_json["tokenBalanceReconstructed"], false);
|
||||
assert_eq!(result.outputs[0].payload_json["splTokenCpiMutationsDuplicated"], false);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn idempotent_parent_does_not_invent_created_or_reused_branch() {
|
||||
let result = crate::MtApiEventMaterializer::materialize(
|
||||
&crate::MtTokenAccountsMaterializer,
|
||||
&ata_observation("create_idempotent", false, true),
|
||||
);
|
||||
assert_eq!(
|
||||
result.outputs[0].payload_json["idempotentOutcome"],
|
||||
"created_or_reused_not_distinguishable_from_parent_instruction_alone"
|
||||
);
|
||||
assert!(result.outputs[0].payload_json.get("reused").is_none());
|
||||
assert!(result.outputs[0].payload_json.get("created").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn uncommitted_or_invalid_ata_lifecycle_is_refused() {
|
||||
for observation in [
|
||||
ata_observation("create", true, true),
|
||||
ata_observation("recover_nested", false, false),
|
||||
] {
|
||||
let result = crate::MtApiEventMaterializer::materialize(
|
||||
&crate::MtTokenAccountsMaterializer,
|
||||
&observation,
|
||||
);
|
||||
assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Refused);
|
||||
assert!(result.outputs.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ata_parent_and_classic_token_cpi_children_own_distinct_facts() {
|
||||
let parent = crate::MtApiEventMaterializer::materialize(
|
||||
&crate::MtTokenAccountsMaterializer,
|
||||
&ata_observation("recover_nested", false, true),
|
||||
);
|
||||
let transfer = crate::MtApiEventMaterializer::materialize(
|
||||
&crate::MtTokenAccountsMaterializer,
|
||||
&observation("transfer", crate::MdEventFamily::TokenAccount, false),
|
||||
);
|
||||
let close = crate::MtApiEventMaterializer::materialize(
|
||||
&crate::MtTokenAccountsMaterializer,
|
||||
&observation("close_account", crate::MdEventFamily::TokenAccount, false),
|
||||
);
|
||||
assert_eq!(parent.outputs[0].payload_json["lifecycleKind"], "nested_ata_recovered");
|
||||
assert_eq!(
|
||||
parent.outputs[0].payload_json["ownership"]["ataParent"],
|
||||
"associated_address_lifecycle_only"
|
||||
);
|
||||
assert!(parent.outputs[0].payload_json.get("amountRaw").is_none());
|
||||
assert_eq!(transfer.outputs[0].payload_json["operation"], "transfer");
|
||||
assert_eq!(close.outputs[0].payload_json["operation"], "close_account");
|
||||
assert_eq!(transfer.outputs[0].payload_json["domain"], "spl_token_account_mutation");
|
||||
assert_eq!(close.outputs[0].payload_json["domain"], "spl_token_account_mutation");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ata_matrix_and_compiled_token_account_projection_ownership_are_equal() {
|
||||
let matrix: serde_json::Value = match serde_json::from_str(include_str!(
|
||||
"../../../../docs/SPL_ASSOCIATED_TOKEN_ACCOUNT_MATRIX.json"
|
||||
)) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("ATA matrix is invalid: {error}"),
|
||||
};
|
||||
let instructions = match matrix.get("instructions").and_then(serde_json::Value::as_array) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => panic!("ATA matrix instructions are absent"),
|
||||
};
|
||||
for (entry, fact) in super::ATA_LIFECYCLE_ENTRIES {
|
||||
let row = match instructions.iter().find(|row| {
|
||||
return row.get("name").and_then(serde_json::Value::as_str)
|
||||
== std::option::Option::Some(*entry);
|
||||
}) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => panic!("ATA matrix entry {entry} is absent"),
|
||||
};
|
||||
let expected = format!("token_accounts:{fact}");
|
||||
assert!(row["authorizedProjections"].as_array().is_some_and(|values| {
|
||||
return values.iter().any(|value| return value == expected.as_str());
|
||||
}));
|
||||
}
|
||||
let owner = &matrix["materializationContract"]["projectionOwners"][0];
|
||||
let matrix_facts = match owner["facts"].as_array() {
|
||||
std::option::Option::Some(values) => values
|
||||
.iter()
|
||||
.filter_map(|value| return value.as_str())
|
||||
.collect::<std::vec::Vec<_>>(),
|
||||
std::option::Option::None => panic!("ATA token-account owner facts are absent"),
|
||||
};
|
||||
let compiled_facts = super::ATA_LIFECYCLE_ENTRIES
|
||||
.iter()
|
||||
.map(|(_, fact)| return *fact)
|
||||
.collect::<std::vec::Vec<_>>();
|
||||
assert_eq!(matrix_facts, compiled_facts);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn second_ata_materialization_is_byte_for_byte_idempotent() {
|
||||
let observation = ata_observation("create_idempotent", false, true);
|
||||
let first = crate::MtApiEventMaterializer::materialize(
|
||||
&crate::MtTokenAccountsMaterializer,
|
||||
&observation,
|
||||
);
|
||||
let second = crate::MtApiEventMaterializer::materialize(
|
||||
&crate::MtTokenAccountsMaterializer,
|
||||
&observation,
|
||||
);
|
||||
assert_eq!(first, second);
|
||||
assert_eq!(first.outputs[0].output_key, second.outputs[0].output_key);
|
||||
assert_eq!(
|
||||
first.outputs[0].payload_json["idempotenceKey"],
|
||||
second.outputs[0].payload_json["idempotenceKey"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn token2022_state_snapshot_preserves_base_and_ordered_extensions() {
|
||||
let state = crate::DcToken2022State {
|
||||
kind: crate::DcToken2022StateKind::Mint,
|
||||
base_fields: serde_json::json!({"supply":"42","decimals":9,"initialized":true}),
|
||||
base_hex: "00".repeat(82),
|
||||
account_type: std::option::Option::Some(1),
|
||||
extensions: std::vec![
|
||||
crate::DcToken2022TlvEntry {
|
||||
extension_type: 1,
|
||||
extension_name: "transfer_fee_config",
|
||||
value_hex: "11".repeat(108),
|
||||
value_fields: serde_json::json!({"withheldAmount":"7"}),
|
||||
},
|
||||
crate::DcToken2022TlvEntry {
|
||||
extension_type: 4,
|
||||
extension_name: "confidential_transfer_mint",
|
||||
value_hex: "22".repeat(65),
|
||||
value_fields: serde_json::json!({"autoApproveNewAccounts":true}),
|
||||
},
|
||||
],
|
||||
};
|
||||
let output = match crate::materialize_token2022_state_snapshot("mint111", 99, &state) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("snapshot failed: {error}"),
|
||||
};
|
||||
assert_eq!(output.payload_json["stateKind"], "mint");
|
||||
assert_eq!(output.payload_json["extensionCount"], 2);
|
||||
assert_eq!(output.payload_json["extensionNames"][0], "transfer_fee_config");
|
||||
assert_eq!(output.payload_json["extensionNames"][1], "confidential_transfer_mint");
|
||||
assert_eq!(output.payload_json["containsConfidentialState"], true);
|
||||
assert_eq!(output.payload_json["confidentialValuesDecrypted"], false);
|
||||
assert_eq!(output.payload_json["baseFields"]["supply"], "42");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn token2022_state_snapshot_is_idempotent_and_rejects_missing_identity() {
|
||||
let state = crate::DcToken2022State {
|
||||
kind: crate::DcToken2022StateKind::Account,
|
||||
base_fields: serde_json::json!({"mint":"mint111","owner":"wallet111","amount":"5"}),
|
||||
base_hex: "00".repeat(165),
|
||||
account_type: std::option::Option::None,
|
||||
extensions: std::vec::Vec::new(),
|
||||
};
|
||||
let first = crate::materialize_token2022_state_snapshot("account111", 7, &state);
|
||||
let second = crate::materialize_token2022_state_snapshot("account111", 7, &state);
|
||||
assert_eq!(first, second);
|
||||
assert!(crate::materialize_token2022_state_snapshot("", 7, &state).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
11
kb-lib/src/materializer/token/constants.rs
Normal file
11
kb-lib/src/materializer/token/constants.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
// file: kb-lib/src/materializer/token/constants.rs
|
||||
// version: 1
|
||||
|
||||
//! Local constants for classic SPL Token account projections.
|
||||
|
||||
/// Stable processor name persisted by the replay ledger.
|
||||
pub(crate) const MT_TOKEN_ACCOUNTS_PROCESSOR_NAME: &str = "spl_token_accounts";
|
||||
/// Stable projection payload contract version.
|
||||
pub(crate) const MT_TOKEN_ACCOUNTS_PROJECTION_VERSION: u32 = 3;
|
||||
/// Canonical tracing target for this component.
|
||||
pub(crate) const MT_TOKEN_ACCOUNTS_TRACING_TARGET: &str = "kb-lib.materializer.token";
|
||||
@@ -1,10 +1,10 @@
|
||||
// file: kb-lib/src/materializer/token/metadata_risk.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! Migration boundary for legacy crate `kb_materializer_token_metadata_risk`.
|
||||
|
||||
/// Legacy crate name retained for migration and compatibility tracking.
|
||||
pub const LEGACY_CRATE: &str = "kb_materializer_token_metadata_risk";
|
||||
const LEGACY_CRATE: &str = "kb_materializer_token_metadata_risk";
|
||||
|
||||
/// Current porting status.
|
||||
pub const MIGRATION_STATUS: &str = "source-preserved-pending-port";
|
||||
const MIGRATION_STATUS: &str = "source-preserved-pending-port";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// file: kb-lib/src/materializer/trades.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! `trades` materializer family.
|
||||
|
||||
pub mod core;
|
||||
mod core;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// file: kb-lib/src/materializer/trades/core.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! Migration boundary for legacy crate `kb_materializer_trades`.
|
||||
|
||||
/// Legacy crate name retained for migration and compatibility tracking.
|
||||
pub const LEGACY_CRATE: &str = "kb_materializer_trades";
|
||||
const LEGACY_CRATE: &str = "kb_materializer_trades";
|
||||
|
||||
/// Current porting status.
|
||||
pub const MIGRATION_STATUS: &str = "source-preserved-pending-port";
|
||||
const MIGRATION_STATUS: &str = "source-preserved-pending-port";
|
||||
|
||||
@@ -1,6 +1,19 @@
|
||||
// file: kb-lib/src/materializer/transaction.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! `transaction` materializer family.
|
||||
|
||||
pub mod annotations;
|
||||
mod annotations;
|
||||
mod constants;
|
||||
|
||||
/// Maximum Memo payload accepted by transaction annotations.
|
||||
pub(crate) use self::constants::MT_TRANSACTION_ANNOTATIONS_MAX_MEMO_PAYLOAD_BYTES;
|
||||
/// Stable processor name for transaction annotations.
|
||||
pub(crate) use self::constants::MT_TRANSACTION_ANNOTATIONS_PROCESSOR_NAME;
|
||||
/// Stable projection contract version for transaction annotations.
|
||||
pub(crate) use self::constants::MT_TRANSACTION_ANNOTATIONS_PROJECTION_VERSION;
|
||||
/// Canonical tracing target for transaction annotations.
|
||||
pub(crate) use self::constants::MT_TRANSACTION_ANNOTATIONS_TRACING_TARGET;
|
||||
|
||||
/// Stable committed Memo transaction annotation materializer.
|
||||
pub use self::annotations::MtTransactionAnnotationMaterializer;
|
||||
|
||||
@@ -1,10 +1,614 @@
|
||||
// file: kb-lib/src/materializer/transaction/annotations.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! Migration boundary for legacy crate `kb_materializer_transaction_annotations`.
|
||||
//! Exact committed SPL Memo transaction annotation projection.
|
||||
|
||||
/// Legacy crate name retained for migration and compatibility tracking.
|
||||
pub const LEGACY_CRATE: &str = "kb_materializer_transaction_annotations";
|
||||
const ACCEPTED_FAMILIES: &[crate::MdEventFamily] = &[crate::MdEventFamily::Audit];
|
||||
const MEMO_ENTRY_NAMES: &[&str] = &["add_memo", "memo_intent", "invalid_memo_attempt"];
|
||||
|
||||
/// Current porting status.
|
||||
pub const MIGRATION_STATUS: &str = "source-preserved-pending-port";
|
||||
#[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::MT_TRANSACTION_ANNOTATIONS_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 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_materializer_transaction_annotations"
|
||||
);
|
||||
assert_eq!(
|
||||
matrix["materializationContract"]["transactionPolicy"],
|
||||
"SuccessfulCommittedOnly"
|
||||
);
|
||||
assert_eq!(matrix["materializationContract"]["failedOrUncommittedOutput"], false);
|
||||
}
|
||||
}
|
||||
|
||||
14
kb-lib/src/materializer/transaction/constants.rs
Normal file
14
kb-lib/src/materializer/transaction/constants.rs
Normal file
@@ -0,0 +1,14 @@
|
||||
// file: kb-lib/src/materializer/transaction/constants.rs
|
||||
// version: 1
|
||||
|
||||
//! Local constants for the transaction annotation materializer.
|
||||
|
||||
/// Maximum Memo payload accepted from the decoded contract.
|
||||
pub(crate) const MT_TRANSACTION_ANNOTATIONS_MAX_MEMO_PAYLOAD_BYTES: u64 = 4_096;
|
||||
/// Stable materializer processor name.
|
||||
pub(crate) const MT_TRANSACTION_ANNOTATIONS_PROCESSOR_NAME: &str = "transaction_annotations";
|
||||
/// Stable projection contract version.
|
||||
pub(crate) const MT_TRANSACTION_ANNOTATIONS_PROJECTION_VERSION: u32 = 1;
|
||||
/// Canonical tracing target for this component.
|
||||
pub(crate) const MT_TRANSACTION_ANNOTATIONS_TRACING_TARGET: &str =
|
||||
"kb-lib.materializer.transaction";
|
||||
@@ -1,6 +1,6 @@
|
||||
// file: kb-lib/src/materializer/vault.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! `vault` materializer family.
|
||||
|
||||
pub mod core;
|
||||
mod core;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// file: kb-lib/src/materializer/vault/core.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! Migration boundary for legacy crate `kb_materializer_vault`.
|
||||
|
||||
/// Legacy crate name retained for migration and compatibility tracking.
|
||||
pub const LEGACY_CRATE: &str = "kb_materializer_vault";
|
||||
const LEGACY_CRATE: &str = "kb_materializer_vault";
|
||||
|
||||
/// Current porting status.
|
||||
pub const MIGRATION_STATUS: &str = "source-preserved-pending-port";
|
||||
const MIGRATION_STATUS: &str = "source-preserved-pending-port";
|
||||
|
||||
Reference in New Issue
Block a user