0.1.0-pre.004

This commit is contained in:
2026-07-23 18:25:10 +02:00
parent 0da75c1311
commit 149d4c6ef6
85 changed files with 25696 additions and 227 deletions

View File

@@ -0,0 +1,99 @@
// file: kb-store/src/contracts/dto.rs
// version: 1
//! Backend-neutral DTO exports for storage repository contracts.
mod core;
mod core_extraction;
mod decode;
mod event;
mod ledger;
mod raw;
mod store;
/// Core account key insert contract.
pub use crate::contracts::dto::core::CoreAccountKeyInsert;
/// Core account key source category.
pub use crate::contracts::dto::core::CoreAccountKeySource;
/// Core balance change insert contract.
pub use crate::contracts::dto::core::CoreBalanceChangeInsert;
/// Core balance change kind.
pub use crate::contracts::dto::core::CoreBalanceChangeKind;
/// Core inner instruction insert contract.
pub use crate::contracts::dto::core::CoreInnerInstructionInsert;
/// Core instruction insert contract.
pub use crate::contracts::dto::core::CoreInstructionInsert;
/// Core instruction lifecycle mark request.
pub use crate::contracts::dto::core::CoreInstructionLifecycleMark;
/// Core instruction processing state.
pub use crate::contracts::dto::core::CoreInstructionProcessingState;
/// Core instruction replay filter contract.
pub use crate::contracts::dto::core::CoreInstructionReplayFilter;
/// Core log insert contract.
pub use crate::contracts::dto::core::CoreLogInsert;
/// Core transaction insert contract.
pub use crate::contracts::dto::core::CoreTransactionInsert;
/// Complete normalized core extraction write bundle.
pub use crate::contracts::dto::core_extraction::CoreExtractionBundle;
/// Failure details persisted for one canonical to core extraction attempt.
pub use crate::contracts::dto::core_extraction::CoreExtractionFailure;
/// Bounded canonical transaction selection filter for core extraction.
pub use crate::contracts::dto::core_extraction::CoreExtractionSelectionFilter;
/// Stable processing ledger identity.
pub use crate::contracts::dto::core_extraction::ProcessingLedgerIdentity;
/// Stable processing ledger status.
pub use crate::contracts::dto::core_extraction::ProcessingLedgerStatus;
/// One machine-readable decoder coverage declaration row.
pub use crate::contracts::dto::decode::DecodeCoverageDeclarationInsert;
/// One observed coverage classification row owned by one decode attempt.
pub use crate::contracts::dto::decode::DecodeCoverageObservationInsert;
/// One row of aggregated decoder coverage diagnostics.
pub use crate::contracts::dto::decode::DecodeCoverageSummaryRow;
/// Failed decode attempt persisted in the common ledger.
pub use crate::contracts::dto::decode::DecodeFailure;
/// One processor-owned decoded observation row.
pub use crate::contracts::dto::decode::DecodeObservationInsert;
/// Atomic persistence bundle for one decoder and one contextual input.
pub use crate::contracts::dto::decode::DecodePersistenceBundle;
/// Bounded contextual instruction selection filter for decode campaigns.
pub use crate::contracts::dto::decode::DecodeSelectionFilter;
/// Maximum number of materialized rows returned by one bounded query.
pub use crate::contracts::dto::decode::MAX_MATERIALIZED_EVENT_QUERY_ROWS;
/// Atomic persistence bundle for one materializer and one decoded observation.
pub use crate::contracts::dto::decode::MaterializationPersistenceBundle;
/// Bounded read-only materialized event selection.
pub use crate::contracts::dto::decode::MaterializedEventFilter;
/// One materialized output returned by a bounded query.
pub use crate::contracts::dto::decode::MaterializedEventQueryRow;
/// One processor-owned materialized output row.
pub use crate::contracts::dto::decode::MaterializedOutputInsert;
/// Decoded event insert contract.
pub use crate::contracts::dto::event::DecodedEventInsert;
/// Insert or upsert result contract returned by repositories.
pub use crate::contracts::dto::event::InsertOutcome;
/// Materialized event insert contract.
pub use crate::contracts::dto::event::MaterializedEventInsert;
/// Processing ledger mark request contract.
pub use crate::contracts::dto::ledger::ProcessingLedgerMark;
/// Raw payload lifecycle mark request.
pub use crate::contracts::dto::raw::RawPayloadLifecycleMark;
/// Raw payload processing state.
pub use crate::contracts::dto::raw::RawPayloadProcessingState;
/// Raw payload retention state.
pub use crate::contracts::dto::raw::RawPayloadRetentionState;
/// Canonical raw Solana transaction insert contract.
pub use crate::contracts::dto::raw::RawTransactionInsert;
/// Transaction acquisition observation insert contract.
pub use crate::contracts::dto::raw::TransactionObservationInsert;
/// Transaction acquisition observation origin.
pub use crate::contracts::dto::raw::TransactionObservationOrigin;
/// Transaction acquisition observation status.
pub use crate::contracts::dto::raw::TransactionObservationStatus;
/// Store backend diagnostic contract.
pub use crate::contracts::dto::store::StoreBackendDescriptor;
/// Store backend kind contract.
pub use crate::contracts::dto::store::StoreBackendKind;
/// Store migration diagnostic snapshot contract.
pub use crate::contracts::dto::store::StoreMigrationSnapshot;
/// Store migration status contract.
pub use crate::contracts::dto::store::StoreMigrationStatus;

View File

