This commit is contained in:
2026-04-25 06:29:48 +02:00
parent fbd4d5d6ef
commit 04e09b0c97
7 changed files with 467 additions and 21 deletions

View File

@@ -23,4 +23,4 @@
0.5.4 - Ajout du modèle métier normalisé initial pour les DEX, tokens, pools, paires, composition des pools et listings 0.5.4 - Ajout du modèle métier normalisé initial pour les DEX, tokens, pools, paires, composition des pools et listings
0.5.5 - Ajout des événements métier normalisés pour les swaps, liquidités, mints et burns de tokens 0.5.5 - Ajout des événements métier normalisés pour les swaps, liquidités, mints et burns de tokens
0.5.6 - Consolidation de la couche stockage : activation des foreign keys SQLite, lectures ciblées sur le modèle métier normalisé, index supplémentaires et tests unitaires dédiés 0.5.6 - Consolidation de la couche stockage : activation des foreign keys SQLite, lectures ciblées sur le modèle métier normalisé, index supplémentaires et tests unitaires dédiés
0.6.0 - Ajout du pipeline de détection technique : façade de persistance pour observations on-chain, signaux danalyse et candidats tokens depuis les connecteurs RPC

View File

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

View File

@@ -383,17 +383,27 @@ Objectif : stabiliser le schéma avant la détection technique réelle.
- durcir les relations, contraintes et index utiles, - durcir les relations, contraintes et index utiles,
- préparer une future compatibilité PostgreSQL sans casser lorganisation générale. - préparer une future compatibilité PostgreSQL sans casser lorganisation générale.
### 6.25. Version `0.6.x` — Détection technique on-chain / RPC ### 6.25. Version `0.6.0` — Pipeline de détection technique
Objectif : commencer la détection utile pour lapplication. Objectif : relier les connecteurs RPC à la couche de stockage technique et métier.
À faire : À faire :
- réception de notifications ciblées, - ajouter une façade de persistance pour les observations et signaux issus des connecteurs,
- détection de créations de comptes/programmes dintérêt, - préparer lenregistrement des candidats tokens détectés depuis les sources RPC,
- débuts de normalisation dévénements, - éviter que les futurs watchers RPC écrivent directement dans la DB sans couche intermédiaire,
- premiers connecteurs DEX. - préparer les prochaines étapes de détection technique on-chain / RPC.
### 6.26. Version `0.7.x` — DEX connectors v1 ### 6.26. Version `0.6.1` — Détection technique RPC
Objectif : brancher les premiers watchers et règles techniques sur la façade de détection.
À faire :
- relier les notifications WS / RPC au pipeline de détection,
- produire des observations on-chain normalisées,
- générer les premiers signaux techniques exploitables,
- préparer la découverte effective des tokens et pools avant les connecteurs DEX dédiés.
### 6.27. Version `0.7.x` — DEX connectors v1
Objectif : structurer les connecteurs par protocole. Objectif : structurer les connecteurs par protocole.
Cibles initiales possibles : Cibles initiales possibles :
@@ -412,7 +422,7 @@ Cibles initiales possibles :
- création de types métiers propres, - création de types métiers propres,
- enrichissement des métadonnées token/pool/pair. - enrichissement des métadonnées token/pool/pair.
### 6.27. Version `0.8.x` — Analyse et filtrage ### 6.28. Version `0.8.x` — Analyse et filtrage
Objectif : transformer les événements bruts en signaux exploitables. Objectif : transformer les événements bruts en signaux exploitables.
À faire : À faire :
@@ -423,7 +433,7 @@ Objectif : transformer les événements bruts en signaux exploitables.
- statistiques de comportement, - statistiques de comportement,
- premiers patterns. - premiers patterns.
### 6.28. Version `1.x.y` — Wallets et swap préparatoire ### 6.29. Version `1.x.y` — Wallets et swap préparatoire
Objectif : préparer la couche daction. Objectif : préparer la couche daction.
À faire : À faire :
@@ -434,7 +444,7 @@ Objectif : préparer la couche daction.
- préparation dordres et de swaps, - préparation dordres et de swaps,
- simulation et garde-fous. - simulation et garde-fous.
### 6.29. Version `2.x.y` — Trading semi-automatisé ### 6.30. Version `2.x.y` — Trading semi-automatisé
Objectif : brancher lanalyse à laction tout en gardant des garde-fous explicites. Objectif : brancher lanalyse à laction tout en gardant des garde-fous explicites.
À faire : À faire :
@@ -445,7 +455,7 @@ Objectif : brancher lanalyse à laction tout en gardant des garde-fous exp
- confirmations explicites ou semi-automatiques, - confirmations explicites ou semi-automatiques,
- journaux dexécution. - journaux dexécution.
### 6.30. Version `3.x.y` — Yellowstone gRPC ### 6.31. Version `3.x.y` — Yellowstone gRPC
Objectif : ajouter le connecteur gRPC dédié. Objectif : ajouter le connecteur gRPC dédié.
À faire : À faire :
@@ -530,9 +540,9 @@ Le projet doit maintenir au minimum :
## 12. Priorité immédiate ## 12. Priorité immédiate
La priorité immédiate est désormais la suivante : La priorité immédiate est désormais la suivante :
1. démarrer la version `0.5.4` avec le modèle métier normalisé initial, 1. démarrer la version `0.6.0` avec le pipeline de détection technique,
2. poser les tables de référence pour les DEX, tokens, pools, paires et listings, 2. ajouter une façade unique entre les connecteurs RPC et la base de données,
3. séparer clairement les objets métier des observations techniques brutes, 3. préparer lenregistrement des observations on-chain, des signaux et des candidats tokens,
4. préparer les relations nécessaires avant la détection technique `0.6.x`, 4. éviter que les watchers futurs accèdent directement à la DB sans couche intermédiaire,
5. conserver labstraction du backend dès cette phase SQLite, 5. conserver labstraction du backend et la séparation entre stockage brut et modèle métier,
6. reporter la couche analytique agrégée après la fin de `0.6.x`. 6. préparer ensuite la version `0.6.1` pour brancher les premières règles de détection technique RPC.

