This commit is contained in:
2026-04-23 17:18:15 +02:00
parent 3b8e029cde
commit 6d00c0ddf4
14 changed files with 545 additions and 65 deletions

View File

@@ -17,4 +17,5 @@
0.4.3 - Pool dendpoints HTTP 0.4.3 - Pool dendpoints HTTP
0.4.4 - Ajout de la fenêtre Demo Http dans kb_app, exécution manuelle des méthodes HTTP via le pool, snapshot des endpoints et amélioration des presets UI 0.4.4 - Ajout de la fenêtre Demo Http dans kb_app, exécution manuelle des méthodes HTTP via le pool, snapshot des endpoints et amélioration des presets UI
0.5.0 - Début du socle SQLite : configuration database, ouverture/validation de la base et premières briques de persistance 0.5.0 - Début du socle SQLite : configuration database, ouverture/validation de la base et premières briques de persistance
0.5.1 - Ajout des premières tables métier SQLite pour les endpoints connus et les événements runtime, avec séparation entities/dtos/queries/types 0.5.1 - Ajout des premières tables métier SQLite pour les endpoints connus HTTP/WS et les événements runtime, avec séparation entities/dtos/queries/types
0.5.2 - Ajout de la table des tokens observés, de leur statut local et des premières requêtes de persistance associées

View File

@@ -8,7 +8,7 @@ members = [
] ]
[workspace.package] [workspace.package]
version = "0.5.1" version = "0.5.2"
edition = "2024" edition = "2024"
license = "MIT" license = "MIT"
repository = "https://git.sasedev.com/Sasedev/khadhroony-bobobot" repository = "https://git.sasedev.com/Sasedev/khadhroony-bobobot"

View File