@@ -0,0 +1,888 @@
// file: kb-store/src/contracts/dto/core.rs
// version: 1
//! Core Solana storage DTOs.
/// Processing state for one normalized core instruction.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub enum CoreInstructionProcessingState {
/// Instruction is available for replay or first processing.
Pending,
/// Instruction was decoded by at least one decoder version.
Decoded,
/// Instruction produced materialized outputs.
Materialized,
/// Instruction is intentionally skipped for the current pipeline policy.
Ignored,
/// Instruction processing failed and requires diagnostics.
Failed,
/// Instruction must be replayed even if a previous processor marked it.
ReplayRequested,
}
/// Source category for one normalized Solana account key.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub enum CoreAccountKeySource {
/// Account key came from the static transaction message account keys.
Static,
/// Account key came from loaded writable address table entries.
LoadedWritable,
/// Account key came from loaded readonly address table entries.
LoadedReadonly,
}
/// Balance change family extracted from Solana transaction metadata.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub enum CoreBalanceChangeKind {
/// Native lamports balance change.
NativeLamports,
/// SPL token balance change.
TokenAmount,
}
/// Core transaction insert contract.
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct CoreTransactionInsert {
/// Transaction signature as non-empty base58 text.
pub signature: std::string::String,
/// Transaction slot in Solana unsigned representation.
pub slot: u64,
/// Whether the transaction failed on-chain.
pub failed: bool,
/// Optional raw error JSON extracted from transaction metadata.
pub err_json: std::option::Option<serde_json::Value>,
/// Optional canonical raw transaction row id used for lineage when available.
pub raw_transaction_id: std::option::Option<i64>,
}
impl CoreTransactionInsert {
/// Builds a core transaction insert contract after minimal validation.
pub fn new(
signature: impl std::convert::Into<std::string::String>,
slot: u64,
failed: bool,
err_json: std::option::Option<serde_json::Value>,
) -> kb_core::Result<Self> {
let signature_value = signature.into();
let signature_result = validate_required_text(
&signature_value,
"core transaction signature must not be empty",
);
if let std::result::Result::Err(error) = signature_result {
return std::result::Result::Err(error);
}
return std::result::Result::Ok(Self {
signature: signature_value,
slot,
failed,
err_json,
raw_transaction_id: std::option::Option::None,
});
}
/// Adds canonical raw transaction lineage to an already validated core transaction insert.
pub fn with_raw_transaction_id(mut self, raw_transaction_id: i64) -> Self {
self.raw_transaction_id = std::option::Option::Some(raw_transaction_id);
return self;
}
}
/// Core account key insert contract.
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct CoreAccountKeyInsert {
/// Transaction signature as non-empty base58 text.
pub signature: std::string::String,
/// Transaction slot in Solana unsigned representation.
pub slot: u64,
/// Stable account index after static and loaded keys are resolved.
pub account_index: u32,
/// Account public key as non-empty base58 text.
pub account_key: std::string::String,
/// Source category for this account key.
pub source: CoreAccountKeySource,
/// Whether the resolved account is writable for the transaction.
pub writable: bool,
/// Whether the resolved account signed the transaction.
pub signer: bool,
/// Whether the resolved account is executable when known.
pub executable: std::option::Option<bool>,
}
impl CoreAccountKeyInsert {
/// Builds a core account key insert contract after minimal validation.
#[allow(clippy::too_many_arguments)]
pub fn new(
signature: impl std::convert::Into<std::string::String>,
slot: u64,
account_index: u32,
account_key: impl std::convert::Into<std::string::String>,
source: CoreAccountKeySource,
writable: bool,
signer: bool,
executable: std::option::Option<bool>,
) -> kb_core::Result<Self> {
let signature_value = signature.into();
let account_key_value = account_key.into();
let signature_result = validate_required_text(
&signature_value,
"core account key signature must not be empty",
);
if let std::result::Result::Err(error) = signature_result {
return std::result::Result::Err(error);
}
let account_key_result =
validate_required_text(&account_key_value, "core account key must not be empty");
if let std::result::Result::Err(error) = account_key_result {
return std::result::Result::Err(error);
}
return std::result::Result::Ok(Self {
signature: signature_value,
slot,
account_index,
account_key: account_key_value,
source,
writable,
signer,
executable,
});
}
}
/// Core instruction insert contract.
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct CoreInstructionInsert {
/// Transaction signature as non-empty base58 text.
pub signature: std::string::String,
/// Transaction slot in Solana unsigned representation.
pub slot: u64,
/// Stable instruction path, for example `0` or `2/1`.
pub instruction_path: std::string::String,
/// Program id as non-empty base58 text.
pub program_id: std::string::String,
/// Instruction accounts as JSON, preserving unresolved forms when needed.
pub accounts_json: serde_json::Value,
/// Instruction payload JSON, preserving raw and partially decoded forms when needed.
pub payload_json: serde_json::Value,
/// Optional deterministic payload JSON hash.
pub payload_json_hash: std::option::Option<std::string::String>,
/// Initial processing state used by replay schedulers.
pub processing_state: CoreInstructionProcessingState,
}
impl CoreInstructionInsert {
/// Builds a core instruction insert contract after minimal validation.
pub fn new(
signature: impl std::convert::Into<std::string::String>,
slot: u64,
instruction_path: impl std::convert::Into<std::string::String>,
program_id: impl std::convert::Into<std::string::String>,
accounts_json: serde_json::Value,
payload_json: serde_json::Value,
) -> kb_core::Result<Self> {
let signature_value = signature.into();
let instruction_path_value = instruction_path.into();
let program_id_value = program_id.into();
let validation_result = validate_instruction_identity(
signature_value.as_str(),
instruction_path_value.as_str(),
program_id_value.as_str(),
"core instruction",
);
if let std::result::Result::Err(error) = validation_result {
return std::result::Result::Err(error);
}
return std::result::Result::Ok(Self {
signature: signature_value,
slot,
instruction_path: instruction_path_value,
program_id: program_id_value,
accounts_json,
payload_json,
payload_json_hash: std::option::Option::None,
processing_state: CoreInstructionProcessingState::Pending,
});
}
/// Adds a deterministic payload JSON hash.
pub fn with_payload_json_hash(
mut self,
payload_json_hash: impl std::convert::Into<std::string::String>,
) -> kb_core::Result<Self> {
let value = payload_json_hash.into();
if value.trim().is_empty() {
return std::result::Result::Err(kb_core::Error::db(
"core instruction payload hash must not be empty",
));
}
self.payload_json_hash = std::option::Option::Some(value);
return std::result::Result::Ok(self);
}
}
/// Core inner instruction insert contract.
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct CoreInnerInstructionInsert {
/// Transaction signature as non-empty base58 text.
pub signature: std::string::String,
/// Transaction slot in Solana unsigned representation.
pub slot: u64,
/// Parent top-level or inner instruction path.
pub parent_instruction_path: std::string::String,
/// Stable inner instruction path, for example `2/1`.
pub instruction_path: std::string::String,
/// Program id as non-empty base58 text.
pub program_id: std::string::String,
/// Inner instruction accounts as JSON.
pub accounts_json: serde_json::Value,
/// Inner instruction payload JSON.
pub payload_json: serde_json::Value,
/// Optional deterministic payload JSON hash.
pub payload_json_hash: std::option::Option<std::string::String>,
}
impl CoreInnerInstructionInsert {
/// Builds a core inner instruction insert contract after minimal validation.
pub fn new(
signature: impl std::convert::Into<std::string::String>,
slot: u64,
parent_instruction_path: impl std::convert::Into<std::string::String>,
instruction_path: impl std::convert::Into<std::string::String>,
program_id: impl std::convert::Into<std::string::String>,
accounts_json: serde_json::Value,
payload_json: serde_json::Value,
) -> kb_core::Result<Self> {
let signature_value = signature.into();
let parent_instruction_path_value = parent_instruction_path.into();
let instruction_path_value = instruction_path.into();
let program_id_value = program_id.into();
let parent_result = validate_required_text(
&parent_instruction_path_value,
"core inner instruction parent path must not be empty",
);
if let std::result::Result::Err(error) = parent_result {
return std::result::Result::Err(error);
}
let validation_result = validate_instruction_identity(
signature_value.as_str(),
instruction_path_value.as_str(),
program_id_value.as_str(),
"core inner instruction",
);
if let std::result::Result::Err(error) = validation_result {
return std::result::Result::Err(error);
}
return std::result::Result::Ok(Self {
signature: signature_value,
slot,
parent_instruction_path: parent_instruction_path_value,
instruction_path: instruction_path_value,
program_id: program_id_value,
accounts_json,
payload_json,
payload_json_hash: std::option::Option::None,
});
}
/// Adds a deterministic payload JSON hash.
pub fn with_payload_json_hash(
mut self,
payload_json_hash: impl std::convert::Into<std::string::String>,
) -> kb_core::Result<Self> {
let value = payload_json_hash.into();
if value.trim().is_empty() {
return std::result::Result::Err(kb_core::Error::db(
"core inner instruction payload hash must not be empty",
));
}
self.payload_json_hash = std::option::Option::Some(value);
return std::result::Result::Ok(self);
}
}
/// Core log insert contract.
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct CoreLogInsert {
/// Transaction signature as non-empty base58 text.
pub signature: std::string::String,
/// Transaction slot in Solana unsigned representation.
pub slot: u64,
/// Log index preserving transaction log order.
pub log_index: u32,
/// Optional instruction path resolved from invocation depth when known.
pub instruction_path: std::option::Option<std::string::String>,
/// Optional program id resolved from the log line or invocation context.
pub program_id: std::option::Option<std::string::String>,
/// Original log text.
pub log_text: std::string::String,
/// Optional deterministic log text hash.
pub log_text_hash: std::option::Option<std::string::String>,
}
impl CoreLogInsert {
/// Builds a core log insert contract after minimal validation.
pub fn new(
signature: impl std::convert::Into<std::string::String>,
slot: u64,
log_index: u32,
instruction_path: std::option::Option<std::string::String>,
program_id: std::option::Option<std::string::String>,
log_text: impl std::convert::Into<std::string::String>,
) -> kb_core::Result<Self> {
let signature_value = signature.into();
let log_text_value = log_text.into();
let signature_result =
validate_required_text(&signature_value, "core log signature must not be empty");
if let std::result::Result::Err(error) = signature_result {
return std::result::Result::Err(error);
}
let log_text_result =
validate_required_text(&log_text_value, "core log text must not be empty");
if let std::result::Result::Err(error) = log_text_result {
return std::result::Result::Err(error);
}
let instruction_path_result = validate_optional_text(
instruction_path.as_deref(),
"core log instruction path must not be empty when present",
);
if let std::result::Result::Err(error) = instruction_path_result {
return std::result::Result::Err(error);
}
let program_id_result = validate_optional_text(
program_id.as_deref(),
"core log program id must not be empty when present",
);
if let std::result::Result::Err(error) = program_id_result {
return std::result::Result::Err(error);
}
return std::result::Result::Ok(Self {
signature: signature_value,
slot,
log_index,
instruction_path,
program_id,
log_text: log_text_value,
log_text_hash: std::option::Option::None,
});
}
/// Adds a deterministic log text hash.
pub fn with_log_text_hash(
mut self,
log_text_hash: impl std::convert::Into<std::string::String>,
) -> kb_core::Result<Self> {
let value = log_text_hash.into();
if value.trim().is_empty() {
return std::result::Result::Err(kb_core::Error::db(
"core log text hash must not be empty",
));
}
self.log_text_hash = std::option::Option::Some(value);
return std::result::Result::Ok(self);
}
}
/// Core balance change insert contract.
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct CoreBalanceChangeInsert {
/// Transaction signature as non-empty base58 text.
pub signature: std::string::String,
/// Transaction slot in Solana unsigned representation.
pub slot: u64,
/// Stable balance change index preserving extraction order.
pub balance_change_index: u32,
/// Balance change family.
pub balance_kind: CoreBalanceChangeKind,
/// Optional account index when available.
pub account_index: std::option::Option<u32>,
/// Optional account public key when available.
pub account_key: std::option::Option<std::string::String>,
/// Optional SPL token mint for token balances.
pub mint: std::option::Option<std::string::String>,
/// Optional owner public key for token balances.
pub owner: std::option::Option<std::string::String>,
/// Pre-balance JSON value preserving RPC representation.
pub pre_balance_json: std::option::Option<serde_json::Value>,
/// Post-balance JSON value preserving RPC representation.
pub post_balance_json: std::option::Option<serde_json::Value>,
/// Delta JSON value preserving integer or decimal-safe representation.
pub delta_json: std::option::Option<serde_json::Value>,
}
impl CoreBalanceChangeInsert {
/// Builds a core balance change insert contract after minimal validation.
#[allow(clippy::too_many_arguments)]
pub fn new(
signature: impl std::convert::Into<std::string::String>,
slot: u64,
balance_change_index: u32,
balance_kind: CoreBalanceChangeKind,
account_index: std::option::Option<u32>,
account_key: std::option::Option<std::string::String>,
mint: std::option::Option<std::string::String>,
owner: std::option::Option<std::string::String>,
pre_balance_json: std::option::Option<serde_json::Value>,
post_balance_json: std::option::Option<serde_json::Value>,
delta_json: std::option::Option<serde_json::Value>,
) -> kb_core::Result<Self> {
let signature_value = signature.into();
let signature_result = validate_required_text(
&signature_value,
"core balance change signature must not be empty",
);
if let std::result::Result::Err(error) = signature_result {
return std::result::Result::Err(error);
}
let account_key_result = validate_optional_text(
account_key.as_deref(),
"core balance change account key must not be empty when present",
);
if let std::result::Result::Err(error) = account_key_result {
return std::result::Result::Err(error);
}
let mint_result = validate_optional_text(
mint.as_deref(),
"core balance change mint must not be empty when present",
);
if let std::result::Result::Err(error) = mint_result {
return std::result::Result::Err(error);
}
let owner_result = validate_optional_text(
owner.as_deref(),
"core balance change owner must not be empty when present",
);
if let std::result::Result::Err(error) = owner_result {
return std::result::Result::Err(error);
}
return std::result::Result::Ok(Self {
signature: signature_value,
slot,
balance_change_index,
balance_kind,
account_index,
account_key,
mint,
owner,
pre_balance_json,
post_balance_json,
delta_json,
});
}
}
/// Core instruction replay filter contract.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct CoreInstructionReplayFilter {
/// Optional processing state to select, usually `Pending`, `Failed` or `ReplayRequested`.
pub processing_state: std::option::Option<CoreInstructionProcessingState>,
/// Optional program id filter.
pub program_id: std::option::Option<std::string::String>,
/// Optional inclusive minimum slot.
pub min_slot: std::option::Option<u64>,
/// Optional inclusive maximum slot.
pub max_slot: std::option::Option<u64>,
}
impl CoreInstructionReplayFilter {
/// Builds a replay filter after minimal validation.
pub fn new(
processing_state: std::option::Option<CoreInstructionProcessingState>,
program_id: std::option::Option<std::string::String>,
min_slot: std::option::Option<u64>,
max_slot: std::option::Option<u64>,
) -> kb_core::Result<Self> {
if let std::option::Option::Some(program_id_value) = program_id.as_ref() {
if program_id_value.trim().is_empty() {
return std::result::Result::Err(kb_core::Error::db(
"core instruction replay program id must not be empty when present",
));
}
}
if let (
std::option::Option::Some(min_slot_value),
std::option::Option::Some(max_slot_value),
) = (min_slot, max_slot)
{
if min_slot_value > max_slot_value {
return std::result::Result::Err(kb_core::Error::db(
"core instruction replay min slot must be lower than or equal to max slot",
));
}
}
return std::result::Result::Ok(Self {
processing_state,
program_id,
min_slot,
max_slot,
});
}
/// Builds the default pending instruction replay filter.
pub fn pending() -> Self {
return Self {
processing_state: std::option::Option::Some(CoreInstructionProcessingState::Pending),
program_id: std::option::Option::None,
min_slot: std::option::Option::None,
max_slot: std::option::Option::None,
};
}
}
/// Core instruction lifecycle mark request.
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct CoreInstructionLifecycleMark {
/// Transaction signature as non-empty base58 text.
pub signature: std::string::String,
/// Stable instruction path, for example `0` or `2/1`.
pub instruction_path: std::string::String,
/// New processing state.
pub processing_state: CoreInstructionProcessingState,
/// Optional processor name that produced the lifecycle transition.
pub processor_name: std::option::Option<std::string::String>,
/// Optional processor version that produced the lifecycle transition.
pub processor_version: std::option::Option<std::string::String>,
/// Optional reason visible in diagnostics.
pub reason: std::option::Option<std::string::String>,
}
impl CoreInstructionLifecycleMark {
/// Builds a core instruction lifecycle mark after minimal validation.
pub fn new(
signature: impl std::convert::Into<std::string::String>,
instruction_path: impl std::convert::Into<std::string::String>,
processing_state: CoreInstructionProcessingState,
processor_name: std::option::Option<std::string::String>,
processor_version: std::option::Option<std::string::String>,
reason: std::option::Option<std::string::String>,
) -> kb_core::Result<Self> {
let signature_value = signature.into();
let instruction_path_value = instruction_path.into();
let signature_result = validate_required_text(
&signature_value,
"core instruction lifecycle signature must not be empty",
);
if let std::result::Result::Err(error) = signature_result {
return std::result::Result::Err(error);
}
let instruction_path_result = validate_required_text(
&instruction_path_value,
"core instruction lifecycle path must not be empty",
);
if let std::result::Result::Err(error) = instruction_path_result {
return std::result::Result::Err(error);
}
let processor_name_result = validate_optional_text(
processor_name.as_deref(),
"core instruction lifecycle processor name must not be empty when present",
);
if let std::result::Result::Err(error) = processor_name_result {
return std::result::Result::Err(error);
}
let processor_version_result = validate_optional_text(
processor_version.as_deref(),
"core instruction lifecycle processor version must not be empty when present",
);
if let std::result::Result::Err(error) = processor_version_result {
return std::result::Result::Err(error);
}
let reason_result = validate_optional_text(
reason.as_deref(),
"core instruction lifecycle reason must not be empty when present",
);
if let std::result::Result::Err(error) = reason_result {
return std::result::Result::Err(error);
}
return std::result::Result::Ok(Self {
signature: signature_value,
instruction_path: instruction_path_value,
processing_state,
processor_name,
processor_version,
reason,
});
}
}
fn validate_instruction_identity(
signature: &str,
instruction_path: &str,
program_id: &str,
label: &str,
) -> kb_core::Result<()> {
let signature_result =
validate_required_text(signature, "instruction signature must not be empty");
if let std::result::Result::Err(_error) = signature_result {
return std::result::Result::Err(kb_core::Error::db(format!(
"{label} signature must not be empty"
)));
}
let instruction_path_result =
validate_required_text(instruction_path, "instruction path must not be empty");
if let std::result::Result::Err(_error) = instruction_path_result {
return std::result::Result::Err(kb_core::Error::db(format!(
"{label} path must not be empty"
)));
}
let program_id_result =
validate_required_text(program_id, "instruction program id must not be empty");
if let std::result::Result::Err(_error) = program_id_result {
return std::result::Result::Err(kb_core::Error::db(format!(
"{label} program id must not be empty"
)));
}
return std::result::Result::Ok(());
}
fn validate_required_text(
value: &str,
message: impl std::convert::Into<std::string::String>,
) -> kb_core::Result<()> {
if value.trim().is_empty() {
return std::result::Result::Err(kb_core::Error::db(message.into()));
}
return std::result::Result::Ok(());
}
fn validate_optional_text(
value: std::option::Option<&str>,
message: &'static str,
) -> kb_core::Result<()> {
if let std::option::Option::Some(text_value) = value {
if text_value.trim().is_empty() {
return std::result::Result::Err(kb_core::Error::db(message));
}
}
return std::result::Result::Ok(());
}
#[cfg(test)]
mod tests {
#[test]
fn core_transaction_rejects_empty_signature() {
let result = crate::CoreTransactionInsert::new(" ", 1, false, std::option::Option::None);
assert!(result.is_err());
}
#[test]
fn core_transaction_accepts_optional_raw_lineage() {
let result = crate::CoreTransactionInsert::new("abc", 1, false, std::option::Option::None);
let input = match result {
std::result::Result::Ok(value) => value.with_raw_transaction_id(7),
std::result::Result::Err(error) => panic!("unexpected transaction error: {error}"),
};
assert_eq!(input.raw_transaction_id, std::option::Option::Some(7));
}
#[test]
fn core_account_key_rejects_empty_key() {
let result = crate::CoreAccountKeyInsert::new(
"abc",
1,
0,
" ",
crate::CoreAccountKeySource::Static,
false,
false,
std::option::Option::None,
);
assert!(result.is_err());
}
#[test]
fn core_instruction_rejects_empty_program_id() {
let result = crate::CoreInstructionInsert::new(
"abc",
1,
"0",
" ",
serde_json::json!([]),
serde_json::json!({}),
);
assert!(result.is_err());
}
#[test]
fn core_instruction_defaults_to_pending() {
let result = crate::CoreInstructionInsert::new(
"abc",
1,
"0",
"program",
serde_json::json!([]),
serde_json::json!({}),
);
if let std::result::Result::Ok(input) = result {
assert_eq!(crate::CoreInstructionProcessingState::Pending, input.processing_state);
} else {
panic!("core instruction insert should be valid");
}
}
#[test]
fn core_inner_instruction_rejects_empty_parent_path() {
let result = crate::CoreInnerInstructionInsert::new(
"abc",
1,
" ",
"0/0",
"program",
serde_json::json!([]),
serde_json::json!({}),
);
assert!(result.is_err());
}
#[test]
fn core_log_rejects_empty_text() {
let result = crate::CoreLogInsert::new(
"abc",
1,
0,
std::option::Option::None,
std::option::Option::None,
" ",
);
assert!(result.is_err());
}
#[test]
fn core_balance_change_rejects_empty_optional_mint() {
let result = crate::CoreBalanceChangeInsert::new(
"abc",
1,
0,
crate::CoreBalanceChangeKind::TokenAmount,
std::option::Option::Some(0),
std::option::Option::Some("account".to_string()),
std::option::Option::Some(" ".to_string()),
std::option::Option::None,
std::option::Option::None,
std::option::Option::None,
std::option::Option::None,
);
assert!(result.is_err());
}
#[test]
fn replay_input_rejects_empty_key() {
let result = crate::CoreInstructionReplayInput::new(
" ",
"abc",
1,
"0",
"program",
false,
std::option::Option::None,
serde_json::json!([]),
serde_json::json!([]),
std::option::Option::Some(serde_json::json!({})),
std::option::Option::None,
serde_json::json!([]),
serde_json::json!([]),
serde_json::json!([]),
serde_json::json!([]),
);
assert!(result.is_err());
}
#[test]
fn replay_input_accepts_ordered_outer_instruction_array() {
let result = crate::CoreInstructionReplayInput::new(
"abc:2",
"abc",
1,
"2",
"program",
false,
std::option::Option::None,
serde_json::json!([]),
serde_json::json!([]),
std::option::Option::Some(serde_json::json!({"dataBase64": "AQ=="})),
std::option::Option::Some("payload-hash".to_string()),
serde_json::json!([
{
"instructionIndex": 0,
"instructionPath": "0",
"programId": "other",
"payloadJson": {"dataBase64": "Ag=="},
"payloadHash": "other-hash"
},
{
"instructionIndex": 2,
"instructionPath": "2",
"programId": "program",
"payloadJson": {"dataBase64": "AQ=="},
"payloadHash": "payload-hash"
}
]),
serde_json::json!([]),
serde_json::json!([]),
serde_json::json!([]),
);
let input = match result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("unexpected replay input error: {error}"),
};
assert_eq!(input.core_contract_version, 2);
assert_eq!(
input.outer_instructions_json.as_array().map(std::vec::Vec::len),
std::option::Option::Some(2)
);
}
#[test]
fn replay_input_rejects_non_array_outer_instruction_context() {
let result = crate::CoreInstructionReplayInput::new(
"abc:0",
"abc",
1,
"0",
"program",
false,
std::option::Option::None,
serde_json::json!([]),
serde_json::json!([]),
std::option::Option::Some(serde_json::json!({"dataBase64": "AQ=="})),
std::option::Option::None,
serde_json::json!({"instructionIndex": 0}),
serde_json::json!([]),
serde_json::json!([]),
serde_json::json!([]),
);
assert!(result.is_err());
}
#[test]
fn replay_filter_rejects_empty_program_id() {
let result = crate::CoreInstructionReplayFilter::new(
std::option::Option::Some(crate::CoreInstructionProcessingState::Pending),
std::option::Option::Some(" ".to_string()),
std::option::Option::None,
std::option::Option::None,
);
assert!(result.is_err());
}
#[test]
fn replay_filter_rejects_inverted_slots() {
let result = crate::CoreInstructionReplayFilter::new(
std::option::Option::None,
std::option::Option::None,
std::option::Option::Some(10),
std::option::Option::Some(1),
);
assert!(result.is_err());
}
#[test]
fn lifecycle_mark_rejects_empty_path() {
let result = crate::CoreInstructionLifecycleMark::new(
"signature",
" ",
crate::CoreInstructionProcessingState::Decoded,
std::option::Option::None,
std::option::Option::None,
std::option::Option::None,
);
assert!(result.is_err());
}
}

