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

View File

@@ -0,0 +1,21 @@
# file: kb_store_core/Cargo.toml
# version: 2
[package]
name = "kb_store_core"
version.workspace = true
edition.workspace = true
license.workspace = true
publish.workspace = true
[dependencies]
async-trait.workspace = true
chrono.workspace = true
kb_core = { path = "../kb_core" }
kb_model = { path = "../kb_model" }
serde.workspace = true
serde_json.workspace = true
[lints]
workspace = true

View File

@@ -0,0 +1,222 @@
<!-- file: kb_store_core/README.md -->
<!-- version: 12 -->
# kb_store_core
`kb_store_core` déclare les contrats de stockage indépendants du backend concret.
Ce crate ne dépend pas de PostgreSQL, SQLite, Tauri ou du RPC. Il sert de frontière commune entre le pipeline, les applications, les workers et les implémentations de stockage.
## Rôle exact
`kb_store_core` contient :
- les traits repository backend-agnostiques ;
- les DTOs applicatifs ou repository ;
- les entities proches des lignes SQL, sans dépendance vers PostgreSQL ;
- les types communs de pagination, tri et limites ;
- les contrats de healthcheck et de statut migrations ;
- les helpers d'erreur storage basés sur `kb_core::Error` et `kb_core::Result`.
`kb_store_core` ne contient pas :
- de pool PostgreSQL ;
- de SQL ;
- de migrations ;
- de logique Tauri ;
- de client RPC ;
- de dépendance vers `kb_store_pg`.
## Layout obligatoire
```text
src/
lib.rs
pagination.rs
health.rs
db_error.rs
dtos.rs
entities.rs
repositories.rs
dtos/
entities/
repositories/
```
Les fichiers `dtos.rs`, `entities.rs` et `repositories.rs` servent uniquement de façade de modules. Ils évitent `mod.rs` et conservent les sous-dossiers spécialisés.
## Conventions de dossiers
| Dossier | Rôle |
|-----------------|--------------------------------------------|
| `entities/` | Représentations proches des lignes SQL. |
| `dtos/` | Contrats applicatifs, Tauri ou repository. |
| `repositories/` | Traits repository backend-agnostiques. |
Les structures Rust ne doivent pas être placées dans des modules de requêtes SQL.
## Contrats actifs `0.3.4`
Les premiers contrats stabilisés sont volontairement minimaux :
| Type | Rôle |
|----------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------|
| `RawTransactionInsert` | Entrée décriture pour une transaction canonique dans `kb_sol_raw_transactions`; `from_canonical` calcule le JSON et le hash déterministes. |
| `TransactionObservationInsert` | Entrée décriture légère pour `kb_sol_obs_transaction_observations`, sans payload source complet. |
| `TransactionObservationOrigin` | Origine `Live`, `Backfill`, `Replay`, `Repair` ou `Migration`. |
| `TransactionObservationStatus` | État technique de détection, réception, normalisation, persistance, échec ou absence temporaire. |
| `CoreTransactionInsert` | Transaction normalisée liée à la ligne canonical raw. |
| `CoreAccountKeyInsert` | Compte résolu statique ou ALT avec flags signer/writable. |
| `CoreInstructionInsert` | Instruction top-level résolue avec chemin et hash de payload. |
| `CoreInnerInstructionInsert` | Instruction CPI résolue avec parent, chemin et hash de payload. |
| `CoreLogInsert` | Log ordonné avec rattachement prudent et hash de texte. |
| `CoreBalanceChangeInsert` | Delta SOL ou token exact, déterministe et lié au compte résolu. |
| `CoreInstructionReplayInput` | Instruction avec contexte extrait, dont les instructions outer ordonnées depuis le contrat `2`, pour les décodeurs. |
| `CoreInstructionReplayFilter` | Filtre de sélection des instructions à traiter ou rejouer. |
| `CoreInstructionLifecycleMark` | Marquage d'état pour une instruction normalisée. |
| `CoreInstructionProcessingState` | État opérationnel d'une instruction pour replay partiel. |
| `DecodedEventInsert` | Entrée d'écriture future pour `kb_sol_decode_decoded_events`. |
| `MaterializedEventInsert` | Entrée d'écriture future pour `kb_sol_mat_*`. |
| `ProcessingLedgerMark` | Contrat de marquage pour `kb_sol_ops_processing_ledger`. |
| `CoreExtractionSelectionFilter` | Sélection bornée par signatures, slots, état raw ou programme déjà indexé. |
| `ProcessingLedgerIdentity` | Identité stable stage/processor/version/input/hash. |
| `CoreExtractionBundle` | Graphe complet à persister atomiquement pour une signature. |
| `CoreExtractionFailure` | Échec rejouable avec code et message explicites. |
| `RawPayloadLifecycleMark` | Marquage d'état de rétention et de traitement raw. |
| `InsertOutcome` | Résultat commun d'insert, upsert ou skip. |
| `PageRequest` | Contrat de pagination borné. |
| `SortDirection` | Contrat de tri générique. |
| `StoreBackendDescriptor` | Diagnostic backend sans exposer le DSN complet. |
| `StoreMigrationSnapshot` | Diagnostic de version migrations sans imposer une stratégie SQL. |
## Traits repository
Les traits publics de `0.2.1` sont async et sans implémentation SQL :
```text
StoreHealthStore
RawTransactionStore
CoreTransactionStore
CoreExtractionStore
ProgramObservationStore
DecodedEventStore
MaterializedEventStore
ProcessingLedgerStore
```
Ils définissent les frontières utilisées par `kb_store_pg`, `kb_store_sqlite`, le pipeline et les futures fenêtres de diagnostic.
## Convention `Entity`
Une `Entity` représente une ligne logique stockée. Elle reste proche de la DB, mais ne doit pas imposer PostgreSQL dans `kb_store_core`.
Exemples actuels :
```text
RawTransactionRow
TransactionObservationRow
CoreTransactionRow
CoreAccountKeyRow
CoreInstructionRow
CoreInnerInstructionRow
CoreLogRow
CoreBalanceChangeRow
DecodedEventRow
MaterializedEventRow
ProcessingLedgerRow
```
## Convention `Dto`
Un `Dto` représente un contrat d'entrée, de sortie ou de diagnostic.
Exemples actuels :
```text
RawTransactionInsert
TransactionObservationInsert
CoreTransactionInsert
CoreAccountKeyInsert
CoreInstructionInsert
CoreInnerInstructionInsert
CoreLogInsert
CoreBalanceChangeInsert
CoreInstructionReplayInput
StoreBackendDescriptor
StoreMigrationSnapshot
```
## Tables Solana référencées
Les traits et DTOs doivent référencer les tables Solana par convention logique, mais sans SQL concret. Le nom physique PostgreSQL suit :
```text
kb_sol_<domain>_<name>
```
Exemples :
```text
kb_sol_raw_transactions
kb_sol_obs_transaction_observations
kb_sol_core_transactions
kb_sol_core_account_keys
kb_sol_core_instructions
kb_sol_core_inner_instructions
kb_sol_core_logs
kb_sol_core_balance_changes
kb_sol_obs_program_observations
kb_sol_ops_processing_ledger
```
## Règles locales
- Les commentaires de code restent en anglais.
- La documentation Markdown reste en français.
- Les exports publics sont contrôlés depuis `lib.rs`.
- Les erreurs passent par `kb_core::Error` et `kb_core::Result`.
- Le crate doit rester testable offline.
## Replay instruction-level
Le replay opérationnel doit pouvoir cibler `CoreInstructionRow`, pas seulement `CoreTransactionRow`. Cela permet à un décodeur de demander uniquement les instructions `Pending`, `Failed` ou `ReplayRequested`, filtrées par `program_id` et plage de slots.
Le décodage réel doit ensuite recevoir `CoreInstructionReplayInput`, c'est-à-dire une instruction avec son contexte extrait : comptes résolus, inner instructions, logs, balances et erreur de transaction. Cette décision prépare les futures tables `kb_sol_core_instructions`, `kb_sol_core_logs`, `kb_sol_core_balance_changes` et `kb_sol_ops_processing_ledger` sans figer encore leur SQL exact.
## Extension `0.2.4`
`0.2.4` rend les contrats core effectivement utilisés par `kb_store_pg`. Le replay reste planifié depuis `CoreInstructionRow`, mais les décodeurs reçoivent `CoreInstructionReplayInput` avec account keys, inner instructions, logs et balance changes.
Le champ `balance_change_index` est ajouté au contrat `CoreBalanceChangeInsert` afin de dédupliquer les balance changes par ordre d'extraction dans une transaction.
## Migration corrective `0.3.1`
Les noms historiques `RawRpcTransaction*`, `RawWsNotification*`, `kb_sol_raw_rpc_transactions` et `kb_sol_raw_ws_notifications` restent uniquement dans les migrations et la documentation historique `0.2.x`.
Le contrat actif utilise :
```text
RawTransaction*
TransactionObservation*
kb_sol_raw_transactions
kb_sol_obs_transaction_observations
```
La transaction canonique contient le document rejouable source-indépendant et sa version. Lobservation conserve uniquement la provenance, la méthode, les timestamps, les tailles, les hashes et les statuts techniques.
## Contrat atomique `0.3.4`
`CoreExtractionStore` sépare la logique de transformation de limplémentation SQL. Le pipeline peut sélectionner les lignes raw, vérifier le ledger, persister un `CoreExtractionBundle` ou enregistrer un `CoreExtractionFailure` sans dépendre de PostgreSQL.
## Contrats decode et matérialisation `0.4.0`
`DecodePipelineStore` regroupe les opérations backend-agnostiques de sélection, skip, couverture, persistance atomique decode, échec et matérialisation. Les DTOs `DecodePersistenceBundle` et `MaterializationPersistenceBundle` imposent la cohérence entre processor, version, input key/hash, signature, instruction path, sorties et ledger.
La couverture machine-readable est portée par `DecodeCoverageDeclarationInsert`, `DecodeCoverageObservationInsert` et `DecodeCoverageSummaryRow`.
## Contrat contextualisé `2`
Depuis `0.4.1-pre.014`, `CoreInstructionReplayInput` transporte `outer_instructions_json`, obligatoirement un tableau JSON. Chaque entrée représente une instruction outer avec `instructionIndex`, `instructionPath`, `programId`, `payloadJson` et `payloadHash`. La liste inclut linstruction cible et conserve lordre numérique du message. Ce champ participe à la sérialisation déterministe et donc au hash de replay.
Ce changement est purement contractuel : il ne requiert aucune migration SQL et ne modifie pas les garanties didempotence ou de rollback des stores.