@@ -141,7 +141,6 @@ Le tracing est centralisé dans `kb_lib`.
## 6. Phasage par versions ## 6. Phasage par versions
### 6.1. Version `0.0.2` — Socle conforme ### 6.1. Version `0.0.2` — Socle conforme
Objectif : corriger le squelette et poser la base de travail. Objectif : corriger le squelette et poser la base de travail.
Réalisé : Réalisé :
@@ -157,7 +156,6 @@ Réalisé :
- UI Tauri minimale. - UI Tauri minimale.
### 6.2. Version `0.1.x` — Transport WebSocket générique ### 6.2. Version `0.1.x` — Transport WebSocket générique
Objectif : construire un vrai `WsClient` asynchrone clonable. Objectif : construire un vrai `WsClient` asynchrone clonable.
Réalisé : Réalisé :
@@ -172,7 +170,6 @@ Réalisé :
- tests offline avec serveur mock. - tests offline avec serveur mock.
### 6.3. Version `0.1.1` — Intégration Tauri minimale du `WsClient` ### 6.3. Version `0.1.1` — Intégration Tauri minimale du `WsClient`
Objectif : valider le transport via lapplication desktop. Objectif : valider le transport via lapplication desktop.
Réalisé : Réalisé :
@@ -183,7 +180,6 @@ Réalisé :
- validation du flux `frontend -> tauri -> kb_lib -> frontend`. - validation du flux `frontend -> tauri -> kb_lib -> frontend`.
### 6.4. Version `0.2.0` — Couche JSON-RPC WS Solana ### 6.4. Version `0.2.0` — Couche JSON-RPC WS Solana
Objectif : séparer clairement transport, réponses RPC et notifications. Objectif : séparer clairement transport, réponses RPC et notifications.
Réalisé : Réalisé :
@@ -195,7 +191,6 @@ Réalisé :
- premiers helpers JSON-RPC sur `WsClient`. - premiers helpers JSON-RPC sur `WsClient`.
### 6.5. Version `0.3.0` — Registre subscriptions / notifications ### 6.5. Version `0.3.0` — Registre subscriptions / notifications
Objectif : fiabiliser la gestion des subscriptions. Objectif : fiabiliser la gestion des subscriptions.
Réalisé : Réalisé :
@@ -208,7 +203,6 @@ Réalisé :
- routage séparé des notifications. - routage séparé des notifications.
### 6.6. Version `0.3.1` — Helpers subscribe/unsubscribe WebSocket ### 6.6. Version `0.3.1` — Helpers subscribe/unsubscribe WebSocket
Objectif : ajouter les helpers haut niveau correspondant aux principales méthodes PubSub Solana. Objectif : ajouter les helpers haut niveau correspondant aux principales méthodes PubSub Solana.
Réalisé : Réalisé :
@@ -218,7 +212,6 @@ Réalisé :
- premiers tests de validation des noms de méthodes. - premiers tests de validation des noms de méthodes.
### 6.7. Version `0.3.2` — Helpers typed et notifications typed ### 6.7. Version `0.3.2` — Helpers typed et notifications typed
Objectif : sappuyer principalement sur `solana-rpc-client-api` pour typer les subscribe et les notifications. Objectif : sappuyer principalement sur `solana-rpc-client-api` pour typer les subscribe et les notifications.
Réalisé : Réalisé :
@@ -228,7 +221,6 @@ Réalisé :
- base de travail pour réduire lusage direct de `serde_json::Value`. - base de travail pour réduire lusage direct de `serde_json::Value`.
### 6.8. Version `0.3.3` — Distinction API typed / raw ### 6.8. Version `0.3.3` — Distinction API typed / raw
Objectif : clarifier lAPI publique de `WsClient`. Objectif : clarifier lAPI publique de `WsClient`.
Réalisé : Réalisé :
@@ -238,7 +230,6 @@ Réalisé :
- préparation dune hiérarchie API plus explicite. - préparation dune hiérarchie API plus explicite.
### 6.9. Version `0.3.4` — Fenêtre `Demo Ws` dans `kb_app` ### 6.9. Version `0.3.4` — Fenêtre `Demo Ws` dans `kb_app`
Objectif : tester manuellement les souscriptions live dans une fenêtre dédiée. Objectif : tester manuellement les souscriptions live dans une fenêtre dédiée.
Réalisé : Réalisé :
@@ -251,7 +242,6 @@ Réalisé :
- premiers tests réels sur `wss://api.mainnet.solana.com`. - premiers tests réels sur `wss://api.mainnet.solana.com`.
### 6.10. Version `0.3.5` — Stabilisation de `Demo Ws` ### 6.10. Version `0.3.5` — Stabilisation de `Demo Ws`
Objectif : rendre la fenêtre de démonstration robuste sous flux élevé et cohérente avec la configuration. Objectif : rendre la fenêtre de démonstration robuste sous flux élevé et cohérente avec la configuration.
Réalisé : Réalisé :
@@ -269,8 +259,7 @@ Réalisé :
Objectif : construire un `HttpClient` clonable, limité et extensible, puis ajouter les premiers helpers HTTP Solana. Objectif : construire un `HttpClient` clonable, limité et extensible, puis ajouter les premiers helpers HTTP Solana.
### 0.4.0 — Socle `HttpClient` ### 6.12. Version `0.4.0` — Socle `HttpClient`
Réalisé : Réalisé :
- client `reqwest` asynchrone clonable, - client `reqwest` asynchrone clonable,
@@ -291,8 +280,7 @@ Livrables :
- `getVersion` - `getVersion`
- `getSlot` - `getSlot`
### 0.4.1 — Helpers HTTP Solana ### 6.13. Version `0.4.1` — Helpers HTTP Solana
Réalisé : Réalisé :
- ajouter des helpers HTTP haut niveau comme pour le client WS, - ajouter des helpers HTTP haut niveau comme pour le client WS,
@@ -300,8 +288,7 @@ Réalisé :
- couvrir les premières méthodes utiles du RPC HTTP Solana, - couvrir les premières méthodes utiles du RPC HTTP Solana,
- conserver `HttpClient` comme couche générique réutilisable. - conserver `HttpClient` comme couche générique réutilisable.
### 0.4.2 — Politique HTTP avancée ### 6.14. Version `0.4.2` — Politique HTTP avancée
Réalisé : Réalisé :
- préparer un état de pause avant envoi pour un endpoint HTTP, - préparer un état de pause avant envoi pour un endpoint HTTP,
@@ -309,8 +296,7 @@ Réalisé :
- distinguer quota RPC général et quota `sendTransaction`, - distinguer quota RPC général et quota `sendTransaction`,
- préparer un futur pool dendpoints HTTP et larbitrage entre eux. - préparer un futur pool dendpoints HTTP et larbitrage entre eux.
### 0.4.3 — Pool dendpoints HTTP ### 6.15. Version `0.4.3` — Pool dendpoints HTTP
Réalisé : Réalisé :
- ajouter un pool d`HttpClient`, - ajouter un pool d`HttpClient`,
@@ -320,8 +306,7 @@ Réalisé :
- prendre en compte la classe de méthode HTTP, - prendre en compte la classe de méthode HTTP,
- préparer le routage multi-RPC et la limitation de concurrence par endpoint. - préparer le routage multi-RPC et la limitation de concurrence par endpoint.
### 0.4.4 — Démo HTTP dans `kb_app` ### 6.16. Version `0.4.4` — Démo HTTP dans `kb_app`
Réalisé : Réalisé :
- ajout dune fenêtre `Demo Http`, - ajout dune fenêtre `Demo Http`,
@@ -332,11 +317,10 @@ Réalisé :
- alignement visuel de la fenêtre sur le gabarit `Demo Ws`, - alignement visuel de la fenêtre sur le gabarit `Demo Ws`,
- amélioration des presets UI, copie de réponse et bascule pretty/raw. - amélioration des presets UI, copie de réponse et bascule pretty/raw.
## 6.12. Version `0.5.x` — Base de données SQLite ### 6.17. Version `0.5.x` — Base de données SQLite
Objectif : poser la persistance locale avec une organisation préparée dès le départ à une future évolution vers PostgreSQL ou un autre backend. Objectif : poser la persistance locale avec une organisation préparée dès le départ à une future évolution vers PostgreSQL ou un autre backend.
#### 0.5.0 — Socle SQLite ### 6.18. Version `0.5.0` — Socle SQLite
Réalisé : Réalisé :
- configuration DB dans `config.json`, - configuration DB dans `config.json`,
@@ -346,29 +330,32 @@ Réalisé :
- table `kb_db_metadata`, - table `kb_db_metadata`,
- séparation `db/entities`, `db/dtos`, `db/queries`, `db/types`. - séparation `db/entities`, `db/dtos`, `db/queries`, `db/types`.
#### 0.5.1 — Premières tables métier de stockage local ### 6.19. Version `0.5.1` — Premières tables métier de stockage local
À faire : Réalisé :
- ajouter les tables de référence pour les endpoints connus, - ajout des tables de référence pour les endpoints connus HTTP/WS,
- ajouter les tables techniques pour les événements runtime locaux, - ajout des tables techniques pour les événements runtime locaux,
- poser les `entities`, `dtos`, `queries` et `types` associés, - mise en place des `entities`, `dtos`, `queries` et `types` associés,
- préparer le stockage local des endpoints HTTP/WS connus et de leur état utile. - préparation du stockage local des endpoints HTTP/WS connus et de leur état utile.
#### 0.5.2 — Stockage des tokens observés ### 6.20. Version `0.5.2` — Stockage des tokens observés
À faire : Réalisé :
- ajouter les premières tables liées aux tokens observés, - ajout de la table `kb_observed_tokens`,
- préparer le stockage minimal des mints, symboles, statuts et dates de découverte, - stockage minimal des mints, symboles, noms, statuts et dates dobservation,
- préparer les relations futures avec pools, paires et événements on-chain. - ajout du `token_program`,
- préparation des relations futures avec pools, paires et événements on-chain,
- conservation dunicité locale par mint sans duplication par endpoint.
#### 0.5.3 — Événements et signaux locaux ### 6.21. Version `0.5.3` — Événements et signaux locaux
À faire : À faire :
- stocker les événements techniques importants remontés par les connecteurs, - stocker les événements techniques importants remontés par les connecteurs,
- préparer la conservation locale des signaux utiles à lanalyse, - préparer la conservation locale des signaux utiles à lanalyse,
- distinguer événements runtime, observations on-chain et événements métier. - distinguer événements runtime, observations on-chain et événements métier,
- préparer la traçabilité de provenance si plusieurs sources détectent un même objet, sans remettre en cause lunicité locale dun token par mint.
#### 0.5.x — Consolidation de la couche stockage ### 6.22. Version `0.5.x` — Consolidation de la couche stockage
À faire : À faire :
- conserver labstraction du backend dès le départ, - conserver labstraction du backend dès le départ,
@@ -376,8 +363,7 @@ Réalisé :
- garder les conversions explicites entre entités DB et DTOs applicatifs, - garder les conversions explicites entre entités DB et DTOs applicatifs,
- préparer une future compatibilité PostgreSQL sans casser lorganisation générale. - préparer une future compatibilité PostgreSQL sans casser lorganisation générale.
### 6.13. Version `0.6.x` — Détection technique on-chain / RPC ### 6.23. Version `0.6.x` — Détection technique on-chain / RPC
Objectif : commencer la détection utile pour lapplication. Objectif : commencer la détection utile pour lapplication.
À faire : À faire :
@@ -387,8 +373,7 @@ Objectif : commencer la détection utile pour lapplication.
- débuts de normalisation dévénements, - débuts de normalisation dévénements,
- premiers connecteurs DEX. - premiers connecteurs DEX.
### 6.14. Version `0.7.x` — DEX connectors v1 ### 6.24. Version `0.7.x` — DEX connectors v1
Objectif : structurer les connecteurs par protocole. Objectif : structurer les connecteurs par protocole.
Cibles initiales possibles : Cibles initiales possibles :
@@ -407,8 +392,7 @@ Cibles initiales possibles :
- création de types métiers propres, - création de types métiers propres,
- enrichissement des métadonnées token/pool/pair. - enrichissement des métadonnées token/pool/pair.
### 6.15. Version `0.8.x` — Analyse et filtrage ### 6.25. Version `0.8.x` — Analyse et filtrage
Objectif : transformer les événements bruts en signaux exploitables. Objectif : transformer les événements bruts en signaux exploitables.
À faire : À faire :
@@ -419,8 +403,7 @@ Objectif : transformer les événements bruts en signaux exploitables.
- statistiques de comportement, - statistiques de comportement,
- premiers patterns. - premiers patterns.
### 6.16. Version `1.x.y` — Wallets et swap préparatoire ### 6.26. Version `1.x.y` — Wallets et swap préparatoire
Objectif : préparer la couche daction. Objectif : préparer la couche daction.
À faire : À faire :
@@ -431,8 +414,7 @@ Objectif : préparer la couche daction.
- préparation dordres et de swaps, - préparation dordres et de swaps,
- simulation et garde-fous. - simulation et garde-fous.
### 6.17. Version `2.x.y` — Trading semi-automatisé ### 6.27. Version `2.x.y` — Trading semi-automatisé
Objectif : brancher lanalyse à laction tout en gardant des garde-fous explicites. Objectif : brancher lanalyse à laction tout en gardant des garde-fous explicites.
À faire : À faire :
@@ -443,8 +425,7 @@ Objectif : brancher lanalyse à laction tout en gardant des garde-fous exp
- confirmations explicites ou semi-automatiques, - confirmations explicites ou semi-automatiques,
- journaux dexécution. - journaux dexécution.
### 6.18. Version `3.x.y` — Yellowstone gRPC ### 6.28. Version `3.x.y` — Yellowstone gRPC
Objectif : ajouter le connecteur gRPC dédié. Objectif : ajouter le connecteur gRPC dédié.
À faire : À faire :
@@ -457,7 +438,6 @@ Objectif : ajouter le connecteur gRPC dédié.
## 7. Organisation des modules ciblés ## 7. Organisation des modules ciblés
### 7.1. `kb_lib` ### 7.1. `kb_lib`
Modules cibles à court terme : Modules cibles à court terme :
- `error.rs` - `error.rs`
@@ -472,7 +452,6 @@ Modules cibles à court terme :
- `rpc_ws_solana.rs` - `rpc_ws_solana.rs`
### 7.2. `kb_app` ### 7.2. `kb_app`
Responsabilités cibles : Responsabilités cibles :
- lancement Tauri, - lancement Tauri,
@@ -483,7 +462,6 @@ Responsabilités cibles :
- fenêtres de démonstration / diagnostic isolées. - fenêtres de démonstration / diagnostic isolées.
## 8. Ligne de conduite sur le `WsClient` ## 8. Ligne de conduite sur le `WsClient`
Le `WsClient` doit être conçu en plusieurs couches : Le `WsClient` doit être conçu en plusieurs couches :
1. transport brut WebSocket, 1. transport brut WebSocket,
@@ -500,7 +478,6 @@ Cette séparation évite de mélanger :
- les notifications push. - les notifications push.
## 9. Politique initiale de reconnexion ## 9. Politique initiale de reconnexion
Au départ : Au départ :
- pas de reconnexion automatique, - pas de reconnexion automatique,
@@ -510,7 +487,6 @@ Au départ :
Plus tard, ce comportement pourra devenir configurable dans `config.json` et pilotable depuis lapplication. Plus tard, ce comportement pourra devenir configurable dans `config.json` et pilotable depuis lapplication.
## 10. Politique initiale de fermeture ## 10. Politique initiale de fermeture
À la fermeture dun `WsClient` : À la fermeture dun `WsClient` :
1. marquer le client en arrêt, 1. marquer le client en arrêt,
@@ -522,7 +498,6 @@ Plus tard, ce comportement pourra devenir configurable dans `config.json` et pil
7. journaliser clairement les cas dégradés. 7. journaliser clairement les cas dégradés.
## 11. Documentation et livrables de référence ## 11. Documentation et livrables de référence
Le projet doit maintenir au minimum : Le projet doit maintenir au minimum :
- un `README.md` global, - un `README.md` global,
@@ -533,12 +508,11 @@ Le projet doit maintenir au minimum :
- les bindings TS générés via `cargo test export_bindings` lorsque les types partagés évoluent. - les bindings TS générés via `cargo test export_bindings` lorsque les types partagés évoluent.
## 12. Priorité immédiate ## 12. Priorité immédiate
La priorité immédiate est désormais la suivante : La priorité immédiate est désormais la suivante :
1. démarrer la version `0.5.1` avec les premières tables métier SQLite, 1. démarrer la version `0.5.3` avec les événements et signaux locaux,
2. ajouter les tables locales pour les endpoints connus HTTP/WS, 2. stocker les événements techniques importants remontés par les connecteurs,
3. ajouter les tables locales pour les événements runtime techniques, 3. distinguer clairement événements runtime, observations on-chain et événements métier,
4. structurer ces tables via `db/entities`, `db/dtos`, `db/queries` et `db/types`, 4. préparer la conservation locale des signaux utiles à lanalyse,
5. conserver labstraction du backend dès cette phase SQLite, 5. conserver labstraction du backend dès cette phase SQLite,
6. préparer ensuite le stockage des tokens observés et des premiers signaux persistés. 6. préparer ensuite lexploitation de ces signaux pour la future détection technique on-chain / RPC.