View File

@@ -0,0 +1,289 @@
// file: kb-store/src/contracts/dto/core_extraction.rs
// version: 1
//! Canonical transaction to core extraction storage contracts.
/// Stable processing status for one extraction ledger entry.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub enum ProcessingLedgerStatus {
/// Processing is currently running.
Running,
/// Processing completed successfully.
Succeeded,
/// Processing failed and may be retried.
Failed,
}
/// Bounded selection filter for canonical transactions awaiting core extraction.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct CoreExtractionSelectionFilter {
/// Optional exact signatures selected by the operator.
pub signatures: std::vec::Vec<std::string::String>,
/// Optional inclusive minimum slot.
pub min_slot: std::option::Option<u64>,
/// Optional inclusive maximum slot.
pub max_slot: std::option::Option<u64>,
/// Optional raw processing state restriction.
pub processing_state: std::option::Option<crate::RawPayloadProcessingState>,
/// Optional program id previously resolved in core instructions.
pub program_id: std::option::Option<std::string::String>,
/// Maximum number of canonical transactions returned.
pub limit: u32,
}
impl CoreExtractionSelectionFilter {
/// Builds a validated extraction selection filter.
pub fn new(
signatures: std::vec::Vec<std::string::String>,
min_slot: std::option::Option<u64>,
max_slot: std::option::Option<u64>,
processing_state: std::option::Option<crate::RawPayloadProcessingState>,
program_id: std::option::Option<std::string::String>,
limit: u32,
) -> kb_core::Result<Self> {
if limit == 0 {
return std::result::Result::Err(kb_core::Error::db(
"core extraction selection limit must be greater than zero",
));
}
if let (std::option::Option::Some(minimum), std::option::Option::Some(maximum)) =
(min_slot, max_slot)
{
if minimum > maximum {
return std::result::Result::Err(kb_core::Error::db(
"core extraction minimum slot must not exceed maximum slot",
));
}
}
for signature in &signatures {
if signature.trim().is_empty() {
return std::result::Result::Err(kb_core::Error::db(
"core extraction signature filter must not contain empty values",
));
}
}
if program_id.as_deref().is_some_and(|value| return value.trim().is_empty()) {
return std::result::Result::Err(kb_core::Error::db(
"core extraction program id filter must not be empty",
));
}
return std::result::Result::Ok(Self {
signatures,
min_slot,
max_slot,
processing_state,
program_id,
limit,
});
}
/// Builds a pending raw transaction selection.
pub fn pending(limit: u32) -> kb_core::Result<Self> {
return Self::new(
std::vec::Vec::new(),
std::option::Option::None,
std::option::Option::None,
std::option::Option::Some(crate::RawPayloadProcessingState::Received),
std::option::Option::None,
limit,
);
}
}
/// Stable identity of one processor input in the processing ledger.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct ProcessingLedgerIdentity {
/// Processing stage code.
pub stage: std::string::String,
/// Processor implementation name.
pub processor_name: std::string::String,
/// Processor semantic version.
pub processor_version: std::string::String,
/// Stable input key.
pub input_key: std::string::String,
/// Deterministic input hash.
pub input_hash: std::string::String,
}
impl ProcessingLedgerIdentity {
/// Builds a validated processing ledger identity.
pub fn new(
stage: impl std::convert::Into<std::string::String>,
processor_name: impl std::convert::Into<std::string::String>,
processor_version: impl std::convert::Into<std::string::String>,
input_key: impl std::convert::Into<std::string::String>,
input_hash: impl std::convert::Into<std::string::String>,
) -> kb_core::Result<Self> {
let value = Self {
stage: stage.into(),
processor_name: processor_name.into(),
processor_version: processor_version.into(),
input_key: input_key.into(),
input_hash: input_hash.into(),
};
if value.stage.trim().is_empty()
|| value.processor_name.trim().is_empty()
|| value.processor_version.trim().is_empty()
|| value.input_key.trim().is_empty()
|| value.input_hash.trim().is_empty()
{
return std::result::Result::Err(kb_core::Error::db(
"processing ledger identity fields must not be empty",
));
}
return std::result::Result::Ok(value);
}
}
/// Complete set of normalized rows produced from one canonical transaction.
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct CoreExtractionBundle {
/// Source canonical raw transaction technical id.
pub raw_transaction_id: i64,
/// Processing ledger identity.
pub ledger_identity: crate::ProcessingLedgerIdentity,
/// Core transaction row.
pub transaction: crate::CoreTransactionInsert,
/// Resolved account keys.
pub account_keys: std::vec::Vec<crate::CoreAccountKeyInsert>,
/// Top-level instructions.
pub instructions: std::vec::Vec<crate::CoreInstructionInsert>,
/// Inner instructions.
pub inner_instructions: std::vec::Vec<crate::CoreInnerInstructionInsert>,
/// Ordered transaction logs.
pub logs: std::vec::Vec<crate::CoreLogInsert>,
/// Native and token balance changes.
pub balance_changes: std::vec::Vec<crate::CoreBalanceChangeInsert>,
}
impl CoreExtractionBundle {
/// Validates lineage and stable signature consistency across the bundle.
pub fn validate(&self) -> kb_core::Result<()> {
if self.raw_transaction_id <= 0 {
return std::result::Result::Err(kb_core::Error::db(
"core extraction raw transaction id must be positive",
));
}
if self.transaction.raw_transaction_id != std::option::Option::Some(self.raw_transaction_id)
{
return std::result::Result::Err(kb_core::Error::db(
"core extraction transaction lineage does not match the raw transaction id",
));
}
if self.transaction.signature != self.ledger_identity.input_key {
return std::result::Result::Err(kb_core::Error::db(
"core extraction ledger input key must equal the transaction signature",
));
}
let signature = self.transaction.signature.as_str();
for input in &self.account_keys {
if input.signature != signature {
return std::result::Result::Err(kb_core::Error::db(
"core extraction account key signature mismatch",
));
}
}
for input in &self.instructions {
if input.signature != signature {
return std::result::Result::Err(kb_core::Error::db(
"core extraction instruction signature mismatch",
));
}
}
for input in &self.inner_instructions {
if input.signature != signature {
return std::result::Result::Err(kb_core::Error::db(
"core extraction inner instruction signature mismatch",
));
}
}
for input in &self.logs {
if input.signature != signature {
return std::result::Result::Err(kb_core::Error::db(
"core extraction log signature mismatch",
));
}
}
for input in &self.balance_changes {
if input.signature != signature {
return std::result::Result::Err(kb_core::Error::db(
"core extraction balance signature mismatch",
));
}
}
return std::result::Result::Ok(());
}
}
/// Failure details persisted when canonical to core extraction cannot complete.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct CoreExtractionFailure {
/// Source canonical raw transaction technical id.
pub raw_transaction_id: i64,
/// Processing ledger identity.
pub ledger_identity: crate::ProcessingLedgerIdentity,
/// Stable machine-readable error code.
pub error_code: std::string::String,
/// Human-readable diagnostic message.
pub error_message: std::string::String,
}
impl CoreExtractionFailure {
/// Builds a validated failure record.
pub fn new(
raw_transaction_id: i64,
ledger_identity: crate::ProcessingLedgerIdentity,
error_code: impl std::convert::Into<std::string::String>,
error_message: impl std::convert::Into<std::string::String>,
) -> kb_core::Result<Self> {
let value = Self {
raw_transaction_id,
ledger_identity,
error_code: error_code.into(),
error_message: error_message.into(),
};
if value.raw_transaction_id <= 0
|| value.error_code.trim().is_empty()
|| value.error_message.trim().is_empty()
{
return std::result::Result::Err(kb_core::Error::db(
"core extraction failure fields are invalid",
));
}
return std::result::Result::Ok(value);
}
}
#[cfg(test)]
mod tests {
#[test]
fn pending_filter_requires_positive_limit() {
let result = crate::CoreExtractionSelectionFilter::pending(0);
assert!(result.is_err());
}
#[test]
fn selection_filter_rejects_inverted_slots() {
let result = crate::CoreExtractionSelectionFilter::new(
std::vec::Vec::new(),
std::option::Option::Some(20),
std::option::Option::Some(10),
std::option::Option::None,
std::option::Option::None,
10,
);
assert!(result.is_err());
}
#[test]
fn ledger_identity_requires_input_hash() {
let result = crate::ProcessingLedgerIdentity::new(
"core_extraction",
"canonical_to_core",
"1",
"signature",
" ",
);
assert!(result.is_err());
}
}

View File

