v0.4.8-pre.006
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
// file: kb-lib/src/lib.rs
|
||||
// version: 32
|
||||
// version: 33
|
||||
|
||||
//! Consolidated decoder, executor, materializer and shared model library.
|
||||
#![warn(missing_docs)]
|
||||
@@ -1391,7 +1391,7 @@ pub use self::materializer::MtLifecycleMaterializer;
|
||||
pub use self::materializer::MtLiquidityMaterializer;
|
||||
/// Exposes the legacy common business materializer trait.
|
||||
pub use self::materializer::MtMaterializer;
|
||||
/// Token-2022 and Metaplex metadata snapshot materializer.
|
||||
/// Token-2022, Metaplex and Solana Program Metadata materializer.
|
||||
pub use self::materializer::MtMetadataMaterializer;
|
||||
/// Reserved NFT materializer.
|
||||
pub use self::materializer::MtNftMaterializer;
|
||||
@@ -1439,6 +1439,12 @@ pub use self::materializer::materializer_fees_materialize_token_2022_state_snaps
|
||||
pub use self::materializer::materializer_metadata_materialize_metaplex_account_snapshot;
|
||||
/// Materializes one canonical Metaplex Metadata account snapshot.
|
||||
pub use self::materializer::materializer_metadata_materialize_metaplex_metadata_snapshot;
|
||||
/// Materializes one decoded Solana Program Metadata account snapshot.
|
||||
pub use self::materializer::materializer_metadata_materialize_solana_program_metadata_account_snapshot;
|
||||
/// Materializes one bounded Solana Program Metadata Buffer account snapshot.
|
||||
pub use self::materializer::materializer_metadata_materialize_solana_program_metadata_buffer_snapshot;
|
||||
/// Materializes one bounded Solana Program Metadata Metadata account snapshot.
|
||||
pub use self::materializer::materializer_metadata_materialize_solana_program_metadata_metadata_snapshot;
|
||||
/// Materializes Token-2022 embedded metadata and group snapshots.
|
||||
pub use self::materializer::materializer_metadata_materialize_token_2022_snapshot;
|
||||
/// Materializes one bounded Token-2022 account state snapshot.
|
||||
@@ -2048,6 +2054,12 @@ pub(crate) use self::executor::executor_spl_token_2022_build_prepared_plan;
|
||||
pub(crate) use self::executor::executor_spl_token_build_prepared_plan;
|
||||
/// Canonical processor name for the native and SPL administration materializer.
|
||||
pub(crate) use self::materializer::MT_ADMIN_PROCESSOR_NAME;
|
||||
/// Event families accepted by the Solana Program Metadata instruction materializer.
|
||||
pub(crate) use self::materializer::MT_METADATA_SPM_ACCEPTED_FAMILIES;
|
||||
/// Stable processor name for Solana Program Metadata projections.
|
||||
pub(crate) use self::materializer::MT_METADATA_SPM_PROCESSOR_NAME;
|
||||
/// Stable projection schema version for Solana Program Metadata outputs.
|
||||
pub(crate) use self::materializer::MT_METADATA_SPM_PROJECTION_VERSION;
|
||||
/// Stable processor name for SPL Token and ATA risk facts.
|
||||
pub(crate) use self::materializer::MT_RISK_PROCESSOR_NAME;
|
||||
/// Stable projection contract version for SPL Token and ATA risk facts.
|
||||
@@ -2068,6 +2080,8 @@ pub(crate) use self::materializer::TRACING_TARGET_MATERIALIZER_ADMIN;
|
||||
pub(crate) use self::materializer::TRACING_TARGET_MATERIALIZER_COMPLIANCE_AUDIT;
|
||||
/// Canonical tracing target for the native lifecycle materializer.
|
||||
pub(crate) use self::materializer::TRACING_TARGET_MATERIALIZER_LIFECYCLE;
|
||||
/// Canonical tracing target for Solana Program Metadata materialization.
|
||||
pub(crate) use self::materializer::TRACING_TARGET_MATERIALIZER_METADATA_SOLANA_PROGRAM_METADATA;
|
||||
/// Canonical tracing target for SPL Token and ATA risk facts.
|
||||
pub(crate) use self::materializer::TRACING_TARGET_MATERIALIZER_RISK;
|
||||
/// Canonical tracing target for the native stake and vote materializer.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: kb-lib/src/materializer.rs
|
||||
// version: 8
|
||||
// version: 9
|
||||
|
||||
//! Consolidated materializer modules.
|
||||
|
||||
@@ -73,12 +73,18 @@ pub use self::lending::MtLendingMaterializer;
|
||||
pub use self::lifecycle::MtLifecycleMaterializer;
|
||||
/// Reserved liquidity materializer.
|
||||
pub use self::liquidity::MtLiquidityMaterializer;
|
||||
/// Token-2022 and Metaplex metadata snapshot materializer.
|
||||
/// Token-2022, Metaplex and Solana Program Metadata materializer.
|
||||
pub use self::metadata::MtMetadataMaterializer;
|
||||
/// Materializes one canonical non-Metadata Metaplex account snapshot.
|
||||
pub use self::metadata::materializer_metadata_materialize_metaplex_account_snapshot;
|
||||
/// Materializes one canonical Metaplex Metadata account snapshot.
|
||||
pub use self::metadata::materializer_metadata_materialize_metaplex_metadata_snapshot;
|
||||
/// Materializes one decoded Solana Program Metadata account snapshot.
|
||||
pub use self::metadata::materializer_metadata_materialize_solana_program_metadata_account_snapshot;
|
||||
/// Materializes one bounded Solana Program Metadata Buffer account snapshot.
|
||||
pub use self::metadata::materializer_metadata_materialize_solana_program_metadata_buffer_snapshot;
|
||||
/// Materializes one bounded Solana Program Metadata Metadata account snapshot.
|
||||
pub use self::metadata::materializer_metadata_materialize_solana_program_metadata_metadata_snapshot;
|
||||
/// Materializes Token-2022 embedded metadata and group snapshots.
|
||||
pub use self::metadata::materializer_metadata_materialize_token_2022_snapshot;
|
||||
/// Reserved NFT materializer.
|
||||
@@ -120,6 +126,14 @@ pub(crate) use self::admin::TRACING_TARGET_MATERIALIZER_ADMIN;
|
||||
pub(crate) use self::compliance::TRACING_TARGET_MATERIALIZER_COMPLIANCE_AUDIT;
|
||||
/// Canonical tracing target for the native lifecycle materializer.
|
||||
pub(crate) use self::lifecycle::TRACING_TARGET_MATERIALIZER_LIFECYCLE;
|
||||
/// Event families accepted by the Solana Program Metadata instruction materializer.
|
||||
pub(crate) use self::metadata::MT_METADATA_SPM_ACCEPTED_FAMILIES;
|
||||
/// Stable processor name for Solana Program Metadata projections.
|
||||
pub(crate) use self::metadata::MT_METADATA_SPM_PROCESSOR_NAME;
|
||||
/// Stable projection schema version for Solana Program Metadata outputs.
|
||||
pub(crate) use self::metadata::MT_METADATA_SPM_PROJECTION_VERSION;
|
||||
/// Canonical tracing target for Solana Program Metadata materialization.
|
||||
pub(crate) use self::metadata::TRACING_TARGET_MATERIALIZER_METADATA_SOLANA_PROGRAM_METADATA;
|
||||
/// Stable processor name for SPL Token and ATA risk facts.
|
||||
pub(crate) use self::risk::MT_RISK_PROCESSOR_NAME;
|
||||
/// Stable projection contract version for SPL Token and ATA risk facts.
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
// file: kb-lib/src/materializer/metadata.rs
|
||||
// version: 4
|
||||
// version: 5
|
||||
|
||||
//! `metadata` materializer family.
|
||||
|
||||
mod core;
|
||||
mod solana_program_metadata;
|
||||
|
||||
/// Token-2022 and Metaplex metadata snapshot materializer.
|
||||
/// Token-2022, Metaplex and Solana Program Metadata materializer.
|
||||
pub use self::core::MtMetadataMaterializer;
|
||||
/// Materializes one canonical non-Metadata Metaplex account snapshot.
|
||||
pub use self::core::materializer_metadata_materialize_metaplex_account_snapshot;
|
||||
@@ -13,3 +14,18 @@ pub use self::core::materializer_metadata_materialize_metaplex_account_snapshot;
|
||||
pub use self::core::materializer_metadata_materialize_metaplex_metadata_snapshot;
|
||||
/// Materializes Token-2022 embedded metadata and group snapshots.
|
||||
pub use self::core::materializer_metadata_materialize_token_2022_snapshot;
|
||||
/// Materializes one decoded Solana Program Metadata account snapshot.
|
||||
pub use self::solana_program_metadata::materializer_metadata_materialize_solana_program_metadata_account_snapshot;
|
||||
/// Materializes one bounded Solana Program Metadata Buffer account snapshot.
|
||||
pub use self::solana_program_metadata::materializer_metadata_materialize_solana_program_metadata_buffer_snapshot;
|
||||
/// Materializes one bounded Solana Program Metadata Metadata account snapshot.
|
||||
pub use self::solana_program_metadata::materializer_metadata_materialize_solana_program_metadata_metadata_snapshot;
|
||||
|
||||
/// Event families accepted by the Solana Program Metadata instruction materializer.
|
||||
pub(crate) use self::solana_program_metadata::MT_METADATA_SPM_ACCEPTED_FAMILIES;
|
||||
/// Stable processor name for Solana Program Metadata projections.
|
||||
pub(crate) use self::solana_program_metadata::MT_METADATA_SPM_PROCESSOR_NAME;
|
||||
/// Stable projection schema version for Solana Program Metadata outputs.
|
||||
pub(crate) use self::solana_program_metadata::MT_METADATA_SPM_PROJECTION_VERSION;
|
||||
/// Canonical tracing target for Solana Program Metadata materialization.
|
||||
pub(crate) use self::solana_program_metadata::TRACING_TARGET_MATERIALIZER_METADATA_SOLANA_PROGRAM_METADATA;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// file: kb-lib/src/materializer/metadata/core.rs
|
||||
// version: 6
|
||||
// version: 7
|
||||
|
||||
//! Stable Token-2022 and Metaplex metadata snapshot materializer.
|
||||
//! Stable Token-2022, Metaplex and Solana Program Metadata materializer.
|
||||
|
||||
/// Initial materializer implementation placeholder.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
@@ -16,8 +16,13 @@ impl crate::MtMaterializer for crate::MtMetadataMaterializer {
|
||||
return env!("CARGO_PKG_VERSION");
|
||||
}
|
||||
|
||||
fn accepts_event(&self, _event: &crate::MdDecodedProtocolEvent) -> bool {
|
||||
return false;
|
||||
fn accepts_event(&self, event: &crate::MdDecodedProtocolEvent) -> bool {
|
||||
return event.event_family == crate::MdEventFamily::Metadata
|
||||
&& event.program_id.0 == kb_program_ids::METADATA_SOLANA_PROGRAM_METADATA_PROGRAM_ID
|
||||
&& event.surface_code.0 == crate::DC_METADATA_SPM_SURFACE_CODE
|
||||
&& crate::DC_METADATA_SPM_INSTRUCTION_ENTRIES
|
||||
.iter()
|
||||
.any(|(_, code)| return *code == event.event_name.0.as_str());
|
||||
}
|
||||
|
||||
fn materialize_event(
|
||||
|
||||
24
kb-lib/src/materializer/metadata/solana_program_metadata.rs
Normal file
24
kb-lib/src/materializer/metadata/solana_program_metadata.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
// file: kb-lib/src/materializer/metadata/solana_program_metadata.rs
|
||||
// version: 1
|
||||
|
||||
//! Solana Program Metadata materialization surface.
|
||||
|
||||
mod constants;
|
||||
mod instruction;
|
||||
mod state;
|
||||
|
||||
/// Materializes one decoded Solana Program Metadata account snapshot.
|
||||
pub use self::state::materializer_metadata_materialize_solana_program_metadata_account_snapshot;
|
||||
/// Materializes one bounded Solana Program Metadata Buffer account snapshot.
|
||||
pub use self::state::materializer_metadata_materialize_solana_program_metadata_buffer_snapshot;
|
||||
/// Materializes one bounded Solana Program Metadata Metadata account snapshot.
|
||||
pub use self::state::materializer_metadata_materialize_solana_program_metadata_metadata_snapshot;
|
||||
|
||||
/// Event families accepted by the Solana Program Metadata instruction materializer.
|
||||
pub(crate) use self::constants::MT_METADATA_SPM_ACCEPTED_FAMILIES;
|
||||
/// Stable processor name for Solana Program Metadata projections.
|
||||
pub(crate) use self::constants::MT_METADATA_SPM_PROCESSOR_NAME;
|
||||
/// Stable projection schema version for Solana Program Metadata outputs.
|
||||
pub(crate) use self::constants::MT_METADATA_SPM_PROJECTION_VERSION;
|
||||
/// Canonical tracing target for Solana Program Metadata materialization.
|
||||
pub(crate) use self::constants::TRACING_TARGET_MATERIALIZER_METADATA_SOLANA_PROGRAM_METADATA;
|
||||
@@ -0,0 +1,16 @@
|
||||
// file: kb-lib/src/materializer/metadata/solana_program_metadata/constants.rs
|
||||
// version: 1
|
||||
|
||||
//! Solana Program Metadata materializer constants.
|
||||
|
||||
/// Event families accepted by the Solana Program Metadata instruction materializer.
|
||||
pub(crate) const MT_METADATA_SPM_ACCEPTED_FAMILIES: &[crate::MdEventFamily] =
|
||||
&[crate::MdEventFamily::Metadata];
|
||||
/// Stable processor name for Solana Program Metadata projections.
|
||||
pub(crate) const MT_METADATA_SPM_PROCESSOR_NAME: &str =
|
||||
"kb-lib.materializer.metadata.solana_program_metadata";
|
||||
/// Stable projection schema version for Solana Program Metadata outputs.
|
||||
pub(crate) const MT_METADATA_SPM_PROJECTION_VERSION: u16 = 1;
|
||||
/// Canonical tracing target for Solana Program Metadata materialization.
|
||||
pub(crate) const TRACING_TARGET_MATERIALIZER_METADATA_SOLANA_PROGRAM_METADATA: &str =
|
||||
"kb-lib.materializer.metadata.solana_program_metadata";
|
||||
@@ -0,0 +1,325 @@
|
||||
// file: kb-lib/src/materializer/metadata/solana_program_metadata/instruction.rs
|
||||
// version: 1
|
||||
|
||||
//! Committed Solana Program Metadata instruction projections.
|
||||
|
||||
impl crate::MtApiEventMaterializer for crate::MtMetadataMaterializer {
|
||||
fn identity(&self) -> crate::MtApiMaterializerIdentity {
|
||||
return crate::MtApiMaterializerIdentity {
|
||||
name: crate::MT_METADATA_SPM_PROCESSOR_NAME.to_string(),
|
||||
version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
fn accepted_families(&self) -> &'static [crate::MdEventFamily] {
|
||||
return crate::MT_METADATA_SPM_ACCEPTED_FAMILIES;
|
||||
}
|
||||
|
||||
fn accepts_observation(&self, observation: &crate::DcApiDecodedObservation) -> bool {
|
||||
return observation.event.event_family == crate::MdEventFamily::Metadata
|
||||
&& observation.event.program_id.0
|
||||
== kb_program_ids::METADATA_SOLANA_PROGRAM_METADATA_PROGRAM_ID
|
||||
&& observation.event.surface_code.0 == crate::DC_METADATA_SPM_SURFACE_CODE
|
||||
&& payload_contract_matches(observation)
|
||||
&& crate::DC_METADATA_SPM_INSTRUCTION_ENTRIES
|
||||
.iter()
|
||||
.any(|(_, code)| return *code == observation.event.event_name.0.as_str());
|
||||
}
|
||||
|
||||
fn transaction_policy(
|
||||
&self,
|
||||
_family: crate::MdEventFamily,
|
||||
) -> crate::MtApiMaterializationTransactionPolicy {
|
||||
return crate::MtApiMaterializationTransactionPolicy::SuccessfulCommittedOnly;
|
||||
}
|
||||
|
||||
fn materialize(
|
||||
&self,
|
||||
observation: &crate::DcApiDecodedObservation,
|
||||
) -> crate::MtApiMaterializerExecutionResult {
|
||||
if !crate::MtApiEventMaterializer::accepts_observation(self, observation) {
|
||||
return crate::MtApiMaterializerExecutionResult::ignored();
|
||||
}
|
||||
if observation.transaction_failed || !observation.observation_committed {
|
||||
return crate::MtApiMaterializerExecutionResult::refused(
|
||||
"failed_transaction_solana_program_metadata_refused",
|
||||
"failed or uncommitted Solana Program Metadata instructions cannot create mutation outputs",
|
||||
);
|
||||
}
|
||||
let operation = observation.event.event_name.0.as_str();
|
||||
let projection = instruction_projection(operation, &observation.payload_json);
|
||||
let output = crate::MtApiMaterializedOutput {
|
||||
output_key: format!(
|
||||
"solana_program_metadata_instruction:{}:{}:{}",
|
||||
observation.event.signature.0, observation.event.instruction_path.0, operation
|
||||
),
|
||||
family: crate::MdMaterializedEventFamily::Metadata,
|
||||
payload_json: serde_json::json!({
|
||||
"projectionVersion": crate::MT_METADATA_SPM_PROJECTION_VERSION,
|
||||
"domain": "solana_program_metadata",
|
||||
"projectionKind": "committed_instruction_mutation",
|
||||
"projectionSemantics": "committed_instruction_fact_non_authoritative_state",
|
||||
"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(),
|
||||
"sourceKind": observation.event.source_kind,
|
||||
"transactionSucceeded": true,
|
||||
"stateProjectionAuthority": "post_execution_account_snapshot",
|
||||
"authoritativeFinalStateCaptured": false,
|
||||
"projection": projection,
|
||||
"decodedPayload": observation.payload_json.clone(),
|
||||
"externalContentFetched": false,
|
||||
"externalAccountRead": false,
|
||||
"provenance": {
|
||||
"processorName": crate::MT_METADATA_SPM_PROCESSOR_NAME,
|
||||
"processorVersion": env!("CARGO_PKG_VERSION"),
|
||||
"decoderProof": observation.proof.clone()
|
||||
}
|
||||
}),
|
||||
};
|
||||
tracing::debug!(
|
||||
target: crate::TRACING_TARGET_MATERIALIZER_METADATA_SOLANA_PROGRAM_METADATA,
|
||||
action = "materialize_solana_program_metadata_instruction",
|
||||
operation = operation,
|
||||
signature = %observation.event.signature.0,
|
||||
instruction_path = %observation.event.instruction_path.0,
|
||||
output_count = 1_usize,
|
||||
"materialize committed Solana Program Metadata instruction"
|
||||
);
|
||||
return crate::MtApiMaterializerExecutionResult {
|
||||
status: crate::MtApiMaterializerOutcomeStatus::Inserted,
|
||||
outputs: std::vec![output],
|
||||
diagnostics: std::vec::Vec::new(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn payload_contract_matches(observation: &crate::DcApiDecodedObservation) -> bool {
|
||||
let instruction =
|
||||
observation.payload_json.get("instruction").and_then(serde_json::Value::as_str);
|
||||
let program_id = observation.payload_json.get("programId").and_then(serde_json::Value::as_str);
|
||||
return instruction == std::option::Option::Some(observation.event.event_name.0.as_str())
|
||||
&& program_id
|
||||
== std::option::Option::Some(
|
||||
kb_program_ids::METADATA_SOLANA_PROGRAM_METADATA_PROGRAM_ID,
|
||||
);
|
||||
}
|
||||
|
||||
fn instruction_projection(operation: &str, payload: &serde_json::Value) -> serde_json::Value {
|
||||
let parameters = match payload.get("parameters") {
|
||||
std::option::Option::Some(value) => value.clone(),
|
||||
std::option::Option::None => serde_json::Value::Null,
|
||||
};
|
||||
let accounts = match payload.get("accounts") {
|
||||
std::option::Option::Some(value) => value.clone(),
|
||||
std::option::Option::None => serde_json::Value::Null,
|
||||
};
|
||||
return serde_json::json!({
|
||||
"stateTransition": state_transition(operation),
|
||||
"targetAccount": target_account(operation, &accounts),
|
||||
"authority": account_for_role(&accounts, "authority"),
|
||||
"destination": account_for_role(&accounts, "destination"),
|
||||
"sourceBuffer": account_for_role(&accounts, "source_buffer"),
|
||||
"buffer": account_for_role(&accounts, "buffer"),
|
||||
"program": account_for_role(&accounts, "program"),
|
||||
"programData": account_for_role(&accounts, "program_data"),
|
||||
"parameters": parameters,
|
||||
"accounts": accounts,
|
||||
"currentStableInstruction": true,
|
||||
"deprecated": false,
|
||||
"replaced": false,
|
||||
"executionPolicy": "current_candidate",
|
||||
"preStableAlias": if operation == "trim" {
|
||||
serde_json::Value::String("withdraw_excess_lamports".to_string())
|
||||
} else {
|
||||
serde_json::Value::Null
|
||||
},
|
||||
"preStableAliasDecodedSeparately": false
|
||||
});
|
||||
}
|
||||
|
||||
fn state_transition(operation: &str) -> &'static str {
|
||||
match operation {
|
||||
"write" => return "buffer_content_written",
|
||||
"initialize" => return "metadata_account_initialized",
|
||||
"set_authority" => return "account_authority_updated",
|
||||
"set_data" => return "metadata_data_configuration_updated",
|
||||
"set_immutable" => return "metadata_account_marked_immutable",
|
||||
"trim" => return "account_capacity_trimmed",
|
||||
"close" => return "account_closed",
|
||||
"allocate" => return "buffer_account_allocated",
|
||||
"extend" => return "account_capacity_extended",
|
||||
_ => return "unknown",
|
||||
}
|
||||
}
|
||||
|
||||
fn target_account(operation: &str, accounts: &serde_json::Value) -> serde_json::Value {
|
||||
let roles: &[&str] = match operation {
|
||||
"write" | "allocate" => &["buffer"],
|
||||
"initialize" | "set_data" | "set_immutable" => &["metadata"],
|
||||
"set_authority" | "trim" | "close" | "extend" => &["account"],
|
||||
_ => &[],
|
||||
};
|
||||
for role in roles {
|
||||
let value = account_for_role(accounts, role);
|
||||
if !value.is_null() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return serde_json::Value::Null;
|
||||
}
|
||||
|
||||
fn account_for_role(accounts: &serde_json::Value, role: &str) -> serde_json::Value {
|
||||
let values = match accounts.as_array() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return serde_json::Value::Null,
|
||||
};
|
||||
for value in values {
|
||||
let candidate = value.get("role").and_then(serde_json::Value::as_str);
|
||||
let present = match value.get("present").and_then(serde_json::Value::as_bool) {
|
||||
std::option::Option::Some(candidate) => candidate,
|
||||
std::option::Option::None => true,
|
||||
};
|
||||
if candidate == std::option::Option::Some(role) && present {
|
||||
return match value.get("accountKey") {
|
||||
std::option::Option::Some(candidate) => candidate.clone(),
|
||||
std::option::Option::None => serde_json::Value::Null,
|
||||
};
|
||||
}
|
||||
}
|
||||
return serde_json::Value::Null;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
const MATRIX_JSON: &str = include_str!(
|
||||
"../../../../../test-fixtures/contract-matrices/SOLANA_PROGRAM_METADATA_MATERIALIZATION_MATRIX.json"
|
||||
);
|
||||
|
||||
fn observation(operation: &str, committed: bool) -> crate::DcApiDecodedObservation {
|
||||
return crate::DcApiDecodedObservation {
|
||||
event_key: format!("solana_program_metadata:{operation}:0"),
|
||||
event: crate::MdDecodedProtocolEvent {
|
||||
signature: crate::MdSignature("signature111".to_string()),
|
||||
slot: crate::MdSlot(42),
|
||||
instruction_path: crate::MdInstructionPath("0".to_string()),
|
||||
program_id: crate::MdProgramId(
|
||||
kb_program_ids::METADATA_SOLANA_PROGRAM_METADATA_PROGRAM_ID.to_string(),
|
||||
),
|
||||
protocol_code: crate::MdProtocolCode(
|
||||
crate::DC_METADATA_SPM_SURFACE_CODE.to_string(),
|
||||
),
|
||||
surface_code: crate::MdSurfaceCode(crate::DC_METADATA_SPM_SURFACE_CODE.to_string()),
|
||||
event_code: crate::MdEventCode(format!(
|
||||
"{}.{}",
|
||||
crate::DC_METADATA_SPM_SURFACE_CODE,
|
||||
operation
|
||||
)),
|
||||
event_name: crate::MdEventName(operation.to_string()),
|
||||
event_family: crate::MdEventFamily::Metadata,
|
||||
source_kind: crate::MdEventSourceKind::Instruction,
|
||||
confidence: crate::MdDecoderConfidence::ManualExact,
|
||||
},
|
||||
payload_json: serde_json::json!({
|
||||
"programId": kb_program_ids::METADATA_SOLANA_PROGRAM_METADATA_PROGRAM_ID,
|
||||
"instruction": operation,
|
||||
"parameters": {"length": 8},
|
||||
"accounts": [
|
||||
{"role":"account","accountKey":"account111","present":true},
|
||||
{"role":"authority","accountKey":"authority111","present":true}
|
||||
],
|
||||
"wire": {"sha256":"00"}
|
||||
}),
|
||||
transaction_failed: !committed,
|
||||
transaction_error: if committed {
|
||||
std::option::Option::None
|
||||
} else {
|
||||
std::option::Option::Some(serde_json::json!({"InstructionError":[0,"Custom"]}))
|
||||
},
|
||||
observation_committed: committed,
|
||||
proof: crate::DcApiDecoderProof {
|
||||
kind: crate::DcApiDecoderProofKind::Manual,
|
||||
confidence: crate::MdDecoderConfidence::ManualExact,
|
||||
evidence: std::vec!["wire".to_string()],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_stable_instruction_has_one_materialized_mutation_fact() {
|
||||
let materializer = crate::MtMetadataMaterializer;
|
||||
for (_, operation) in crate::DC_METADATA_SPM_INSTRUCTION_ENTRIES.iter().copied() {
|
||||
let value = observation(operation, true);
|
||||
assert!(crate::MtApiEventMaterializer::accepts_observation(&materializer, &value));
|
||||
let result = crate::MtApiEventMaterializer::materialize(&materializer, &value);
|
||||
assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Inserted);
|
||||
assert_eq!(result.outputs.len(), 1);
|
||||
assert_eq!(result.outputs[0].payload_json["operation"], *operation);
|
||||
assert_eq!(result.outputs[0].payload_json["projection"]["deprecated"], false);
|
||||
assert_eq!(result.outputs[0].payload_json["projection"]["replaced"], false);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_instruction_is_refused_and_foreign_surface_is_ignored() {
|
||||
let materializer = crate::MtMetadataMaterializer;
|
||||
let failed = observation("set_data", false);
|
||||
let failure = crate::MtApiEventMaterializer::materialize(&materializer, &failed);
|
||||
assert_eq!(failure.status, crate::MtApiMaterializerOutcomeStatus::Refused);
|
||||
let mut foreign = observation("set_data", true);
|
||||
foreign.event.program_id =
|
||||
crate::MdProgramId(kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID.to_string());
|
||||
let ignored = crate::MtApiEventMaterializer::materialize(&materializer, &foreign);
|
||||
assert_eq!(ignored.status, crate::MtApiMaterializerOutcomeStatus::Ignored);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn materialization_matrix_matches_two_account_states_and_nine_instructions() {
|
||||
let parsed = serde_json::from_str::<serde_json::Value>(MATRIX_JSON);
|
||||
assert!(parsed.is_ok());
|
||||
let matrix = match parsed {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
let accounts = match matrix.get("accountSnapshots").and_then(serde_json::Value::as_array) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
let instructions =
|
||||
match matrix.get("instructionFacts").and_then(serde_json::Value::as_array) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
assert_eq!(accounts.len(), 2);
|
||||
assert_eq!(instructions.len(), crate::DC_METADATA_SPM_INSTRUCTION_ENTRIES.len());
|
||||
for (position, (tag, operation)) in
|
||||
crate::DC_METADATA_SPM_INSTRUCTION_ENTRIES.iter().copied().enumerate()
|
||||
{
|
||||
assert_eq!(instructions[position]["tag"], tag);
|
||||
assert_eq!(instructions[position]["entryCode"], operation);
|
||||
assert_eq!(instructions[position]["deprecated"], false);
|
||||
assert_eq!(instructions[position]["replaced"], false);
|
||||
assert_eq!(
|
||||
instructions[position]["stateTransition"],
|
||||
super::state_transition(operation)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trim_keeps_prestable_alias_as_provenance_only() {
|
||||
let materializer = crate::MtMetadataMaterializer;
|
||||
let value = observation("trim", true);
|
||||
let result = crate::MtApiEventMaterializer::materialize(&materializer, &value);
|
||||
assert_eq!(
|
||||
result.outputs[0].payload_json["projection"]["preStableAlias"],
|
||||
"withdraw_excess_lamports"
|
||||
);
|
||||
assert_eq!(
|
||||
result.outputs[0].payload_json["projection"]["preStableAliasDecodedSeparately"],
|
||||
false
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,343 @@
|
||||
// file: kb-lib/src/materializer/metadata/solana_program_metadata/state.rs
|
||||
// version: 2
|
||||
|
||||
//! Authoritative Solana Program Metadata account-state projections.
|
||||
|
||||
use base64::Engine; // rust-rules: trait-import
|
||||
use sha2::Digest; // rust-rules: trait-import
|
||||
|
||||
/// Materializes one decoded Solana Program Metadata account snapshot.
|
||||
pub fn materializer_metadata_materialize_solana_program_metadata_account_snapshot(
|
||||
slot: u64,
|
||||
snapshot: &crate::DcMetadataSpmAccountSnapshot,
|
||||
) -> std::result::Result<crate::MtApiMaterializedOutput, String> {
|
||||
match snapshot {
|
||||
crate::DcMetadataSpmAccountSnapshot::Buffer(value) => {
|
||||
return crate::materializer_metadata_materialize_solana_program_metadata_buffer_snapshot(
|
||||
slot,
|
||||
value,
|
||||
);
|
||||
},
|
||||
crate::DcMetadataSpmAccountSnapshot::Metadata(value) => {
|
||||
return crate::materializer_metadata_materialize_solana_program_metadata_metadata_snapshot(
|
||||
slot,
|
||||
value,
|
||||
);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Materializes one bounded Solana Program Metadata Buffer account snapshot.
|
||||
pub fn materializer_metadata_materialize_solana_program_metadata_buffer_snapshot(
|
||||
slot: u64,
|
||||
snapshot: &crate::DcMetadataSpmBufferAccountSnapshot,
|
||||
) -> std::result::Result<crate::MtApiMaterializedOutput, String> {
|
||||
if snapshot.account.trim().is_empty() {
|
||||
return std::result::Result::Err(
|
||||
"Solana Program Metadata Buffer materialization requires a non-empty account"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
if snapshot.program.as_ref().is_some_and(|value| return value.trim().is_empty())
|
||||
|| snapshot.authority.as_ref().is_some_and(|value| return value.trim().is_empty())
|
||||
{
|
||||
return std::result::Result::Err(
|
||||
"Solana Program Metadata Buffer optional identities must not be empty".to_string(),
|
||||
);
|
||||
}
|
||||
if snapshot.data.len() != snapshot.allocated_data_bytes {
|
||||
return std::result::Result::Err(
|
||||
"Solana Program Metadata Buffer allocated byte count does not match decoded data"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
if snapshot.allocated_data_bytes
|
||||
> crate::DC_METADATA_SPM_MAX_ACCOUNT_BYTES
|
||||
.saturating_sub(crate::DC_METADATA_SPM_BUFFER_HEADER_BYTES)
|
||||
{
|
||||
return std::result::Result::Err(
|
||||
"Solana Program Metadata Buffer exceeds the bounded account-data contract".to_string(),
|
||||
);
|
||||
}
|
||||
let seed_hex = hex(snapshot.seed.as_bytes());
|
||||
let data = bytes_projection(snapshot.data.as_slice());
|
||||
return std::result::Result::Ok(crate::MtApiMaterializedOutput {
|
||||
output_key: format!("solana_program_metadata_account_state:{}", snapshot.account),
|
||||
family: crate::MdMaterializedEventFamily::Metadata,
|
||||
payload_json: serde_json::json!({
|
||||
"projectionVersion": crate::MT_METADATA_SPM_PROJECTION_VERSION,
|
||||
"domain": "solana_program_metadata",
|
||||
"projectionKind": "buffer_state",
|
||||
"projectionSemantics": "authoritative_bounded_owned_account_snapshot",
|
||||
"account": snapshot.account,
|
||||
"slot": slot,
|
||||
"programId": kb_program_ids::METADATA_SOLANA_PROGRAM_METADATA_PROGRAM_ID,
|
||||
"accountDiscriminator": "buffer",
|
||||
"metadataProgram": snapshot.program,
|
||||
"authority": snapshot.authority,
|
||||
"mutable": snapshot.authority.is_some(),
|
||||
"canonical": snapshot.canonical,
|
||||
"seedHex": seed_hex,
|
||||
"allocatedDataBytes": snapshot.allocated_data_bytes,
|
||||
"data": data,
|
||||
"metadataSource": "solana_program_metadata_owned_buffer_account",
|
||||
"externalContentFetched": false,
|
||||
"externalAccountRead": false,
|
||||
"instructionProjectionRole": "mutation_observation_only_no_duplicate_state",
|
||||
"provenance": {
|
||||
"processorName": crate::MT_METADATA_SPM_PROCESSOR_NAME,
|
||||
"processorVersion": env!("CARGO_PKG_VERSION"),
|
||||
"source": "bounded_solana_program_metadata_account_decoder"
|
||||
}
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
/// Materializes one bounded Solana Program Metadata Metadata account snapshot.
|
||||
pub fn materializer_metadata_materialize_solana_program_metadata_metadata_snapshot(
|
||||
slot: u64,
|
||||
snapshot: &crate::DcMetadataSpmMetadataAccountSnapshot,
|
||||
) -> std::result::Result<crate::MtApiMaterializedOutput, String> {
|
||||
if snapshot.account.trim().is_empty() || snapshot.program.trim().is_empty() {
|
||||
return std::result::Result::Err(
|
||||
"Solana Program Metadata state materialization requires account and program identities"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
if snapshot.authority.as_ref().is_some_and(|value| return value.trim().is_empty()) {
|
||||
return std::result::Result::Err(
|
||||
"Solana Program Metadata authority must not be empty when present".to_string(),
|
||||
);
|
||||
}
|
||||
if snapshot.data_source != snapshot.data.source() {
|
||||
return std::result::Result::Err(
|
||||
"Solana Program Metadata data source does not match decoded content".to_string(),
|
||||
);
|
||||
}
|
||||
if let std::result::Result::Err(error) = snapshot.data.validate() {
|
||||
return std::result::Result::Err(error.to_string());
|
||||
}
|
||||
let logical_data_bytes = match usize::try_from(snapshot.data_length) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => {
|
||||
return std::result::Result::Err(
|
||||
"Solana Program Metadata logical data length is not representable".to_string(),
|
||||
);
|
||||
},
|
||||
};
|
||||
if logical_data_bytes != snapshot.data.wire_byte_len() {
|
||||
return std::result::Result::Err(
|
||||
"Solana Program Metadata logical data length does not match decoded content"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
if snapshot.allocated_data_bytes
|
||||
> crate::DC_METADATA_SPM_MAX_ACCOUNT_BYTES
|
||||
.saturating_sub(crate::DC_METADATA_SPM_METADATA_HEADER_BYTES)
|
||||
{
|
||||
return std::result::Result::Err(
|
||||
"Solana Program Metadata state exceeds the bounded account-data contract".to_string(),
|
||||
);
|
||||
}
|
||||
if snapshot.allocated_data_bytes
|
||||
!= logical_data_bytes.saturating_add(snapshot.trailing_capacity_bytes)
|
||||
{
|
||||
return std::result::Result::Err(
|
||||
"Solana Program Metadata allocated capacity is inconsistent".to_string(),
|
||||
);
|
||||
}
|
||||
let seed_hex = hex(snapshot.seed.as_bytes());
|
||||
let data = data_projection(&snapshot.data);
|
||||
return std::result::Result::Ok(crate::MtApiMaterializedOutput {
|
||||
output_key: format!("solana_program_metadata_account_state:{}", snapshot.account),
|
||||
family: crate::MdMaterializedEventFamily::Metadata,
|
||||
payload_json: serde_json::json!({
|
||||
"projectionVersion": crate::MT_METADATA_SPM_PROJECTION_VERSION,
|
||||
"domain": "solana_program_metadata",
|
||||
"projectionKind": "metadata_state",
|
||||
"projectionSemantics": "authoritative_bounded_owned_account_snapshot",
|
||||
"account": snapshot.account,
|
||||
"slot": slot,
|
||||
"programId": kb_program_ids::METADATA_SOLANA_PROGRAM_METADATA_PROGRAM_ID,
|
||||
"accountDiscriminator": "metadata",
|
||||
"bump": snapshot.bump,
|
||||
"metadataProgram": snapshot.program,
|
||||
"authority": snapshot.authority,
|
||||
"mutable": snapshot.mutable,
|
||||
"canonical": snapshot.canonical,
|
||||
"seedHex": seed_hex,
|
||||
"encoding": snapshot.encoding,
|
||||
"compression": snapshot.compression,
|
||||
"format": snapshot.format,
|
||||
"dataSource": snapshot.data_source,
|
||||
"dataLength": snapshot.data_length,
|
||||
"allocatedDataBytes": snapshot.allocated_data_bytes,
|
||||
"trailingCapacityBytes": snapshot.trailing_capacity_bytes,
|
||||
"data": data,
|
||||
"metadataSource": "solana_program_metadata_owned_metadata_account",
|
||||
"externalContentFetched": false,
|
||||
"externalAccountRead": false,
|
||||
"instructionProjectionRole": "mutation_observation_only_no_duplicate_state",
|
||||
"provenance": {
|
||||
"processorName": crate::MT_METADATA_SPM_PROCESSOR_NAME,
|
||||
"processorVersion": env!("CARGO_PKG_VERSION"),
|
||||
"source": "bounded_solana_program_metadata_account_decoder"
|
||||
}
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
fn data_projection(data: &crate::DcMetadataSpmData) -> serde_json::Value {
|
||||
match data {
|
||||
crate::DcMetadataSpmData::Direct(value) => {
|
||||
let mut projection = bytes_projection(value.as_slice());
|
||||
projection["kind"] = serde_json::json!("direct");
|
||||
return projection;
|
||||
},
|
||||
crate::DcMetadataSpmData::Url(value) => {
|
||||
return serde_json::json!({
|
||||
"kind": "url",
|
||||
"value": value,
|
||||
"lengthBytes": value.len(),
|
||||
"sha256": hash(value.as_bytes()),
|
||||
"fetched": false
|
||||
});
|
||||
},
|
||||
crate::DcMetadataSpmData::External(value) => {
|
||||
return serde_json::json!({
|
||||
"kind": "external",
|
||||
"address": value.address.to_string(),
|
||||
"offset": value.offset,
|
||||
"length": value.length.map(std::num::NonZeroU32::get),
|
||||
"wireLengthBytes": crate::DC_METADATA_SPM_EXTERNAL_DATA_BYTES,
|
||||
"accountRead": false
|
||||
});
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn bytes_projection(bytes: &[u8]) -> serde_json::Value {
|
||||
return serde_json::json!({
|
||||
"lengthBytes": bytes.len(),
|
||||
"sha256": hash(bytes),
|
||||
"base64": base64::engine::general_purpose::STANDARD.encode(bytes)
|
||||
});
|
||||
}
|
||||
|
||||
fn hash(bytes: &[u8]) -> std::string::String {
|
||||
let digest = sha2::Sha256::digest(bytes);
|
||||
let mut output = std::string::String::with_capacity(64);
|
||||
for byte in digest {
|
||||
output.push_str(format!("{byte:02x}").as_str());
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
fn hex(bytes: &[u8]) -> std::string::String {
|
||||
let mut output = std::string::String::with_capacity(bytes.len().saturating_mul(2));
|
||||
for byte in bytes {
|
||||
output.push_str(format!("{byte:02x}").as_str());
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn buffer_snapshot_preserves_all_bounded_bytes_and_authority_state() {
|
||||
let snapshot = crate::DcMetadataSpmBufferAccountSnapshot {
|
||||
account: "buffer111".to_string(),
|
||||
program: std::option::Option::Some("program111".to_string()),
|
||||
authority: std::option::Option::Some("authority111".to_string()),
|
||||
canonical: true,
|
||||
seed: match crate::DcMetadataSpmSeed::from_wire_bytes(&[7_u8; 16]) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => crate::DcMetadataSpmSeed::default(),
|
||||
},
|
||||
data: std::vec![1, 2, 3],
|
||||
allocated_data_bytes: 3,
|
||||
};
|
||||
let output =
|
||||
crate::materializer_metadata_materialize_solana_program_metadata_buffer_snapshot(
|
||||
42, &snapshot,
|
||||
);
|
||||
assert!(output.is_ok());
|
||||
let payload = match output.as_ref() {
|
||||
std::result::Result::Ok(value) => value.payload_json.clone(),
|
||||
std::result::Result::Err(_) => serde_json::Value::Null,
|
||||
};
|
||||
assert_eq!(payload["projectionKind"], "buffer_state");
|
||||
assert_eq!(payload["data"]["base64"], "AQID");
|
||||
assert_eq!(payload["mutable"], true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metadata_snapshots_materialize_every_onchain_source_without_resolution() {
|
||||
let direct = metadata_snapshot(crate::DcMetadataSpmData::Direct(std::vec![4, 5]));
|
||||
let direct_output =
|
||||
crate::materializer_metadata_materialize_solana_program_metadata_metadata_snapshot(
|
||||
9, &direct,
|
||||
);
|
||||
assert_eq!(
|
||||
direct_output
|
||||
.as_ref()
|
||||
.map(|value| return value.payload_json["data"]["kind"].clone()),
|
||||
std::result::Result::Ok(serde_json::json!("direct"))
|
||||
);
|
||||
let url_value = "https://example.invalid/idl.json".to_string();
|
||||
let mut url = metadata_snapshot(crate::DcMetadataSpmData::Url(url_value.clone()));
|
||||
url.data_source = crate::DcMetadataSpmDataSource::Url;
|
||||
url.data_length = match u32::try_from(url_value.len()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => 0,
|
||||
};
|
||||
url.allocated_data_bytes = url_value.len();
|
||||
let url_output =
|
||||
crate::materializer_metadata_materialize_solana_program_metadata_metadata_snapshot(
|
||||
10, &url,
|
||||
);
|
||||
assert_eq!(
|
||||
url_output
|
||||
.as_ref()
|
||||
.map(|value| return value.payload_json["data"]["fetched"].clone()),
|
||||
std::result::Result::Ok(serde_json::json!(false))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inconsistent_snapshot_lengths_fail_closed() {
|
||||
let mut snapshot = metadata_snapshot(crate::DcMetadataSpmData::Direct(std::vec![1, 2]));
|
||||
snapshot.data_length = 1;
|
||||
assert!(
|
||||
crate::materializer_metadata_materialize_solana_program_metadata_metadata_snapshot(
|
||||
1, &snapshot
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
|
||||
fn metadata_snapshot(
|
||||
data: crate::DcMetadataSpmData,
|
||||
) -> crate::DcMetadataSpmMetadataAccountSnapshot {
|
||||
return crate::DcMetadataSpmMetadataAccountSnapshot {
|
||||
account: "metadata111".to_string(),
|
||||
bump: 2,
|
||||
program: "program111".to_string(),
|
||||
authority: std::option::Option::Some("authority111".to_string()),
|
||||
mutable: true,
|
||||
canonical: true,
|
||||
seed: crate::DcMetadataSpmSeed::default(),
|
||||
encoding: crate::DcMetadataSpmEncoding::Utf8,
|
||||
compression: crate::DcMetadataSpmCompression::None,
|
||||
format: crate::DcMetadataSpmFormat::Json,
|
||||
data_source: crate::DcMetadataSpmDataSource::Direct,
|
||||
data_length: match u32::try_from(data.wire_byte_len()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => 0,
|
||||
},
|
||||
allocated_data_bytes: data.wire_byte_len(),
|
||||
trailing_capacity_bytes: 0,
|
||||
data,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user