View File

@@ -19,20 +19,26 @@ pub use crate::db::dtos::KbDbMetadataDto;
pub use crate::db::dtos::KbDbRuntimeEventDto; pub use crate::db::dtos::KbDbRuntimeEventDto;
pub use crate::db::dtos::KbKnownHttpEndpointDto; pub use crate::db::dtos::KbKnownHttpEndpointDto;
pub use crate::db::dtos::KbKnownWsEndpointDto; pub use crate::db::dtos::KbKnownWsEndpointDto;
pub use crate::db::dtos::KbObservedTokenDto;
pub use crate::db::entities::KbDbMetadataEntity; pub use crate::db::entities::KbDbMetadataEntity;
pub use crate::db::entities::KbDbRuntimeEventEntity; pub use crate::db::entities::KbDbRuntimeEventEntity;
pub use crate::db::entities::KbKnownHttpEndpointEntity; pub use crate::db::entities::KbKnownHttpEndpointEntity;
pub use crate::db::entities::KbKnownWsEndpointEntity; pub use crate::db::entities::KbKnownWsEndpointEntity;
pub use crate::db::entities::KbObservedTokenEntity;
pub use crate::db::queries::get_db_metadata; pub use crate::db::queries::get_db_metadata;
pub use crate::db::queries::get_known_http_endpoint; pub use crate::db::queries::get_known_http_endpoint;
pub use crate::db::queries::get_known_ws_endpoint; pub use crate::db::queries::get_known_ws_endpoint;
pub use crate::db::queries::get_observed_token_by_mint;
pub use crate::db::queries::insert_db_runtime_event; pub use crate::db::queries::insert_db_runtime_event;
pub use crate::db::queries::list_db_metadata; pub use crate::db::queries::list_db_metadata;
pub use crate::db::queries::list_known_http_endpoints; pub use crate::db::queries::list_known_http_endpoints;
pub use crate::db::queries::list_known_ws_endpoints; pub use crate::db::queries::list_known_ws_endpoints;
pub use crate::db::queries::list_observed_tokens;
pub use crate::db::queries::list_recent_db_runtime_events; pub use crate::db::queries::list_recent_db_runtime_events;
pub use crate::db::queries::upsert_db_metadata; pub use crate::db::queries::upsert_db_metadata;
pub use crate::db::queries::upsert_known_http_endpoint; pub use crate::db::queries::upsert_known_http_endpoint;
pub use crate::db::queries::upsert_known_ws_endpoint; pub use crate::db::queries::upsert_known_ws_endpoint;
pub use crate::db::queries::upsert_observed_token;
pub use crate::db::types::KbDatabaseBackend; pub use crate::db::types::KbDatabaseBackend;
pub use crate::db::types::KbDbRuntimeEventLevel; pub use crate::db::types::KbDbRuntimeEventLevel;
pub use crate::db::types::KbObservedTokenStatus;

