From 54778373b8454094bc39e5fb01684f22f60f59fb Mon Sep 17 00:00:00 2001 From: SinuS Von SifriduS Date: Fri, 24 Apr 2026 08:53:40 +0200 Subject: [PATCH] 0.5.4 --- CHANGELOG.md | 41 +++-- Cargo.toml | 2 +- ROADMAP.md | 4 +- kb_lib/src/db.rs | 23 +++ kb_lib/src/db/dtos.rs | 12 ++ kb_lib/src/db/dtos/dex.rs | 84 +++++++++ kb_lib/src/db/dtos/pair.rs | 84 +++++++++ kb_lib/src/db/dtos/pool.rs | 89 +++++++++ kb_lib/src/db/dtos/pool_listing.rs | 104 +++++++++++ kb_lib/src/db/dtos/pool_token.rs | 89 +++++++++ kb_lib/src/db/dtos/token.rs | 104 +++++++++++ kb_lib/src/db/entities.rs | 12 ++ kb_lib/src/db/entities/dex.rs | 24 +++ kb_lib/src/db/entities/pair.rs | 24 +++ kb_lib/src/db/entities/pool.rs | 22 +++ kb_lib/src/db/entities/pool_listing.rs | 30 +++ kb_lib/src/db/entities/pool_token.rs | 24 +++ kb_lib/src/db/entities/token.rs | 26 +++ kb_lib/src/db/queries.rs | 14 ++ kb_lib/src/db/queries/dex.rs | 113 ++++++++++++ kb_lib/src/db/queries/pair.rs | 68 +++++++ kb_lib/src/db/queries/pool.rs | 64 +++++++ kb_lib/src/db/queries/pool_listing.rs | 211 +++++++++++++++++++++ kb_lib/src/db/queries/pool_token.rs | 67 +++++++ kb_lib/src/db/queries/token.rs | 121 ++++++++++++ kb_lib/src/db/schema.rs | 243 +++++++++++-------------- kb_lib/src/db/types.rs | 6 + kb_lib/src/db/types/pool_kind.rs | 49 +++++ kb_lib/src/db/types/pool_status.rs | 46 +++++ kb_lib/src/db/types/pool_token_role.rs | 46 +++++ kb_lib/src/lib.rs | 24 ++- 31 files changed, 1706 insertions(+), 164 deletions(-) create mode 100644 kb_lib/src/db/dtos/dex.rs create mode 100644 kb_lib/src/db/dtos/pair.rs create mode 100644 kb_lib/src/db/dtos/pool.rs create mode 100644 kb_lib/src/db/dtos/pool_listing.rs create mode 100644 kb_lib/src/db/dtos/pool_token.rs create mode 100644 kb_lib/src/db/dtos/token.rs create mode 100644 kb_lib/src/db/entities/dex.rs create mode 100644 kb_lib/src/db/entities/pair.rs create mode 100644 kb_lib/src/db/entities/pool.rs create mode 100644 kb_lib/src/db/entities/pool_listing.rs create mode 100644 kb_lib/src/db/entities/pool_token.rs create mode 100644 kb_lib/src/db/entities/token.rs create mode 100644 kb_lib/src/db/queries/dex.rs create mode 100644 kb_lib/src/db/queries/pair.rs create mode 100644 kb_lib/src/db/queries/pool.rs create mode 100644 kb_lib/src/db/queries/pool_listing.rs create mode 100644 kb_lib/src/db/queries/pool_token.rs create mode 100644 kb_lib/src/db/queries/token.rs create mode 100644 kb_lib/src/db/types/pool_kind.rs create mode 100644 kb_lib/src/db/types/pool_status.rs create mode 100644 kb_lib/src/db/types/pool_token_role.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index cc61aa1..8fce33b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,22 +1,23 @@ -0.0.1 - initial skel -0.0.2 - Socle conforme -0.1.0 - Transport WebSocket générique -0.1.1 - Intégration Tauri minimale du WsClient -0.2.0 - Couche JSON-RPC WS Solana -0.3.0 - Registre subscriptions / notifications -0.3.1 - Ajout des helpers subscribe/unsubscribe à WsClient -0.3.2 - Ajout des helpers typed et du parsing typed basé sur solana-rpc-client-api -0.3.3 - Ajout du suffixe _raw aux helpers raw pour distinguer typed et raw -0.3.4 - Ajout de la fenêtre Demo Ws dans kb_app pour tester les souscriptions live -0.3.5 - Stabilisation de Demo Ws, lecture correcte des endpoints activés depuis la config, limitation/throttling de l’affichage UI sous fort débit -0.4.0 - Socle HttpClient générique async clonable, JSON-RPC HTTP 2.0, résolution d’URL avec api_key_env_var, limiteur local req/sec + burst, helpers initiaux getHealth/getVersion/getSlot -0.4.1 - Ajout des premiers helpers HTTP Solana haut niveau, dans la continuité de l’API du client WebSocket -0.4.2 - Préparation de la politique HTTP avancée : états de pause avant envoi, quotas par famille de méthodes et futur pool d’endpoints -0.4.3 - Pool d’endpoints 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 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 -0.5.3 - Préparation du stockage local des événements techniques et des signaux utiles à l’analyse, avec distinction runtime / on-chain / métier +0.0.1 - initial skel +0.0.2 - Socle conforme +0.1.0 - Transport WebSocket générique +0.1.1 - Intégration Tauri minimale du WsClient +0.2.0 - Couche JSON-RPC WS Solana +0.3.0 - Registre subscriptions / notifications +0.3.1 - Ajout des helpers subscribe/unsubscribe à WsClient +0.3.2 - Ajout des helpers typed et du parsing typed basé sur solana-rpc-client-api +0.3.3 - Ajout du suffixe _raw aux helpers raw pour distinguer typed et raw +0.3.4 - Ajout de la fenêtre Demo Ws dans kb_app pour tester les souscriptions live +0.3.5 - Stabilisation de Demo Ws, lecture correcte des endpoints activés depuis la config, limitation/throttling de l’affichage UI sous fort débit +0.4.0 - Socle HttpClient générique async clonable, JSON-RPC HTTP 2.0, résolution d’URL avec api_key_env_var, limiteur local req/sec + burst, helpers initiaux getHealth/getVersion/getSlot +0.4.1 - Ajout des premiers helpers HTTP Solana haut niveau, dans la continuité de l’API du client WebSocket +0.4.2 - Préparation de la politique HTTP avancée : états de pause avant envoi, quotas par famille de méthodes et futur pool d’endpoints +0.4.3 - Pool d’endpoints 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 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 +0.5.3 - Préparation du stockage local des événements techniques et des signaux utiles à l’analyse, avec distinction runtime / on-chain / métier +0.5.4 - Ajout du modèle métier normalisé initial pour les DEX, tokens, pools, paires, composition des pools et listings diff --git a/Cargo.toml b/Cargo.toml index 3bccac7..b5f19b9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ members = [ ] [workspace.package] -version = "0.5.3" +version = "0.5.4" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/khadhroony-bobobot" diff --git a/ROADMAP.md b/ROADMAP.md index 1ab3bad..aa535f1 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -357,9 +357,7 @@ Réalisé : - préparation de la traçabilité de provenance par type de source et endpoint, sans remettre en cause l’unicité locale d’un token par mint. ### 6.22. Version `0.5.4` — Modèle métier normalisé initial -Objectif : poser la couche relationnelle métier entre les observations brutes et la future détection technique. - -À faire : +Réalisé : - ajouter les tables de référence métier pour les DEX, tokens, pools et paires, - distinguer clairement objets de référence et événements d’activité, diff --git a/kb_lib/src/db.rs b/kb_lib/src/db.rs index 31f0275..0917a90 100644 --- a/kb_lib/src/db.rs +++ b/kb_lib/src/db.rs @@ -18,25 +18,39 @@ pub use crate::db::connection::KbDatabaseConnection; pub use crate::db::dtos::KbAnalysisSignalDto; pub use crate::db::dtos::KbDbMetadataDto; pub use crate::db::dtos::KbDbRuntimeEventDto; +pub use crate::db::dtos::KbDexDto; pub use crate::db::dtos::KbKnownHttpEndpointDto; pub use crate::db::dtos::KbKnownWsEndpointDto; pub use crate::db::dtos::KbObservedTokenDto; pub use crate::db::dtos::KbOnchainObservationDto; +pub use crate::db::dtos::KbPairDto; +pub use crate::db::dtos::KbPoolDto; +pub use crate::db::dtos::KbPoolListingDto; +pub use crate::db::dtos::KbPoolTokenDto; +pub use crate::db::dtos::KbTokenDto; pub use crate::db::entities::KbAnalysisSignalEntity; pub use crate::db::entities::KbDbMetadataEntity; pub use crate::db::entities::KbDbRuntimeEventEntity; +pub use crate::db::entities::KbDexEntity; pub use crate::db::entities::KbKnownHttpEndpointEntity; pub use crate::db::entities::KbKnownWsEndpointEntity; pub use crate::db::entities::KbObservedTokenEntity; pub use crate::db::entities::KbOnchainObservationEntity; +pub use crate::db::entities::KbPairEntity; +pub use crate::db::entities::KbPoolEntity; +pub use crate::db::entities::KbPoolListingEntity; +pub use crate::db::entities::KbPoolTokenEntity; +pub use crate::db::entities::KbTokenEntity; 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::get_token_by_mint; pub use crate::db::queries::insert_analysis_signal; pub use crate::db::queries::insert_db_runtime_event; pub use crate::db::queries::insert_onchain_observation; pub use crate::db::queries::list_db_metadata; +pub use crate::db::queries::list_dexes; 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; @@ -44,11 +58,20 @@ pub use crate::db::queries::list_recent_analysis_signals; pub use crate::db::queries::list_recent_db_runtime_events; pub use crate::db::queries::list_recent_onchain_observations; pub use crate::db::queries::upsert_db_metadata; +pub use crate::db::queries::upsert_dex; 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::queries::upsert_pair; +pub use crate::db::queries::upsert_pool; +pub use crate::db::queries::upsert_pool_listing; +pub use crate::db::queries::upsert_pool_token; +pub use crate::db::queries::upsert_token; pub use crate::db::types::KbAnalysisSignalSeverity; pub use crate::db::types::KbDatabaseBackend; pub use crate::db::types::KbDbRuntimeEventLevel; pub use crate::db::types::KbObservationSourceKind; pub use crate::db::types::KbObservedTokenStatus; +pub use crate::db::types::KbPoolKind; +pub use crate::db::types::KbPoolStatus; +pub use crate::db::types::KbPoolTokenRole; diff --git a/kb_lib/src/db/dtos.rs b/kb_lib/src/db/dtos.rs index b36f2bd..3ae4628 100644 --- a/kb_lib/src/db/dtos.rs +++ b/kb_lib/src/db/dtos.rs @@ -5,15 +5,27 @@ mod analysis_signal; mod db_metadata; mod db_runtime_event; +mod dex; mod known_http_endpoint; mod known_ws_endpoint; mod observed_token; mod onchain_observation; +mod pair; +mod pool; +mod pool_listing; +mod pool_token; +mod token; pub use crate::db::dtos::analysis_signal::KbAnalysisSignalDto; pub use crate::db::dtos::db_metadata::KbDbMetadataDto; pub use crate::db::dtos::db_runtime_event::KbDbRuntimeEventDto; +pub use crate::db::dtos::dex::KbDexDto; 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; pub use crate::db::dtos::onchain_observation::KbOnchainObservationDto; +pub use crate::db::dtos::pair::KbPairDto; +pub use crate::db::dtos::pool::KbPoolDto; +pub use crate::db::dtos::pool_listing::KbPoolListingDto; +pub use crate::db::dtos::pool_token::KbPoolTokenDto; +pub use crate::db::dtos::token::KbTokenDto; diff --git a/kb_lib/src/db/dtos/dex.rs b/kb_lib/src/db/dtos/dex.rs new file mode 100644 index 0000000..1aa2cb4 --- /dev/null +++ b/kb_lib/src/db/dtos/dex.rs @@ -0,0 +1,84 @@ +// file: kb_lib/src/db/dtos/dex.rs + +//! DEX DTO. + +/// Application-facing normalized DEX DTO. +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct KbDexDto { + /// Optional numeric primary key. + pub id: std::option::Option, + /// Stable short code. + pub code: std::string::String, + /// Display name. + pub name: std::string::String, + /// Optional primary program id. + pub program_id: std::option::Option, + /// Optional router program id. + pub router_program_id: std::option::Option, + /// Whether this DEX is enabled. + pub is_enabled: bool, + /// Creation timestamp. + pub created_at: chrono::DateTime, + /// Update timestamp. + pub updated_at: chrono::DateTime, +} + +impl KbDexDto { + /// Creates a new DEX DTO. + pub fn new( + code: std::string::String, + name: std::string::String, + program_id: std::option::Option, + router_program_id: std::option::Option, + is_enabled: bool, + ) -> Self { + let now = chrono::Utc::now(); + Self { + id: None, + code, + name, + program_id, + router_program_id, + is_enabled, + created_at: now, + updated_at: now, + } + } +} + +impl TryFrom for KbDexDto { + type Error = crate::KbError; + + fn try_from(entity: crate::KbDexEntity) -> Result { + let created_at_result = chrono::DateTime::parse_from_rfc3339(&entity.created_at); + let created_at = match created_at_result { + Ok(created_at) => created_at.with_timezone(&chrono::Utc), + Err(error) => { + return Err(crate::KbError::Db(format!( + "cannot parse dex created_at '{}': {}", + entity.created_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 dex updated_at '{}': {}", + entity.updated_at, error + ))); + } + }; + Ok(Self { + id: Some(entity.id), + code: entity.code, + name: entity.name, + program_id: entity.program_id, + router_program_id: entity.router_program_id, + is_enabled: entity.is_enabled != 0, + created_at, + updated_at, + }) + } +} diff --git a/kb_lib/src/db/dtos/pair.rs b/kb_lib/src/db/dtos/pair.rs new file mode 100644 index 0000000..2fd95b6 --- /dev/null +++ b/kb_lib/src/db/dtos/pair.rs @@ -0,0 +1,84 @@ +// file: kb_lib/src/db/dtos/pair.rs + +//! Normalized pair DTO. + +/// Application-facing normalized pair DTO. +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct KbPairDto { + /// Optional numeric primary key. + pub id: std::option::Option, + /// Related DEX id. + pub dex_id: i64, + /// Related pool id. + pub pool_id: i64, + /// Base token id. + pub base_token_id: i64, + /// Quote token id. + pub quote_token_id: i64, + /// Optional display symbol. + pub symbol: std::option::Option, + /// First seen timestamp. + pub first_seen_at: chrono::DateTime, + /// Update timestamp. + pub updated_at: chrono::DateTime, +} + +impl KbPairDto { + /// Creates a new pair DTO. + pub fn new( + dex_id: i64, + pool_id: i64, + base_token_id: i64, + quote_token_id: i64, + symbol: std::option::Option, + ) -> Self { + let now = chrono::Utc::now(); + Self { + id: None, + dex_id, + pool_id, + base_token_id, + quote_token_id, + symbol, + first_seen_at: now, + updated_at: now, + } + } +} + +impl TryFrom for KbPairDto { + type Error = crate::KbError; + + fn try_from(entity: crate::KbPairEntity) -> Result { + 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 pair first_seen_at '{}': {}", + entity.first_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 pair updated_at '{}': {}", + entity.updated_at, error + ))); + } + }; + Ok(Self { + id: Some(entity.id), + dex_id: entity.dex_id, + pool_id: entity.pool_id, + base_token_id: entity.base_token_id, + quote_token_id: entity.quote_token_id, + symbol: entity.symbol, + first_seen_at, + updated_at, + }) + } +} diff --git a/kb_lib/src/db/dtos/pool.rs b/kb_lib/src/db/dtos/pool.rs new file mode 100644 index 0000000..f5910df --- /dev/null +++ b/kb_lib/src/db/dtos/pool.rs @@ -0,0 +1,89 @@ +// file: kb_lib/src/db/dtos/pool.rs + +//! Normalized pool DTO. + +/// Application-facing normalized pool DTO. +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct KbPoolDto { + /// Optional numeric primary key. + pub id: std::option::Option, + /// Related DEX id. + pub dex_id: i64, + /// Pool address. + pub address: std::string::String, + /// Pool kind. + pub pool_kind: crate::KbPoolKind, + /// Pool status. + pub status: crate::KbPoolStatus, + /// First seen timestamp. + pub first_seen_at: chrono::DateTime, + /// Update timestamp. + pub updated_at: chrono::DateTime, +} + +impl KbPoolDto { + /// Creates a new pool DTO. + pub fn new( + dex_id: i64, + address: std::string::String, + pool_kind: crate::KbPoolKind, + status: crate::KbPoolStatus, + ) -> Self { + let now = chrono::Utc::now(); + Self { + id: None, + dex_id, + address, + pool_kind, + status, + first_seen_at: now, + updated_at: now, + } + } +} + +impl TryFrom for KbPoolDto { + type Error = crate::KbError; + + fn try_from(entity: crate::KbPoolEntity) -> Result { + let pool_kind_result = crate::KbPoolKind::from_i16(entity.pool_kind); + let pool_kind = match pool_kind_result { + Ok(pool_kind) => pool_kind, + Err(error) => return Err(error), + }; + let status_result = crate::KbPoolStatus::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 pool first_seen_at '{}': {}", + entity.first_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 pool updated_at '{}': {}", + entity.updated_at, error + ))); + } + }; + Ok(Self { + id: Some(entity.id), + dex_id: entity.dex_id, + address: entity.address, + pool_kind, + status, + first_seen_at, + updated_at, + }) + } +} diff --git a/kb_lib/src/db/dtos/pool_listing.rs b/kb_lib/src/db/dtos/pool_listing.rs new file mode 100644 index 0000000..095574b --- /dev/null +++ b/kb_lib/src/db/dtos/pool_listing.rs @@ -0,0 +1,104 @@ +// file: kb_lib/src/db/dtos/pool_listing.rs + +//! Pool listing DTO. + +/// Application-facing normalized pool listing DTO. +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct KbPoolListingDto { + /// Optional numeric primary key. + pub id: std::option::Option, + /// Related DEX id. + pub dex_id: i64, + /// Related pool id. + pub pool_id: i64, + /// Optional related pair id. + pub pair_id: std::option::Option, + /// Discovery source family. + pub source_kind: crate::KbObservationSourceKind, + /// Optional source endpoint logical name. + pub source_endpoint_name: std::option::Option, + /// Detection timestamp. + pub detected_at: chrono::DateTime, + /// Optional initial base reserve estimate. + pub initial_base_reserve: std::option::Option, + /// Optional initial quote reserve estimate. + pub initial_quote_reserve: std::option::Option, + /// Optional initial price estimate in quote units. + pub initial_price_quote: std::option::Option, + /// Update timestamp. + pub updated_at: chrono::DateTime, +} + +impl KbPoolListingDto { + /// Creates a new pool listing DTO. + pub fn new( + dex_id: i64, + pool_id: i64, + pair_id: std::option::Option, + source_kind: crate::KbObservationSourceKind, + source_endpoint_name: std::option::Option, + initial_base_reserve: std::option::Option, + initial_quote_reserve: std::option::Option, + initial_price_quote: std::option::Option, + ) -> Self { + let now = chrono::Utc::now(); + Self { + id: None, + dex_id, + pool_id, + pair_id, + source_kind, + source_endpoint_name, + detected_at: now, + initial_base_reserve, + initial_quote_reserve, + initial_price_quote, + updated_at: now, + } + } +} + +impl TryFrom for KbPoolListingDto { + type Error = crate::KbError; + + fn try_from(entity: crate::KbPoolListingEntity) -> Result { + let source_kind_result = crate::KbObservationSourceKind::from_i16(entity.source_kind); + let source_kind = match source_kind_result { + Ok(source_kind) => source_kind, + Err(error) => return Err(error), + }; + let detected_at_result = chrono::DateTime::parse_from_rfc3339(&entity.detected_at); + let detected_at = match detected_at_result { + Ok(detected_at) => detected_at.with_timezone(&chrono::Utc), + Err(error) => { + return Err(crate::KbError::Db(format!( + "cannot parse pool_listing detected_at '{}': {}", + entity.detected_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 pool_listing updated_at '{}': {}", + entity.updated_at, error + ))); + } + }; + Ok(Self { + id: Some(entity.id), + dex_id: entity.dex_id, + pool_id: entity.pool_id, + pair_id: entity.pair_id, + source_kind, + source_endpoint_name: entity.source_endpoint_name, + detected_at, + initial_base_reserve: entity.initial_base_reserve, + initial_quote_reserve: entity.initial_quote_reserve, + initial_price_quote: entity.initial_price_quote, + updated_at, + }) + } +} diff --git a/kb_lib/src/db/dtos/pool_token.rs b/kb_lib/src/db/dtos/pool_token.rs new file mode 100644 index 0000000..3a0030c --- /dev/null +++ b/kb_lib/src/db/dtos/pool_token.rs @@ -0,0 +1,89 @@ +// file: kb_lib/src/db/dtos/pool_token.rs + +//! Pool token DTO. + +/// Application-facing normalized pool token DTO. +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct KbPoolTokenDto { + /// Optional numeric primary key. + pub id: std::option::Option, + /// Related pool id. + pub pool_id: i64, + /// Related token id. + pub token_id: i64, + /// Token role. + pub role: crate::KbPoolTokenRole, + /// Optional vault address. + pub vault_address: std::option::Option, + /// Optional token order inside the pool. + pub token_order: std::option::Option, + /// Creation timestamp. + pub created_at: chrono::DateTime, + /// Update timestamp. + pub updated_at: chrono::DateTime, +} + +impl KbPoolTokenDto { + /// Creates a new pool token DTO. + pub fn new( + pool_id: i64, + token_id: i64, + role: crate::KbPoolTokenRole, + vault_address: std::option::Option, + token_order: std::option::Option, + ) -> Self { + let now = chrono::Utc::now(); + Self { + id: None, + pool_id, + token_id, + role, + vault_address, + token_order, + created_at: now, + updated_at: now, + } + } +} + +impl TryFrom for KbPoolTokenDto { + type Error = crate::KbError; + + fn try_from(entity: crate::KbPoolTokenEntity) -> Result { + let role_result = crate::KbPoolTokenRole::from_i16(entity.role); + let role = match role_result { + Ok(role) => role, + Err(error) => return Err(error), + }; + let created_at_result = chrono::DateTime::parse_from_rfc3339(&entity.created_at); + let created_at = match created_at_result { + Ok(created_at) => created_at.with_timezone(&chrono::Utc), + Err(error) => { + return Err(crate::KbError::Db(format!( + "cannot parse pool_token created_at '{}': {}", + entity.created_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 pool_token updated_at '{}': {}", + entity.updated_at, error + ))); + } + }; + Ok(Self { + id: Some(entity.id), + pool_id: entity.pool_id, + token_id: entity.token_id, + role, + vault_address: entity.vault_address, + token_order: entity.token_order, + created_at, + updated_at, + }) + } +} diff --git a/kb_lib/src/db/dtos/token.rs b/kb_lib/src/db/dtos/token.rs new file mode 100644 index 0000000..23fc3e4 --- /dev/null +++ b/kb_lib/src/db/dtos/token.rs @@ -0,0 +1,104 @@ +// file: kb_lib/src/db/dtos/token.rs + +//! Normalized token DTO. + +/// Application-facing normalized token DTO. +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct KbTokenDto { + /// Optional numeric primary key. + pub id: std::option::Option, + /// Mint address. + pub mint: std::string::String, + /// Optional token symbol. + pub symbol: std::option::Option, + /// Optional token display name. + pub name: std::option::Option, + /// Optional decimals value. + pub decimals: std::option::Option, + /// Token program id. + pub token_program: std::string::String, + /// Whether this token is typically used as quote token. + pub is_quote_token: bool, + /// First seen timestamp. + pub first_seen_at: chrono::DateTime, + /// Update timestamp. + pub updated_at: chrono::DateTime, +} + +impl KbTokenDto { + /// Creates a new token DTO. + pub fn new( + mint: std::string::String, + symbol: std::option::Option, + name: std::option::Option, + decimals: std::option::Option, + token_program: std::string::String, + is_quote_token: bool, + ) -> Self { + let now = chrono::Utc::now(); + Self { + id: None, + mint, + symbol, + name, + decimals, + token_program, + is_quote_token, + first_seen_at: now, + updated_at: now, + } + } +} + +impl TryFrom for KbTokenDto { + type Error = crate::KbError; + + fn try_from(entity: crate::KbTokenEntity) -> Result { + 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 token first_seen_at '{}': {}", + entity.first_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 token updated_at '{}': {}", + entity.updated_at, error + ))); + } + }; + let decimals = match entity.decimals { + Some(decimals) => { + let decimals_result = u8::try_from(decimals); + match decimals_result { + Ok(decimals) => Some(decimals), + Err(error) => { + return Err(crate::KbError::Db(format!( + "cannot convert 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, + is_quote_token: entity.is_quote_token != 0, + first_seen_at, + updated_at, + }) + } +} diff --git a/kb_lib/src/db/entities.rs b/kb_lib/src/db/entities.rs index 924b19c..0cd5ee5 100644 --- a/kb_lib/src/db/entities.rs +++ b/kb_lib/src/db/entities.rs @@ -7,15 +7,27 @@ mod analysis_signal; mod db_metadata; mod db_runtime_event; +mod dex; mod known_http_endpoint; mod known_ws_endpoint; mod observed_token; mod onchain_observation; +mod pair; +mod pool; +mod pool_listing; +mod pool_token; +mod token; pub use crate::db::entities::analysis_signal::KbAnalysisSignalEntity; pub use crate::db::entities::db_metadata::KbDbMetadataEntity; pub use crate::db::entities::db_runtime_event::KbDbRuntimeEventEntity; +pub use crate::db::entities::dex::KbDexEntity; 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; pub use crate::db::entities::onchain_observation::KbOnchainObservationEntity; +pub use crate::db::entities::pair::KbPairEntity; +pub use crate::db::entities::pool::KbPoolEntity; +pub use crate::db::entities::pool_listing::KbPoolListingEntity; +pub use crate::db::entities::pool_token::KbPoolTokenEntity; +pub use crate::db::entities::token::KbTokenEntity; diff --git a/kb_lib/src/db/entities/dex.rs b/kb_lib/src/db/entities/dex.rs new file mode 100644 index 0000000..9c312e6 --- /dev/null +++ b/kb_lib/src/db/entities/dex.rs @@ -0,0 +1,24 @@ +// file: kb_lib/src/db/entities/dex.rs + +//! DEX entity. + +/// Persisted normalized DEX row. +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, sqlx::FromRow)] +pub struct KbDexEntity { + /// Numeric primary key. + pub id: i64, + /// Stable short code. + pub code: std::string::String, + /// Display name. + pub name: std::string::String, + /// Optional primary program id. + pub program_id: std::option::Option, + /// Optional router program id. + pub router_program_id: std::option::Option, + /// Whether this DEX is enabled. + pub is_enabled: i64, + /// Creation timestamp encoded as RFC3339 UTC text. + pub created_at: std::string::String, + /// Update timestamp encoded as RFC3339 UTC text. + pub updated_at: std::string::String, +} diff --git a/kb_lib/src/db/entities/pair.rs b/kb_lib/src/db/entities/pair.rs new file mode 100644 index 0000000..a86e559 --- /dev/null +++ b/kb_lib/src/db/entities/pair.rs @@ -0,0 +1,24 @@ +// file: kb_lib/src/db/entities/pair.rs + +//! Normalized pair entity. + +/// Persisted normalized pair row. +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, sqlx::FromRow)] +pub struct KbPairEntity { + /// Numeric primary key. + pub id: i64, + /// Related DEX id. + pub dex_id: i64, + /// Related pool id. + pub pool_id: i64, + /// Base token id. + pub base_token_id: i64, + /// Quote token id. + pub quote_token_id: i64, + /// Optional display symbol. + pub symbol: std::option::Option, + /// First seen timestamp encoded as RFC3339 UTC text. + pub first_seen_at: std::string::String, + /// Update timestamp encoded as RFC3339 UTC text. + pub updated_at: std::string::String, +} diff --git a/kb_lib/src/db/entities/pool.rs b/kb_lib/src/db/entities/pool.rs new file mode 100644 index 0000000..ef91f7b --- /dev/null +++ b/kb_lib/src/db/entities/pool.rs @@ -0,0 +1,22 @@ +// file: kb_lib/src/db/entities/pool.rs + +//! Normalized pool entity. + +/// Persisted normalized pool row. +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, sqlx::FromRow)] +pub struct KbPoolEntity { + /// Numeric primary key. + pub id: i64, + /// Related DEX id. + pub dex_id: i64, + /// Pool address. + pub address: std::string::String, + /// Pool kind stored as stable integer. + pub pool_kind: i16, + /// Pool status stored as stable integer. + pub status: i16, + /// First seen timestamp encoded as RFC3339 UTC text. + pub first_seen_at: std::string::String, + /// Update timestamp encoded as RFC3339 UTC text. + pub updated_at: std::string::String, +} diff --git a/kb_lib/src/db/entities/pool_listing.rs b/kb_lib/src/db/entities/pool_listing.rs new file mode 100644 index 0000000..1c76a40 --- /dev/null +++ b/kb_lib/src/db/entities/pool_listing.rs @@ -0,0 +1,30 @@ +// file: kb_lib/src/db/entities/pool_listing.rs + +//! Pool listing entity. + +/// Persisted normalized pool listing row. +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, sqlx::FromRow)] +pub struct KbPoolListingEntity { + /// Numeric primary key. + pub id: i64, + /// Related DEX id. + pub dex_id: i64, + /// Related pool id. + pub pool_id: i64, + /// Optional related pair id. + pub pair_id: std::option::Option, + /// Discovery source family stored as stable integer. + pub source_kind: i16, + /// Optional source endpoint logical name. + pub source_endpoint_name: std::option::Option, + /// Detection timestamp encoded as RFC3339 UTC text. + pub detected_at: std::string::String, + /// Optional initial base reserve estimate. + pub initial_base_reserve: std::option::Option, + /// Optional initial quote reserve estimate. + pub initial_quote_reserve: std::option::Option, + /// Optional initial price estimate in quote units. + pub initial_price_quote: std::option::Option, + /// Update timestamp encoded as RFC3339 UTC text. + pub updated_at: std::string::String, +} diff --git a/kb_lib/src/db/entities/pool_token.rs b/kb_lib/src/db/entities/pool_token.rs new file mode 100644 index 0000000..fbe78ae --- /dev/null +++ b/kb_lib/src/db/entities/pool_token.rs @@ -0,0 +1,24 @@ +// file: kb_lib/src/db/entities/pool_token.rs + +//! Pool token composition entity. + +/// Persisted normalized pool token composition row. +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, sqlx::FromRow)] +pub struct KbPoolTokenEntity { + /// Numeric primary key. + pub id: i64, + /// Related pool id. + pub pool_id: i64, + /// Related token id. + pub token_id: i64, + /// Token role stored as stable integer. + pub role: i16, + /// Optional vault address. + pub vault_address: std::option::Option, + /// Optional token order inside the pool. + pub token_order: std::option::Option, + /// Creation timestamp encoded as RFC3339 UTC text. + pub created_at: std::string::String, + /// Update timestamp encoded as RFC3339 UTC text. + pub updated_at: std::string::String, +} diff --git a/kb_lib/src/db/entities/token.rs b/kb_lib/src/db/entities/token.rs new file mode 100644 index 0000000..3e59787 --- /dev/null +++ b/kb_lib/src/db/entities/token.rs @@ -0,0 +1,26 @@ +// file: kb_lib/src/db/entities/token.rs + +//! Normalized token entity. + +/// Persisted normalized token row. +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, sqlx::FromRow)] +pub struct KbTokenEntity { + /// Numeric primary key. + pub id: i64, + /// Mint address. + pub mint: std::string::String, + /// Optional token symbol. + pub symbol: std::option::Option, + /// Optional token display name. + pub name: std::option::Option, + /// Optional decimals value. + pub decimals: std::option::Option, + /// Token program id. + pub token_program: std::string::String, + /// Whether this token is typically used as quote token. + pub is_quote_token: i64, + /// First seen timestamp encoded as RFC3339 UTC text. + pub first_seen_at: std::string::String, + /// Update timestamp encoded as RFC3339 UTC text. + pub updated_at: std::string::String, +} diff --git a/kb_lib/src/db/queries.rs b/kb_lib/src/db/queries.rs index f8271af..7f38005 100644 --- a/kb_lib/src/db/queries.rs +++ b/kb_lib/src/db/queries.rs @@ -5,10 +5,16 @@ mod analysis_signal; mod db_metadata; mod db_runtime_event; +mod dex; mod known_http_endpoint; mod known_ws_endpoint; mod observed_token; mod onchain_observation; +mod pair; +mod pool; +mod pool_listing; +mod pool_token; +mod token; pub use crate::db::queries::analysis_signal::insert_analysis_signal; pub use crate::db::queries::analysis_signal::list_recent_analysis_signals; @@ -17,6 +23,8 @@ pub use crate::db::queries::db_metadata::list_db_metadata; pub use crate::db::queries::db_metadata::upsert_db_metadata; pub use crate::db::queries::db_runtime_event::insert_db_runtime_event; pub use crate::db::queries::db_runtime_event::list_recent_db_runtime_events; +pub use crate::db::queries::dex::list_dexes; +pub use crate::db::queries::dex::upsert_dex; pub use crate::db::queries::known_http_endpoint::get_known_http_endpoint; pub use crate::db::queries::known_http_endpoint::list_known_http_endpoints; pub use crate::db::queries::known_http_endpoint::upsert_known_http_endpoint; @@ -28,3 +36,9 @@ pub use crate::db::queries::observed_token::list_observed_tokens; pub use crate::db::queries::observed_token::upsert_observed_token; pub use crate::db::queries::onchain_observation::insert_onchain_observation; pub use crate::db::queries::onchain_observation::list_recent_onchain_observations; +pub use crate::db::queries::pair::upsert_pair; +pub use crate::db::queries::pool::upsert_pool; +pub use crate::db::queries::pool_listing::upsert_pool_listing; +pub use crate::db::queries::pool_token::upsert_pool_token; +pub use crate::db::queries::token::get_token_by_mint; +pub use crate::db::queries::token::upsert_token; diff --git a/kb_lib/src/db/queries/dex.rs b/kb_lib/src/db/queries/dex.rs new file mode 100644 index 0000000..f73018d --- /dev/null +++ b/kb_lib/src/db/queries/dex.rs @@ -0,0 +1,113 @@ +// file: kb_lib/src/db/queries/dex.rs + +//! Queries for `kb_dexes`. + +/// Inserts or updates one normalized DEX row by code. +pub async fn upsert_dex( + database: &crate::KbDatabase, + dto: &crate::KbDexDto, +) -> Result { + match database.connection() { + crate::KbDatabaseConnection::Sqlite(pool) => { + let query_result = sqlx::query( + r#" +INSERT INTO kb_dexes ( + code, + name, + program_id, + router_program_id, + is_enabled, + created_at, + updated_at +) +VALUES (?, ?, ?, ?, ?, ?, ?) +ON CONFLICT(code) DO UPDATE SET + name = excluded.name, + program_id = excluded.program_id, + router_program_id = excluded.router_program_id, + is_enabled = excluded.is_enabled, + updated_at = excluded.updated_at + "#, + ) + .bind(dto.code.clone()) + .bind(dto.name.clone()) + .bind(dto.program_id.clone()) + .bind(dto.router_program_id.clone()) + .bind(if dto.is_enabled { 1_i64 } else { 0_i64 }) + .bind(dto.created_at.to_rfc3339()) + .bind(dto.updated_at.to_rfc3339()) + .execute(pool) + .await; + if let Err(error) = query_result { + return Err(crate::KbError::Db(format!( + "cannot upsert kb_dexes on sqlite: {}", + error + ))); + } + let id_result = sqlx::query_scalar::( + r#" +SELECT id +FROM kb_dexes +WHERE code = ? +LIMIT 1 + "#, + ) + .bind(dto.code.clone()) + .fetch_one(pool) + .await; + match id_result { + Ok(id) => Ok(id), + Err(error) => Err(crate::KbError::Db(format!( + "cannot fetch kb_dexes id for code '{}' on sqlite: {}", + dto.code, error + ))), + } + } + } +} + +/// Lists normalized DEX rows. +pub async fn list_dexes( + database: &crate::KbDatabase, +) -> Result, crate::KbError> { + match database.connection() { + crate::KbDatabaseConnection::Sqlite(pool) => { + let query_result = sqlx::query_as::( + r#" +SELECT + id, + code, + name, + program_id, + router_program_id, + is_enabled, + created_at, + updated_at +FROM kb_dexes +ORDER BY code ASC + "#, + ) + .fetch_all(pool) + .await; + let entities = match query_result { + Ok(entities) => entities, + Err(error) => { + return Err(crate::KbError::Db(format!( + "cannot list kb_dexes on sqlite: {}", + error + ))); + } + }; + let mut dtos = std::vec::Vec::new(); + for entity in entities { + let dto_result = crate::KbDexDto::try_from(entity); + let dto = match dto_result { + Ok(dto) => dto, + Err(error) => return Err(error), + }; + dtos.push(dto); + } + Ok(dtos) + } + } +} diff --git a/kb_lib/src/db/queries/pair.rs b/kb_lib/src/db/queries/pair.rs new file mode 100644 index 0000000..5931c46 --- /dev/null +++ b/kb_lib/src/db/queries/pair.rs @@ -0,0 +1,68 @@ +// file: kb_lib/src/db/queries/pair.rs + +//! Queries for `kb_pairs`. + +/// Inserts or updates one normalized pair row by pool id. +pub async fn upsert_pair( + database: &crate::KbDatabase, + dto: &crate::KbPairDto, +) -> Result { + match database.connection() { + crate::KbDatabaseConnection::Sqlite(pool) => { + let query_result = sqlx::query( + r#" +INSERT INTO kb_pairs ( + dex_id, + pool_id, + base_token_id, + quote_token_id, + symbol, + first_seen_at, + updated_at +) +VALUES (?, ?, ?, ?, ?, ?, ?) +ON CONFLICT(pool_id) DO UPDATE SET + dex_id = excluded.dex_id, + base_token_id = excluded.base_token_id, + quote_token_id = excluded.quote_token_id, + symbol = excluded.symbol, + updated_at = excluded.updated_at + "#, + ) + .bind(dto.dex_id) + .bind(dto.pool_id) + .bind(dto.base_token_id) + .bind(dto.quote_token_id) + .bind(dto.symbol.clone()) + .bind(dto.first_seen_at.to_rfc3339()) + .bind(dto.updated_at.to_rfc3339()) + .execute(pool) + .await; + if let Err(error) = query_result { + return Err(crate::KbError::Db(format!( + "cannot upsert kb_pairs on sqlite: {}", + error + ))); + } + let id_result = sqlx::query_scalar::( + r#" +SELECT id +FROM kb_pairs +WHERE pool_id = ? +LIMIT 1 + "#, + ) + .bind(dto.pool_id) + .fetch_one(pool) + .await; + match id_result { + Ok(id) => Ok(id), + Err(error) => Err(crate::KbError::Db(format!( + "cannot fetch kb_pairs id for pool_id '{}' on sqlite: {}", + dto.pool_id, + error + ))), + } + }, + } +} diff --git a/kb_lib/src/db/queries/pool.rs b/kb_lib/src/db/queries/pool.rs new file mode 100644 index 0000000..b06393b --- /dev/null +++ b/kb_lib/src/db/queries/pool.rs @@ -0,0 +1,64 @@ +// file: kb_lib/src/db/queries/pool.rs + +//! Queries for `kb_pools`. + +/// Inserts or updates one normalized pool row by address. +pub async fn upsert_pool( + database: &crate::KbDatabase, + dto: &crate::KbPoolDto, +) -> Result { + match database.connection() { + crate::KbDatabaseConnection::Sqlite(pool) => { + let query_result = sqlx::query( + r#" +INSERT INTO kb_pools ( + dex_id, + address, + pool_kind, + status, + first_seen_at, + updated_at +) +VALUES (?, ?, ?, ?, ?, ?) +ON CONFLICT(address) DO UPDATE SET + dex_id = excluded.dex_id, + pool_kind = excluded.pool_kind, + status = excluded.status, + updated_at = excluded.updated_at + "#, + ) + .bind(dto.dex_id) + .bind(dto.address.clone()) + .bind(dto.pool_kind.to_i16()) + .bind(dto.status.to_i16()) + .bind(dto.first_seen_at.to_rfc3339()) + .bind(dto.updated_at.to_rfc3339()) + .execute(pool) + .await; + if let Err(error) = query_result { + return Err(crate::KbError::Db(format!( + "cannot upsert kb_pools on sqlite: {}", + error + ))); + } + let id_result = sqlx::query_scalar::( + r#" +SELECT id +FROM kb_pools +WHERE address = ? +LIMIT 1 + "#, + ) + .bind(dto.address.clone()) + .fetch_one(pool) + .await; + match id_result { + Ok(id) => Ok(id), + Err(error) => Err(crate::KbError::Db(format!( + "cannot fetch kb_pools id for address '{}' on sqlite: {}", + dto.address, error + ))), + } + } + } +} diff --git a/kb_lib/src/db/queries/pool_listing.rs b/kb_lib/src/db/queries/pool_listing.rs new file mode 100644 index 0000000..ff49fc0 --- /dev/null +++ b/kb_lib/src/db/queries/pool_listing.rs @@ -0,0 +1,211 @@ +// file: kb_lib/src/db/queries/pool_listing.rs + +//! Queries for `kb_pool_listings`. + +/// Inserts or updates one normalized pool listing row by pool id. +pub async fn upsert_pool_listing( + database: &crate::KbDatabase, + dto: &crate::KbPoolListingDto, +) -> Result { + match database.connection() { + crate::KbDatabaseConnection::Sqlite(pool) => { + let query_result = sqlx::query( + r#" +INSERT INTO kb_pool_listings ( + dex_id, + pool_id, + pair_id, + source_kind, + source_endpoint_name, + detected_at, + initial_base_reserve, + initial_quote_reserve, + initial_price_quote, + updated_at +) +VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) +ON CONFLICT(pool_id) DO UPDATE SET + dex_id = excluded.dex_id, + pair_id = excluded.pair_id, + source_kind = excluded.source_kind, + source_endpoint_name = excluded.source_endpoint_name, + detected_at = excluded.detected_at, + initial_base_reserve = excluded.initial_base_reserve, + initial_quote_reserve = excluded.initial_quote_reserve, + initial_price_quote = excluded.initial_price_quote, + updated_at = excluded.updated_at + "#, + ) + .bind(dto.dex_id) + .bind(dto.pool_id) + .bind(dto.pair_id) + .bind(dto.source_kind.to_i16()) + .bind(dto.source_endpoint_name.clone()) + .bind(dto.detected_at.to_rfc3339()) + .bind(dto.initial_base_reserve) + .bind(dto.initial_quote_reserve) + .bind(dto.initial_price_quote) + .bind(dto.updated_at.to_rfc3339()) + .execute(pool) + .await; + if let Err(error) = query_result { + return Err(crate::KbError::Db(format!( + "cannot upsert kb_pool_listings on sqlite: {}", + error + ))); + } + let id_result = sqlx::query_scalar::( + r#" +SELECT id +FROM kb_pool_listings +WHERE pool_id = ? +LIMIT 1 + "#, + ) + .bind(dto.pool_id) + .fetch_one(pool) + .await; + match id_result { + Ok(id) => Ok(id), + Err(error) => Err(crate::KbError::Db(format!( + "cannot fetch kb_pool_listings id for pool_id '{}' on sqlite: {}", + dto.pool_id, error + ))), + } + } + } +} + +#[cfg(test)] +mod tests { + #[tokio::test] + async fn normalized_model_roundtrip_works() { + let tempdir = tempfile::tempdir().expect("tempdir must succeed"); + let database_path = tempdir.path().join("normalized_model.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 dex_id = crate::upsert_dex( + &database, + &crate::KbDexDto::new( + "raydium".to_string(), + "Raydium".to_string(), + None, + None, + true, + ), + ) + .await + .expect("dex upsert must succeed"); + let base_token_id = crate::upsert_token( + &database, + &crate::KbTokenDto::new( + "So11111111111111111111111111111111111111112".to_string(), + Some("WSOL".to_string()), + Some("Wrapped SOL".to_string()), + Some(9), + crate::SPL_TOKEN_PROGRAM_ID.to_string(), + true, + ), + ) + .await + .expect("base token upsert must succeed"); + let quote_token_id = crate::upsert_token( + &database, + &crate::KbTokenDto::new( + "DezX111111111111111111111111111111111111111".to_string(), + Some("TEST".to_string()), + Some("Test Token".to_string()), + Some(6), + crate::SPL_TOKEN_PROGRAM_ID.to_string(), + false, + ), + ) + .await + .expect("quote token upsert must succeed"); + let pool_id = crate::upsert_pool( + &database, + &crate::KbPoolDto::new( + dex_id, + "Pool111111111111111111111111111111111111111".to_string(), + crate::KbPoolKind::Amm, + crate::KbPoolStatus::Active, + ), + ) + .await + .expect("pool upsert must succeed"); + let pair_id = crate::upsert_pair( + &database, + &crate::KbPairDto::new( + dex_id, + pool_id, + quote_token_id, + base_token_id, + Some("TEST/WSOL".to_string()), + ), + ) + .await + .expect("pair upsert must succeed"); + let _pool_token_a_id = crate::upsert_pool_token( + &database, + &crate::KbPoolTokenDto::new( + pool_id, + quote_token_id, + crate::KbPoolTokenRole::Base, + None, + Some(0), + ), + ) + .await + .expect("pool token a upsert must succeed"); + let _pool_token_b_id = crate::upsert_pool_token( + &database, + &crate::KbPoolTokenDto::new( + pool_id, + base_token_id, + crate::KbPoolTokenRole::Quote, + None, + Some(1), + ), + ) + .await + .expect("pool token b upsert must succeed"); + let listing_id = crate::upsert_pool_listing( + &database, + &crate::KbPoolListingDto::new( + dex_id, + pool_id, + Some(pair_id), + crate::KbObservationSourceKind::WsRpc, + Some("mainnet_public_ws_slots".to_string()), + Some(1000.0), + Some(42.0), + Some(0.042), + ), + ) + .await + .expect("pool listing upsert must succeed"); + assert!(listing_id > 0); + let dexes = crate::list_dexes(&database) + .await + .expect("dex list must succeed"); + assert_eq!(dexes.len(), 1); + let token = + crate::get_token_by_mint(&database, "So11111111111111111111111111111111111111112") + .await + .expect("token get must succeed"); + assert!(token.is_some()); + } +} diff --git a/kb_lib/src/db/queries/pool_token.rs b/kb_lib/src/db/queries/pool_token.rs new file mode 100644 index 0000000..fe7df84 --- /dev/null +++ b/kb_lib/src/db/queries/pool_token.rs @@ -0,0 +1,67 @@ +// file: kb_lib/src/db/queries/pool_token.rs + +//! Queries for `kb_pool_tokens`. + +/// Inserts or updates one normalized pool token composition row. +pub async fn upsert_pool_token( + database: &crate::KbDatabase, + dto: &crate::KbPoolTokenDto, +) -> Result { + match database.connection() { + crate::KbDatabaseConnection::Sqlite(pool) => { + let query_result = sqlx::query( + r#" +INSERT INTO kb_pool_tokens ( + pool_id, + token_id, + role, + vault_address, + token_order, + created_at, + updated_at +) +VALUES (?, ?, ?, ?, ?, ?, ?) +ON CONFLICT(pool_id, token_id, role) DO UPDATE SET + vault_address = excluded.vault_address, + token_order = excluded.token_order, + updated_at = excluded.updated_at + "#, + ) + .bind(dto.pool_id) + .bind(dto.token_id) + .bind(dto.role.to_i16()) + .bind(dto.vault_address.clone()) + .bind(dto.token_order) + .bind(dto.created_at.to_rfc3339()) + .bind(dto.updated_at.to_rfc3339()) + .execute(pool) + .await; + if let Err(error) = query_result { + return Err(crate::KbError::Db(format!( + "cannot upsert kb_pool_tokens on sqlite: {}", + error + ))); + } + let id_result = sqlx::query_scalar::( + r#" +SELECT id +FROM kb_pool_tokens +WHERE pool_id = ? AND token_id = ? AND role = ? +LIMIT 1 + "#, + ) + .bind(dto.pool_id) + .bind(dto.token_id) + .bind(dto.role.to_i16()) + .fetch_one(pool) + .await; + match id_result { + Ok(id) => Ok(id), + Err(error) => Err(crate::KbError::Db(format!( + "cannot fetch kb_pool_tokens id on sqlite: {}", + error + ))), + } + }, + } +} diff --git a/kb_lib/src/db/queries/token.rs b/kb_lib/src/db/queries/token.rs new file mode 100644 index 0000000..9beab15 --- /dev/null +++ b/kb_lib/src/db/queries/token.rs @@ -0,0 +1,121 @@ +// file: kb_lib/src/db/queries/token.rs + +//! Queries for `kb_tokens`. + +/// Inserts or updates one normalized token row by mint. +pub async fn upsert_token( + database: &crate::KbDatabase, + dto: &crate::KbTokenDto, +) -> Result { + match database.connection() { + crate::KbDatabaseConnection::Sqlite(pool) => { + let decimals = dto.decimals.map(i64::from); + let query_result = sqlx::query( + r#" +INSERT INTO kb_tokens ( + mint, + symbol, + name, + decimals, + token_program, + is_quote_token, + first_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, + is_quote_token = excluded.is_quote_token, + updated_at = excluded.updated_at + "#, + ) + .bind(dto.mint.clone()) + .bind(dto.symbol.clone()) + .bind(dto.name.clone()) + .bind(decimals) + .bind(dto.token_program.clone()) + .bind(if dto.is_quote_token { 1_i64 } else { 0_i64 }) + .bind(dto.first_seen_at.to_rfc3339()) + .bind(dto.updated_at.to_rfc3339()) + .execute(pool) + .await; + if let Err(error) = query_result { + return Err(crate::KbError::Db(format!( + "cannot upsert kb_tokens on sqlite: {}", + error + ))); + } + let id_result = sqlx::query_scalar::( + r#" +SELECT id +FROM kb_tokens +WHERE mint = ? +LIMIT 1 + "#, + ) + .bind(dto.mint.clone()) + .fetch_one(pool) + .await; + match id_result { + Ok(id) => Ok(id), + Err(error) => Err(crate::KbError::Db(format!( + "cannot fetch kb_tokens id for mint '{}' on sqlite: {}", + dto.mint, error + ))), + } + } + } +} + +/// Reads one normalized token row by mint. +pub async fn get_token_by_mint( + database: &crate::KbDatabase, + mint: &str, +) -> Result, crate::KbError> { + match database.connection() { + crate::KbDatabaseConnection::Sqlite(pool) => { + let query_result = sqlx::query_as::( + r#" +SELECT + id, + mint, + symbol, + name, + decimals, + token_program, + is_quote_token, + first_seen_at, + updated_at +FROM kb_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 kb_tokens '{}' on sqlite: {}", + mint, error + ))); + } + }; + match entity_option { + Some(entity) => { + let dto_result = crate::KbTokenDto::try_from(entity); + match dto_result { + Ok(dto) => Ok(Some(dto)), + Err(error) => Err(error), + } + } + None => Ok(None), + } + } + } +} diff --git a/kb_lib/src/db/schema.rs b/kb_lib/src/db/schema.rs index 569b95e..93a7b5b 100644 --- a/kb_lib/src/db/schema.rs +++ b/kb_lib/src/db/schema.rs @@ -6,7 +6,7 @@ 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( + let statements = vec![ r#" CREATE TABLE IF NOT EXISTS kb_db_metadata ( key TEXT NOT NULL PRIMARY KEY, @@ -14,16 +14,6 @@ CREATE TABLE IF NOT EXISTS kb_db_metadata ( updated_at TEXT NOT NULL ) "#, - ) - .execute(pool) - .await; - if let Err(error) = metadata_table_result { - return Err(crate::KbError::Db(format!( - "cannot create table kb_db_metadata on sqlite: {}", - error - ))); - } - let known_http_endpoints_result = sqlx::query( r#" CREATE TABLE IF NOT EXISTS kb_known_http_endpoints ( name TEXT NOT NULL PRIMARY KEY, @@ -35,16 +25,6 @@ CREATE TABLE IF NOT EXISTS kb_known_http_endpoints ( updated_at TEXT NOT NULL ) "#, - ) - .execute(pool) - .await; - if let Err(error) = known_http_endpoints_result { - return Err(crate::KbError::Db(format!( - "cannot create table kb_known_http_endpoints on sqlite: {}", - error - ))); - } - let known_ws_endpoints_result = sqlx::query( r#" CREATE TABLE IF NOT EXISTS kb_known_ws_endpoints ( name TEXT NOT NULL PRIMARY KEY, @@ -56,16 +36,6 @@ CREATE TABLE IF NOT EXISTS kb_known_ws_endpoints ( updated_at TEXT NOT NULL ) "#, - ) - .execute(pool) - .await; - if let Err(error) = known_ws_endpoints_result { - return Err(crate::KbError::Db(format!( - "cannot create table kb_known_ws_endpoints on sqlite: {}", - error - ))); - } - let runtime_events_result = sqlx::query( r#" CREATE TABLE IF NOT EXISTS kb_db_runtime_events ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, @@ -76,30 +46,10 @@ CREATE TABLE IF NOT EXISTS kb_db_runtime_events ( created_at TEXT NOT NULL ) "#, - ) - .execute(pool) - .await; - if let Err(error) = runtime_events_result { - return Err(crate::KbError::Db(format!( - "cannot create table kb_db_runtime_events on sqlite: {}", - error - ))); - } - let runtime_events_index_result = sqlx::query( r#" CREATE INDEX IF NOT EXISTS kb_idx_db_runtime_events_created_at ON kb_db_runtime_events (created_at) "#, - ) - .execute(pool) - .await; - if let Err(error) = runtime_events_index_result { - return Err(crate::KbError::Db(format!( - "cannot create index kb_idx_db_runtime_events_created_at on sqlite: {}", - error - ))); - } - let observed_tokens_result = sqlx::query( r#" CREATE TABLE IF NOT EXISTS kb_observed_tokens ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, @@ -114,44 +64,14 @@ CREATE TABLE IF NOT EXISTS kb_observed_tokens ( 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 onchain_observations_result = sqlx::query( r#" CREATE TABLE IF NOT EXISTS kb_onchain_observations ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, @@ -164,44 +84,14 @@ CREATE TABLE IF NOT EXISTS kb_onchain_observations ( observed_at TEXT NOT NULL ) "#, - ) - .execute(pool) - .await; - if let Err(error) = onchain_observations_result { - return Err(crate::KbError::Db(format!( - "cannot create table kb_onchain_observations on sqlite: {}", - error - ))); - } - let onchain_observations_object_key_index_result = sqlx::query( r#" CREATE INDEX IF NOT EXISTS kb_idx_onchain_observations_object_key ON kb_onchain_observations (object_key) "#, - ) - .execute(pool) - .await; - if let Err(error) = onchain_observations_object_key_index_result { - return Err(crate::KbError::Db(format!( - "cannot create index kb_idx_onchain_observations_object_key on sqlite: {}", - error - ))); - } - let onchain_observations_observed_at_index_result = sqlx::query( r#" CREATE INDEX IF NOT EXISTS kb_idx_onchain_observations_observed_at ON kb_onchain_observations (observed_at) "#, - ) - .execute(pool) - .await; - if let Err(error) = onchain_observations_observed_at_index_result { - return Err(crate::KbError::Db(format!( - "cannot create index kb_idx_onchain_observations_observed_at on sqlite: {}", - error - ))); - } - let analysis_signals_result = sqlx::query( r#" CREATE TABLE IF NOT EXISTS kb_analysis_signals ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, @@ -215,42 +105,117 @@ CREATE TABLE IF NOT EXISTS kb_analysis_signals ( FOREIGN KEY(related_observation_id) REFERENCES kb_onchain_observations(id) ) "#, - ) - .execute(pool) - .await; - if let Err(error) = analysis_signals_result { - return Err(crate::KbError::Db(format!( - "cannot create table kb_analysis_signals on sqlite: {}", - error - ))); - } - let analysis_signals_object_key_index_result = sqlx::query( r#" CREATE INDEX IF NOT EXISTS kb_idx_analysis_signals_object_key ON kb_analysis_signals (object_key) "#, - ) - .execute(pool) - .await; - if let Err(error) = analysis_signals_object_key_index_result { - return Err(crate::KbError::Db(format!( - "cannot create index kb_idx_analysis_signals_object_key on sqlite: {}", - error - ))); - } - let analysis_signals_created_at_index_result = sqlx::query( r#" CREATE INDEX IF NOT EXISTS kb_idx_analysis_signals_created_at ON kb_analysis_signals (created_at) "#, - ) - .execute(pool) - .await; - if let Err(error) = analysis_signals_created_at_index_result { - return Err(crate::KbError::Db(format!( - "cannot create index kb_idx_analysis_signals_created_at on sqlite: {}", - error - ))); + r#" +CREATE TABLE IF NOT EXISTS kb_dexes ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + code TEXT NOT NULL UNIQUE, + name TEXT NOT NULL, + program_id TEXT NULL, + router_program_id TEXT NULL, + is_enabled INTEGER NOT NULL, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL +) + "#, + r#" +CREATE TABLE IF NOT EXISTS kb_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, + is_quote_token INTEGER NOT NULL, + first_seen_at TEXT NOT NULL, + updated_at TEXT NOT NULL +) + "#, + r#" +CREATE TABLE IF NOT EXISTS kb_pools ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + dex_id INTEGER NOT NULL, + address TEXT NOT NULL UNIQUE, + pool_kind INTEGER NOT NULL, + status INTEGER NOT NULL, + first_seen_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + FOREIGN KEY(dex_id) REFERENCES kb_dexes(id) + ) + "#, + r#" +CREATE INDEX IF NOT EXISTS kb_idx_pools_dex_id +ON kb_pools (dex_id) + "#, + r#" +CREATE TABLE IF NOT EXISTS kb_pairs ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + dex_id INTEGER NOT NULL, + pool_id INTEGER NOT NULL UNIQUE, + base_token_id INTEGER NOT NULL, + quote_token_id INTEGER NOT NULL, + symbol TEXT NULL, + first_seen_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + FOREIGN KEY(dex_id) REFERENCES kb_dexes(id), + FOREIGN KEY(pool_id) REFERENCES kb_pools(id), + FOREIGN KEY(base_token_id) REFERENCES kb_tokens(id), + FOREIGN KEY(quote_token_id) REFERENCES kb_tokens(id) + ) + "#, + r#" +CREATE TABLE IF NOT EXISTS kb_pool_tokens ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + pool_id INTEGER NOT NULL, + token_id INTEGER NOT NULL, + role INTEGER NOT NULL, + vault_address TEXT NULL, + token_order INTEGER NULL, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + FOREIGN KEY(pool_id) REFERENCES kb_pools(id), + FOREIGN KEY(token_id) REFERENCES kb_tokens(id), + UNIQUE(pool_id, token_id, role) + ) + "#, + r#" +CREATE TABLE IF NOT EXISTS kb_pool_listings ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + dex_id INTEGER NOT NULL, + pool_id INTEGER NOT NULL UNIQUE, + pair_id INTEGER NULL, + source_kind INTEGER NOT NULL, + source_endpoint_name TEXT NULL, + detected_at TEXT NOT NULL, + initial_base_reserve REAL NULL, + initial_quote_reserve REAL NULL, + initial_price_quote REAL NULL, + updated_at TEXT NOT NULL, + FOREIGN KEY(dex_id) REFERENCES kb_dexes(id), + FOREIGN KEY(pool_id) REFERENCES kb_pools(id), + FOREIGN KEY(pair_id) REFERENCES kb_pairs(id) + ) + "#, + r#" +CREATE INDEX IF NOT EXISTS kb_idx_pool_listings_detected_at +ON kb_pool_listings (detected_at) + "#, + ]; + for statement in statements { + let execute_result = sqlx::query(statement).execute(pool).await; + if let Err(error) = execute_result { + return Err(crate::KbError::Db(format!( + "cannot initialize sqlite schema statement '{}': {}", + statement, error + ))); + } } let schema_version = crate::KbDbMetadataDto::new( "schema_version".to_string(), diff --git a/kb_lib/src/db/types.rs b/kb_lib/src/db/types.rs index 4d53a99..727e295 100644 --- a/kb_lib/src/db/types.rs +++ b/kb_lib/src/db/types.rs @@ -6,10 +6,16 @@ mod analysis_signal_severity; mod database_backend; mod observation_source_kind; mod observed_token_status; +mod pool_kind; +mod pool_status; +mod pool_token_role; mod runtime_event_level; pub use crate::db::types::analysis_signal_severity::KbAnalysisSignalSeverity; pub use crate::db::types::database_backend::KbDatabaseBackend; pub use crate::db::types::observation_source_kind::KbObservationSourceKind; pub use crate::db::types::observed_token_status::KbObservedTokenStatus; +pub use crate::db::types::pool_kind::KbPoolKind; +pub use crate::db::types::pool_status::KbPoolStatus; +pub use crate::db::types::pool_token_role::KbPoolTokenRole; pub use crate::db::types::runtime_event_level::KbDbRuntimeEventLevel; diff --git a/kb_lib/src/db/types/pool_kind.rs b/kb_lib/src/db/types/pool_kind.rs new file mode 100644 index 0000000..efc4797 --- /dev/null +++ b/kb_lib/src/db/types/pool_kind.rs @@ -0,0 +1,49 @@ + +// file: kb_lib/src/db/types/pool_kind.rs + +//! Pool kind. + +/// Normalized pool kind. +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub enum KbPoolKind { + /// Unknown pool kind. + Unknown, + /// Constant-product AMM. + Amm, + /// Concentrated liquidity market maker. + Clmm, + /// Bonding curve pool. + BondingCurve, + /// Order book market. + OrderBook, +} + +impl KbPoolKind { + /// Converts the kind to its stable integer representation. + pub fn to_i16(self) -> i16 { + match self { + Self::Unknown => 0, + Self::Amm => 1, + Self::Clmm => 2, + Self::BondingCurve => 3, + Self::OrderBook => 4, + } + } + + /// Restores a kind from its stable integer representation. + pub fn from_i16( + value: i16, + ) -> Result { + match value { + 0 => Ok(Self::Unknown), + 1 => Ok(Self::Amm), + 2 => Ok(Self::Clmm), + 3 => Ok(Self::BondingCurve), + 4 => Ok(Self::OrderBook), + _ => Err(crate::KbError::Db(format!( + "invalid KbPoolKind value: {}", + value + ))), + } + } +} diff --git a/kb_lib/src/db/types/pool_status.rs b/kb_lib/src/db/types/pool_status.rs new file mode 100644 index 0000000..cab2be0 --- /dev/null +++ b/kb_lib/src/db/types/pool_status.rs @@ -0,0 +1,46 @@ +// file: kb_lib/src/db/types/pool_status.rs + +//! Pool status. + +/// Normalized pool status. +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub enum KbPoolStatus { + /// Unknown status. + Unknown, + /// Pool detected but not yet fully active. + Pending, + /// Pool active. + Active, + /// Pool inactive. + Inactive, + /// Pool closed. + Closed, +} + +impl KbPoolStatus { + /// Converts the status to its stable integer representation. + pub fn to_i16(self) -> i16 { + match self { + Self::Unknown => 0, + Self::Pending => 1, + Self::Active => 2, + Self::Inactive => 3, + Self::Closed => 4, + } + } + + /// Restores a status from its stable integer representation. + pub fn from_i16(value: i16) -> Result { + match value { + 0 => Ok(Self::Unknown), + 1 => Ok(Self::Pending), + 2 => Ok(Self::Active), + 3 => Ok(Self::Inactive), + 4 => Ok(Self::Closed), + _ => Err(crate::KbError::Db(format!( + "invalid KbPoolStatus value: {}", + value + ))), + } + } +} diff --git a/kb_lib/src/db/types/pool_token_role.rs b/kb_lib/src/db/types/pool_token_role.rs new file mode 100644 index 0000000..362acd4 --- /dev/null +++ b/kb_lib/src/db/types/pool_token_role.rs @@ -0,0 +1,46 @@ +// file: kb_lib/src/db/types/pool_token_role.rs + +//! Pool token role. + +/// Role of one token inside a normalized pool. +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub enum KbPoolTokenRole { + /// Base token. + Base, + /// Quote token. + Quote, + /// LP mint token. + LpMint, + /// Reserve token or reserve-side tracked token. + Reserve, + /// Other role. + Other, +} + +impl KbPoolTokenRole { + /// Converts the role to its stable integer representation. + pub fn to_i16(self) -> i16 { + match self { + Self::Base => 0, + Self::Quote => 1, + Self::LpMint => 2, + Self::Reserve => 3, + Self::Other => 4, + } + } + + /// Restores a role from its stable integer representation. + pub fn from_i16(value: i16) -> Result { + match value { + 0 => Ok(Self::Base), + 1 => Ok(Self::Quote), + 2 => Ok(Self::LpMint), + 3 => Ok(Self::Reserve), + 4 => Ok(Self::Other), + _ => Err(crate::KbError::Db(format!( + "invalid KbPoolTokenRole value: {}", + value + ))), + } + } +} diff --git a/kb_lib/src/lib.rs b/kb_lib/src/lib.rs index 17a34d5..6dcf665 100644 --- a/kb_lib/src/lib.rs +++ b/kb_lib/src/lib.rs @@ -87,6 +87,29 @@ pub use crate::db::KbAnalysisSignalSeverity; pub use crate::db::KbObservationSourceKind; pub use crate::db::KbOnchainObservationDto; pub use crate::db::KbOnchainObservationEntity; +pub use crate::db::KbDexDto; +pub use crate::db::KbDexEntity; +pub use crate::db::KbPairDto; +pub use crate::db::KbPairEntity; +pub use crate::db::KbPoolDto; +pub use crate::db::KbPoolEntity; +pub use crate::db::KbPoolKind; +pub use crate::db::KbPoolListingDto; +pub use crate::db::KbPoolListingEntity; +pub use crate::db::KbPoolStatus; +pub use crate::db::KbPoolTokenDto; +pub use crate::db::KbPoolTokenEntity; +pub use crate::db::KbPoolTokenRole; +pub use crate::db::KbTokenDto; +pub use crate::db::KbTokenEntity; +pub use crate::db::get_token_by_mint; +pub use crate::db::list_dexes; +pub use crate::db::upsert_dex; +pub use crate::db::upsert_pair; +pub use crate::db::upsert_pool; +pub use crate::db::upsert_pool_listing; +pub use crate::db::upsert_pool_token; +pub use crate::db::upsert_token; pub use crate::db::insert_analysis_signal; pub use crate::db::insert_onchain_observation; pub use crate::db::list_recent_analysis_signals; @@ -102,4 +125,3 @@ pub use crate::db::list_known_ws_endpoints; pub use crate::db::list_recent_db_runtime_events; pub use crate::db::upsert_known_http_endpoint; pub use crate::db::upsert_known_ws_endpoint; -