View File

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

View File

@@ -0,0 +1,25 @@
<!-- file: kb_store_core/src/dtos/README.md -->
<!-- version: 6 -->
# DTOs
Ce dossier contient les contrats applicatifs et repository indépendants du backend.
## Règles
- Un DTO représente une entrée, une sortie ou un diagnostic.
- Un DTO ne représente pas directement une ligne SQL complète.
- Un DTO peut valider les contraintes minimales communes : signature non vide, program id non vide, limite de pagination, identifiant de module non vide.
- Un DTO ne doit pas dépendre de `sqlx`, PostgreSQL, SQLite ou Tauri.
- Le core store sépare explicitement l'unité de replay (`CoreInstructionInsert`) du contexte d'arbre (`CoreInnerInstructionInsert`, logs, balances et account keys).
## Fichiers actuels
| Fichier | Rôle |
|------------------|---------------------------------------------------------------------------------------------------------------------------|
| `raw_dtos.rs` | Contrats raw RPC, raw WebSocket, déduplication et cycle de vie raw. |
| `core_dtos.rs` | Contrats core transaction, account keys, instructions, inner instructions, logs, balance changes et replay contextualisé. |
| `event_dtos.rs` | Contrats decode/materialization et résultat d'écriture. |
| `ledger_dtos.rs` | Contrat du ledger de traitement. |
| `store_dtos.rs` | Diagnostics backend et migrations. |

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,289 @@
// file: kb_store_core/src/dtos/core_extraction_dtos.rs
// version: 2
//! 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,684 @@
// file: kb_store_core/src/dtos/decode_dtos.rs
// version: 8
//! 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::dtos::decode_dtos::trim_optional_text(processor_name),
materialized_family: crate::dtos::decode_dtos::trim_optional_text(materialized_family),
signature_contains: crate::dtos::decode_dtos::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_core/src/dtos/event_dtos.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_model::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_core/src/dtos/ledger_dtos.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_core/src/dtos/raw_dtos.rs
// version: 5
//! 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_model::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_model::CanonicalTransaction {
return kb_model::CanonicalTransaction {
format_version: kb_model::CANONICAL_TRANSACTION_FORMAT_VERSION,
primary_signature: "2Ana1pUpv2ZbMVkwF5FXapYeBEjdxDatLn7nvJkhgTSXbs59SyZSx866bXirPgj8QQVB57uxHJBG1YFvkRbFj4T".to_string(),
slot: 9,
block_time: std::option::Option::None,
version: kb_model::CanonicalTransactionVersion::Legacy,
signatures: std::vec![
"2Ana1pUpv2ZbMVkwF5FXapYeBEjdxDatLn7nvJkhgTSXbs59SyZSx866bXirPgj8QQVB57uxHJBG1YFvkRbFj4T".to_string(),
],
message: kb_model::CanonicalTransactionMessage {
header: kb_model::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_model::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_model::CanonicalLoadedAddresses::default(),
},
metadata: std::option::Option::Some(kb_model::CanonicalTransactionMetadata {
status: kb_model::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_model::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_core/src/dtos/store_dtos.rs
// version: 2
//! 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_core/src/entities.rs
// version: 5
//! Backend-neutral SQL-like entity exports for storage adapters.
mod core_entities;
mod event_entities;
mod ledger_entities;
mod raw_entities;
/// Core account key SQL-like row contract.
pub use crate::entities::core_entities::CoreAccountKeyRow;
/// Core balance change SQL-like row contract.
pub use crate::entities::core_entities::CoreBalanceChangeRow;
/// Core inner instruction SQL-like row contract.
pub use crate::entities::core_entities::CoreInnerInstructionRow;
/// Core instruction SQL-like row contract.
pub use crate::entities::core_entities::CoreInstructionRow;
/// Core log SQL-like row contract.
pub use crate::entities::core_entities::CoreLogRow;
/// Core transaction SQL-like row contract.
pub use crate::entities::core_entities::CoreTransactionRow;
/// Decoded event SQL-like row contract.
pub use crate::entities::event_entities::DecodedEventRow;
/// Materialized event SQL-like row contract.
pub use crate::entities::event_entities::MaterializedEventRow;
/// Processing ledger SQL-like row contract.
pub use crate::entities::ledger_entities::ProcessingLedgerRow;
/// Canonical raw Solana transaction SQL-like row contract.
pub use crate::entities::raw_entities::RawTransactionRow;
/// Transaction acquisition observation SQL-like row contract.
pub use crate::entities::raw_entities::TransactionObservationRow;

View File

@@ -0,0 +1,26 @@
<!-- file: kb_store_core/src/entities/README.md -->
<!-- version: 5 -->
# Entities
Ce dossier contient les représentations proches des lignes SQL.
## Règles
- Une entity reste proche d'une table ou d'une ligne logique stockée.
- Les timestamps utilisent `chrono::DateTime<chrono::Utc>` côté Rust.
- Les slots proches SQL utilisent `i64`, car PostgreSQL stockera `slot` en `BIGINT`.
- Les payloads bruts utilisent `raw_json`.
- Les payloads décodés ou matérialisés utilisent `payload_json`.
- Les payloads purgables peuvent devenir optionnels si un hash stable est conservé.
- Les entities ne contiennent pas de SQL, de bind ou d'exécution.
## Fichiers actuels
| Fichier | Rôle |
|----------------------|-------------------------------------------------------------------------------|
| `raw_entities.rs` | Lignes raw RPC et raw WebSocket. |
| `core_entities.rs` | Lignes core transaction, account keys, instructions, logs et balance changes. |
| `event_entities.rs` | Lignes decode et materialization. |
| `ledger_entities.rs` | Lignes du ledger de traitement. |

View File

@@ -0,0 +1,166 @@
// file: kb_store_core/src/entities/core_entities.rs
// version: 5
//! 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_core/src/entities/event_entities.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_core/src/entities/ledger_entities.rs
// version: 2
//! 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_core/src/entities/raw_entities.rs
// version: 3
//! 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,58 @@
// file: kb_store_core/src/health.rs
// version: 2
//! 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,159 @@
// file: kb_store_core/src/lib.rs
// version: 14
//! Backend-neutral storage contracts used by pipeline crates.
#![warn(missing_docs)]
#![deny(unreachable_pub)]
#![forbid(unsafe_code)]
mod db_error;
mod dtos;
mod entities;
mod health;
mod pagination;
mod repositories;
/// Storage error helper functions.
pub use crate::db_error::storage_contract_error;
/// Current normalized core replay input contract version.
pub use crate::dtos::CORE_REPLAY_INPUT_CONTRACT_VERSION;
/// Core account key insert contract.
pub use crate::dtos::CoreAccountKeyInsert;
/// Core account key source category.
pub use crate::dtos::CoreAccountKeySource;
/// Core balance change insert contract.
pub use crate::dtos::CoreBalanceChangeInsert;
/// Core balance change kind.
pub use crate::dtos::CoreBalanceChangeKind;
/// Complete normalized core extraction write bundle.
pub use crate::dtos::CoreExtractionBundle;
/// Failure details persisted for one canonical to core extraction attempt.
pub use crate::dtos::CoreExtractionFailure;
/// Bounded canonical transaction selection filter for core extraction.
pub use crate::dtos::CoreExtractionSelectionFilter;
/// Core inner instruction insert contract.
pub use crate::dtos::CoreInnerInstructionInsert;
/// Core instruction insert contract.
pub use crate::dtos::CoreInstructionInsert;
/// Core instruction lifecycle mark request.
pub use crate::dtos::CoreInstructionLifecycleMark;
/// Core instruction processing state.
pub use crate::dtos::CoreInstructionProcessingState;
/// Core instruction replay filter contract.
pub use crate::dtos::CoreInstructionReplayFilter;
/// Decoder replay input containing one instruction plus extracted transaction context.
pub use crate::dtos::CoreInstructionReplayInput;
/// Core log insert contract.
pub use crate::dtos::CoreLogInsert;
/// Core transaction insert contract.
pub use crate::dtos::CoreTransactionInsert;
/// One machine-readable decoder coverage declaration row.
pub use crate::dtos::DecodeCoverageDeclarationInsert;
/// One observed coverage classification row owned by one decode attempt.
pub use crate::dtos::DecodeCoverageObservationInsert;
/// One row of aggregated decoder coverage diagnostics.
pub use crate::dtos::DecodeCoverageSummaryRow;
/// Failed decode attempt persisted in the common ledger.
pub use crate::dtos::DecodeFailure;
/// One processor-owned decoded observation row.
pub use crate::dtos::DecodeObservationInsert;
/// Atomic persistence bundle for one decoder and one contextual input.
pub use crate::dtos::DecodePersistenceBundle;
/// Bounded contextual instruction selection filter for decode campaigns.
pub use crate::dtos::DecodeSelectionFilter;
/// Decoded event insert contract.
pub use crate::dtos::DecodedEventInsert;
/// Insert or upsert result contract returned by repositories.
pub use crate::dtos::InsertOutcome;
/// Maximum number of materialized rows returned by one bounded query.
pub use crate::dtos::MAX_MATERIALIZED_EVENT_QUERY_ROWS;
/// Atomic persistence bundle for one materializer and one decoded observation.
pub use crate::dtos::MaterializationPersistenceBundle;
/// Bounded read-only materialized event selection.
pub use crate::dtos::MaterializedEventFilter;
/// Materialized event insert contract.
pub use crate::dtos::MaterializedEventInsert;
/// One materialized output returned by a bounded query.
pub use crate::dtos::MaterializedEventQueryRow;
/// One processor-owned materialized output row.
pub use crate::dtos::MaterializedOutputInsert;
/// Stable processing ledger identity.
pub use crate::dtos::ProcessingLedgerIdentity;
/// Processing ledger mark request contract.
pub use crate::dtos::ProcessingLedgerMark;
/// Stable processing ledger status.
pub use crate::dtos::ProcessingLedgerStatus;
/// Raw payload lifecycle mark request.
pub use crate::dtos::RawPayloadLifecycleMark;
/// Raw payload processing state.
pub use crate::dtos::RawPayloadProcessingState;
/// Raw payload retention state.
pub use crate::dtos::RawPayloadRetentionState;
/// Canonical raw Solana transaction insert contract.
pub use crate::dtos::RawTransactionInsert;
/// Store backend diagnostic contract.
pub use crate::dtos::StoreBackendDescriptor;
/// Store backend kind contract.
pub use crate::dtos::StoreBackendKind;
/// Store migration diagnostic snapshot contract.
pub use crate::dtos::StoreMigrationSnapshot;
/// Store migration status contract.
pub use crate::dtos::StoreMigrationStatus;
/// Transaction acquisition observation insert contract.
pub use crate::dtos::TransactionObservationInsert;
/// Transaction acquisition observation origin.
pub use crate::dtos::TransactionObservationOrigin;
/// Transaction acquisition observation status.
pub use crate::dtos::TransactionObservationStatus;
/// Core account key SQL-like row contract.
pub use crate::entities::CoreAccountKeyRow;
/// Core balance change SQL-like row contract.
pub use crate::entities::CoreBalanceChangeRow;
/// Core inner instruction SQL-like row contract.
pub use crate::entities::CoreInnerInstructionRow;
/// Core instruction SQL-like row contract.
pub use crate::entities::CoreInstructionRow;
/// Core log SQL-like row contract.
pub use crate::entities::CoreLogRow;
/// Core transaction SQL-like row contract.
pub use crate::entities::CoreTransactionRow;
/// Decoded event SQL-like row contract.
pub use crate::entities::DecodedEventRow;
/// Materialized event SQL-like row contract.
pub use crate::entities::MaterializedEventRow;
/// Processing ledger SQL-like row contract.
pub use crate::entities::ProcessingLedgerRow;
/// Canonical raw Solana transaction SQL-like row contract.
pub use crate::entities::RawTransactionRow;
/// Transaction acquisition observation SQL-like row contract.
pub use crate::entities::TransactionObservationRow;
/// Store backend health snapshot.
pub use crate::health::StoreHealthSnapshot;
/// Store backend health status.
pub use crate::health::StoreHealthStatus;
/// Default repository page size.
pub use crate::pagination::DEFAULT_PAGE_SIZE;
/// Maximum repository page size.
pub use crate::pagination::MAX_PAGE_SIZE;
/// Page request contract for repository list operations.
pub use crate::pagination::PageRequest;
/// Sort direction for repository list operations.
pub use crate::pagination::SortDirection;
/// Canonical transaction to core extraction storage behavior.
pub use crate::repositories::CoreExtractionStore;
/// Core Solana storage behavior.
pub use crate::repositories::CoreTransactionStore;
/// Contextual instruction decode and materialization storage behavior.
pub use crate::repositories::DecodePipelineStore;
/// Decoded event storage behavior.
pub use crate::repositories::DecodedEventStore;
/// Materialized event storage behavior.
pub use crate::repositories::MaterializedEventStore;
/// Processing ledger storage behavior.
pub use crate::repositories::ProcessingLedgerStore;
/// Program observation storage behavior.
pub use crate::repositories::ProgramObservationStore;
/// Raw transaction storage behavior.
pub use crate::repositories::RawTransactionStore;
/// Store health storage behavior.
pub use crate::repositories::StoreHealthStore;

View File

@@ -0,0 +1,75 @@
// file: kb_store_core/src/pagination.rs
// version: 2
//! 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,25 @@
// file: kb_store_core/src/repositories.rs
// version: 4
//! Backend-neutral repository trait exports.
mod storage_repositories;
/// Canonical transaction to core extraction storage behavior.
pub use crate::repositories::storage_repositories::CoreExtractionStore;
/// Core Solana storage behavior.
pub use crate::repositories::storage_repositories::CoreTransactionStore;
/// Contextual instruction decode and materialization storage behavior.
pub use crate::repositories::storage_repositories::DecodePipelineStore;
/// Decoded event storage behavior.
pub use crate::repositories::storage_repositories::DecodedEventStore;
/// Materialized event storage behavior.
pub use crate::repositories::storage_repositories::MaterializedEventStore;
/// Processing ledger storage behavior.
pub use crate::repositories::storage_repositories::ProcessingLedgerStore;
/// Program observation storage behavior.
pub use crate::repositories::storage_repositories::ProgramObservationStore;
/// Raw transaction storage behavior.
pub use crate::repositories::storage_repositories::RawTransactionStore;
/// Store health storage behavior.
pub use crate::repositories::storage_repositories::StoreHealthStore;

View File

@@ -0,0 +1,29 @@
<!-- file: kb_store_core/src/repositories/README.md -->
<!-- version: 5 -->
# Repositories
Ce dossier contient les APIs de stockage backend-agnostiques.
## Règles
- Les traits sont async pour rester compatibles avec `sqlx` et les stores distants.
- Les traits ne contiennent pas de SQL.
- Les traits manipulent des DTOs ou des modèles partagés.
- Les implémentations concrètes appartiennent à `kb_store_pg`, `kb_store_sqlite` ou à des stores de tests.
- Le replay doit pouvoir sélectionner des instructions normalisées sans rejouer toute une transaction.
- Les décodeurs doivent pouvoir lire une instruction avec contexte extrait : account keys, inner instructions, logs et balance changes.
- `CoreTransactionStore` doit permettre une insertion idempotente séparée des transactions, account keys, instructions, inner instructions, logs et balance changes.
## Traits actuels
```text
StoreHealthStore
RawTransactionStore
CoreTransactionStore
ProgramObservationStore
DecodedEventStore
MaterializedEventStore
ProcessingLedgerStore
```

View File

@@ -0,0 +1,237 @@
// file: kb_store_core/src/repositories/storage_repositories.rs
// version: 11
//! 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_model::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_model::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>;
}