View File

@@ -6,8 +6,10 @@ mod db_metadata;
mod db_runtime_event; mod db_runtime_event;
mod known_http_endpoint; mod known_http_endpoint;
mod known_ws_endpoint; mod known_ws_endpoint;
mod observed_token;
pub use crate::db::dtos::db_metadata::KbDbMetadataDto; pub use crate::db::dtos::db_metadata::KbDbMetadataDto;
pub use crate::db::dtos::db_runtime_event::KbDbRuntimeEventDto; pub use crate::db::dtos::db_runtime_event::KbDbRuntimeEventDto;
pub use crate::db::dtos::known_http_endpoint::KbKnownHttpEndpointDto; pub use crate::db::dtos::known_http_endpoint::KbKnownHttpEndpointDto;
pub use crate::db::dtos::known_ws_endpoint::KbKnownWsEndpointDto; pub use crate::db::dtos::known_ws_endpoint::KbKnownWsEndpointDto;
pub use crate::db::dtos::observed_token::KbObservedTokenDto;

View File

@@ -0,0 +1,129 @@
// file: kb_lib/src/db/dtos/observed_token.rs
//! Observed token DTO.
/// Application-facing observed token DTO.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct KbObservedTokenDto {
/// Optional numeric primary key.
pub id: std::option::Option<i64>,
/// Token mint address.
pub mint: std::string::String,
/// Optional token symbol.
pub symbol: std::option::Option<std::string::String>,
/// Optional token display name.
pub name: std::option::Option<std::string::String>,
/// Optional decimals value.
pub decimals: std::option::Option<u8>,
/// Token program id.
pub token_program: std::string::String,
/// Local status.
pub status: crate::KbObservedTokenStatus,
/// First seen timestamp.
pub first_seen_at: chrono::DateTime<chrono::Utc>,
/// Last seen timestamp.
pub last_seen_at: chrono::DateTime<chrono::Utc>,
/// Last update timestamp.
pub updated_at: chrono::DateTime<chrono::Utc>,
}
impl KbObservedTokenDto {
/// Creates a new observed token DTO with current timestamps.
pub fn new(
mint: std::string::String,
symbol: std::option::Option<std::string::String>,
name: std::option::Option<std::string::String>,
decimals: std::option::Option<u8>,
token_program: std::string::String,
status: crate::KbObservedTokenStatus,
) -> Self {
let now = chrono::Utc::now();
Self {
id: None,
mint,
symbol,
name,
decimals,
token_program,
status,
first_seen_at: now,
last_seen_at: now,
updated_at: now,
}
}
}
impl TryFrom<crate::KbObservedTokenEntity> for KbObservedTokenDto {
type Error = crate::KbError;
fn try_from(
entity: crate::KbObservedTokenEntity,
) -> Result<Self, Self::Error> {
let status_result = crate::KbObservedTokenStatus::from_i16(entity.status);
let status = match status_result {
Ok(status) => status,
Err(error) => return Err(error),
};
let first_seen_at_result = chrono::DateTime::parse_from_rfc3339(&entity.first_seen_at);
let first_seen_at = match first_seen_at_result {
Ok(first_seen_at) => first_seen_at.with_timezone(&chrono::Utc),
Err(error) => {
return Err(crate::KbError::Db(format!(
"cannot parse observed token first_seen_at '{}': {}",
entity.first_seen_at,
error
)));
},
};
let last_seen_at_result = chrono::DateTime::parse_from_rfc3339(&entity.last_seen_at);
let last_seen_at = match last_seen_at_result {
Ok(last_seen_at) => last_seen_at.with_timezone(&chrono::Utc),
Err(error) => {
return Err(crate::KbError::Db(format!(
"cannot parse observed token last_seen_at '{}': {}",
entity.last_seen_at,
error
)));
},
};
let updated_at_result = chrono::DateTime::parse_from_rfc3339(&entity.updated_at);
let updated_at = match updated_at_result {
Ok(updated_at) => updated_at.with_timezone(&chrono::Utc),
Err(error) => {
return Err(crate::KbError::Db(format!(
"cannot parse observed token updated_at '{}': {}",
entity.updated_at,
error
)));
},
};
let decimals = match entity.decimals {
Some(decimals) => {
let decimals_u8_result = u8::try_from(decimals);
match decimals_u8_result {
Ok(decimals_u8) => Some(decimals_u8),
Err(error) => {
return Err(crate::KbError::Db(format!(
"cannot convert observed token decimals '{}' to u8: {}",
decimals,
error
)));
},
}
},
None => None,
};
Ok(Self {
id: Some(entity.id),
mint: entity.mint,
symbol: entity.symbol,
name: entity.name,
decimals,
token_program: entity.token_program,
status,
first_seen_at,
last_seen_at,
updated_at,
})
}
}