16
kb_lib/src/detect.rs Normal file
View File

@@ -0,0 +1,16 @@
// file: kb_lib/src/detect.rs
//! Detection pipeline facade.
//!
//! This module sits between transport/connectors and persistence.
//! It centralizes how technical observations, analysis signals and
//! candidate tokens are persisted before richer detection logic is added.
mod service;
mod types;
pub use crate::detect::service::KbDetectionPersistenceService;
pub use crate::detect::types::KbDetectionObservationInput;
pub use crate::detect::types::KbDetectionSignalInput;
pub use crate::detect::types::KbDetectionTokenCandidateInput;
pub use crate::detect::types::KbDetectionTokenCandidateResult;

View File

@@ -0,0 +1,250 @@
// file: kb_lib/src/detect/service.rs
//! Detection persistence service.
/// Persistence façade between technical detection and database storage.
#[derive(Debug, Clone)]
pub struct KbDetectionPersistenceService {
/// Shared database handle.
database: std::sync::Arc<crate::KbDatabase>,
}
impl KbDetectionPersistenceService {
/// Creates a new detection persistence service.
pub fn new(database: std::sync::Arc<crate::KbDatabase>) -> Self {
Self { database }
}
/// Returns the shared database handle.
pub fn database(&self) -> &std::sync::Arc<crate::KbDatabase> {
&self.database
}
/// Persists one on-chain observation.
pub async fn record_observation(
&self,
input: &crate::KbDetectionObservationInput,
) -> Result<i64, crate::KbError> {
let dto = crate::KbOnchainObservationDto::new(
input.observation_kind.clone(),
input.source_kind,
input.endpoint_name.clone(),
input.object_key.clone(),
input.slot,
input.payload.clone(),
);
crate::insert_onchain_observation(self.database.as_ref(), &dto).await
}
/// Persists one analysis signal.
pub async fn record_signal(
&self,
input: &crate::KbDetectionSignalInput,
) -> Result<i64, crate::KbError> {
let dto = crate::KbAnalysisSignalDto::new(
input.signal_kind.clone(),
input.severity,
input.object_key.clone(),
input.related_observation_id,
input.score,
input.payload.clone(),
);
crate::insert_analysis_signal(self.database.as_ref(), &dto).await
}
/// Registers one token candidate from a technical source.
///
/// This method:
/// - upserts the normalized token entry,
/// - stores the underlying technical observation,
/// - stores the derived signal linked to that observation.
pub async fn register_token_candidate(
&self,
input: &crate::KbDetectionTokenCandidateInput,
) -> Result<crate::KbDetectionTokenCandidateResult, crate::KbError> {
let token_dto = crate::KbTokenDto::new(
input.mint.clone(),
input.symbol.clone(),
input.name.clone(),
input.decimals,
input.token_program.clone(),
input.is_quote_token,
);
let token_id_result = crate::upsert_token(self.database.as_ref(), &token_dto).await;
let token_id = match token_id_result {
Ok(token_id) => token_id,
Err(error) => return Err(error),
};
let observation_input = crate::KbDetectionObservationInput::new(
input.observation_kind.clone(),
input.source_kind,
input.endpoint_name.clone(),
input.mint.clone(),
input.slot,
input.observation_payload.clone(),
);
let observation_id_result = self.record_observation(&observation_input).await;
let observation_id = match observation_id_result {
Ok(observation_id) => observation_id,
Err(error) => return Err(error),
};
let signal_payload = match &input.signal_payload {
Some(signal_payload) => signal_payload.clone(),
None => input.observation_payload.clone(),
};
let signal_input = crate::KbDetectionSignalInput::new(
input.signal_kind.clone(),
input.signal_severity,
input.mint.clone(),
Some(observation_id),
input.signal_score,
signal_payload,
);
let signal_id_result = self.record_signal(&signal_input).await;
let signal_id = match signal_id_result {
Ok(signal_id) => signal_id,
Err(error) => return Err(error),
};
Ok(crate::KbDetectionTokenCandidateResult {
token_id,
observation_id,
signal_id,
})
}
}
#[cfg(test)]
mod tests {
async fn create_database() -> crate::KbDatabase {
let tempdir = tempfile::tempdir().expect("tempdir must succeed");
let database_path = tempdir.path().join("detect_pipeline.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,
},
};
crate::KbDatabase::connect_and_initialize(&config)
.await
.expect("database init must succeed")
}
#[tokio::test]
async fn record_observation_and_signal_work() {
let database = create_database().await;
let service = crate::KbDetectionPersistenceService::new(std::sync::Arc::new(database));
let observation_id = service
.record_observation(&crate::KbDetectionObservationInput::new(
"rpc.program_notification".to_string(),
crate::KbObservationSourceKind::WsRpc,
Some("mainnet_public_ws_slots".to_string()),
"So11111111111111111111111111111111111111112".to_string(),
Some(123456),
serde_json::json!({
"mint": "So11111111111111111111111111111111111111112"
}),
))
.await
.expect("record observation must succeed");
assert!(observation_id > 0);
let signal_id = service
.record_signal(&crate::KbDetectionSignalInput::new(
"rpc.token_candidate".to_string(),
crate::KbAnalysisSignalSeverity::Low,
"So11111111111111111111111111111111111111112".to_string(),
Some(observation_id),
Some(0.25),
serde_json::json!({
"reason": "test"
}),
))
.await
.expect("record signal must succeed");
assert!(signal_id > 0);
let observations = crate::list_recent_onchain_observations(service.database().as_ref(), 10)
.await
.expect("list observations must succeed");
let signals = crate::list_recent_analysis_signals(service.database().as_ref(), 10)
.await
.expect("list signals must succeed");
assert_eq!(observations.len(), 1);
assert_eq!(signals.len(), 1);
assert_eq!(
observations[0].object_key,
"So11111111111111111111111111111111111111112"
);
assert_eq!(
signals[0].object_key,
"So11111111111111111111111111111111111111112"
);
}
#[tokio::test]
async fn register_token_candidate_persists_token_observation_and_signal() {
let database = create_database().await;
let service = crate::KbDetectionPersistenceService::new(std::sync::Arc::new(database));
let result = service
.register_token_candidate(&crate::KbDetectionTokenCandidateInput::new(
"Mint111111111111111111111111111111111111111".to_string(),
Some("TEST".to_string()),
Some("Test Token".to_string()),
Some(6),
crate::SPL_TOKEN_PROGRAM_ID.to_string(),
false,
crate::KbObservationSourceKind::WsRpc,
Some("helius_primary_ws_programs".to_string()),
Some(777777),
"rpc.token_candidate".to_string(),
serde_json::json!({
"mint": "Mint111111111111111111111111111111111111111",
"source": "ws"
}),
"signal.token_candidate".to_string(),
crate::KbAnalysisSignalSeverity::Medium,
Some(0.70),
None,
))
.await
.expect("register token candidate must succeed");
assert!(result.token_id > 0);
assert!(result.observation_id > 0);
assert!(result.signal_id > 0);
let token = crate::get_token_by_mint(
service.database().as_ref(),
"Mint111111111111111111111111111111111111111",
)
.await
.expect("get token must succeed");
assert!(token.is_some());
assert_eq!(
token.expect("token must exist").symbol.as_deref(),
Some("TEST")
);
let observations = crate::list_recent_onchain_observations(service.database().as_ref(), 10)
.await
.expect("list observations must succeed");
let signals = crate::list_recent_analysis_signals(service.database().as_ref(), 10)
.await
.expect("list signals must succeed");
assert_eq!(observations.len(), 1);
assert_eq!(signals.len(), 1);
assert_eq!(
observations[0].object_key,
"Mint111111111111111111111111111111111111111"
);
assert_eq!(
signals[0].object_key,
"Mint111111111111111111111111111111111111111"
);
assert_eq!(
signals[0].related_observation_id,
Some(result.observation_id)
);
}
}

