This commit is contained in:
2026-04-25 11:36:36 +02:00
parent 2fee5db206
commit 2391d4c061
4 changed files with 464 additions and 58 deletions

View File

@@ -26,3 +26,4 @@
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
0.6.1 - Ajout du bridge de détection Solana WS : notifications JSON-RPC persistées en observations, avec détection initiale des mints SPL / Token-2022 depuis programNotification
0.6.2 - Branchement de WsClient vers le pipeline de détection via un relais asynchrone de notifications JSON-RPC WebSocket
0.6.3 - Enrichissement des notifications WebSocket utiles : extraction améliorée de pubkey, signature, owner, parsed account type et slot pour account/logs/signature notifications

View File

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

View File

@@ -140,7 +140,7 @@ Le tracing est centralisé dans `kb_lib`.
## 6. Phasage par versions
### 6.1. Version `0.0.2` — Socle conforme
### 6.001. Version `0.0.2` — Socle conforme
Objectif : corriger le squelette et poser la base de travail.
Réalisé :
@@ -155,7 +155,7 @@ Réalisé :
- documentation de `kb_app/src/splash.rs`,
- UI Tauri minimale.
### 6.2. Version `0.1.x` — Transport WebSocket générique
### 6.002. Version `0.1.x` — Transport WebSocket générique
Objectif : construire un vrai `WsClient` asynchrone clonable.
Réalisé :
@@ -169,7 +169,7 @@ Réalisé :
- fermeture avec timeout,
- tests offline avec serveur mock.
### 6.3. Version `0.1.1` — Intégration Tauri minimale du `WsClient`
### 6.003. Version `0.1.1` — Intégration Tauri minimale du `WsClient`
Objectif : valider le transport via lapplication desktop.
Réalisé :
@@ -179,7 +179,7 @@ Réalisé :
- zone de logs,
- validation du flux `frontend -> tauri -> kb_lib -> frontend`.
### 6.4. Version `0.2.0` — Couche JSON-RPC WS Solana
### 6.004. Version `0.2.0` — Couche JSON-RPC WS Solana
Objectif : séparer clairement transport, réponses RPC et notifications.
Réalisé :
@@ -190,7 +190,7 @@ Réalisé :
- parsing des notifications,
- premiers helpers JSON-RPC sur `WsClient`.
### 6.5. Version `0.3.0` — Registre subscriptions / notifications
### 6.005. Version `0.3.0` — Registre subscriptions / notifications
Objectif : fiabiliser la gestion des subscriptions.
Réalisé :
@@ -202,7 +202,7 @@ Réalisé :
- purge locale si nécessaire,
- routage séparé des notifications.
### 6.6. Version `0.3.1` — Helpers subscribe/unsubscribe WebSocket
### 6.006. Version `0.3.1` — Helpers subscribe/unsubscribe WebSocket
Objectif : ajouter les helpers haut niveau correspondant aux principales méthodes PubSub Solana.
Réalisé :
@@ -211,7 +211,7 @@ Réalisé :
- helpers dunsubscribe correspondants,
- premiers tests de validation des noms de méthodes.
### 6.7. Version `0.3.2` — Helpers typed et notifications typed
### 6.007. 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é :
@@ -220,7 +220,7 @@ Réalisé :
- parsing typed des notifications,
- base de travail pour réduire lusage direct de `serde_json::Value`.
### 6.8. Version `0.3.3` — Distinction API typed / raw
### 6.008. Version `0.3.3` — Distinction API typed / raw
Objectif : clarifier lAPI publique de `WsClient`.
Réalisé :
@@ -229,7 +229,7 @@ Réalisé :
- conservation des helpers typed comme interface plus propre,
- préparation dune hiérarchie API plus explicite.
### 6.9. Version `0.3.4` — Fenêtre `Demo Ws` dans `kb_app`
### 6.009. 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é :
@@ -241,7 +241,7 @@ Réalisé :
- affichage des événements raw et typed,
- premiers tests réels sur `wss://api.mainnet.solana.com`.
### 6.10. Version `0.3.5` — Stabilisation de `Demo Ws`
### 6.010. 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é :
@@ -255,11 +255,11 @@ Réalisé :
- conserver des compteurs et états UI exploitables,
- mieux gérer les fermetures/ralentissements dendpoints publics.
### 6.11. Version `0.4.x` — Transport HTTP générique et helpers RPC
### 6.011. Version `0.4.x` — Transport HTTP générique et helpers RPC
Objectif : construire un `HttpClient` clonable, limité et extensible, puis ajouter les premiers helpers HTTP Solana.
### 6.12. Version `0.4.0` — Socle `HttpClient`
### 6.012. Version `0.4.0` — Socle `HttpClient`
Réalisé :
- client `reqwest` asynchrone clonable,
@@ -280,7 +280,7 @@ Livrables :
- `getVersion`
- `getSlot`
### 6.13. Version `0.4.1` — Helpers HTTP Solana
### 6.013. Version `0.4.1` — Helpers HTTP Solana
Réalisé :
- ajouter des helpers HTTP haut niveau comme pour le client WS,
@@ -288,7 +288,7 @@ Réalisé :
- couvrir les premières méthodes utiles du RPC HTTP Solana,
- conserver `HttpClient` comme couche générique réutilisable.
### 6.14. Version `0.4.2` — Politique HTTP avancée
### 6.014. Version `0.4.2` — Politique HTTP avancée
Réalisé :
- préparer un état de pause avant envoi pour un endpoint HTTP,
@@ -296,7 +296,7 @@ Réalisé :
- distinguer quota RPC général et quota `sendTransaction`,
- préparer un futur pool dendpoints HTTP et larbitrage entre eux.
### 6.15. Version `0.4.3` — Pool dendpoints HTTP
### 6.015. Version `0.4.3` — Pool dendpoints HTTP
Réalisé :
- ajouter un pool d`HttpClient`,
@@ -306,7 +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.
### 6.16. Version `0.4.4` — Démo HTTP dans `kb_app`
### 6.016. Version `0.4.4` — Démo HTTP dans `kb_app`
Réalisé :
- ajout dune fenêtre `Demo Http`,
@@ -317,10 +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.17. Version `0.5.x` — Base de données SQLite
### 6.017. 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.
### 6.18. Version `0.5.0` — Socle SQLite
### 6.018. Version `0.5.0` — Socle SQLite
Réalisé :
- configuration DB dans `config.json`,
@@ -330,7 +330,7 @@ Réalisé :
- table `kb_db_metadata`,
- séparation `db/entities`, `db/dtos`, `db/queries`, `db/types`.
### 6.19. Version `0.5.1` — Premières tables métier de stockage local
### 6.019. Version `0.5.1` — Premières tables métier de stockage local
Réalisé :
- ajout des tables de référence pour les endpoints connus HTTP/WS,
@@ -338,7 +338,7 @@ Réalisé :
- 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.
### 6.20. Version `0.5.2` — Stockage des tokens observés
### 6.020. Version `0.5.2` — Stockage des tokens observés
Réalisé :
- ajout de la table `kb_observed_tokens`,
@@ -347,7 +347,7 @@ Réalisé :
- préparation des relations futures avec pools, paires et événements on-chain,
- conservation dunicité locale par mint sans duplication par endpoint.
### 6.21. Version `0.5.3` — Événements et signaux locaux
### 6.021. Version `0.5.3` — Événements et signaux locaux
Réalisé :
- conservation des événements runtime techniques via `kb_db_runtime_events`,
@@ -356,7 +356,7 @@ Réalisé :
- distinction explicite entre événements runtime, observations on-chain et événements métier,
- préparation de la traçabilité de provenance par type de source et endpoint, sans remettre en cause lunicité locale dun token par mint.
### 6.22. Version `0.5.4` — Modèle métier normalisé initial
### 6.022. Version `0.5.4` — Modèle métier normalisé initial
Réalisé :
- ajouter les tables de référence métier pour les DEX, tokens, pools et paires,
@@ -364,7 +364,7 @@ Réalisé :
- préparer les relations entre tokens, pools, paires et listings,
- éviter que la détection technique `0.6.x` écrive directement dans des tables trop brutes ou ambiguës.
### 6.23. Version `0.5.5` — Activité métier normalisée
### 6.023. Version `0.5.5` — Activité métier normalisée
Réalisé :
- ajout des tables de swaps,
@@ -372,7 +372,7 @@ Réalisé :
- ajout des événements de mint et burn utiles au suivi des tokens,
- préparation de lhistorique métier nécessaire avant larrivée des connecteurs DEX complets.
### 6.24. Version `0.5.6` — Consolidation de la couche stockage
### 6.024. Version `0.5.6` — Consolidation de la couche stockage
Objectif : stabiliser le schéma avant la détection technique réelle.
À faire :
@@ -383,7 +383,7 @@ Objectif : stabiliser le schéma avant la détection technique réelle.
- durcir les relations, contraintes et index utiles,
- préparer une future compatibilité PostgreSQL sans casser lorganisation générale.
### 6.25. Version `0.6.0` — Pipeline de détection technique
### 6.025. Version `0.6.0` — Pipeline de détection technique
Objectif : relier les connecteurs RPC à la couche de stockage technique et métier.
À faire :
@@ -393,7 +393,7 @@ Objectif : relier les connecteurs RPC à la couche de stockage technique et mét
- éviter que les futurs watchers RPC écrivent directement dans la DB sans couche intermédiaire,
- préparer les prochaines étapes de détection technique on-chain / RPC.
### 6.26. Version `0.6.1` — Détection technique RPC
### 6.026. Version `0.6.1` — Détection technique RPC
Réalisé :
- ajout dun bridge `Solana WS notification -> pipeline de détection`,
@@ -401,27 +401,23 @@ Réalisé :
- génération dun candidat token quand une `programNotification` expose un mint SPL / Token-2022 en JSON parsé,
- préparation du branchement futur des watchers et règles RPC réelles sur une façade de détection unique.
### 6.27. Version `0.6.2` — Branchement `WsClient` vers la détection
Objectif : relier directement les notifications WS issues de `WsClient` au pipeline de détection.
À faire :
### 6.027. Version `0.6.2` — Branchement `WsClient` vers la détection
Réalisé :
- ajouter un relais interne de notifications WS vers la couche de détection,
- permettre à `WsClient` de forwarder les `JsonRpcWsNotification` vers un worker dédié,
- conserver le découplage entre transport WS et logique de détection,
- éviter de bloquer la boucle de lecture WS si la détection est lente.
### 6.28. Version `0.6.3` — Enrichissement des notifications WS utiles
Objectif : améliorer la couverture technique des notifications WS.
### 6.028. Version `0.6.3` — Enrichissement des notifications WS utiles
Réalisé :
À faire :
- enrichir `accountNotification`, `logsNotification`, `signatureNotification`,
- mieux extraire slot, pubkey, signature, owner, parsed account type,
- enrichir `accountNotification`, `logsNotification` et `signatureNotification`,
- mieux extraire slot, pubkey, signature, owner, parsed account type et clés pertinentes,
- produire des observations plus précises et plus homogènes,
- préparer les règles de détection techniques réelles.
### 6.29. Version `0.6.4` — Premières règles de détection technique
### 6.029. Version `0.6.4` — Premières règles de détection technique
Objectif : commencer la détection technique on-chain utile avant les connecteurs DEX dédiés.
À faire :
@@ -431,7 +427,17 @@ Objectif : commencer la détection technique on-chain utile avant les connecteur
- préparer lalimentation des tables métier normalisées,
- garder la logique encore indépendante des connecteurs DEX `0.7.x`.
### 6.30. Version `0.7.x` — DEX connectors v1
### 6.030. Version `0.6.5` — Orchestration multi-clients WebSocket
Objectif : préparer la gestion coordonnée de plusieurs `WsClient`.
À faire :
- introduire une abstraction `ws_pool.rs` ou `ws_manager.rs`,
- piloter plusieurs clients WS selon les rôles dendpoints configurés,
- centraliser le démarrage, larrêt, létat et les relais de notifications WS,
- préparer les futures politiques de répartition, supervision et reconnexion.
### 6.031. Version `0.7.x` — DEX connectors v1
Objectif : structurer les connecteurs par protocole.
Cibles initiales possibles :
@@ -450,7 +456,7 @@ Cibles initiales possibles :
- création de types métiers propres,
- enrichissement des métadonnées token/pool/pair.
### 6.31. Version `0.8.x` — Analyse et filtrage
### 6.032. Version `0.8.x` — Analyse et filtrage
Objectif : transformer les événements bruts en signaux exploitables.
À faire :
@@ -461,7 +467,7 @@ Objectif : transformer les événements bruts en signaux exploitables.
- statistiques de comportement,
- premiers patterns.
### 6.32. Version `1.x.y` — Wallets et swap préparatoire
### 6.033. Version `1.x.y` — Wallets et swap préparatoire
Objectif : préparer la couche daction.
À faire :
@@ -472,7 +478,7 @@ Objectif : préparer la couche daction.
- préparation dordres et de swaps,
- simulation et garde-fous.
### 6.33. Version `2.x.y` — Trading semi-automatisé
### 6.034. Version `2.x.y` — Trading semi-automatisé
Objectif : brancher lanalyse à laction tout en gardant des garde-fous explicites.
À faire :
@@ -483,7 +489,7 @@ Objectif : brancher lanalyse à laction tout en gardant des garde-fous exp
- confirmations explicites ou semi-automatiques,
- journaux dexécution.
### 6.34. Version `3.x.y` — Yellowstone gRPC
### 6.035. Version `3.x.y` — Yellowstone gRPC
Objectif : ajouter le connecteur gRPC dédié.
À faire :
@@ -568,9 +574,9 @@ Le projet doit maintenir au minimum :
## 12. Priorité immédiate
La priorité immédiate est désormais la suivante :
1. démarrer la version `0.6.2` avec le branchement `WsClient` vers la détection,
2. ajouter un relais asynchrone entre notifications WS et worker de détection,
3. éviter de bloquer la boucle de lecture du client WS,
4. préparer ensuite la version `0.6.3` pour enrichir les notifications WS utiles,
1. démarrer la version `0.6.3` avec lenrichissement des notifications WS utiles,
2. améliorer lextraction des métadonnées utiles depuis `accountNotification`, `logsNotification` et `signatureNotification`,
3. produire des observations on-chain plus précises et homogènes,
4. préparer ensuite la version `0.6.4` pour les premières règles de détection technique,
5. conserver le découplage entre transport, détection et stockage,
6. préparer enfin `0.6.4` pour les premières règles de détection technique.
6. planifier enfin `0.6.5` pour lorchestration multi-clients WebSocket via `ws_pool.rs` ou `ws_manager.rs`.