View File

@@ -8,8 +8,10 @@ mod db_metadata;
mod db_runtime_event; mod db_runtime_event;
mod known_http_endpoint; mod known_http_endpoint;
mod known_ws_endpoint; mod known_ws_endpoint;
mod observed_token;
pub use crate::db::entities::db_metadata::KbDbMetadataEntity; pub use crate::db::entities::db_metadata::KbDbMetadataEntity;
pub use crate::db::entities::db_runtime_event::KbDbRuntimeEventEntity; pub use crate::db::entities::db_runtime_event::KbDbRuntimeEventEntity;
pub use crate::db::entities::known_http_endpoint::KbKnownHttpEndpointEntity; pub use crate::db::entities::known_http_endpoint::KbKnownHttpEndpointEntity;
pub use crate::db::entities::known_ws_endpoint::KbKnownWsEndpointEntity; pub use crate::db::entities::known_ws_endpoint::KbKnownWsEndpointEntity;
pub use crate::db::entities::observed_token::KbObservedTokenEntity;

View File

@@ -0,0 +1,28 @@
// file: kb_lib/src/db/entities/observed_token.rs
//! Observed token entity.
/// Persisted observed token row.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, sqlx::FromRow)]
pub struct KbObservedTokenEntity {
/// Numeric primary key.
pub id: i64,
/// Token mint address.
pub mint: std::string::String,
/// Optional token symbol.
pub symbol: std::option::Option<std::string::String>,
/// Optional token display name.
pub name: std::option::Option<std::string::String>,
/// Optional decimals value.
pub decimals: std::option::Option<i64>,
/// Token program id.
pub token_program: std::string::String,
/// Local status stored as stable integer.
pub status: i16,
/// First seen timestamp encoded as RFC3339 UTC text.
pub first_seen_at: std::string::String,
/// Last seen timestamp encoded as RFC3339 UTC text.
pub last_seen_at: std::string::String,
/// Last update timestamp encoded as RFC3339 UTC text.
pub updated_at: std::string::String,
}

