0.1.0
This commit is contained in:
21
migration/khadhroony-bot2-reference/kb_store_core/Cargo.toml
Normal file
21
migration/khadhroony-bot2-reference/kb_store_core/Cargo.toml
Normal 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
|
||||
|
||||
222
migration/khadhroony-bot2-reference/kb_store_core/README.md
Normal file
222
migration/khadhroony-bot2-reference/kb_store_core/README.md
Normal 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. L’observation 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 l’implé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 l’instruction cible et conserve l’ordre 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 d’idempotence ou de rollback des stores.
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
103
migration/khadhroony-bot2-reference/kb_store_core/src/dtos.rs
Normal file
103
migration/khadhroony-bot2-reference/kb_store_core/src/dtos.rs
Normal 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;
|
||||
@@ -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
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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. |
|
||||
|
||||
@@ -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>,
|
||||
}
|
||||
@@ -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>,
|
||||
}
|
||||
@@ -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>,
|
||||
}
|
||||
@@ -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>,
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
159
migration/khadhroony-bot2-reference/kb_store_core/src/lib.rs
Normal file
159
migration/khadhroony-bot2-reference/kb_store_core/src/lib.rs
Normal 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;
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
Reference in New Issue
Block a user