View File

@@ -73,10 +73,10 @@ impl KbSolanaWsDetectionService {
let observation_input = crate::KbDetectionObservationInput::new(
observation_kind,
crate::KbObservationSourceKind::WsRpc,
endpoint_name,
object_key,
endpoint_name.clone(),
object_key.clone(),
slot,
payload,
payload.clone(),
);
let observation_id_result = self
.persistence
@@ -86,6 +86,32 @@ impl KbSolanaWsDetectionService {
Ok(observation_id) => observation_id,
Err(error) => return Err(error),
};
let should_emit_signal = match notification.method.as_str() {
"accountNotification" => true,
"logsNotification" => true,
"signatureNotification" => true,
_ => false,
};
if should_emit_signal {
let signal_input = crate::KbDetectionSignalInput::new(
build_signal_kind_for_notification(
notification.method.as_str(),
&notification.params.result,
),
build_signal_severity_for_notification(
notification.method.as_str(),
&notification.params.result,
),
object_key,
Some(observation_id),
None,
payload,
);
let signal_result = self.persistence.record_signal(&signal_input).await;
if let Err(error) = signal_result {
return Err(error);
}
}
Ok(crate::KbSolanaWsDetectionOutcome::ObservationRecorded { observation_id })
}
@@ -114,9 +140,6 @@ impl KbSolanaWsDetectionService {
Some(parsed_type) => parsed_type,
None => return Ok(None),
};
if parsed_type.as_str() != "mint" {
return Ok(None);
}
let token_program_option = extract_account_owner(account_value);
let token_program = match token_program_option {
Some(token_program) => token_program,
@@ -127,13 +150,30 @@ impl KbSolanaWsDetectionService {
{
return Ok(None);
}
let decimals = extract_decimals_from_account_value(account_value);
let decimals = if parsed_type.as_str() == "mint" {
extract_decimals_from_account_value(account_value)
} else if parsed_type.as_str() == "account" {
extract_token_account_decimals_from_account_value(account_value)
} else {
None
};
let mint = if parsed_type.as_str() == "mint" {
pubkey.clone()
} else if parsed_type.as_str() == "account" {
let mint_option = extract_parsed_account_mint(account_value);
match mint_option {
Some(mint) => mint,
None => return Ok(None),
}
} else {
return Ok(None);
};
let slot =
extract_slot_from_result(notification.method.as_str(), &notification.params.result);
let payload = build_notification_payload(notification);
let is_quote_token = pubkey == crate::WSOL_MINT_ID.to_string();
let is_quote_token = mint == crate::WSOL_MINT_ID.to_string();
let input = crate::KbDetectionTokenCandidateInput::new(
pubkey,
mint,
None,
None,
decimals,
@@ -144,7 +184,11 @@ impl KbSolanaWsDetectionService {
slot,
"ws.program_notification".to_string(),
payload.clone(),
"signal.token_mint_account_detected".to_string(),
if parsed_type.as_str() == "mint" {
"signal.token_mint_account_detected".to_string()
} else {
"signal.token_account_detected".to_string()
},
crate::KbAnalysisSignalSeverity::Medium,
None,
Some(payload),
@@ -342,6 +386,235 @@ fn extract_decimals_from_account_value(
}
}
/// Extracts the owner program id from one program notification result.
fn extract_program_notification_owner(
result: &serde_json::Value,
) -> std::option::Option<std::string::String> {
let account_value_option = extract_account_value_from_result(result);
let account_value = match account_value_option {
Some(account_value) => account_value,
None => return None,
};
extract_account_owner(account_value)
}
/// Extracts the parsed token amount decimals from one parsed token account notification.
fn extract_token_account_decimals_from_account_value(
account_value: &serde_json::Value,
) -> std::option::Option<u8> {
let data_option = account_value.get("data");
let data = match data_option {
Some(data) => data,
None => return None,
};
let parsed_option = data.get("parsed");
let parsed = match parsed_option {
Some(parsed) => parsed,
None => return None,
};
let info_option = parsed.get("info");
let info = match info_option {
Some(info) => info,
None => return None,
};
let token_amount_option = info.get("tokenAmount");
let token_amount = match token_amount_option {
Some(token_amount) => token_amount,
None => return None,
};
let decimals_option = token_amount
.get("decimals")
.and_then(serde_json::Value::as_u64);
let decimals = match decimals_option {
Some(decimals) => decimals,
None => return None,
};
let convert_result = u8::try_from(decimals);
match convert_result {
Ok(decimals) => Some(decimals),
Err(_) => None,
}
}
/// Extracts a parsed account mint from one account-like JSON object.
fn extract_parsed_account_mint(
account_value: &serde_json::Value,
) -> std::option::Option<std::string::String> {
let data_option = account_value.get("data");
let data = match data_option {
Some(data) => data,
None => return None,
};
let parsed_option = data.get("parsed");
let parsed = match parsed_option {
Some(parsed) => parsed,
None => return None,
};
let info_option = parsed.get("info");
let info = match info_option {
Some(info) => info,
None => return None,
};
let mint_option = info.get("mint").and_then(serde_json::Value::as_str);
match mint_option {
Some(mint) => Some(mint.to_string()),
None => None,
}
}
/// Extracts log lines from one logs notification result.
fn extract_logs_lines(result: &serde_json::Value) -> std::vec::Vec<std::string::String> {
let mut lines = std::vec::Vec::new();
let value_option = result.get("value");
let value = match value_option {
Some(value) => value,
None => return lines,
};
let logs_option = value.get("logs");
let logs = match logs_option {
Some(logs) => logs,
None => return lines,
};
let array_option = logs.as_array();
let array = match array_option {
Some(array) => array,
None => return lines,
};
for item in array {
let line_option = item.as_str();
if let Some(line) = line_option {
lines.push(line.to_string());
}
}
lines
}
/// Extracts the error field from a signature notification result.
fn extract_signature_notification_err(
result: &serde_json::Value,
) -> std::option::Option<serde_json::Value> {
let value_option = result.get("value");
let value = match value_option {
Some(value) => value,
None => return None,
};
match value.get("err") {
Some(err) => Some(err.clone()),
None => None,
}
}
/// Builds a more specific signal kind for one WebSocket notification.
fn build_signal_kind_for_notification(
method: &str,
result: &serde_json::Value,
) -> std::string::String {
match method {
"accountNotification" => {
let account_value_option = extract_account_value_from_result(result);
if let Some(account_value) = account_value_option {
let parsed_type_option = extract_parsed_account_type(account_value);
if let Some(parsed_type) = parsed_type_option {
return format!("signal.account_notification.{parsed_type}");
}
let owner_option = extract_account_owner(account_value);
if let Some(owner) = owner_option {
if owner == crate::SPL_TOKEN_PROGRAM_ID.to_string() {
return "signal.account_notification.spl_token".to_string();
}
if owner == crate::SPL_TOKEN_2022_PROGRAM_ID.to_string() {
return "signal.account_notification.spl_token_2022".to_string();
}
}
}
"signal.account_notification.generic".to_string()
}
"logsNotification" => {
let lines = extract_logs_lines(result);
for line in &lines {
if line.contains("Instruction: InitializeMint") {
return "signal.logs_notification.initialize_mint".to_string();
}
if line.contains("Instruction: MintTo") {
return "signal.logs_notification.mint_to".to_string();
}
if line.contains("Instruction: Burn") {
return "signal.logs_notification.burn".to_string();
}
if line.contains("Instruction: InitializeAccount") {
return "signal.logs_notification.initialize_account".to_string();
}
}
"signal.logs_notification.generic".to_string()
}
"signatureNotification" => {
let err_option = extract_signature_notification_err(result);
match err_option {
Some(err) => {
if err.is_null() {
"signal.signature_notification.confirmed".to_string()
} else {
"signal.signature_notification.failed".to_string()
}
}
None => "signal.signature_notification.generic".to_string(),
}
}
"programNotification" => {
let owner_option = extract_program_notification_owner(result);
let owner = match owner_option {
Some(owner) => owner,
None => return "signal.program_notification.generic".to_string(),
};
if owner == crate::SPL_TOKEN_PROGRAM_ID.to_string() {
return "signal.program_notification.spl_token".to_string();
}
if owner == crate::SPL_TOKEN_2022_PROGRAM_ID.to_string() {
return "signal.program_notification.spl_token_2022".to_string();
}
"signal.program_notification.generic".to_string()
}
_ => format!(
"signal.{}",
method.replace("Notification", "").to_lowercase()
),
}
}
/// Builds a more specific severity for one WebSocket notification.
fn build_signal_severity_for_notification(
method: &str,
result: &serde_json::Value,
) -> crate::KbAnalysisSignalSeverity {
match method {
"programNotification" => crate::KbAnalysisSignalSeverity::Medium,
"accountNotification" => crate::KbAnalysisSignalSeverity::Low,
"logsNotification" => {
let lines = extract_logs_lines(result);
for line in &lines {
if line.contains("Instruction: InitializeMint") {
return crate::KbAnalysisSignalSeverity::Medium;
}
}
crate::KbAnalysisSignalSeverity::Low
}
"signatureNotification" => {
let err_option = extract_signature_notification_err(result);
match err_option {
Some(err) => {
if err.is_null() {
crate::KbAnalysisSignalSeverity::Low
} else {
crate::KbAnalysisSignalSeverity::Medium
}
}
None => crate::KbAnalysisSignalSeverity::Low,
}
}
_ => crate::KbAnalysisSignalSeverity::Low,
}
}
#[cfg(test)]
mod tests {
async fn create_database() -> crate::KbDatabase {
@@ -409,6 +682,46 @@ mod tests {
}
}
fn build_logs_notification() -> crate::KbJsonRpcWsNotification {
crate::KbJsonRpcWsNotification {
jsonrpc: "2.0".to_string(),
method: "logsNotification".to_string(),
params: crate::KbJsonRpcWsNotificationParams {
result: serde_json::json!({
"context": {
"slot": 888888_u64
},
"value": {
"signature": "Sig111111111111111111111111111111111111111111111111",
"err": null,
"logs": [
"Program log: Instruction: InitializeMint"
]
}
}),
subscription: 3001_u64,
},
}
}
fn build_signature_notification() -> crate::KbJsonRpcWsNotification {
crate::KbJsonRpcWsNotification {
jsonrpc: "2.0".to_string(),
method: "signatureNotification".to_string(),
params: crate::KbJsonRpcWsNotificationParams {
result: serde_json::json!({
"context": {
"slot": 999999_u64
},
"value": {
"err": null
}
}),
subscription: 4001_u64,
},
}
}
#[tokio::test]
async fn slot_notification_records_observation() {
let database = create_database().await;
@@ -500,4 +813,90 @@ mod tests {
"Mint111111111111111111111111111111111111111"
);
}
#[tokio::test]
async fn logs_notification_records_observation_and_signal() {
let database = create_database().await;
let persistence = crate::KbDetectionPersistenceService::new(std::sync::Arc::new(database));
let detector = crate::KbSolanaWsDetectionService::new(persistence);
let outcome_result = detector
.process_notification(
Some("helius_primary_ws_programs".to_string()),
&build_logs_notification(),
)
.await;
let outcome = match outcome_result {
Ok(outcome) => outcome,
Err(error) => panic!("process_notification failed: {error}"),
};
match outcome {
crate::KbSolanaWsDetectionOutcome::ObservationRecorded { observation_id } => {
assert!(observation_id > 0);
}
_ => panic!("unexpected detection outcome"),
}
let observations_result =
crate::list_recent_onchain_observations(detector.persistence().database().as_ref(), 10)
.await;
let observations = match observations_result {
Ok(observations) => observations,
Err(error) => panic!("list_recent_onchain_observations failed: {error}"),
};
let signals_result =
crate::list_recent_analysis_signals(detector.persistence().database().as_ref(), 10)
.await;
let signals = match signals_result {
Ok(signals) => signals,
Err(error) => panic!("list_recent_analysis_signals failed: {error}"),
};
assert_eq!(observations.len(), 1);
assert_eq!(signals.len(), 1);
assert_eq!(
signals[0].signal_kind,
"signal.logs_notification.initialize_mint"
);
}
#[tokio::test]
async fn signature_notification_records_observation_and_signal() {
let database = create_database().await;
let persistence = crate::KbDetectionPersistenceService::new(std::sync::Arc::new(database));
let detector = crate::KbSolanaWsDetectionService::new(persistence);
let outcome_result = detector
.process_notification(
Some("mainnet_public_ws_slots".to_string()),
&build_signature_notification(),
)
.await;
let outcome = match outcome_result {
Ok(outcome) => outcome,
Err(error) => panic!("process_notification failed: {error}"),
};
match outcome {
crate::KbSolanaWsDetectionOutcome::ObservationRecorded { observation_id } => {
assert!(observation_id > 0);
}
_ => panic!("unexpected detection outcome"),
}
let observations_result =
crate::list_recent_onchain_observations(detector.persistence().database().as_ref(), 10)
.await;
let observations = match observations_result {
Ok(observations) => observations,
Err(error) => panic!("list_recent_onchain_observations failed: {error}"),
};
let signals_result =
crate::list_recent_analysis_signals(detector.persistence().database().as_ref(), 10)
.await;
let signals = match signals_result {
Ok(signals) => signals,
Err(error) => panic!("list_recent_analysis_signals failed: {error}"),
};
assert_eq!(observations.len(), 1);
assert_eq!(signals.len(), 1);
assert_eq!(
signals[0].signal_kind,
"signal.signature_notification.confirmed"
);
}
}