View File

@@ -6,6 +6,7 @@ mod db_metadata;
mod db_runtime_event; mod db_runtime_event;
mod known_http_endpoint; mod known_http_endpoint;
mod known_ws_endpoint; mod known_ws_endpoint;
mod observed_token;
pub use crate::db::queries::db_metadata::get_db_metadata; pub use crate::db::queries::db_metadata::get_db_metadata;
pub use crate::db::queries::db_metadata::list_db_metadata; pub use crate::db::queries::db_metadata::list_db_metadata;
@@ -18,3 +19,6 @@ pub use crate::db::queries::known_http_endpoint::upsert_known_http_endpoint;
pub use crate::db::queries::known_ws_endpoint::get_known_ws_endpoint; pub use crate::db::queries::known_ws_endpoint::get_known_ws_endpoint;
pub use crate::db::queries::known_ws_endpoint::list_known_ws_endpoints; pub use crate::db::queries::known_ws_endpoint::list_known_ws_endpoints;
pub use crate::db::queries::known_ws_endpoint::upsert_known_ws_endpoint; pub use crate::db::queries::known_ws_endpoint::upsert_known_ws_endpoint;
pub use crate::db::queries::observed_token::get_observed_token_by_mint;
pub use crate::db::queries::observed_token::list_observed_tokens;
pub use crate::db::queries::observed_token::upsert_observed_token;

View File