@@ -0,0 +1,688 @@
// file: kb-store/src/contracts/dto/decode.rs
// version: 2
//! Backend-neutral decode, coverage and materialization persistence DTOs.
/// Maximum number of materialized rows returned by one bounded query.
pub const MAX_MATERIALIZED_EVENT_QUERY_ROWS: u32 = 500;
/// Bounded read-only materialized event selection.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct MaterializedEventFilter {
/// Optional exact materializer processor name.
pub processor_name: std::option::Option<std::string::String>,
/// Optional exact materialized family code.
pub materialized_family: std::option::Option<std::string::String>,
/// Optional partial transaction signature.
pub signature_contains: std::option::Option<std::string::String>,
/// Maximum returned rows.
pub limit: u32,
}
impl MaterializedEventFilter {
/// Builds and validates a bounded materialized event filter.
pub fn new(
processor_name: std::option::Option<std::string::String>,
materialized_family: std::option::Option<std::string::String>,
signature_contains: std::option::Option<std::string::String>,
limit: u32,
) -> kb_core::Result<Self> {
if limit == 0 || limit > crate::MAX_MATERIALIZED_EVENT_QUERY_ROWS {
return std::result::Result::Err(kb_core::Error::db(format!(
"materialized event query limit must be between 1 and {}",
crate::MAX_MATERIALIZED_EVENT_QUERY_ROWS
)));
}
return std::result::Result::Ok(Self {
processor_name: crate::contracts::dto::decode::trim_optional_text(processor_name),
materialized_family: crate::contracts::dto::decode::trim_optional_text(
materialized_family,
),
signature_contains: crate::contracts::dto::decode::trim_optional_text(
signature_contains,
),
limit,
});
}
}
/// One materialized output returned by the common bounded query contract.
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct MaterializedEventQueryRow {
/// Materializer processor name.
pub processor_name: std::string::String,
/// Materializer processor version.
pub processor_version: std::string::String,
/// Stable materializer input key.
pub input_key: std::string::String,
/// Stable processor-owned output key.
pub output_key: std::string::String,
/// Source decoded event key.
pub source_event_key: std::string::String,
/// Source decoder name.
pub source_decoder_name: std::string::String,
/// Source decoder version.
pub source_decoder_version: std::string::String,
/// Source transaction signature.
pub signature: std::string::String,
/// Source transaction slot.
pub slot: u64,
/// Materialized family code.
pub materialized_family: std::string::String,
/// Typed materialized payload.
pub payload_json: serde_json::Value,
/// Creation timestamp rendered by the backend.
pub created_at: std::string::String,
/// Last replacement timestamp rendered by the backend.
pub updated_at: std::string::String,
}
/// Bounded contextual instruction selection filter for decode campaigns.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct DecodeSelectionFilter {
/// Explicit transaction signatures to select.
pub signatures: std::vec::Vec<std::string::String>,
/// Explicit instruction processing states to select.
pub processing_states: std::vec::Vec<crate::CoreInstructionProcessingState>,
/// Optional inclusive minimum slot.
pub min_slot: std::option::Option<u64>,
/// Optional inclusive maximum slot.
pub max_slot: std::option::Option<u64>,
/// Explicit program identifiers to select.
pub program_ids: std::vec::Vec<std::string::String>,
/// Explicit stable instruction paths to select.
pub instruction_paths: std::vec::Vec<std::string::String>,
/// Expands any incomplete instruction match to every instruction in the same signature.
pub incomplete_signatures: bool,
/// Maximum number of contextual inputs, or signatures when expansion is enabled.
pub limit: u32,
}
impl DecodeSelectionFilter {
/// Builds a validated bounded decode selection filter.
#[allow(clippy::too_many_arguments)]
pub fn new(
signatures: std::vec::Vec<std::string::String>,
processing_states: std::vec::Vec<crate::CoreInstructionProcessingState>,
min_slot: std::option::Option<u64>,
max_slot: std::option::Option<u64>,
program_ids: std::vec::Vec<std::string::String>,
instruction_paths: std::vec::Vec<std::string::String>,
incomplete_signatures: bool,
limit: u32,
) -> kb_core::Result<Self> {
if limit == 0 {
return std::result::Result::Err(kb_core::Error::db(
"decode selection limit must be greater than zero",
));
}
if min_slot.is_some() && max_slot.is_some() && min_slot > max_slot {
return std::result::Result::Err(kb_core::Error::db(
"decode selection minimum slot must not exceed maximum slot",
));
}
let signatures_result = validate_text_list(&signatures, "decode selection signature");
if let std::result::Result::Err(error) = signatures_result {
return std::result::Result::Err(error);
}
let program_ids_result = validate_text_list(&program_ids, "decode selection program id");
if let std::result::Result::Err(error) = program_ids_result {
return std::result::Result::Err(error);
}
let paths_result =
validate_text_list(&instruction_paths, "decode selection instruction path");
if let std::result::Result::Err(error) = paths_result {
return std::result::Result::Err(error);
}
return std::result::Result::Ok(Self {
signatures,
processing_states,
min_slot,
max_slot,
program_ids,
instruction_paths,
incomplete_signatures,
limit,
});
}
/// Builds the default pending, failed and replay-requested selection.
pub fn actionable(limit: u32) -> kb_core::Result<Self> {
return crate::DecodeSelectionFilter::new(
std::vec::Vec::new(),
std::vec![
crate::CoreInstructionProcessingState::Pending,
crate::CoreInstructionProcessingState::Failed,
crate::CoreInstructionProcessingState::ReplayRequested,
],
std::option::Option::None,
std::option::Option::None,
std::vec::Vec::new(),
std::vec::Vec::new(),
false,
limit,
);
}
}
/// One processor-owned decoded observation row.
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct DecodeObservationInsert {
/// Stable decode processor name.
pub processor_name: std::string::String,
/// Stable decode processor version.
pub processor_version: std::string::String,
/// Stable contextual input key.
pub input_key: std::string::String,
/// Deterministic contextual input hash.
pub input_hash: std::string::String,
/// Stable event key within the processor and input.
pub event_key: std::string::String,
/// Source transaction signature.
pub signature: std::string::String,
/// Source transaction slot.
pub slot: u64,
/// Source instruction path.
pub instruction_path: std::string::String,
/// Source program identifier.
pub program_id: std::string::String,
/// Stable protocol code.
pub protocol_code: std::string::String,
/// Stable surface code.
pub surface_code: std::string::String,
/// Stable event code.
pub event_code: std::string::String,
/// Stable event name.
pub event_name: std::string::String,
/// Stable event family code.
pub event_family: std::string::String,
/// Stable event source code.
pub source_kind: std::string::String,
/// Stable decoder confidence code.
pub confidence: std::string::String,
/// Stable proof kind code.
pub proof_kind: std::string::String,
/// Proof evidence JSON.
pub proof_json: serde_json::Value,
/// Typed decoded payload JSON.
pub payload_json: serde_json::Value,
/// Whether the source transaction failed on-chain.
pub transaction_failed: bool,
/// Optional source transaction error JSON.
pub transaction_error: std::option::Option<serde_json::Value>,
/// Whether the observed state mutation was committed on-chain.
pub observation_committed: bool,
}
impl DecodeObservationInsert {
/// Validates stable identities and failed transaction commit semantics.
pub fn validate(&self) -> kb_core::Result<()> {
let fields = [
self.processor_name.as_str(),
self.processor_version.as_str(),
self.input_key.as_str(),
self.input_hash.as_str(),
self.event_key.as_str(),
self.signature.as_str(),
self.instruction_path.as_str(),
self.program_id.as_str(),
self.protocol_code.as_str(),
self.surface_code.as_str(),
self.event_code.as_str(),
self.event_name.as_str(),
self.event_family.as_str(),
self.source_kind.as_str(),
self.confidence.as_str(),
self.proof_kind.as_str(),
];
if fields.iter().any(|value| return value.trim().is_empty()) {
return std::result::Result::Err(kb_core::Error::db(
"decoded observation identity fields must not be empty",
));
}
if self.transaction_failed && self.observation_committed {
return std::result::Result::Err(kb_core::Error::db(
"failed transaction decoded observations must not be committed",
));
}
return std::result::Result::Ok(());
}
}
/// One machine-readable decoder coverage declaration row.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct DecodeCoverageDeclarationInsert {
/// Stable decoder name.
pub processor_name: std::string::String,
/// Stable decoder version.
pub processor_version: std::string::String,
/// Exact Solana program identifier.
pub program_id: std::string::String,
/// Optional stable surface code.
pub surface_code: std::option::Option<std::string::String>,
/// Stable entry kind code.
pub entry_kind: std::string::String,
/// Stable instruction, event or discriminator code.
pub entry_code: std::string::String,
/// Optional normalized hexadecimal discriminator.
pub discriminator_hex: std::option::Option<std::string::String>,
/// Whether the entry is historical or deprecated.
pub historical: bool,
}
/// One observed coverage classification row owned by one decode attempt.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct DecodeCoverageObservationInsert {
/// Stable decoder name.
pub processor_name: std::string::String,
/// Stable decoder version.
pub processor_version: std::string::String,
/// Stable contextual input key.
pub input_key: std::string::String,
/// Deterministic contextual input hash.
pub input_hash: std::string::String,
/// Source transaction signature.
pub signature: std::string::String,
/// Source transaction slot.
pub slot: u64,
/// Source instruction path.
pub instruction_path: std::string::String,
/// Source program identifier.
pub program_id: std::string::String,
/// Optional stable surface code.
pub surface_code: std::option::Option<std::string::String>,
/// Optional recognized entry code.
pub entry_code: std::option::Option<std::string::String>,
/// Optional discriminator.
pub discriminator_hex: std::option::Option<std::string::String>,
/// Stable decode terminal status code.
pub status: std::string::String,
/// Whether the processor recognized the input as compatible.
pub recognized: bool,
/// Number of decoded observations produced.
pub decoded_count: u32,
/// Number of materialized outputs produced immediately after decoding.
pub materialized_count: u32,
/// Number of decoder diagnostics classified as errors.
pub error_count: u32,
/// Whether the source transaction failed on-chain.
pub transaction_failed: bool,
}
/// Atomic persistence bundle for one decoder and one contextual input.
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct DecodePersistenceBundle {
/// Processing ledger identity for the decode attempt.
pub ledger_identity: crate::ProcessingLedgerIdentity,
/// Source transaction signature.
pub signature: std::string::String,
/// Source instruction path.
pub instruction_path: std::string::String,
/// Stable terminal status code.
pub status: std::string::String,
/// Optional stable machine-readable terminal error code.
pub error_code: std::option::Option<std::string::String>,
/// Optional human-readable terminal error message.
pub error_message: std::option::Option<std::string::String>,
/// Processor-owned decoded observations.
pub observations: std::vec::Vec<DecodeObservationInsert>,
/// Coverage observation for this attempt.
pub coverage: DecodeCoverageObservationInsert,
}
impl DecodePersistenceBundle {
/// Validates identities shared by every atomic decode output.
pub fn validate(&self) -> kb_core::Result<()> {
if self.ledger_identity.stage != "instruction_decode"
|| self.signature.trim().is_empty()
|| self.instruction_path.trim().is_empty()
|| self.status.trim().is_empty()
{
return std::result::Result::Err(kb_core::Error::db(
"decode persistence bundle identity is invalid",
));
}
if !matches!(self.status.as_str(), "decoded" | "ignored" | "unsupported" | "failed") {
return std::result::Result::Err(kb_core::Error::db(
"decode persistence bundle status is unsupported",
));
}
if self.coverage.processor_name != self.ledger_identity.processor_name
|| self.coverage.processor_version != self.ledger_identity.processor_version
|| self.coverage.input_key != self.ledger_identity.input_key
|| self.coverage.input_hash != self.ledger_identity.input_hash
|| self.coverage.signature != self.signature
|| self.coverage.instruction_path != self.instruction_path
|| self.coverage.status != self.status
{
return std::result::Result::Err(kb_core::Error::db(
"decode coverage observation does not match bundle identity",
));
}
let observation_count_result = u32::try_from(self.observations.len());
let observation_count = match observation_count_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(kb_core::Error::db(
"decode observation count exceeds the supported range",
));
},
};
if self.coverage.decoded_count != observation_count
|| (self.status == "decoded" && self.observations.is_empty())
|| (self.status != "decoded" && !self.observations.is_empty())
{
return std::result::Result::Err(kb_core::Error::db(
"decode status, coverage count and observations are inconsistent",
));
}
let missing_decode_error = match (&self.error_code, &self.error_message) {
(std::option::Option::Some(code), std::option::Option::Some(message)) => {
code.trim().is_empty() || message.trim().is_empty()
},
_ => true,
};
if self.status == "failed" && missing_decode_error {
return std::result::Result::Err(kb_core::Error::db(
"failed decode bundle requires error code and message",
));
}
for observation in &self.observations {
let validation_result = observation.validate();
if let std::result::Result::Err(error) = validation_result {
return std::result::Result::Err(error);
}
if observation.processor_name != self.ledger_identity.processor_name
|| observation.processor_version != self.ledger_identity.processor_version
|| observation.input_key != self.ledger_identity.input_key
|| observation.input_hash != self.ledger_identity.input_hash
|| observation.signature != self.signature
|| observation.instruction_path != self.instruction_path
|| observation.transaction_failed != self.coverage.transaction_failed
{
return std::result::Result::Err(kb_core::Error::db(
"decoded observation does not match bundle identity",
));
}
}
return std::result::Result::Ok(());
}
}
/// Failed decode attempt persisted in the common ledger.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct DecodeFailure {
/// Processing ledger identity.
pub ledger_identity: crate::ProcessingLedgerIdentity,
/// Source transaction signature.
pub signature: std::string::String,
/// Source instruction path.
pub instruction_path: std::string::String,
/// Stable machine-readable error code.
pub error_code: std::string::String,
/// Human-readable diagnostic message.
pub error_message: std::string::String,
}
/// One processor-owned materialized output row.
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct MaterializedOutputInsert {
/// Stable materializer name.
pub processor_name: std::string::String,
/// Stable materializer version.
pub processor_version: std::string::String,
/// Stable decoded observation source key.
pub input_key: std::string::String,
/// Deterministic decoded observation input hash.
pub input_hash: std::string::String,
/// Stable output key within the materializer and input.
pub output_key: std::string::String,
/// Source decoded event key.
pub source_event_key: std::string::String,
/// Source transaction signature.
pub signature: std::string::String,
/// Source transaction slot.
pub slot: u64,
/// Stable materialized family code.
pub materialized_family: std::string::String,
/// Typed business payload JSON.
pub payload_json: serde_json::Value,
}
/// Atomic persistence bundle for one materializer and one decoded observation.
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct MaterializationPersistenceBundle {
/// Processing ledger identity for the materialization attempt.
pub ledger_identity: crate::ProcessingLedgerIdentity,
/// Source decoder name owning the decoded observation.
pub source_decoder_name: std::string::String,
/// Source decoder version owning the decoded observation.
pub source_decoder_version: std::string::String,
/// Source contextual decode input key.
pub source_decode_input_key: std::string::String,
/// Source transaction signature.
pub signature: std::string::String,
/// Source stable instruction path.
pub instruction_path: std::string::String,
/// Stable terminal materializer status code.
pub status: std::string::String,
/// Optional stable machine-readable terminal error code.
pub error_code: std::option::Option<std::string::String>,
/// Optional human-readable terminal error message.
pub error_message: std::option::Option<std::string::String>,
/// Processor-owned materialized outputs.
pub outputs: std::vec::Vec<MaterializedOutputInsert>,
}
impl MaterializationPersistenceBundle {
/// Validates the materializer, source decoder and output identities.
pub fn validate(&self) -> kb_core::Result<()> {
if self.ledger_identity.stage != "event_materialization"
|| self.source_decoder_name.trim().is_empty()
|| self.source_decoder_version.trim().is_empty()
|| self.source_decode_input_key.trim().is_empty()
|| self.signature.trim().is_empty()
|| self.instruction_path.trim().is_empty()
|| self.status.trim().is_empty()
{
return std::result::Result::Err(kb_core::Error::db(
"materialization persistence bundle identity is invalid",
));
}
if !matches!(
self.status.as_str(),
"inserted" | "replaced" | "ignored" | "refused" | "failed"
) {
return std::result::Result::Err(kb_core::Error::db(
"materialization persistence bundle status is unsupported",
));
}
if (matches!(self.status.as_str(), "inserted" | "replaced") && self.outputs.is_empty())
|| (matches!(self.status.as_str(), "ignored" | "refused" | "failed")
&& !self.outputs.is_empty())
{
return std::result::Result::Err(kb_core::Error::db(
"materialization status and outputs are inconsistent",
));
}
let missing_materialization_error = match (&self.error_code, &self.error_message) {
(std::option::Option::Some(code), std::option::Option::Some(message)) => {
code.trim().is_empty() || message.trim().is_empty()
},
_ => true,
};
if self.status == "failed" && missing_materialization_error {
return std::result::Result::Err(kb_core::Error::db(
"failed materialization bundle requires error code and message",
));
}
for output in &self.outputs {
if output.output_key.trim().is_empty()
|| output.source_event_key.trim().is_empty()
|| output.materialized_family.trim().is_empty()
|| output.processor_name != self.ledger_identity.processor_name
|| output.processor_version != self.ledger_identity.processor_version
|| output.input_key != self.ledger_identity.input_key
|| output.input_hash != self.ledger_identity.input_hash
|| output.signature != self.signature
{
return std::result::Result::Err(kb_core::Error::db(
"materialized output does not match bundle identity",
));
}
}
return std::result::Result::Ok(());
}
}
/// One row of aggregated decoder coverage diagnostics.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct DecodeCoverageSummaryRow {
/// Stable decoder name.
pub processor_name: std::string::String,
/// Stable decoder version.
pub processor_version: std::string::String,
/// Exact Solana program identifier.
pub program_id: std::string::String,
/// Optional stable surface code.
pub surface_code: std::option::Option<std::string::String>,
/// Stable entry code or unknown classifier.
pub entry_code: std::string::String,
/// Number of declared matching entries.
pub declared_count: i64,
/// Number of observed inputs.
pub observed_count: i64,
/// Number of recognized inputs.
pub recognized_count: i64,
/// Number of decoded observations.
pub decoded_count: i64,
/// Number of materialized outputs.
pub materialized_count: i64,
/// Number of errors.
pub error_count: i64,
/// Number of observations classified as unknown or unsupported.
pub unknown_count: i64,
/// Number of successful source transactions.
pub successful_transaction_count: i64,
/// Number of failed source transactions.
pub failed_transaction_count: i64,
}
fn validate_text_list(values: &[std::string::String], label: &str) -> kb_core::Result<()> {
if values.iter().any(|value| return value.trim().is_empty()) {
return std::result::Result::Err(kb_core::Error::db(format!("{label} must not be empty")));
}
return std::result::Result::Ok(());
}
fn trim_optional_text(
value: std::option::Option<std::string::String>,
) -> std::option::Option<std::string::String> {
return value.and_then(|text| {
let trimmed = text.trim();
if trimmed.is_empty() {
return std::option::Option::None;
}
return std::option::Option::Some(trimmed.to_string());
});
}
#[cfg(test)]
mod tests {
#[test]
fn actionable_filter_requires_positive_limit() {
assert!(crate::DecodeSelectionFilter::actionable(0).is_err());
}
#[test]
fn incomplete_signature_filter_preserves_signature_limit_semantics() {
let result = crate::DecodeSelectionFilter::new(
std::vec::Vec::new(),
std::vec![crate::CoreInstructionProcessingState::Failed],
std::option::Option::None,
std::option::Option::None,
std::vec::Vec::new(),
std::vec::Vec::new(),
true,
25,
);
let filter = match result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("unexpected filter error: {error}"),
};
assert!(filter.incomplete_signatures);
assert_eq!(filter.limit, 25);
}
#[test]
fn selection_filter_rejects_inverted_slots() {
let result = crate::DecodeSelectionFilter::new(
std::vec::Vec::new(),
std::vec::Vec::new(),
std::option::Option::Some(20),
std::option::Option::Some(10),
std::vec::Vec::new(),
std::vec::Vec::new(),
false,
10,
);
assert!(result.is_err());
}
#[test]
fn materialized_event_filter_is_bounded_and_trims_optional_text() {
let result = crate::MaterializedEventFilter::new(
std::option::Option::Some(" transaction_annotations ".to_string()),
std::option::Option::Some(" transaction_annotation ".to_string()),
std::option::Option::Some(" signature ".to_string()),
crate::MAX_MATERIALIZED_EVENT_QUERY_ROWS,
);
let filter = match result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("unexpected filter error: {error}"),
};
assert_eq!(
filter.processor_name.as_deref(),
std::option::Option::Some("transaction_annotations")
);
assert_eq!(filter.signature_contains.as_deref(), std::option::Option::Some("signature"));
assert!(
crate::MaterializedEventFilter::new(
std::option::Option::None,
std::option::Option::None,
std::option::Option::None,
0,
)
.is_err()
);
}
#[test]
fn failed_decoded_observation_cannot_be_committed() {
let input = crate::DecodeObservationInsert {
processor_name: "decoder".to_string(),
processor_version: "1".to_string(),
input_key: "signature:0".to_string(),
input_hash: "hash".to_string(),
event_key: "event".to_string(),
signature: "signature".to_string(),
slot: 1,
instruction_path: "0".to_string(),
program_id: "program".to_string(),
protocol_code: "protocol".to_string(),
surface_code: "surface".to_string(),
event_code: "event".to_string(),
event_name: "event".to_string(),
event_family: "audit".to_string(),
source_kind: "instruction".to_string(),
confidence: "exact".to_string(),
proof_kind: "exact_layout".to_string(),
proof_json: serde_json::json!({}),
payload_json: serde_json::json!({}),
transaction_failed: true,
transaction_error: std::option::Option::Some(serde_json::json!({})),
observation_committed: true,
};
assert!(input.validate().is_err());
}
}

