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.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.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.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] [workspace.package]
version = "0.6.2" version = "0.6.3"
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

@@ -140,7 +140,7 @@ Le tracing est centralisé dans `kb_lib`.
## 6. Phasage par versions ## 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. Objectif : corriger le squelette et poser la base de travail.
Réalisé : Réalisé :
@@ -155,7 +155,7 @@ Réalisé :
- documentation de `kb_app/src/splash.rs`, - documentation de `kb_app/src/splash.rs`,
- UI Tauri minimale. - 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. Objectif : construire un vrai `WsClient` asynchrone clonable.
Réalisé : Réalisé :
@@ -169,7 +169,7 @@ Réalisé :
- fermeture avec timeout, - fermeture avec timeout,
- tests offline avec serveur mock. - 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. Objectif : valider le transport via lapplication desktop.
Réalisé : Réalisé :
@@ -179,7 +179,7 @@ Réalisé :
- zone de logs, - zone de logs,
- validation du flux `frontend -> tauri -> kb_lib -> frontend`. - validation du flux `frontend -> tauri -> kb_lib -> frontend`.
### 6.4. Version `0.2.0` — Couche JSON-RPC WS Solana ### 6.004. Version `0.2.0` — Couche JSON-RPC WS Solana
Objectif : séparer clairement transport, réponses RPC et notifications. Objectif : séparer clairement transport, réponses RPC et notifications.
Réalisé : Réalisé :
@@ -190,7 +190,7 @@ Réalisé :
- parsing des notifications, - parsing des notifications,
- premiers helpers JSON-RPC sur `WsClient`. - 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. Objectif : fiabiliser la gestion des subscriptions.
Réalisé : Réalisé :
@@ -202,7 +202,7 @@ Réalisé :
- purge locale si nécessaire, - purge locale si nécessaire,
- routage séparé des notifications. - 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. Objectif : ajouter les helpers haut niveau correspondant aux principales méthodes PubSub Solana.
Réalisé : Réalisé :
@@ -211,7 +211,7 @@ Réalisé :
- helpers dunsubscribe correspondants, - helpers dunsubscribe correspondants,
- premiers tests de validation des noms de méthodes. - premiers tests de validation des noms de méthodes.
### 6.7. Version `0.3.2` — Helpers typed et notifications typed ### 6.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. Objectif : sappuyer principalement sur `solana-rpc-client-api` pour typer les subscribe et les notifications.
Réalisé : Réalisé :
@@ -220,7 +220,7 @@ Réalisé :
- parsing typed des notifications, - parsing typed des notifications,
- base de travail pour réduire lusage direct de `serde_json::Value`. - base de travail pour réduire lusage direct de `serde_json::Value`.
### 6.8. Version `0.3.3` — Distinction API typed / raw ### 6.008. Version `0.3.3` — Distinction API typed / raw
Objectif : clarifier lAPI publique de `WsClient`. Objectif : clarifier lAPI publique de `WsClient`.
Réalisé : Réalisé :
@@ -229,7 +229,7 @@ Réalisé :
- conservation des helpers typed comme interface plus propre, - conservation des helpers typed comme interface plus propre,
- préparation dune hiérarchie API plus explicite. - préparation dune hiérarchie API plus explicite.
### 6.9. Version `0.3.4` — Fenêtre `Demo Ws` dans `kb_app` ### 6.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. Objectif : tester manuellement les souscriptions live dans une fenêtre dédiée.
Réalisé : Réalisé :
@@ -241,7 +241,7 @@ Réalisé :
- affichage des événements raw et typed, - affichage des événements raw et typed,
- premiers tests réels sur `wss://api.mainnet.solana.com`. - premiers tests réels sur `wss://api.mainnet.solana.com`.
### 6.10. Version `0.3.5` — Stabilisation de `Demo Ws` ### 6.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. Objectif : rendre la fenêtre de démonstration robuste sous flux élevé et cohérente avec la configuration.
Réalisé : Réalisé :
@@ -255,11 +255,11 @@ Réalisé :
- conserver des compteurs et états UI exploitables, - conserver des compteurs et états UI exploitables,
- mieux gérer les fermetures/ralentissements dendpoints publics. - 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. 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é : Réalisé :
- client `reqwest` asynchrone clonable, - client `reqwest` asynchrone clonable,
@@ -280,7 +280,7 @@ Livrables :
- `getVersion` - `getVersion`
- `getSlot` - `getSlot`
### 6.13. Version `0.4.1` — Helpers HTTP Solana ### 6.013. Version `0.4.1` — Helpers HTTP Solana
Réalisé : Réalisé :
- ajouter des helpers HTTP haut niveau comme pour le client WS, - ajouter des helpers HTTP haut niveau comme pour le client WS,
@@ -288,7 +288,7 @@ Réalisé :
- couvrir les premières méthodes utiles du RPC HTTP Solana, - couvrir les premières méthodes utiles du RPC HTTP Solana,
- conserver `HttpClient` comme couche générique réutilisable. - conserver `HttpClient` comme couche générique réutilisable.
### 6.14. Version `0.4.2` — Politique HTTP avancée ### 6.014. Version `0.4.2` — Politique HTTP avancée
Réalisé : Réalisé :
- préparer un état de pause avant envoi pour un endpoint HTTP, - préparer un état de pause avant envoi pour un endpoint HTTP,
@@ -296,7 +296,7 @@ Réalisé :
- distinguer quota RPC général et quota `sendTransaction`, - distinguer quota RPC général et quota `sendTransaction`,
- préparer un futur pool dendpoints HTTP et larbitrage entre eux. - préparer un futur pool dendpoints HTTP et larbitrage entre eux.
### 6.15. Version `0.4.3` — Pool dendpoints HTTP ### 6.015. Version `0.4.3` — Pool dendpoints HTTP
Réalisé : Réalisé :
- ajouter un pool d`HttpClient`, - ajouter un pool d`HttpClient`,
@@ -306,7 +306,7 @@ Réalisé :
- prendre en compte la classe de méthode HTTP, - prendre en compte la classe de méthode HTTP,
- préparer le routage multi-RPC et la limitation de concurrence par endpoint. - préparer le routage multi-RPC et la limitation de concurrence par endpoint.
### 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é : Réalisé :
- ajout dune fenêtre `Demo Http`, - ajout dune fenêtre `Demo Http`,
@@ -317,10 +317,10 @@ Réalisé :
- alignement visuel de la fenêtre sur le gabarit `Demo Ws`, - alignement visuel de la fenêtre sur le gabarit `Demo Ws`,
- amélioration des presets UI, copie de réponse et bascule pretty/raw. - amélioration des presets UI, copie de réponse et bascule pretty/raw.
### 6.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. 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é : Réalisé :
- configuration DB dans `config.json`, - configuration DB dans `config.json`,
@@ -330,7 +330,7 @@ Réalisé :
- table `kb_db_metadata`, - table `kb_db_metadata`,
- séparation `db/entities`, `db/dtos`, `db/queries`, `db/types`. - séparation `db/entities`, `db/dtos`, `db/queries`, `db/types`.
### 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é : Réalisé :
- ajout des tables de référence pour les endpoints connus HTTP/WS, - 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, - 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. - 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é : Réalisé :
- ajout de la table `kb_observed_tokens`, - 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, - préparation des relations futures avec pools, paires et événements on-chain,
- conservation dunicité locale par mint sans duplication par endpoint. - 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é : Réalisé :
- conservation des événements runtime techniques via `kb_db_runtime_events`, - 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, - 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. - 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é : Réalisé :
- ajouter les tables de référence métier pour les DEX, tokens, pools et paires, - 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, - 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. - é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é : Réalisé :
- ajout des tables de swaps, - ajout des tables de swaps,
@@ -372,7 +372,7 @@ Réalisé :
- ajout des événements de mint et burn utiles au suivi des tokens, - 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. - 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. Objectif : stabiliser le schéma avant la détection technique réelle.
À faire : À faire :
@@ -383,7 +383,7 @@ 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.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. Objectif : relier les connecteurs RPC à la couche de stockage technique et métier.
À faire : À 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, - é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. - 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é : Réalisé :
- ajout dun bridge `Solana WS notification -> pipeline de détection`, - 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é, - 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. - 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 ### 6.027. Version `0.6.2` — Branchement `WsClient` vers la détection
Objectif : relier directement les notifications WS issues de `WsClient` au pipeline de détection. Réalisé :
À faire :
- ajouter un relais interne de notifications WS vers la couche de détection, - ajouter un relais interne de notifications WS vers la couche de détection,
- permettre à `WsClient` de forwarder les `JsonRpcWsNotification` vers un worker dédié, - permettre à `WsClient` de forwarder les `JsonRpcWsNotification` vers un worker dédié,
- conserver le découplage entre transport WS et logique de détection, - 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. - é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 ### 6.028. Version `0.6.3` — Enrichissement des notifications WS utiles
Objectif : améliorer la couverture technique des notifications WS. Réalisé :
À faire : - enrichir `accountNotification`, `logsNotification` et `signatureNotification`,
- mieux extraire slot, pubkey, signature, owner, parsed account type et clés pertinentes,
- enrichir `accountNotification`, `logsNotification`, `signatureNotification`,
- mieux extraire slot, pubkey, signature, owner, parsed account type,
- produire des observations plus précises et plus homogènes, - produire des observations plus précises et plus homogènes,
- préparer les règles de détection techniques réelles. - 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. Objectif : commencer la détection technique on-chain utile avant les connecteurs DEX dédiés.
À faire : À 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, - préparer lalimentation des tables métier normalisées,
- garder la logique encore indépendante des connecteurs DEX `0.7.x`. - 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. Objectif : structurer les connecteurs par protocole.
Cibles initiales possibles : Cibles initiales possibles :
@@ -450,7 +456,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.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. Objectif : transformer les événements bruts en signaux exploitables.
À faire : À faire :
@@ -461,7 +467,7 @@ Objectif : transformer les événements bruts en signaux exploitables.
- statistiques de comportement, - statistiques de comportement,
- premiers patterns. - 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. Objectif : préparer la couche daction.
À faire : À faire :
@@ -472,7 +478,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.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. Objectif : brancher lanalyse à laction tout en gardant des garde-fous explicites.
À faire : À faire :
@@ -483,7 +489,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.34. Version `3.x.y` — Yellowstone gRPC ### 6.035. Version `3.x.y` — Yellowstone gRPC
Objectif : ajouter le connecteur gRPC dédié. Objectif : ajouter le connecteur gRPC dédié.
À faire : À faire :
@@ -568,9 +574,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.6.2` avec le branchement `WsClient` vers la détection, 1. démarrer la version `0.6.3` avec lenrichissement des notifications WS utiles,
2. ajouter un relais asynchrone entre notifications WS et worker de détection, 2. améliorer lextraction des métadonnées utiles depuis `accountNotification`, `logsNotification` et `signatureNotification`,
3. éviter de bloquer la boucle de lecture du client WS, 3. produire des observations on-chain plus précises et homogènes,
4. préparer ensuite la version `0.6.3` pour enrichir les notifications WS utiles, 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, 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( let observation_input = crate::KbDetectionObservationInput::new(
observation_kind, observation_kind,
crate::KbObservationSourceKind::WsRpc, crate::KbObservationSourceKind::WsRpc,
endpoint_name, endpoint_name.clone(),
object_key, object_key.clone(),
slot, slot,
payload, payload.clone(),
); );
let observation_id_result = self let observation_id_result = self
.persistence .persistence
@@ -86,6 +86,32 @@ impl KbSolanaWsDetectionService {
Ok(observation_id) => observation_id, Ok(observation_id) => observation_id,
Err(error) => return Err(error), 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 }) Ok(crate::KbSolanaWsDetectionOutcome::ObservationRecorded { observation_id })
} }
@@ -114,9 +140,6 @@ impl KbSolanaWsDetectionService {
Some(parsed_type) => parsed_type, Some(parsed_type) => parsed_type,
None => return Ok(None), 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_option = extract_account_owner(account_value);
let token_program = match token_program_option { let token_program = match token_program_option {
Some(token_program) => token_program, Some(token_program) => token_program,
@@ -127,13 +150,30 @@ impl KbSolanaWsDetectionService {
{ {
return Ok(None); 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 = let slot =
extract_slot_from_result(notification.method.as_str(), &notification.params.result); extract_slot_from_result(notification.method.as_str(), &notification.params.result);
let payload = build_notification_payload(notification); 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( let input = crate::KbDetectionTokenCandidateInput::new(
pubkey, mint,
None, None,
None, None,
decimals, decimals,
@@ -144,7 +184,11 @@ impl KbSolanaWsDetectionService {
slot, slot,
"ws.program_notification".to_string(), "ws.program_notification".to_string(),
payload.clone(), 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, crate::KbAnalysisSignalSeverity::Medium,
None, None,
Some(payload), 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)] #[cfg(test)]
mod tests { mod tests {
async fn create_database() -> crate::KbDatabase { 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] #[tokio::test]
async fn slot_notification_records_observation() { async fn slot_notification_records_observation() {
let database = create_database().await; let database = create_database().await;
@@ -500,4 +813,90 @@ mod tests {
"Mint111111111111111111111111111111111111111" "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"
);
}
} }