164
kb_lib/src/detect/types.rs Normal file
View File

@@ -0,0 +1,164 @@
// file: kb_lib/src/detect/types.rs
//! Detection pipeline input and output types.
/// One normalized observation ready to be persisted.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct KbDetectionObservationInput {
/// Observation kind.
pub observation_kind: std::string::String,
/// Observation source family.
pub source_kind: crate::KbObservationSourceKind,
/// Optional logical source endpoint name.
pub endpoint_name: std::option::Option<std::string::String>,
/// Logical object key, for example a mint, signature or pool address.
pub object_key: std::string::String,
/// Optional slot number.
pub slot: std::option::Option<u64>,
/// JSON payload.
pub payload: serde_json::Value,
}
impl KbDetectionObservationInput {
/// Creates a new detection observation input.
pub fn new(
observation_kind: std::string::String,
source_kind: crate::KbObservationSourceKind,
endpoint_name: std::option::Option<std::string::String>,
object_key: std::string::String,
slot: std::option::Option<u64>,
payload: serde_json::Value,
) -> Self {
Self {
observation_kind,
source_kind,
endpoint_name,
object_key,
slot,
payload,
}
}
}
/// One normalized signal ready to be persisted.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct KbDetectionSignalInput {
/// Signal kind.
pub signal_kind: std::string::String,
/// Signal severity.
pub severity: crate::KbAnalysisSignalSeverity,
/// Logical object key, for example a mint, signature or pool address.
pub object_key: std::string::String,
/// Optional related observation id.
pub related_observation_id: std::option::Option<i64>,
/// Optional score.
pub score: std::option::Option<f64>,
/// JSON payload.
pub payload: serde_json::Value,
}
impl KbDetectionSignalInput {
/// Creates a new detection signal input.
pub fn new(
signal_kind: std::string::String,
severity: crate::KbAnalysisSignalSeverity,
object_key: std::string::String,
related_observation_id: std::option::Option<i64>,
score: std::option::Option<f64>,
payload: serde_json::Value,
) -> Self {
Self {
signal_kind,
severity,
object_key,
related_observation_id,
score,
payload,
}
}
}
/// One token candidate detected from a technical source.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct KbDetectionTokenCandidateInput {
/// Token mint address.
pub mint: std::string::String,
/// Optional token symbol.
pub symbol: std::option::Option<std::string::String>,
/// Optional token name.
pub name: std::option::Option<std::string::String>,
/// Optional decimals.
pub decimals: std::option::Option<u8>,
/// Token program id.
pub token_program: std::string::String,
/// Whether the token is typically used as quote token.
pub is_quote_token: bool,
/// Observation source family.
pub source_kind: crate::KbObservationSourceKind,
/// Optional source endpoint logical name.
pub endpoint_name: std::option::Option<std::string::String>,
/// Optional slot number.
pub slot: std::option::Option<u64>,
/// Observation kind.
pub observation_kind: std::string::String,
/// Observation payload.
pub observation_payload: serde_json::Value,
/// Signal kind.
pub signal_kind: std::string::String,
/// Signal severity.
pub signal_severity: crate::KbAnalysisSignalSeverity,
/// Optional signal score.
pub signal_score: std::option::Option<f64>,
/// Optional dedicated signal payload.
pub signal_payload: std::option::Option<serde_json::Value>,
}
impl KbDetectionTokenCandidateInput {
/// Creates a new token candidate input.
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,
is_quote_token: bool,
source_kind: crate::KbObservationSourceKind,
endpoint_name: std::option::Option<std::string::String>,
slot: std::option::Option<u64>,
observation_kind: std::string::String,
observation_payload: serde_json::Value,
signal_kind: std::string::String,
signal_severity: crate::KbAnalysisSignalSeverity,
signal_score: std::option::Option<f64>,
signal_payload: std::option::Option<serde_json::Value>,
) -> Self {
Self {
mint,
symbol,
name,
decimals,
token_program,
is_quote_token,
source_kind,
endpoint_name,
slot,
observation_kind,
observation_payload,
signal_kind,
signal_severity,
signal_score,
signal_payload,
}
}
}
/// Result of one token candidate persistence operation.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct KbDetectionTokenCandidateResult {
/// Persisted token id.
pub token_id: i64,
/// Persisted observation id.
pub observation_id: i64,
/// Persisted signal id.
pub signal_id: i64,
}

View File

@@ -19,6 +19,7 @@ mod ws_client;
mod rpc_ws_solana; mod rpc_ws_solana;
mod http_pool; mod http_pool;
mod db; mod db;
mod detect;
pub use crate::config::KbAppConfig; pub use crate::config::KbAppConfig;
pub use crate::config::KbConfig; pub use crate::config::KbConfig;
@@ -151,3 +152,8 @@ pub use crate::db::list_pairs;
pub use crate::db::list_pool_listings; pub use crate::db::list_pool_listings;
pub use crate::db::list_pool_tokens_by_pool_id; pub use crate::db::list_pool_tokens_by_pool_id;
pub use crate::db::list_pools; pub use crate::db::list_pools;
pub use crate::detect::KbDetectionObservationInput;
pub use crate::detect::KbDetectionPersistenceService;
pub use crate::detect::KbDetectionSignalInput;
pub use crate::detect::KbDetectionTokenCandidateInput;
pub use crate::detect::KbDetectionTokenCandidateResult;