View File

@@ -0,0 +1,143 @@
// file: kb-store/src/contracts/dto/event.rs
// version: 1
//! Decoded and materialized event storage DTOs.
/// Insert or upsert result contract returned by repositories.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct InsertOutcome {
/// Number of rows inserted by the repository call.
pub inserted_count: u64,
/// Number of rows updated by the repository call.
pub updated_count: u64,
/// Number of rows skipped by the repository call.
pub skipped_count: u64,
}
impl InsertOutcome {
/// Builds an insert outcome from explicit counters.
pub fn new(inserted_count: u64, updated_count: u64, skipped_count: u64) -> Self {
return Self {
inserted_count,
updated_count,
skipped_count,
};
}
/// Returns the sum of inserted, updated and skipped rows.
pub fn total_count(&self) -> u64 {
return self.inserted_count + self.updated_count + self.skipped_count;
}
}
/// Decoded event insert contract.
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct DecodedEventInsert {
/// Transaction signature as non-empty base58 text.
pub signature: std::string::String,
/// Transaction slot in Solana unsigned representation.
pub slot: u64,
/// Stable instruction path, for example `0` or `2/1`.
pub instruction_path: std::string::String,
/// Program id as non-empty base58 text.
pub program_id: std::string::String,
/// Protocol family code.
pub protocol_code: std::string::String,
/// Protocol surface code.
pub surface_code: std::string::String,
/// Canonical event code.
pub event_code: std::string::String,
/// Decoded payload JSON.
pub payload_json: serde_json::Value,
}
impl DecodedEventInsert {
/// Builds a decoded event insert contract from the shared decoded model.
pub fn from_model(
event: &kb_lib::DecodedProtocolEvent,
payload_json: serde_json::Value,
) -> kb_core::Result<Self> {
if event.signature.0.trim().is_empty() {
return std::result::Result::Err(kb_core::Error::db(
"decoded event signature must not be empty",
));
}
if event.program_id.0.trim().is_empty() {
return std::result::Result::Err(kb_core::Error::db(
"decoded event program id must not be empty",
));
}
return std::result::Result::Ok(Self {
signature: event.signature.0.clone(),
slot: event.slot.0,
instruction_path: event.instruction_path.0.clone(),
program_id: event.program_id.0.clone(),
protocol_code: event.protocol_code.0.clone(),
surface_code: event.surface_code.0.clone(),
event_code: event.event_code.0.clone(),
payload_json,
});
}
}
/// Materialized event insert contract.
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct MaterializedEventInsert {
/// Source transaction signature as non-empty base58 text.
pub signature: std::string::String,
/// Source transaction slot in Solana unsigned representation.
pub slot: u64,
/// Materialized event family code.
pub materialized_family: std::string::String,
/// Materialized payload JSON.
pub payload_json: serde_json::Value,
}
impl MaterializedEventInsert {
/// Builds a materialized event insert contract after minimal validation.
pub fn new(
signature: impl std::convert::Into<std::string::String>,
slot: u64,
materialized_family: impl std::convert::Into<std::string::String>,
payload_json: serde_json::Value,
) -> kb_core::Result<Self> {
let signature_value = signature.into();
let materialized_family_value = materialized_family.into();
if signature_value.trim().is_empty() {
return std::result::Result::Err(kb_core::Error::db(
"materialized event signature must not be empty",
));
}
if materialized_family_value.trim().is_empty() {
return std::result::Result::Err(kb_core::Error::db(
"materialized event family must not be empty",
));
}
return std::result::Result::Ok(Self {
signature: signature_value,
slot,
materialized_family: materialized_family_value,
payload_json,
});
}
}
#[cfg(test)]
mod tests {
#[test]
fn insert_outcome_total_counts_all_buckets() {
let outcome = crate::InsertOutcome::new(1, 2, 3);
assert_eq!(outcome.total_count(), 6);
}
#[test]
fn materialized_event_rejects_empty_family() {
let result = crate::MaterializedEventInsert::new(
"abc",
1,
" ",
serde_json::json!({"kind": "trade"}),
);
assert!(result.is_err());
}
}

View File

@@ -0,0 +1,73 @@
// file: kb-store/src/contracts/dto/ledger.rs
// version: 1
//! Processing ledger storage DTOs.
/// Processing ledger mark request contract.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct ProcessingLedgerMark {
/// Processing stage name, for example `raw_ingest`, `core_extract` or `decode`.
pub stage: std::string::String,
/// Processing module name.
pub module_name: std::string::String,
/// Processing module version.
pub module_version: std::string::String,
/// Stable input key, usually a signature or notification id.
pub input_key: std::string::String,
}
impl ProcessingLedgerMark {
/// Builds a processing ledger mark request after minimal validation.
pub fn new(
stage: impl std::convert::Into<std::string::String>,
module_name: impl std::convert::Into<std::string::String>,
module_version: impl std::convert::Into<std::string::String>,
input_key: impl std::convert::Into<std::string::String>,
) -> kb_core::Result<Self> {
let stage_value = stage.into();
let module_name_value = module_name.into();
let module_version_value = module_version.into();
let input_key_value = input_key.into();
if stage_value.trim().is_empty() {
return std::result::Result::Err(kb_core::Error::db(
"processing ledger stage must not be empty",
));
}
if module_name_value.trim().is_empty() {
return std::result::Result::Err(kb_core::Error::db(
"processing ledger module name must not be empty",
));
}
if module_version_value.trim().is_empty() {
return std::result::Result::Err(kb_core::Error::db(
"processing ledger module version must not be empty",
));
}
if input_key_value.trim().is_empty() {
return std::result::Result::Err(kb_core::Error::db(
"processing ledger input key must not be empty",
));
}
return std::result::Result::Ok(Self {
stage: stage_value,
module_name: module_name_value,
module_version: module_version_value,
input_key: input_key_value,
});
}
}
#[cfg(test)]
mod tests {
#[test]
fn ledger_mark_rejects_empty_input_key() {
let result = crate::ProcessingLedgerMark::new("decode", "module", "1", " ");
assert!(result.is_err());
}
#[test]
fn ledger_mark_accepts_minimal_values() {
let result = crate::ProcessingLedgerMark::new("decode", "module", "1", "signature");
assert!(result.is_ok());
}
}

View File