@@ -0,0 +1,230 @@
// file: kb_lib/src/db/queries/observed_token.rs
//! Queries for `kb_observed_tokens`.
/// Inserts or updates one observed token by mint.
pub async fn upsert_observed_token(
database: &crate::KbDatabase,
dto: &crate::KbObservedTokenDto,
) -> Result<i64, crate::KbError> {
match database.connection() {
crate::KbDatabaseConnection::Sqlite(pool) => {
let decimals_i64 = dto.decimals.map(i64::from);
let insert_result = sqlx::query(
r#"
INSERT INTO kb_observed_tokens (
mint,
symbol,
name,
decimals,
token_program,
status,
first_seen_at,
last_seen_at,
updated_at
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(mint) DO UPDATE SET
symbol = excluded.symbol,
name = excluded.name,
decimals = excluded.decimals,
token_program = excluded.token_program,
status = excluded.status,
last_seen_at = excluded.last_seen_at,
updated_at = excluded.updated_at
"#,
)
.bind(dto.mint.clone())
.bind(dto.symbol.clone())
.bind(dto.name.clone())
.bind(decimals_i64)
.bind(dto.token_program.clone())
.bind(dto.status.to_i16())
.bind(dto.first_seen_at.to_rfc3339())
.bind(dto.last_seen_at.to_rfc3339())
.bind(dto.updated_at.to_rfc3339())
.execute(pool)
.await;
if let Err(error) = insert_result {
return Err(crate::KbError::Db(format!(
"cannot upsert kb_observed_tokens on sqlite: {}",
error
)));
}
let select_result = sqlx::query_scalar::<sqlx::Sqlite, i64>(
r#"
SELECT id
FROM kb_observed_tokens
WHERE mint = ?
LIMIT 1
"#,
)
.bind(dto.mint.clone())
.fetch_one(pool)
.await;
match select_result {
Ok(id) => Ok(id),
Err(error) => Err(crate::KbError::Db(format!(
"cannot fetch kb_observed_tokens id for mint '{}' on sqlite: {}",
dto.mint, error
))),
}
}
}
}
/// Reads one observed token by mint.
pub async fn get_observed_token_by_mint(
database: &crate::KbDatabase,
mint: &str,
) -> Result<std::option::Option<crate::KbObservedTokenDto>, crate::KbError> {
match database.connection() {
crate::KbDatabaseConnection::Sqlite(pool) => {
let query_result = sqlx::query_as::<sqlx::Sqlite, crate::KbObservedTokenEntity>(
r#"
SELECT
id,
mint,
symbol,
name,
decimals,
token_program,
status,
first_seen_at,
last_seen_at,
updated_at
FROM kb_observed_tokens
WHERE mint = ?
LIMIT 1
"#,
)
.bind(mint)
.fetch_optional(pool)
.await;
let entity_option = match query_result {
Ok(entity_option) => entity_option,
Err(error) => {
return Err(crate::KbError::Db(format!(
"cannot read observed token '{}' on sqlite: {}",
mint, error
)));
}
};
match entity_option {
Some(entity) => {
let dto_result = crate::KbObservedTokenDto::try_from(entity);
match dto_result {
Ok(dto) => Ok(Some(dto)),
Err(error) => Err(error),
}
}
None => Ok(None),
}
}
}
}
/// Lists observed tokens ordered by newest first.
pub async fn list_observed_tokens(
database: &crate::KbDatabase,
limit: u32,
) -> Result<std::vec::Vec<crate::KbObservedTokenDto>, crate::KbError> {
if limit == 0 {
return Ok(std::vec::Vec::new());
}
match database.connection() {
crate::KbDatabaseConnection::Sqlite(pool) => {
let query_result = sqlx::query_as::<sqlx::Sqlite, crate::KbObservedTokenEntity>(
r#"
SELECT
id,
mint,
symbol,
name,
decimals,
token_program,
status,
first_seen_at,
last_seen_at,
updated_at
FROM kb_observed_tokens
ORDER BY id DESC
LIMIT ?
"#,
)
.bind(i64::from(limit))
.fetch_all(pool)
.await;
let entities = match query_result {
Ok(entities) => entities,
Err(error) => {
return Err(crate::KbError::Db(format!(
"cannot list observed tokens on sqlite: {}",
error
)));
}
};
let mut dtos = std::vec::Vec::new();
for entity in entities {
let dto_result = crate::KbObservedTokenDto::try_from(entity);
let dto = match dto_result {
Ok(dto) => dto,
Err(error) => return Err(error),
};
dtos.push(dto);
}
Ok(dtos)
}
}
}
#[cfg(test)]
mod tests {
#[tokio::test]
async fn observed_token_roundtrip_works() {
let tempdir = tempfile::tempdir().expect("tempdir must succeed");
let database_path = tempdir.path().join("observed_token.sqlite3");
let config = crate::KbDatabaseConfig {
enabled: true,
backend: crate::KbDatabaseBackend::Sqlite,
sqlite: crate::KbSqliteDatabaseConfig {
path: database_path.to_string_lossy().to_string(),
create_if_missing: true,
busy_timeout_ms: 5000,
max_connections: 1,
auto_initialize_schema: true,
use_wal: true,
},
};
let database = crate::KbDatabase::connect_and_initialize(&config)
.await
.expect("database init must succeed");
let dto = crate::KbObservedTokenDto::new(
"So11111111111111111111111111111111111111112".to_string(),
Some("WSOL".to_string()),
Some("Wrapped SOL".to_string()),
Some(9),
crate::SPL_TOKEN_PROGRAM_ID.to_string(),
crate::KbObservedTokenStatus::Active,
);
let inserted_id = crate::upsert_observed_token(&database, &dto)
.await
.expect("upsert must succeed");
assert!(inserted_id > 0);
let fetched = crate::get_observed_token_by_mint(
&database,
"So11111111111111111111111111111111111111112",
)
.await
.expect("fetch must succeed");
assert!(fetched.is_some());
let fetched = fetched.expect("token must exist");
assert_eq!(fetched.symbol.as_deref(), Some("WSOL"));
assert_eq!(fetched.decimals, Some(9));
assert_eq!(fetched.status, crate::KbObservedTokenStatus::Active);
let listed = crate::list_observed_tokens(&database, 10)
.await
.expect("list must succeed");
assert_eq!(listed.len(), 1);
}
}

View File

@@ -3,7 +3,9 @@
//! Database schema initialization. //! Database schema initialization.
/// Ensures that the database schema exists. /// Ensures that the database schema exists.
pub(crate) async fn ensure_schema(database: &crate::KbDatabase) -> Result<(), crate::KbError> { pub(crate) async fn ensure_schema(
database: &crate::KbDatabase,
) -> Result<(), crate::KbError> {
match database.connection() { match database.connection() {
crate::KbDatabaseConnection::Sqlite(pool) => { crate::KbDatabaseConnection::Sqlite(pool) => {
let metadata_table_result = sqlx::query( let metadata_table_result = sqlx::query(
@@ -99,6 +101,58 @@ ON kb_db_runtime_events (created_at)
error error
))); )));
} }
let observed_tokens_result = sqlx::query(
r#"
CREATE TABLE IF NOT EXISTS kb_observed_tokens (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
mint TEXT NOT NULL UNIQUE,
symbol TEXT NULL,
name TEXT NULL,
decimals INTEGER NULL,
token_program TEXT NOT NULL,
status INTEGER NOT NULL,
first_seen_at TEXT NOT NULL,
last_seen_at TEXT NOT NULL,
updated_at TEXT NOT NULL
)
"#,
)
.execute(pool)
.await;
if let Err(error) = observed_tokens_result {
return Err(crate::KbError::Db(format!(
"cannot create table kb_observed_tokens on sqlite: {}",
error
)));
}
let observed_tokens_mint_index_result = sqlx::query(
r#"
CREATE UNIQUE INDEX IF NOT EXISTS kb_idx_observed_tokens_mint
ON kb_observed_tokens (mint)
"#,
)
.execute(pool)
.await;
if let Err(error) = observed_tokens_mint_index_result {
return Err(crate::KbError::Db(format!(
"cannot create index kb_idx_observed_tokens_mint on sqlite: {}",
error
)));
}
let observed_tokens_status_index_result = sqlx::query(
r#"
CREATE INDEX IF NOT EXISTS kb_idx_observed_tokens_status
ON kb_observed_tokens (status)
"#,
)
.execute(pool)
.await;
if let Err(error) = observed_tokens_status_index_result {
return Err(crate::KbError::Db(format!(
"cannot create index kb_idx_observed_tokens_status on sqlite: {}",
error
)));
}
let schema_version = crate::KbDbMetadataDto::new( let schema_version = crate::KbDbMetadataDto::new(
"schema_version".to_string(), "schema_version".to_string(),
env!("CARGO_PKG_VERSION").to_string(), env!("CARGO_PKG_VERSION").to_string(),
@@ -108,6 +162,6 @@ ON kb_db_runtime_events (created_at)
return Err(error); return Err(error);
} }
Ok(()) Ok(())
} },
} }
} }

