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.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.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]
version = "0.5.1"
version = "0.5.2"
edition = "2024"
license = "MIT"
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.1. Version `0.0.2` — Socle conforme
Objectif : corriger le squelette et poser la base de travail.
Réalisé :
@@ -157,7 +156,6 @@ Réalisé :
- UI Tauri minimale.
### 6.2. Version `0.1.x` — Transport WebSocket générique
Objectif : construire un vrai `WsClient` asynchrone clonable.
Réalisé :
@@ -172,7 +170,6 @@ Réalisé :
- tests offline avec serveur mock.
### 6.3. Version `0.1.1` — Intégration Tauri minimale du `WsClient`
Objectif : valider le transport via lapplication desktop.
Réalisé :
@@ -183,7 +180,6 @@ Réalisé :
- validation du flux `frontend -> tauri -> kb_lib -> frontend`.
### 6.4. Version `0.2.0` — Couche JSON-RPC WS Solana
Objectif : séparer clairement transport, réponses RPC et notifications.
Réalisé :
@@ -195,7 +191,6 @@ Réalisé :
- premiers helpers JSON-RPC sur `WsClient`.
### 6.5. Version `0.3.0` — Registre subscriptions / notifications
Objectif : fiabiliser la gestion des subscriptions.
Réalisé :
@@ -208,7 +203,6 @@ Réalisé :
- routage séparé des notifications.
### 6.6. Version `0.3.1` — Helpers subscribe/unsubscribe WebSocket
Objectif : ajouter les helpers haut niveau correspondant aux principales méthodes PubSub Solana.
Réalisé :
@@ -218,7 +212,6 @@ Réalisé :
- premiers tests de validation des noms de méthodes.
### 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.
Réalisé :
@@ -228,7 +221,6 @@ Réalisé :
- base de travail pour réduire lusage direct de `serde_json::Value`.
### 6.8. Version `0.3.3` — Distinction API typed / raw
Objectif : clarifier lAPI publique de `WsClient`.
Réalisé :
@@ -238,7 +230,6 @@ Réalisé :
- préparation dune hiérarchie API plus explicite.
### 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.
Réalisé :
@@ -251,7 +242,6 @@ Réalisé :
- premiers tests réels sur `wss://api.mainnet.solana.com`.
### 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.
Réalisé :
@@ -269,8 +259,7 @@ Réalisé :
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é :
- client `reqwest` asynchrone clonable,
@@ -291,8 +280,7 @@ Livrables :
- `getVersion`
- `getSlot`
### 0.4.1 — Helpers HTTP Solana
### 6.13. Version `0.4.1` — Helpers HTTP Solana
Réalisé :
- 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,
- 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é :
- 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`,
- 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é :
- ajouter un pool d`HttpClient`,
@@ -320,8 +306,7 @@ Réalisé :
- prendre en compte la classe de méthode HTTP,
- 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é :
- ajout dune fenêtre `Demo Http`,
@@ -332,11 +317,10 @@ Réalisé :
- alignement visuel de la fenêtre sur le gabarit `Demo Ws`,
- 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.
#### 0.5.0 — Socle SQLite
### 6.18. Version `0.5.0` — Socle SQLite
Réalisé :
- configuration DB dans `config.json`,
@@ -346,29 +330,32 @@ Réalisé :
- table `kb_db_metadata`,
- séparation `db/entities`, `db/dtos`, `db/queries`, `db/types`.
#### 0.5.1 — Premières tables métier de stockage local
À faire :
### 6.19. Version `0.5.1` — Premières tables métier de stockage local
Réalisé :
- ajouter les tables de référence pour les endpoints connus,
- ajouter les tables techniques pour les événements runtime locaux,
- poser les `entities`, `dtos`, `queries` et `types` associés,
- préparer le stockage local des endpoints HTTP/WS connus et de leur état utile.
- ajout des tables de référence pour les endpoints connus HTTP/WS,
- ajout des tables techniques pour les événements runtime locaux,
- mise en place des `entities`, `dtos`, `queries` et `types` associés,
- préparation du stockage local des endpoints HTTP/WS connus et de leur état utile.
#### 0.5.2 — Stockage des tokens observés
À faire :
### 6.20. Version `0.5.2` — Stockage des tokens observés
Réalisé :
- ajouter les premières tables liées aux tokens observés,
- préparer le stockage minimal des mints, symboles, statuts et dates de découverte,
- préparer les relations futures avec pools, paires et événements on-chain.
- ajout de la table `kb_observed_tokens`,
- stockage minimal des mints, symboles, noms, statuts et dates dobservation,
- 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 :
- stocker les événements techniques importants remontés par les connecteurs,
- 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 :
- 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,
- 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.
À faire :
@@ -387,8 +373,7 @@ Objectif : commencer la détection utile pour lapplication.
- débuts de normalisation dévénements,
- 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.
Cibles initiales possibles :
@@ -407,8 +392,7 @@ Cibles initiales possibles :
- création de types métiers propres,
- 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.
À faire :
@@ -419,8 +403,7 @@ Objectif : transformer les événements bruts en signaux exploitables.
- statistiques de comportement,
- 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.
À faire :
@@ -431,8 +414,7 @@ Objectif : préparer la couche daction.
- préparation dordres et de swaps,
- 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.
À faire :
@@ -443,8 +425,7 @@ Objectif : brancher lanalyse à laction tout en gardant des garde-fous exp
- confirmations explicites ou semi-automatiques,
- 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é.
À faire :
@@ -457,7 +438,6 @@ Objectif : ajouter le connecteur gRPC dédié.
## 7. Organisation des modules ciblés
### 7.1. `kb_lib`
Modules cibles à court terme :
- `error.rs`
@@ -472,7 +452,6 @@ Modules cibles à court terme :
- `rpc_ws_solana.rs`
### 7.2. `kb_app`
Responsabilités cibles :
- lancement Tauri,
@@ -483,7 +462,6 @@ Responsabilités cibles :
- fenêtres de démonstration / diagnostic isolées.
## 8. Ligne de conduite sur le `WsClient`
Le `WsClient` doit être conçu en plusieurs couches :
1. transport brut WebSocket,
@@ -500,7 +478,6 @@ Cette séparation évite de mélanger :
- les notifications push.
## 9. Politique initiale de reconnexion
Au départ :
- 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.
## 10. Politique initiale de fermeture
À la fermeture dun `WsClient` :
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.
## 11. Documentation et livrables de référence
Le projet doit maintenir au minimum :
- 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.
## 12. Priorité immédiate
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,
2. ajouter les tables locales pour les endpoints connus HTTP/WS,
3. ajouter les tables locales pour les événements runtime techniques,
4. structurer ces tables via `db/entities`, `db/dtos`, `db/queries` et `db/types`,
1. démarrer la version `0.5.3` avec les événements et signaux locaux,
2. stocker les événements techniques importants remontés par les connecteurs,
3. distinguer clairement événements runtime, observations on-chain et événements métier,
4. préparer la conservation locale des signaux utiles à lanalyse,
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::KbKnownHttpEndpointDto;
pub use crate::db::dtos::KbKnownWsEndpointDto;
pub use crate::db::dtos::KbObservedTokenDto;
pub use crate::db::entities::KbDbMetadataEntity;
pub use crate::db::entities::KbDbRuntimeEventEntity;
pub use crate::db::entities::KbKnownHttpEndpointEntity;
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_known_http_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::list_db_metadata;
pub use crate::db::queries::list_known_http_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::upsert_db_metadata;
pub use crate::db::queries::upsert_known_http_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::KbDbRuntimeEventLevel;
pub use crate::db::types::KbObservedTokenStatus;

View File

@@ -6,8 +6,10 @@ mod db_metadata;
mod db_runtime_event;
mod known_http_endpoint;
mod known_ws_endpoint;
mod observed_token;
pub use crate::db::dtos::db_metadata::KbDbMetadataDto;
pub use crate::db::dtos::db_runtime_event::KbDbRuntimeEventDto;
pub use crate::db::dtos::known_http_endpoint::KbKnownHttpEndpointDto;
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 known_http_endpoint;
mod known_ws_endpoint;
mod observed_token;
pub use crate::db::entities::db_metadata::KbDbMetadataEntity;
pub use crate::db::entities::db_runtime_event::KbDbRuntimeEventEntity;
pub use crate::db::entities::known_http_endpoint::KbKnownHttpEndpointEntity;
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 known_http_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::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::list_known_ws_endpoints;
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.
/// 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() {
crate::KbDatabaseConnection::Sqlite(pool) => {
let metadata_table_result = sqlx::query(
@@ -99,6 +101,58 @@ ON kb_db_runtime_events (created_at)
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(
"schema_version".to_string(),
env!("CARGO_PKG_VERSION").to_string(),
@@ -108,6 +162,6 @@ ON kb_db_runtime_events (created_at)
return Err(error);
}
Ok(())
}
},
}
}

View File

@@ -3,7 +3,9 @@
//! Database shared types.
mod database_backend;
mod observed_token_status;
mod runtime_event_level;
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;

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::KbKnownWsEndpointDto;
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_ws_endpoint;
pub use crate::db::insert_db_runtime_event;