@@ -0,0 +1,606 @@
// file: kb-store/src/contracts/dto/raw.rs
// version: 1
//! Canonical Solana transaction and acquisition observation storage DTOs.
/// Retention state for a canonical raw transaction payload.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub enum RawPayloadRetentionState {
/// Full canonical payload is still present in the primary store.
Full,
/// Canonical payload was reduced to a compact audit representation.
Compacted,
/// Canonical payload was moved to an archive tier outside the primary hot store.
Archived,
/// Canonical payload was purged after derived data became authoritative enough.
Purged,
}
/// Processing state for a canonical raw transaction payload.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub enum RawPayloadProcessingState {
/// Transaction was received but not extracted yet.
Received,
/// Generic Solana core data was extracted from the canonical transaction.
CoreExtracted,
/// Decoder outputs were produced from the canonical transaction or its core extraction.
Decoded,
/// Business projections were materialized from decoded or core data.
Materialized,
/// Processing failed and requires diagnostics or replay.
Failed,
}
/// Origin of one transaction acquisition observation.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub enum TransactionObservationOrigin {
/// Transaction was observed from a current live stream.
Live,
/// Transaction was acquired by an explicit historical backfill.
Backfill,
/// Transaction was replayed from an already captured source.
Replay,
/// Transaction was fetched to repair an acquisition gap.
Repair,
/// Observation was converted from a historical storage table.
Migration,
}
/// Technical status of one transaction acquisition observation.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub enum TransactionObservationStatus {
/// A transaction candidate was detected before a complete payload was received.
Detected,
/// A source payload was received.
Received,
/// A source payload was normalized into the canonical transaction contract.
Normalized,
/// The observation and any linked canonical transaction were persisted.
Persisted,
/// Acquisition or normalization failed.
Failed,
/// The source reported or implied a transaction that was temporarily unavailable.
Missing,
}
/// Canonical raw Solana transaction insert contract.
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct RawTransactionInsert {
/// Transaction signature as non-empty base58 text.
pub signature: std::string::String,
/// Transaction slot in Solana unsigned representation.
pub slot: u64,
/// Canonical source-independent transaction document.
pub canonical_json: serde_json::Value,
/// Optional deterministic digest of the canonical document.
pub canonical_json_hash: std::option::Option<std::string::String>,
/// Positive version of the canonical transaction contract.
pub canonical_format_version: u32,
}
impl RawTransactionInsert {
/// Builds a canonical raw transaction insert contract after minimal validation.
pub fn new(
signature: impl std::convert::Into<std::string::String>,
slot: u64,
canonical_json: serde_json::Value,
canonical_format_version: u32,
) -> kb_core::Result<Self> {
let signature_value = signature.into();
if signature_value.trim().is_empty() {
return std::result::Result::Err(kb_core::Error::db(
"canonical transaction signature must not be empty",
));
}
if canonical_format_version == 0 {
return std::result::Result::Err(kb_core::Error::db(
"canonical transaction format version must be greater than zero",
));
}
return std::result::Result::Ok(Self {
signature: signature_value,
slot,
canonical_json,
canonical_json_hash: std::option::Option::None,
canonical_format_version,
});
}
/// Builds a storage insert from the source-independent canonical transaction model.
pub fn from_canonical(transaction: &kb_lib::CanonicalTransaction) -> kb_core::Result<Self> {
let canonical_json_result = transaction.to_canonical_json();
let canonical_json = match canonical_json_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let hash_result = transaction.canonical_json_hash();
let hash = match hash_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let insert_result = Self::new(
transaction.primary_signature.clone(),
transaction.slot,
canonical_json,
transaction.format_version,
);
let insert = match insert_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return insert.with_canonical_json_hash(hash);
}
/// Adds a precomputed deterministic canonical document hash.
pub fn with_canonical_json_hash(
mut self,
canonical_json_hash: impl std::convert::Into<std::string::String>,
) -> kb_core::Result<Self> {
let hash_value = canonical_json_hash.into();
if hash_value.trim().is_empty() {
return std::result::Result::Err(kb_core::Error::db(
"canonical transaction hash must not be empty",
));
}
self.canonical_json_hash = std::option::Option::Some(hash_value);
return std::result::Result::Ok(self);
}
}
/// Lightweight transaction acquisition observation insert contract.
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct TransactionObservationInsert {
/// Stable observation key used for deduplication.
pub observation_key: std::string::String,
/// Optional known canonical transaction row id.
pub raw_transaction_id: std::option::Option<i64>,
/// Optional transaction signature when known.
pub signature: std::option::Option<std::string::String>,
/// Optional transaction slot when known.
pub slot: std::option::Option<u64>,
/// Provider code.
pub provider: std::string::String,
/// Optional endpoint code from the active configuration.
pub endpoint_code: std::option::Option<std::string::String>,
/// Source protocol.
pub protocol: std::string::String,
/// Source acquisition method.
pub acquisition_method: std::string::String,
/// Observation origin.
pub origin: TransactionObservationOrigin,
/// Optional Solana commitment.
pub commitment: std::option::Option<std::string::String>,
/// Optional capture session identifier.
pub capture_session_id: std::option::Option<std::string::String>,
/// Optional configured filter code.
pub filter_code: std::option::Option<std::string::String>,
/// Optional first detection timestamp.
pub detected_at: std::option::Option<chrono::DateTime<chrono::Utc>>,
/// Timestamp at which the source payload was received locally.
pub received_at: chrono::DateTime<chrono::Utc>,
/// Optional timestamp at which canonical normalization completed.
pub normalized_at: std::option::Option<chrono::DateTime<chrono::Utc>>,
/// Optional uncompressed source payload size in bytes.
pub payload_size_bytes: std::option::Option<u64>,
/// Optional digest of the source-specific payload without retaining that payload.
pub source_payload_hash: std::option::Option<std::string::String>,
/// Observation status.
pub status: TransactionObservationStatus,
/// Optional machine-readable error code.
pub error_code: std::option::Option<std::string::String>,
/// Optional diagnostic error message.
pub error_message: std::option::Option<std::string::String>,
}
impl TransactionObservationInsert {
/// Builds a lightweight transaction observation after validating required source metadata.
pub fn new(
observation_key: impl std::convert::Into<std::string::String>,
provider: impl std::convert::Into<std::string::String>,
protocol: impl std::convert::Into<std::string::String>,
acquisition_method: impl std::convert::Into<std::string::String>,
origin: TransactionObservationOrigin,
received_at: chrono::DateTime<chrono::Utc>,
) -> kb_core::Result<Self> {
let observation_key_value = observation_key.into();
let provider_value = provider.into();
let protocol_value = protocol.into();
let acquisition_method_value = acquisition_method.into();
let validation_result = validate_required_observation_texts(
observation_key_value.as_str(),
provider_value.as_str(),
protocol_value.as_str(),
acquisition_method_value.as_str(),
);
if let std::result::Result::Err(error) = validation_result {
return std::result::Result::Err(error);
}
return std::result::Result::Ok(Self {
observation_key: observation_key_value,
raw_transaction_id: std::option::Option::None,
signature: std::option::Option::None,
slot: std::option::Option::None,
provider: provider_value,
endpoint_code: std::option::Option::None,
protocol: protocol_value,
acquisition_method: acquisition_method_value,
origin,
commitment: std::option::Option::None,
capture_session_id: std::option::Option::None,
filter_code: std::option::Option::None,
detected_at: std::option::Option::None,
received_at,
normalized_at: std::option::Option::None,
payload_size_bytes: std::option::Option::None,
source_payload_hash: std::option::Option::None,
status: TransactionObservationStatus::Received,
error_code: std::option::Option::None,
error_message: std::option::Option::None,
});
}
/// Links the observation to a known canonical transaction row id.
pub fn with_raw_transaction_id(mut self, raw_transaction_id: i64) -> kb_core::Result<Self> {
if raw_transaction_id <= 0 {
return std::result::Result::Err(kb_core::Error::db(
"transaction observation raw transaction id must be greater than zero",
));
}
self.raw_transaction_id = std::option::Option::Some(raw_transaction_id);
return std::result::Result::Ok(self);
}
/// Adds the transaction signature and optional slot carried by the source.
pub fn with_transaction_identity(
mut self,
signature: impl std::convert::Into<std::string::String>,
slot: std::option::Option<u64>,
) -> kb_core::Result<Self> {
let signature_value = signature.into();
if signature_value.trim().is_empty() {
return std::result::Result::Err(kb_core::Error::db(
"transaction observation signature must not be empty",
));
}
self.signature = std::option::Option::Some(signature_value);
self.slot = slot;
return std::result::Result::Ok(self);
}
/// Adds an endpoint code after validating non-empty optional text.
pub fn with_endpoint_code(
mut self,
endpoint_code: impl std::convert::Into<std::string::String>,
) -> kb_core::Result<Self> {
let endpoint_code_value = endpoint_code.into();
let validation_result = validate_optional_text(
endpoint_code_value.as_str(),
"transaction observation endpoint code must not be empty",
);
if let std::result::Result::Err(error) = validation_result {
return std::result::Result::Err(error);
}
self.endpoint_code = std::option::Option::Some(endpoint_code_value);
return std::result::Result::Ok(self);
}
/// Adds an optional commitment value after validation.
pub fn with_commitment(
mut self,
commitment: impl std::convert::Into<std::string::String>,
) -> kb_core::Result<Self> {
let commitment_value = commitment.into();
let validation_result = validate_optional_text(
commitment_value.as_str(),
"transaction observation commitment must not be empty",
);
if let std::result::Result::Err(error) = validation_result {
return std::result::Result::Err(error);
}
self.commitment = std::option::Option::Some(commitment_value);
return std::result::Result::Ok(self);
}
/// Adds optional capture session and filter codes after validation.
pub fn with_capture_context(
mut self,
capture_session_id: std::option::Option<std::string::String>,
filter_code: std::option::Option<std::string::String>,
) -> kb_core::Result<Self> {
let session_result = validate_optional_owned_text(
capture_session_id.as_ref(),
"transaction observation capture session id must not be empty when present",
);
if let std::result::Result::Err(error) = session_result {
return std::result::Result::Err(error);
}
let filter_result = validate_optional_owned_text(
filter_code.as_ref(),
"transaction observation filter code must not be empty when present",
);
if let std::result::Result::Err(error) = filter_result {
return std::result::Result::Err(error);
}
self.capture_session_id = capture_session_id;
self.filter_code = filter_code;
return std::result::Result::Ok(self);
}
/// Adds detection and normalization timestamps.
pub fn with_timings(
mut self,
detected_at: std::option::Option<chrono::DateTime<chrono::Utc>>,
normalized_at: std::option::Option<chrono::DateTime<chrono::Utc>>,
) -> Self {
self.detected_at = detected_at;
self.normalized_at = normalized_at;
return self;
}
/// Adds source payload size and hash metadata without retaining the source payload.
pub fn with_payload_metadata(
mut self,
payload_size_bytes: std::option::Option<u64>,
source_payload_hash: std::option::Option<std::string::String>,
) -> kb_core::Result<Self> {
let hash_result = validate_optional_owned_text(
source_payload_hash.as_ref(),
"transaction observation source payload hash must not be empty when present",
);
if let std::result::Result::Err(error) = hash_result {
return std::result::Result::Err(error);
}
self.payload_size_bytes = payload_size_bytes;
self.source_payload_hash = source_payload_hash;
return std::result::Result::Ok(self);
}
/// Replaces the current observation status.
pub fn with_status(mut self, status: TransactionObservationStatus) -> Self {
self.status = status;
return self;
}
/// Adds an acquisition error and marks the observation as failed.
pub fn with_error(
mut self,
error_code: impl std::convert::Into<std::string::String>,
error_message: std::option::Option<std::string::String>,
) -> kb_core::Result<Self> {
let error_code_value = error_code.into();
let code_result = validate_optional_text(
error_code_value.as_str(),
"transaction observation error code must not be empty",
);
if let std::result::Result::Err(error) = code_result {
return std::result::Result::Err(error);
}
let message_result = validate_optional_owned_text(
error_message.as_ref(),
"transaction observation error message must not be empty when present",
);
if let std::result::Result::Err(error) = message_result {
return std::result::Result::Err(error);
}
self.error_code = std::option::Option::Some(error_code_value);
self.error_message = error_message;
self.status = TransactionObservationStatus::Failed;
return std::result::Result::Ok(self);
}
}
/// Canonical raw transaction lifecycle mark request.
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct RawPayloadLifecycleMark {
/// Physical canonical raw table name using the `kb_sol_<domain>_<name>` convention.
pub raw_table_name: std::string::String,
/// Stable canonical raw row key, currently the transaction signature.
pub raw_row_key: std::string::String,
/// Retention state to record for the canonical payload.
pub retention_state: RawPayloadRetentionState,
/// Processing state to record for the canonical payload.
pub processing_state: RawPayloadProcessingState,
/// Optional reason visible in diagnostics.
pub reason: std::option::Option<std::string::String>,
}
impl RawPayloadLifecycleMark {
/// Builds a canonical raw payload lifecycle mark after minimal validation.
pub fn new(
raw_table_name: impl std::convert::Into<std::string::String>,
raw_row_key: impl std::convert::Into<std::string::String>,
retention_state: RawPayloadRetentionState,
processing_state: RawPayloadProcessingState,
reason: std::option::Option<std::string::String>,
) -> kb_core::Result<Self> {
let raw_table_name_value = raw_table_name.into();
let raw_row_key_value = raw_row_key.into();
if raw_table_name_value.trim().is_empty() {
return std::result::Result::Err(kb_core::Error::db(
"raw lifecycle table name must not be empty",
));
}
if raw_row_key_value.trim().is_empty() {
return std::result::Result::Err(kb_core::Error::db(
"raw lifecycle row key must not be empty",
));
}
let reason_result = validate_optional_owned_text(
reason.as_ref(),
"raw lifecycle reason must not be empty when present",
);
if let std::result::Result::Err(error) = reason_result {
return std::result::Result::Err(error);
}
return std::result::Result::Ok(Self {
raw_table_name: raw_table_name_value,
raw_row_key: raw_row_key_value,
retention_state,
processing_state,
reason,
});
}
}
fn validate_required_observation_texts(
observation_key: &str,
provider: &str,
protocol: &str,
acquisition_method: &str,
) -> kb_core::Result<()> {
let values = [
(observation_key, "transaction observation key must not be empty"),
(provider, "transaction observation provider must not be empty"),
(protocol, "transaction observation protocol must not be empty"),
(
acquisition_method,
"transaction observation acquisition method must not be empty",
),
];
for (value, message) in values {
if value.trim().is_empty() {
return std::result::Result::Err(kb_core::Error::db(message));
}
}
return std::result::Result::Ok(());
}
fn validate_optional_text(value: &str, message: &str) -> kb_core::Result<()> {
if value.trim().is_empty() {
return std::result::Result::Err(kb_core::Error::db(message));
}
return std::result::Result::Ok(());
}
fn validate_optional_owned_text(
value: std::option::Option<&std::string::String>,
message: &str,
) -> kb_core::Result<()> {
if let std::option::Option::Some(text) = value {
if text.trim().is_empty() {
return std::result::Result::Err(kb_core::Error::db(message));
}
}
return std::result::Result::Ok(());
}
#[cfg(test)]
mod tests {
#[test]
fn raw_transaction_rejects_empty_signature() {
let result = crate::RawTransactionInsert::new(" ", 1, serde_json::json!({"ok": true}), 1);
assert!(result.is_err());
}
#[test]
fn raw_transaction_rejects_zero_format_version() {
let result = crate::RawTransactionInsert::new("abc", 1, serde_json::json!({"ok": true}), 0);
assert!(result.is_err());
}
fn canonical_transaction_fixture() -> kb_lib::CanonicalTransaction {
return kb_lib::CanonicalTransaction {
format_version: kb_lib::CANONICAL_TRANSACTION_FORMAT_VERSION,
primary_signature: "2Ana1pUpv2ZbMVkwF5FXapYeBEjdxDatLn7nvJkhgTSXbs59SyZSx866bXirPgj8QQVB57uxHJBG1YFvkRbFj4T".to_string(),
slot: 9,
block_time: std::option::Option::None,
version: kb_lib::CanonicalTransactionVersion::Legacy,
signatures: std::vec![
"2Ana1pUpv2ZbMVkwF5FXapYeBEjdxDatLn7nvJkhgTSXbs59SyZSx866bXirPgj8QQVB57uxHJBG1YFvkRbFj4T".to_string(),
],
message: kb_lib::CanonicalTransactionMessage {
header: kb_lib::CanonicalMessageHeader {
num_required_signatures: 1,
num_readonly_signed_accounts: 0,
num_readonly_unsigned_accounts: 1,
},
static_account_keys: std::vec![
"11111111111111111111111111111111".to_string(),
"ComputeBudget111111111111111111111111111111".to_string(),
],
recent_blockhash: "11111111111111111111111111111111".to_string(),
instructions: std::vec![kb_lib::CanonicalCompiledInstruction {
program_id_index: 1,
account_indexes: std::vec![0],
data_base64: "AQ==".to_string(),
stack_height: std::option::Option::Some(1),
}],
address_table_lookups: std::vec::Vec::new(),
loaded_addresses: kb_lib::CanonicalLoadedAddresses::default(),
},
metadata: std::option::Option::Some(kb_lib::CanonicalTransactionMetadata {
status: kb_lib::CanonicalTransactionStatus::Success,
error: std::option::Option::None,
fee: 5000,
pre_balances: std::vec![10000, 1],
post_balances: std::vec![5000, 1],
inner_instructions: std::vec::Vec::new(),
log_messages: std::vec::Vec::new(),
pre_token_balances: std::vec::Vec::new(),
post_token_balances: std::vec::Vec::new(),
rewards: std::vec::Vec::new(),
return_data: std::option::Option::None,
compute_units_consumed: std::option::Option::Some(100),
cost_units: std::option::Option::None,
}),
};
}
#[test]
fn raw_transaction_builds_from_canonical_model() {
let transaction = canonical_transaction_fixture();
let result = crate::RawTransactionInsert::from_canonical(&transaction);
let insert = match result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("canonical insert failed: {error}"),
};
assert_eq!(insert.signature, transaction.primary_signature);
assert_eq!(insert.slot, transaction.slot);
assert_eq!(insert.canonical_format_version, kb_lib::CANONICAL_TRANSACTION_FORMAT_VERSION);
assert_eq!(insert.canonical_json_hash.as_deref().map(|value| return value.len()), Some(64));
}
#[test]
fn raw_transaction_accepts_canonical_payload() {
let result = crate::RawTransactionInsert::new("abc", 1, serde_json::json!({"ok": true}), 1);
assert!(result.is_ok());
}
#[test]
fn transaction_observation_rejects_empty_provider() {
let result = crate::TransactionObservationInsert::new(
"obs:1",
" ",
"solana_http",
"getTransaction",
crate::TransactionObservationOrigin::Backfill,
chrono::Utc::now(),
);
assert!(result.is_err());
}
#[test]
fn transaction_observation_accepts_signatureless_failure_candidate() {
let result = crate::TransactionObservationInsert::new(
"obs:1",
"helius",
"solana_http",
"getTransaction",
crate::TransactionObservationOrigin::Repair,
chrono::Utc::now(),
);
assert!(result.is_ok());
}
#[test]
fn raw_lifecycle_rejects_empty_reason_when_present() {
let result = crate::RawPayloadLifecycleMark::new(
"kb_sol_raw_transactions",
"signature",
crate::RawPayloadRetentionState::Full,
crate::RawPayloadProcessingState::Received,
std::option::Option::Some(" ".to_string()),
);
assert!(result.is_err());
}
}

View File

