diff --git a/CHANGELOG.md b/CHANGELOG.md index caa8476..cae8333 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,3 +6,4 @@ 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 de subscribe/unsubscribe hlpers à WsClient diff --git a/Cargo.toml b/Cargo.toml index b99e676..57980b2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ members = [ ] [workspace.package] -version = "0.3.0" +version = "0.3.1" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/khadhroony-bobobot" diff --git a/README.md b/README.md index 73692ca..ccb711e 100644 --- a/README.md +++ b/README.md @@ -244,21 +244,13 @@ Exemple déjà présent : ## 12. État du projet -Le dépôt est actuellement à un stade de fondation. - -Les premières priorités sont : - -- remettre le squelette en conformité, -- poser la configuration JSON, -- poser le tracing, -- préparer `WsClient` et `HttpClient`, -- brancher une UI Tauri minimale. +voir `CHANGELOG.md` ## 13. Feuille de route La feuille de route détaillée est disponible dans : -- `Roadmap.md` +- `ROADMAP.md` ## 14. Licence diff --git a/kb_lib/src/ws_client.rs b/kb_lib/src/ws_client.rs index ee5922d..a495b57 100644 --- a/kb_lib/src/ws_client.rs +++ b/kb_lib/src/ws_client.rs @@ -557,6 +557,171 @@ impl WsClient { } } + /// Subscribes to account change notifications. + pub async fn account_subscribe( + &self, + pubkey: std::string::String, + config: std::option::Option, + ) -> Result { + let params = kb_build_single_key_optional_config_params(pubkey, config); + self.send_json_rpc_request("accountSubscribe".to_string(), params) + .await + } + + /// Unsubscribes from an account change subscription. + pub async fn account_unsubscribe(&self, subscription_id: u64) -> Result { + self.send_json_rpc_request( + "accountUnsubscribe".to_string(), + vec![serde_json::Value::from(subscription_id)], + ) + .await + } + + /// Subscribes to block notifications. + /// + /// The Solana RPC documentation marks this subscription as unstable. + pub async fn block_subscribe( + &self, + filter: serde_json::Value, + config: std::option::Option, + ) -> Result { + let params = kb_build_first_value_optional_config_params(filter, config); + self.send_json_rpc_request("blockSubscribe".to_string(), params) + .await + } + + /// Unsubscribes from a block notification subscription. + pub async fn block_unsubscribe(&self, subscription_id: u64) -> Result { + self.send_json_rpc_request( + "blockUnsubscribe".to_string(), + vec![serde_json::Value::from(subscription_id)], + ) + .await + } + + /// Subscribes to transaction log notifications. + pub async fn logs_subscribe( + &self, + filter: serde_json::Value, + config: std::option::Option, + ) -> Result { + let params = kb_build_first_value_optional_config_params(filter, config); + self.send_json_rpc_request("logsSubscribe".to_string(), params) + .await + } + + /// Unsubscribes from a logs subscription. + pub async fn logs_unsubscribe(&self, subscription_id: u64) -> Result { + self.send_json_rpc_request( + "logsUnsubscribe".to_string(), + vec![serde_json::Value::from(subscription_id)], + ) + .await + } + + /// Subscribes to program-owned account notifications. + pub async fn program_subscribe( + &self, + program_id: std::string::String, + config: std::option::Option, + ) -> Result { + let params = kb_build_single_key_optional_config_params(program_id, config); + self.send_json_rpc_request("programSubscribe".to_string(), params) + .await + } + + /// Unsubscribes from a program subscription. + pub async fn program_unsubscribe(&self, subscription_id: u64) -> Result { + self.send_json_rpc_request( + "programUnsubscribe".to_string(), + vec![serde_json::Value::from(subscription_id)], + ) + .await + } + + /// Subscribes to root notifications. + pub async fn root_subscribe(&self) -> Result { + self.send_json_rpc_request("rootSubscribe".to_string(), std::vec::Vec::new()) + .await + } + + /// Unsubscribes from a root subscription. + pub async fn root_unsubscribe(&self, subscription_id: u64) -> Result { + self.send_json_rpc_request( + "rootUnsubscribe".to_string(), + vec![serde_json::Value::from(subscription_id)], + ) + .await + } + + /// Subscribes to one transaction signature status. + pub async fn signature_subscribe( + &self, + signature: std::string::String, + config: std::option::Option, + ) -> Result { + let params = kb_build_single_key_optional_config_params(signature, config); + self.send_json_rpc_request("signatureSubscribe".to_string(), params) + .await + } + + /// Unsubscribes from a signature subscription. + pub async fn signature_unsubscribe(&self, subscription_id: u64) -> Result { + self.send_json_rpc_request( + "signatureUnsubscribe".to_string(), + vec![serde_json::Value::from(subscription_id)], + ) + .await + } + + /// Subscribes to slot notifications. + pub async fn slot_subscribe(&self) -> Result { + self.send_json_rpc_request("slotSubscribe".to_string(), std::vec::Vec::new()) + .await + } + + /// Unsubscribes from a slot subscription. + pub async fn slot_unsubscribe(&self, subscription_id: u64) -> Result { + self.send_json_rpc_request( + "slotUnsubscribe".to_string(), + vec![serde_json::Value::from(subscription_id)], + ) + .await + } + + /// Subscribes to slot lifecycle update notifications. + pub async fn slots_updates_subscribe(&self) -> Result { + self.send_json_rpc_request("slotsUpdatesSubscribe".to_string(), std::vec::Vec::new()) + .await + } + + /// Unsubscribes from a slot lifecycle update subscription. + pub async fn slots_updates_unsubscribe( + &self, + subscription_id: u64, + ) -> Result { + self.send_json_rpc_request( + "slotsUpdatesUnsubscribe".to_string(), + vec![serde_json::Value::from(subscription_id)], + ) + .await + } + + /// Subscribes to vote notifications. + pub async fn vote_subscribe(&self) -> Result { + self.send_json_rpc_request("voteSubscribe".to_string(), std::vec::Vec::new()) + .await + } + + /// Unsubscribes from a vote subscription. + pub async fn vote_unsubscribe(&self, subscription_id: u64) -> Result { + self.send_json_rpc_request( + "voteUnsubscribe".to_string(), + vec![serde_json::Value::from(subscription_id)], + ) + .await + } + /// Initiates the close handshake. pub async fn send_close(&self) -> Result<(), crate::KbError> { self.send_message(WsOutgoingMessage::Close).await @@ -1223,7 +1388,6 @@ fn kb_build_pending_json_rpc_request( ) -> std::option::Option { let request_id_option = kb_json_value_to_u64(&request.id); let request_id = request_id_option?; - if kb_is_subscribe_method(&request.method) { let notification_method_option = kb_infer_notification_method_from_subscribe(&request.method); @@ -1261,6 +1425,28 @@ fn kb_build_pending_json_rpc_request( }) } +fn kb_build_single_key_optional_config_params( + key: std::string::String, + config: std::option::Option, +) -> std::vec::Vec { + let mut params = vec![serde_json::Value::String(key)]; + if let Some(config) = config { + params.push(config); + } + params +} + +fn kb_build_first_value_optional_config_params( + first: serde_json::Value, + config: std::option::Option, +) -> std::vec::Vec { + let mut params = vec![first]; + if let Some(config) = config { + params.push(config); + } + params +} + #[cfg(test)] mod tests { use futures_util::SinkExt; @@ -1738,4 +1924,105 @@ mod tests { client.disconnect().await.expect("disconnect must succeed"); server.shutdown().await; } + + #[tokio::test] + async fn helper_methods_send_expected_subscribe_method_names() { + let server = TestWsServer::spawn_json_rpc_server().await; + let endpoint = make_ws_endpoint(server.url.clone()); + let client = crate::WsClient::new(endpoint).expect("client creation must succeed"); + let mut receiver = client.subscribe_events(); + client.connect().await.expect("connect must succeed"); + let _ = recv_event(&mut receiver).await; + let _ = client + .account_subscribe( + "CM78CPUeXjn8o3yroDHxUtKsZZgoy4GPkPPXfouKNH12".to_string(), + Some(serde_json::json!({"encoding": "jsonParsed"})), + ) + .await + .expect("account_subscribe must succeed"); + let _ = client + .block_subscribe( + serde_json::json!("all"), + Some(serde_json::json!({"commitment": "confirmed"})), + ) + .await + .expect("block_subscribe must succeed"); + let _ = client + .logs_subscribe( + serde_json::json!("all"), + Some(serde_json::json!({"commitment": "finalized"})), + ) + .await + .expect("logs_subscribe must succeed"); + let _ = client + .program_subscribe( + "11111111111111111111111111111111".to_string(), + Some(serde_json::json!({"encoding": "base64"})), + ) + .await + .expect("program_subscribe must succeed"); + + let _ = client.root_subscribe().await.expect("root_subscribe must succeed"); + let _ = client + .signature_subscribe( + "2EBVM6cB8vAAD93Ktr6Vd8p67XPbQzCJX47MpReuiCXJAtcjaxpvWpcg9Ege1Nr5Tk3a2GFrByT7WPBjdsTycY9b".to_string(), + Some(serde_json::json!({"commitment": "confirmed"})), + ) + .await + .expect("signature_subscribe must succeed"); + let _ = client.slot_subscribe().await.expect("slot_subscribe must succeed"); + let _ = client + .slots_updates_subscribe() + .await + .expect("slots_updates_subscribe must succeed"); + let _ = client.vote_subscribe().await.expect("vote_subscribe must succeed"); + tokio::time::sleep(std::time::Duration::from_millis(150)).await; + let observed_methods = server.observed_methods_snapshot().await; + assert!(observed_methods.iter().any(|method| method == "accountSubscribe")); + assert!(observed_methods.iter().any(|method| method == "blockSubscribe")); + assert!(observed_methods.iter().any(|method| method == "logsSubscribe")); + assert!(observed_methods.iter().any(|method| method == "programSubscribe")); + assert!(observed_methods.iter().any(|method| method == "rootSubscribe")); + assert!(observed_methods.iter().any(|method| method == "signatureSubscribe")); + assert!(observed_methods.iter().any(|method| method == "slotSubscribe")); + assert!(observed_methods.iter().any(|method| method == "slotsUpdatesSubscribe")); + assert!(observed_methods.iter().any(|method| method == "voteSubscribe")); + client.disconnect().await.expect("disconnect must succeed"); + server.shutdown().await; + } + + #[tokio::test] + async fn helper_methods_send_expected_unsubscribe_method_names() { + let server = TestWsServer::spawn_json_rpc_server().await; + let endpoint = make_ws_endpoint(server.url.clone()); + let client = crate::WsClient::new(endpoint).expect("client creation must succeed"); + let mut receiver = client.subscribe_events(); + client.connect().await.expect("connect must succeed"); + let _ = recv_event(&mut receiver).await; + let _ = client.account_unsubscribe(10).await.expect("account_unsubscribe must succeed"); + let _ = client.block_unsubscribe(11).await.expect("block_unsubscribe must succeed"); + let _ = client.logs_unsubscribe(12).await.expect("logs_unsubscribe must succeed"); + let _ = client.program_unsubscribe(13).await.expect("program_unsubscribe must succeed"); + let _ = client.root_unsubscribe(14).await.expect("root_unsubscribe must succeed"); + let _ = client.signature_unsubscribe(15).await.expect("signature_unsubscribe must succeed"); + let _ = client.slot_unsubscribe(16).await.expect("slot_unsubscribe must succeed"); + let _ = client + .slots_updates_unsubscribe(17) + .await + .expect("slots_updates_unsubscribe must succeed"); + let _ = client.vote_unsubscribe(18).await.expect("vote_unsubscribe must succeed"); + tokio::time::sleep(std::time::Duration::from_millis(150)).await; + let observed_methods = server.observed_methods_snapshot().await; + assert!(observed_methods.iter().any(|method| method == "accountUnsubscribe")); + assert!(observed_methods.iter().any(|method| method == "blockUnsubscribe")); + assert!(observed_methods.iter().any(|method| method == "logsUnsubscribe")); + assert!(observed_methods.iter().any(|method| method == "programUnsubscribe")); + assert!(observed_methods.iter().any(|method| method == "rootUnsubscribe")); + assert!(observed_methods.iter().any(|method| method == "signatureUnsubscribe")); + assert!(observed_methods.iter().any(|method| method == "slotUnsubscribe")); + assert!(observed_methods.iter().any(|method| method == "slotsUpdatesUnsubscribe")); + assert!(observed_methods.iter().any(|method| method == "voteUnsubscribe")); + client.disconnect().await.expect("disconnect must succeed"); + server.shutdown().await; + } }