View File

@@ -3,7 +3,9 @@
//! Database shared types. //! Database shared types.
mod database_backend; mod database_backend;
mod observed_token_status;
mod runtime_event_level; mod runtime_event_level;
pub use crate::db::types::database_backend::KbDatabaseBackend; pub use crate::db::types::database_backend::KbDatabaseBackend;
pub use crate::db::types::observed_token_status::KbObservedTokenStatus;
pub use crate::db::types::runtime_event_level::KbDbRuntimeEventLevel; pub use crate::db::types::runtime_event_level::KbDbRuntimeEventLevel;

View File

@@ -0,0 +1,42 @@
// file: kb_lib/src/db/types/observed_token_status.rs
//! Observed token status.
/// Local status of one observed token.
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum KbObservedTokenStatus {
/// Newly discovered token.
New,
/// Token currently tracked.
Active,
/// Token ignored by local filtering rules.
Ignored,
/// Token marked as suspicious or blocked.
Blocked,
}
impl KbObservedTokenStatus {
/// Converts the status to its stable integer representation.
pub fn to_i16(self) -> i16 {
match self {
Self::New => 0,
Self::Active => 1,
Self::Ignored => 2,
Self::Blocked => 3,
}
}
/// Restores a status from its stable integer representation.
pub fn from_i16(value: i16) -> Result<Self, crate::KbError> {
match value {
0 => Ok(Self::New),
1 => Ok(Self::Active),
2 => Ok(Self::Ignored),
3 => Ok(Self::Blocked),
_ => Err(crate::KbError::Db(format!(
"invalid KbObservedTokenStatus value: {}",
value
))),
}
}
}

View File

@@ -78,6 +78,12 @@ pub use crate::db::KbKnownHttpEndpointDto;
pub use crate::db::KbKnownHttpEndpointEntity; pub use crate::db::KbKnownHttpEndpointEntity;
pub use crate::db::KbKnownWsEndpointDto; pub use crate::db::KbKnownWsEndpointDto;
pub use crate::db::KbKnownWsEndpointEntity; pub use crate::db::KbKnownWsEndpointEntity;
pub use crate::db::KbObservedTokenDto;
pub use crate::db::KbObservedTokenEntity;
pub use crate::db::KbObservedTokenStatus;
pub use crate::db::get_observed_token_by_mint;
pub use crate::db::list_observed_tokens;
pub use crate::db::upsert_observed_token;
pub use crate::db::get_known_http_endpoint; pub use crate::db::get_known_http_endpoint;
pub use crate::db::get_known_ws_endpoint; pub use crate::db::get_known_ws_endpoint;
pub use crate::db::insert_db_runtime_event; pub use crate::db::insert_db_runtime_event;