@@ -0,0 +1,125 @@
// file: kb-store/src/contracts/dto/store.rs
// version: 1
//! Store backend diagnostic DTOs.
/// Store backend kind contract.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub enum StoreBackendKind {
/// PostgreSQL backend.
Postgres,
/// SQLite backend retained for tests and legacy imports.
Sqlite,
/// In-memory backend used by offline tests.
Memory,
/// Backend is not known.
Unknown,
}
/// Store migration status contract.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub enum StoreMigrationStatus {
/// Migration status is not known yet.
Unknown,
/// Store has no migration table or migration history yet.
NotInitialized,
/// Store migrations are current.
Current,
/// Store has pending migrations.
Pending,
/// Store migration history is inconsistent.
Drift,
/// Store migration check failed.
Failed,
}
/// Store backend diagnostic contract.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct StoreBackendDescriptor {
/// Store backend kind.
pub backend_kind: StoreBackendKind,
/// Human-readable backend label.
pub backend_label: std::string::String,
/// Masked DSN or connection descriptor safe for diagnostics.
pub masked_dsn: std::option::Option<std::string::String>,
/// Current PostgreSQL schema or equivalent namespace when known.
pub current_schema: std::option::Option<std::string::String>,
}
impl StoreBackendDescriptor {
/// Builds a store backend descriptor after minimal validation.
pub fn new(
backend_kind: StoreBackendKind,
backend_label: impl std::convert::Into<std::string::String>,
masked_dsn: std::option::Option<std::string::String>,
current_schema: std::option::Option<std::string::String>,
) -> kb_core::Result<Self> {
let backend_label_value = backend_label.into();
if backend_label_value.trim().is_empty() {
return std::result::Result::Err(kb_core::Error::db(
"store backend label must not be empty",
));
}
return std::result::Result::Ok(Self {
backend_kind,
backend_label: backend_label_value,
masked_dsn,
current_schema,
});
}
}
/// Store migration diagnostic snapshot contract.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct StoreMigrationSnapshot {
/// Current migration status.
pub status: StoreMigrationStatus,
/// Last applied migration identifier when known.
pub current_version: std::option::Option<std::string::String>,
/// Pending migration identifiers when known.
pub pending_versions: std::vec::Vec<std::string::String>,
/// Optional human-readable diagnostic message.
pub message: std::option::Option<std::string::String>,
}
impl StoreMigrationSnapshot {
/// Builds a migration snapshot from explicit values.
pub fn new(
status: StoreMigrationStatus,
current_version: std::option::Option<std::string::String>,
pending_versions: std::vec::Vec<std::string::String>,
message: std::option::Option<std::string::String>,
) -> Self {
return Self {
status,
current_version,
pending_versions,
message,
};
}
}
#[cfg(test)]
mod tests {
#[test]
fn store_descriptor_rejects_empty_label() {
let result = crate::StoreBackendDescriptor::new(
crate::StoreBackendKind::Postgres,
" ",
std::option::Option::None,
std::option::Option::None,
);
assert!(result.is_err());
}
#[test]
fn migration_snapshot_keeps_pending_versions() {
let snapshot = crate::StoreMigrationSnapshot::new(
crate::StoreMigrationStatus::Pending,
std::option::Option::None,
std::vec![std::string::String::from("0001")],
std::option::Option::None,
);
assert_eq!(snapshot.pending_versions.len(), 1);
}
}

View File

@@ -0,0 +1,32 @@
// file: kb-store/src/contracts/entity.rs
// version: 1
//! Backend-neutral SQL-like entity exports for storage adapters.
mod core;
mod event;
mod ledger;
mod raw;
/// Core account key SQL-like row contract.
pub use crate::contracts::entity::core::CoreAccountKeyRow;
/// Core balance change SQL-like row contract.
pub use crate::contracts::entity::core::CoreBalanceChangeRow;
/// Core inner instruction SQL-like row contract.
pub use crate::contracts::entity::core::CoreInnerInstructionRow;
/// Core instruction SQL-like row contract.
pub use crate::contracts::entity::core::CoreInstructionRow;
/// Core log SQL-like row contract.
pub use crate::contracts::entity::core::CoreLogRow;
/// Core transaction SQL-like row contract.
pub use crate::contracts::entity::core::CoreTransactionRow;
/// Decoded event SQL-like row contract.
pub use crate::contracts::entity::event::DecodedEventRow;
/// Materialized event SQL-like row contract.
pub use crate::contracts::entity::event::MaterializedEventRow;
/// Processing ledger SQL-like row contract.
pub use crate::contracts::entity::ledger::ProcessingLedgerRow;
/// Canonical raw Solana transaction SQL-like row contract.
pub use crate::contracts::entity::raw::RawTransactionRow;
/// Transaction acquisition observation SQL-like row contract.
pub use crate::contracts::entity::raw::TransactionObservationRow;

View File

@@ -0,0 +1,166 @@
// file: kb-store/src/contracts/entity/core.rs
// version: 1
//! Core Solana SQL-like entities.
/// Core transaction SQL-like row contract.
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct CoreTransactionRow {
/// Technical primary key.
pub id: i64,
/// Transaction signature as non-empty base58 text.
pub signature: std::string::String,
/// Transaction slot stored as SQL `BIGINT`.
pub slot: i64,
/// Whether the transaction failed on-chain.
pub failed: bool,
/// Optional canonical raw transaction row id used for lineage when available.
pub raw_transaction_id: std::option::Option<i64>,
/// Optional raw error JSON extracted from transaction metadata.
pub err_json: std::option::Option<serde_json::Value>,
/// Insert timestamp.
pub created_at: chrono::DateTime<chrono::Utc>,
/// Last update timestamp.
pub updated_at: chrono::DateTime<chrono::Utc>,
}
/// Core account key SQL-like row contract.
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct CoreAccountKeyRow {
/// Technical primary key.
pub id: i64,
/// Parent core transaction technical key.
pub transaction_id: i64,
/// Transaction signature as non-empty base58 text.
pub signature: std::string::String,
/// Transaction slot stored as SQL `BIGINT`.
pub slot: i64,
/// Stable account index after static and loaded keys are resolved.
pub account_index: i32,
/// Account public key as non-empty base58 text.
pub account_key: std::string::String,
/// Source category for this account key.
pub source: crate::CoreAccountKeySource,
/// Whether the resolved account is writable for the transaction.
pub writable: bool,
/// Whether the resolved account signed the transaction.
pub signer: bool,
/// Whether the resolved account is executable when known.
pub executable: std::option::Option<bool>,
/// Insert timestamp.
pub created_at: chrono::DateTime<chrono::Utc>,
}
/// Core instruction SQL-like row contract.
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct CoreInstructionRow {
/// Technical primary key.
pub id: i64,
/// Parent core transaction technical key.
pub transaction_id: i64,
/// Transaction signature as non-empty base58 text.
pub signature: std::string::String,
/// Transaction slot stored as SQL `BIGINT`.
pub slot: i64,
/// Stable instruction path, for example `0` or `2/1`.
pub instruction_path: std::string::String,
/// Program id as non-empty base58 text.
pub program_id: std::string::String,
/// Instruction accounts as JSON, preserving unresolved forms when needed.
pub accounts_json: serde_json::Value,
/// Instruction payload JSON while it is still retained in the hot store.
pub payload_json: std::option::Option<serde_json::Value>,
/// Optional digest of the instruction payload after compaction or purge.
pub payload_json_hash: std::option::Option<std::string::String>,
/// Current processing state used by instruction-level replay.
pub processing_state: crate::CoreInstructionProcessingState,
/// Insert timestamp.
pub created_at: chrono::DateTime<chrono::Utc>,
/// Last lifecycle update timestamp.
pub updated_at: chrono::DateTime<chrono::Utc>,
}
/// Core inner instruction SQL-like row contract.
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct CoreInnerInstructionRow {
/// Technical primary key.
pub id: i64,
/// Parent core transaction technical key.
pub transaction_id: i64,
/// Transaction signature as non-empty base58 text.
pub signature: std::string::String,
/// Transaction slot stored as SQL `BIGINT`.
pub slot: i64,
/// Parent top-level or inner instruction path.
pub parent_instruction_path: std::string::String,
/// Stable inner instruction path, for example `2/1`.
pub instruction_path: std::string::String,
/// Program id as non-empty base58 text.
pub program_id: std::string::String,
/// Inner instruction accounts as JSON.
pub accounts_json: serde_json::Value,
/// Inner instruction payload JSON while it is retained in the hot store.
pub payload_json: std::option::Option<serde_json::Value>,
/// Optional digest of the inner instruction payload after compaction or purge.
pub payload_json_hash: std::option::Option<std::string::String>,
/// Insert timestamp.
pub created_at: chrono::DateTime<chrono::Utc>,
}
/// Core log SQL-like row contract.
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct CoreLogRow {
/// Technical primary key.
pub id: i64,
/// Parent core transaction technical key.
pub transaction_id: i64,
/// Transaction signature as non-empty base58 text.
pub signature: std::string::String,
/// Transaction slot stored as SQL `BIGINT`.
pub slot: i64,
/// Log index preserving transaction log order.
pub log_index: i32,
/// Optional instruction path resolved from invocation depth when known.
pub instruction_path: std::option::Option<std::string::String>,
/// Optional program id resolved from the log line or invocation context.
pub program_id: std::option::Option<std::string::String>,
/// Original log text while it is retained in the hot store.
pub log_text: std::option::Option<std::string::String>,
/// Optional digest of the log text after compaction or purge.
pub log_text_hash: std::option::Option<std::string::String>,
/// Insert timestamp.
pub created_at: chrono::DateTime<chrono::Utc>,
}
/// Core balance change SQL-like row contract.
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct CoreBalanceChangeRow {
/// Technical primary key.
pub id: i64,
/// Parent core transaction technical key.
pub transaction_id: i64,
/// Transaction signature as non-empty base58 text.
pub signature: std::string::String,
/// Transaction slot stored as SQL `BIGINT`.
pub slot: i64,
/// Stable balance change index preserving extraction order.
pub balance_change_index: i32,
/// Balance change family.
pub balance_kind: crate::CoreBalanceChangeKind,
/// Optional account index when available.
pub account_index: std::option::Option<i32>,
/// Optional account public key when available.
pub account_key: std::option::Option<std::string::String>,
/// Optional SPL token mint for token balances.
pub mint: std::option::Option<std::string::String>,
/// Optional owner public key for token balances.
pub owner: std::option::Option<std::string::String>,
/// Pre-balance JSON value preserving RPC representation.
pub pre_balance_json: std::option::Option<serde_json::Value>,
/// Post-balance JSON value preserving RPC representation.
pub post_balance_json: std::option::Option<serde_json::Value>,
/// Delta JSON value preserving integer or decimal-safe representation.
pub delta_json: std::option::Option<serde_json::Value>,
/// Insert timestamp.
pub created_at: chrono::DateTime<chrono::Utc>,
}

View File

@@ -0,0 +1,46 @@
// file: kb-store/src/contracts/entity/event.rs
// version: 1
//! Decoded and materialized event SQL-like entities.
/// Decoded event SQL-like row contract.
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct DecodedEventRow {
/// Technical primary key.
pub id: i64,
/// Transaction signature as non-empty base58 text.
pub signature: std::string::String,
/// Transaction slot stored as SQL `BIGINT`.
pub slot: i64,
/// Stable instruction path, for example `0` or `2/1`.
pub instruction_path: std::string::String,
/// Program id as non-empty base58 text.
pub program_id: std::string::String,
/// Protocol family code.
pub protocol_code: std::string::String,
/// Protocol surface code.
pub surface_code: std::string::String,
/// Canonical event code.
pub event_code: std::string::String,
/// Decoded payload JSON.
pub payload_json: serde_json::Value,
/// Insert timestamp.
pub created_at: chrono::DateTime<chrono::Utc>,
}
/// Materialized event SQL-like row contract.
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct MaterializedEventRow {
/// Technical primary key.
pub id: i64,
/// Source transaction signature as non-empty base58 text.
pub signature: std::string::String,
/// Source transaction slot stored as SQL `BIGINT`.
pub slot: i64,
/// Materialized event family code.
pub materialized_family: std::string::String,
/// Materialized payload JSON.
pub payload_json: serde_json::Value,
/// Insert timestamp.
pub created_at: chrono::DateTime<chrono::Utc>,
}

View File

@@ -0,0 +1,37 @@
// file: kb-store/src/contracts/entity/ledger.rs
// version: 1
//! Processing ledger SQL-like entities.
/// Processing ledger SQL-like row contract.
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct ProcessingLedgerRow {
/// Technical primary key.
pub id: i64,
/// Processing stage name.
pub stage: std::string::String,
/// Processor implementation name.
pub processor_name: std::string::String,
/// Processor semantic version.
pub processor_version: std::string::String,
/// Stable input key, usually a transaction signature.
pub input_key: std::string::String,
/// Deterministic input hash.
pub input_hash: std::string::String,
/// Current processing status.
pub status: crate::ProcessingLedgerStatus,
/// Number of started attempts for this processor identity.
pub attempt_count: i32,
/// Optional start timestamp for the latest attempt.
pub started_at: std::option::Option<chrono::DateTime<chrono::Utc>>,
/// Optional finish timestamp for the latest terminal attempt.
pub finished_at: std::option::Option<chrono::DateTime<chrono::Utc>>,
/// Optional machine-readable error code.
pub error_code: std::option::Option<std::string::String>,
/// Optional diagnostic error message.
pub error_message: std::option::Option<std::string::String>,
/// Insert timestamp.
pub created_at: chrono::DateTime<chrono::Utc>,
/// Last update timestamp.
pub updated_at: chrono::DateTime<chrono::Utc>,
}

View File

@@ -0,0 +1,78 @@
// file: kb-store/src/contracts/entity/raw.rs
// version: 1
//! Canonical Solana transaction and acquisition observation SQL-like entities.
/// Canonical raw Solana transaction SQL-like row contract.
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct RawTransactionRow {
/// Technical primary key.
pub id: i64,
/// Transaction signature as non-empty base58 text.
pub signature: std::string::String,
/// Transaction slot stored as SQL `BIGINT`.
pub slot: i64,
/// Canonical source-independent transaction document while retained in the hot store.
pub canonical_json: std::option::Option<serde_json::Value>,
/// Optional deterministic digest of the canonical document.
pub canonical_json_hash: std::option::Option<std::string::String>,
/// Version of the canonical transaction document contract.
pub canonical_format_version: i32,
/// Current raw payload retention state.
pub retention_state: crate::RawPayloadRetentionState,
/// Current processing state derived from this canonical transaction.
pub processing_state: crate::RawPayloadProcessingState,
/// Insert timestamp.
pub created_at: chrono::DateTime<chrono::Utc>,
/// Last lifecycle update timestamp.
pub updated_at: chrono::DateTime<chrono::Utc>,
}
/// Transaction acquisition observation SQL-like row contract.
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct TransactionObservationRow {
/// Technical primary key.
pub id: i64,
/// Optional linked canonical transaction row id.
pub raw_transaction_id: std::option::Option<i64>,
/// Stable observation key used for deduplication.
pub observation_key: std::string::String,
/// Optional transaction signature when known.
pub signature: std::option::Option<std::string::String>,
/// Optional transaction slot stored as SQL `BIGINT`.
pub slot: std::option::Option<i64>,
/// Provider code, for example `helius`, `triton` or `legacy_unknown`.
pub provider: std::string::String,
/// Optional endpoint code from the active configuration.
pub endpoint_code: std::option::Option<std::string::String>,
/// Acquisition protocol, for example `solana_http`, `solana_websocket` or `yellowstone_grpc`.
pub protocol: std::string::String,
/// Acquisition method, for example `getTransaction`, `transactionSubscribe` or `transactions`.
pub acquisition_method: std::string::String,
/// Acquisition origin category.
pub origin: crate::TransactionObservationOrigin,
/// Optional Solana commitment.
pub commitment: std::option::Option<std::string::String>,
/// Optional capture session identifier.
pub capture_session_id: std::option::Option<std::string::String>,
/// Optional configured filter code.
pub filter_code: std::option::Option<std::string::String>,
/// Optional first detection timestamp.
pub detected_at: std::option::Option<chrono::DateTime<chrono::Utc>>,
/// Timestamp at which the source payload was received locally.
pub received_at: chrono::DateTime<chrono::Utc>,
/// Optional timestamp at which canonical normalization completed.
pub normalized_at: std::option::Option<chrono::DateTime<chrono::Utc>>,
/// Timestamp at which the observation was persisted.
pub persisted_at: chrono::DateTime<chrono::Utc>,
/// Optional uncompressed source payload size in bytes.
pub payload_size_bytes: std::option::Option<i64>,
/// Optional digest of the source-specific payload without retaining that payload.
pub source_payload_hash: std::option::Option<std::string::String>,
/// Current observation status.
pub status: crate::TransactionObservationStatus,
/// Optional machine-readable error code.
pub error_code: std::option::Option<std::string::String>,
/// Optional diagnostic error message.
pub error_message: std::option::Option<std::string::String>,
}

View File

@@ -0,0 +1,27 @@
// file: kb-store/src/contracts/error.rs
// version: 1
//! Backend-neutral storage error helpers.
/// Creates a storage contract error with a stable code.
pub fn storage_contract_error(code: &str, message: &str) -> kb_core::Error {
if code.trim().is_empty() {
return kb_core::Error::db(message);
}
return kb_core::Error::new(code, message);
}
#[cfg(test)]
mod tests {
#[test]
fn storage_error_preserves_non_empty_code() {
let error = crate::storage_contract_error("store_contract", "invalid value");
assert_eq!(error.code(), "store_contract");
}
#[test]
fn storage_error_falls_back_to_db_for_empty_code() {
let error = crate::storage_contract_error(" ", "invalid value");
assert_eq!(error.code(), "db");
}
}

View File

@@ -0,0 +1,58 @@
// file: kb-store/src/contracts/health.rs
// version: 1
//! Backend-neutral health contracts for storage implementations.
/// Store backend health status.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub enum StoreHealthStatus {
/// Health is not known yet.
Unknown,
/// Backend is reachable and usable.
Healthy,
/// Backend is reachable but not fully usable.
Degraded,
/// Backend is not usable.
Unhealthy,
}
/// Store backend health snapshot.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct StoreHealthSnapshot {
/// Stable backend code such as `postgres` or `sqlite`.
pub backend: std::string::String,
/// Current health status.
pub status: StoreHealthStatus,
/// Optional human-readable diagnostic message.
pub message: std::option::Option<std::string::String>,
}
impl StoreHealthSnapshot {
/// Builds a store health snapshot after minimal validation.
pub fn new(
backend: impl std::convert::Into<std::string::String>,
status: StoreHealthStatus,
message: std::option::Option<std::string::String>,
) -> kb_core::Result<Self> {
let backend_value = backend.into();
if backend_value.trim().is_empty() {
return std::result::Result::Err(kb_core::Error::db(
"store health backend must not be empty",
));
}
return std::result::Result::Ok(Self { backend: backend_value, status, message });
}
}
#[cfg(test)]
mod tests {
#[test]
fn health_snapshot_rejects_empty_backend() {
let result = crate::StoreHealthSnapshot::new(
" ",
crate::StoreHealthStatus::Unknown,
std::option::Option::None,
);
assert!(result.is_err());
}
}

View File

@@ -0,0 +1,75 @@
// file: kb-store/src/contracts/pagination.rs
// version: 1
//! Backend-neutral pagination and sorting contracts for repository operations.
/// Default page size for repository list operations.
pub const DEFAULT_PAGE_SIZE: u16 = 100;
/// Maximum page size for repository list operations.
pub const MAX_PAGE_SIZE: u16 = 1000;
/// Sort direction for repository list operations.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub enum SortDirection {
/// Sort values in ascending order.
Asc,
/// Sort values in descending order.
Desc,
}
/// Page request contract for repository list operations.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct PageRequest {
/// Maximum number of rows to return.
pub limit: u16,
/// Zero-based row offset.
pub offset: u64,
}
impl PageRequest {
/// Builds a page request after minimal bounds validation.
pub fn new(limit: u16, offset: u64) -> kb_core::Result<Self> {
if limit == 0 {
return std::result::Result::Err(kb_core::Error::db(
"page limit must be greater than zero",
));
}
if limit > crate::MAX_PAGE_SIZE {
return std::result::Result::Err(kb_core::Error::db(
"page limit exceeds maximum page size",
));
}
return std::result::Result::Ok(Self { limit, offset });
}
/// Builds the default first page request.
pub fn first_page() -> Self {
return Self {
limit: crate::DEFAULT_PAGE_SIZE,
offset: 0,
};
}
}
#[cfg(test)]
mod tests {
#[test]
fn page_request_rejects_zero_limit() {
let result = crate::PageRequest::new(0, 0);
assert!(result.is_err());
}
#[test]
fn page_request_rejects_limit_above_maximum() {
let result = crate::PageRequest::new(crate::MAX_PAGE_SIZE + 1, 0);
assert!(result.is_err());
}
#[test]
fn first_page_uses_default_limit() {
let request = crate::PageRequest::first_page();
assert_eq!(request.limit, crate::DEFAULT_PAGE_SIZE);
assert_eq!(request.offset, 0);
}
}

View File

@@ -0,0 +1,237 @@
// file: kb-store/src/contracts/repository.rs
// version: 1
//! Storage trait definitions shared by concrete stores.
/// Store health storage behavior.
#[async_trait::async_trait]
pub trait StoreHealthStore {
/// Reads the backend descriptor visible to diagnostics.
async fn backend_descriptor(&self) -> kb_core::Result<crate::StoreBackendDescriptor>;
/// Reads the current health snapshot.
async fn health_snapshot(&self) -> kb_core::Result<crate::StoreHealthSnapshot>;
/// Reads the current migration snapshot when the backend supports migrations.
async fn migration_snapshot(&self) -> kb_core::Result<crate::StoreMigrationSnapshot>;
}
/// Canonical raw transaction and acquisition observation storage behavior.
#[async_trait::async_trait]
pub trait RawTransactionStore {
/// Returns true when a canonical transaction signature is already stored.
async fn has_raw_transaction_signature(
&self,
signature: &kb_lib::Signature,
) -> kb_core::Result<bool>;
/// Returns true when a transaction observation key is already stored.
async fn has_transaction_observation_key(&self, observation_key: &str)
-> kb_core::Result<bool>;
/// Stores one canonical source-independent transaction payload.
async fn insert_raw_transaction(
&self,
input: &crate::RawTransactionInsert,
) -> kb_core::Result<crate::InsertOutcome>;
/// Stores one lightweight transaction acquisition observation.
async fn insert_transaction_observation(
&self,
input: &crate::TransactionObservationInsert,
) -> kb_core::Result<crate::InsertOutcome>;
/// Updates canonical raw transaction retention and processing metadata.
async fn mark_raw_payload_lifecycle(
&self,
mark: &crate::RawPayloadLifecycleMark,
) -> kb_core::Result<crate::InsertOutcome>;
}
/// Core Solana storage behavior.
#[async_trait::async_trait]
pub trait CoreTransactionStore {
/// Stores one normalized core transaction.
async fn insert_core_transaction(
&self,
input: &crate::CoreTransactionInsert,
) -> kb_core::Result<crate::InsertOutcome>;
/// Stores normalized core account keys.
async fn insert_core_account_keys(
&self,
inputs: &[crate::CoreAccountKeyInsert],
) -> kb_core::Result<crate::InsertOutcome>;
/// Stores normalized core instructions.
async fn insert_core_instructions(
&self,
inputs: &[crate::CoreInstructionInsert],
) -> kb_core::Result<crate::InsertOutcome>;
/// Stores normalized core inner instructions.
async fn insert_core_inner_instructions(
&self,
inputs: &[crate::CoreInnerInstructionInsert],
) -> kb_core::Result<crate::InsertOutcome>;
/// Stores normalized core logs.
async fn insert_core_logs(
&self,
inputs: &[crate::CoreLogInsert],
) -> kb_core::Result<crate::InsertOutcome>;
/// Stores normalized core balance changes.
async fn insert_core_balance_changes(
&self,
inputs: &[crate::CoreBalanceChangeInsert],
) -> kb_core::Result<crate::InsertOutcome>;
/// Lists normalized core instructions selected for replay or first processing.
async fn list_core_instructions_for_replay(
&self,
filter: &crate::CoreInstructionReplayFilter,
page_request: &crate::PageRequest,
) -> kb_core::Result<std::vec::Vec<crate::CoreInstructionRow>>;
/// Lists replay inputs with instruction context, logs, balances and account keys.
async fn list_core_instruction_replay_inputs(
&self,
filter: &crate::CoreInstructionReplayFilter,
page_request: &crate::PageRequest,
) -> kb_core::Result<std::vec::Vec<crate::CoreInstructionReplayInput>>;
/// Updates one normalized core instruction lifecycle state.
async fn mark_core_instruction_lifecycle(
&self,
mark: &crate::CoreInstructionLifecycleMark,
) -> kb_core::Result<crate::InsertOutcome>;
}
/// Canonical transaction to core extraction storage behavior.
#[async_trait::async_trait]
pub trait CoreExtractionStore {
/// Lists canonical raw transactions selected for core extraction.
async fn list_raw_transactions_for_core_extraction(
&self,
filter: &crate::CoreExtractionSelectionFilter,
) -> kb_core::Result<std::vec::Vec<crate::RawTransactionRow>>;
/// Returns true when the same processor version already succeeded for the same input hash.
async fn is_core_extraction_current(
&self,
identity: &crate::ProcessingLedgerIdentity,
) -> kb_core::Result<bool>;
/// Atomically replaces one signature core graph and marks the processing ledger as succeeded.
async fn persist_core_extraction(
&self,
bundle: &crate::CoreExtractionBundle,
force_replay: bool,
) -> kb_core::Result<crate::InsertOutcome>;
/// Persists one failed extraction attempt and marks the canonical raw transaction as failed.
async fn mark_core_extraction_failed(
&self,
failure: &crate::CoreExtractionFailure,
) -> kb_core::Result<crate::InsertOutcome>;
}
/// Contextual instruction decode and materialization storage behavior.
#[async_trait::async_trait]
pub trait DecodePipelineStore {
/// Lists contextual core instructions selected for a bounded decode campaign.
async fn list_decode_inputs(
&self,
filter: &crate::DecodeSelectionFilter,
) -> kb_core::Result<std::vec::Vec<crate::CoreInstructionReplayInput>>;
/// Returns true when the same processor version already succeeded for the same input hash.
async fn is_decode_current(
&self,
identity: &crate::ProcessingLedgerIdentity,
) -> kb_core::Result<bool>;
/// Replaces declarations owned by one decoder version.
async fn persist_decode_coverage_declarations(
&self,
declarations: &[crate::DecodeCoverageDeclarationInsert],
) -> kb_core::Result<crate::InsertOutcome>;
/// Atomically persists decoded observations, coverage and the common ledger.
async fn persist_decode_result(
&self,
bundle: &crate::DecodePersistenceBundle,
force_replay: bool,
) -> kb_core::Result<crate::InsertOutcome>;
/// Persists a failed decode attempt and marks the source instruction as failed.
async fn mark_decode_failed(
&self,
failure: &crate::DecodeFailure,
) -> kb_core::Result<crate::InsertOutcome>;
/// Atomically persists materialized outputs and the common ledger.
async fn persist_materialization_result(
&self,
bundle: &crate::MaterializationPersistenceBundle,
force_replay: bool,
) -> kb_core::Result<crate::InsertOutcome>;
/// Reads aggregated machine-readable coverage diagnostics.
async fn list_decode_coverage_summary(
&self,
processor_name: std::option::Option<&str>,
processor_version: std::option::Option<&str>,
limit: u32,
) -> kb_core::Result<std::vec::Vec<crate::DecodeCoverageSummaryRow>>;
/// Lists bounded materialized outputs for read-only application views.
async fn list_materialized_events(
&self,
filter: &crate::MaterializedEventFilter,
) -> kb_core::Result<std::vec::Vec<crate::MaterializedEventQueryRow>>;
}
/// Program observation storage behavior.
#[async_trait::async_trait]
pub trait ProgramObservationStore {
/// Stores program observations.
async fn store_observations(
&self,
observations: &[kb_lib::ProgramObservation],
) -> kb_core::Result<crate::InsertOutcome>;
}
/// Decoded event storage behavior.
#[async_trait::async_trait]
pub trait DecodedEventStore {
/// Stores decoded protocol events.
async fn store_decoded_events(
&self,
events: &[crate::DecodedEventInsert],
) -> kb_core::Result<crate::InsertOutcome>;
}
/// Materialized event storage behavior.
#[async_trait::async_trait]
pub trait MaterializedEventStore {
/// Stores materialized business events.
async fn store_materialized_events(
&self,
events: &[crate::MaterializedEventInsert],
) -> kb_core::Result<crate::InsertOutcome>;
}
/// Processing ledger storage behavior.
#[async_trait::async_trait]
pub trait ProcessingLedgerStore {
/// Marks an input as processed for a module version.
async fn mark_processed(
&self,
mark: &crate::ProcessingLedgerMark,
) -> kb_core::Result<crate::InsertOutcome>;
/// Returns true when an input was already processed for a module version.
async fn is_processed(&self, mark: &crate::ProcessingLedgerMark) -> kb_core::Result<bool>;
}