use { crate::{ rpc_config::{ RpcAccountInfoConfig, RpcBlockSubscribeConfig, RpcBlockSubscribeFilter, RpcProgramAccountsConfig, RpcSignatureSubscribeConfig, RpcTransactionLogsConfig, RpcTransactionLogsFilter, }, rpc_response::{ Response as RpcResponse, RpcBlockUpdate, RpcKeyedAccount, RpcLogsResponse, RpcSignatureResult, RpcVote, SlotInfo, SlotUpdate, }, }, crossbeam_channel::{unbounded, Receiver, Sender}, log::*, serde::de::DeserializeOwned, serde_json::{ json, value::Value::{Number, Object}, Map, Value, }, solana_account_decoder::UiAccount, solana_sdk::{clock::Slot, pubkey::Pubkey, signature::Signature}, std::{ marker::PhantomData, net::TcpStream, sync::{ atomic::{AtomicBool, Ordering}, Arc, RwLock, }, thread::{sleep, JoinHandle}, time::Duration, }, thiserror::Error, tungstenite::{connect, stream::MaybeTlsStream, Message, WebSocket}, url::{ParseError, Url}, }; #[derive(Debug, Error)] pub enum PubsubClientError { #[error("url parse error")] UrlParseError(#[from] ParseError), #[error("unable to connect to server")] ConnectionError(#[from] tungstenite::Error), #[error("json parse error")] JsonParseError(#[from] serde_json::error::Error), #[error("unexpected message format: {0}")] UnexpectedMessageError(String), } pub struct PubsubClientSubscription where T: DeserializeOwned, { message_type: PhantomData, operation: &'static str, socket: Arc>>>, subscription_id: u64, t_cleanup: Option>, exit: Arc, } impl Drop for PubsubClientSubscription where T: DeserializeOwned, { fn drop(&mut self) { self.send_unsubscribe() .unwrap_or_else(|_| warn!("unable to unsubscribe from websocket")); self.socket .write() .unwrap() .close(None) .unwrap_or_else(|_| warn!("unable to close websocket")); } } impl PubsubClientSubscription where T: DeserializeOwned, { fn send_subscribe( writable_socket: &Arc>>>, body: String, ) -> Result { writable_socket .write() .unwrap() .write_message(Message::Text(body))?; let message = writable_socket.write().unwrap().read_message()?; Self::extract_subscription_id(message) } fn extract_subscription_id(message: Message) -> Result { let message_text = &message.into_text()?; let json_msg: Map = serde_json::from_str(message_text)?; if let Some(Number(x)) = json_msg.get("result") { if let Some(x) = x.as_u64() { return Ok(x); } } // TODO: Add proper JSON RPC response/error handling... Err(PubsubClientError::UnexpectedMessageError(format!( "{:?}", json_msg ))) } pub fn send_unsubscribe(&self) -> Result<(), PubsubClientError> { let method = format!("{}Unsubscribe", self.operation); self.socket .write() .unwrap() .write_message(Message::Text( json!({ "jsonrpc":"2.0","id":1,"method":method,"params":[self.subscription_id] }) .to_string(), )) .map_err(|err| err.into()) } fn read_message( writable_socket: &Arc>>>, ) -> Result { let message = writable_socket.write().unwrap().read_message()?; let message_text = &message.into_text().unwrap(); let json_msg: Map = serde_json::from_str(message_text)?; if let Some(Object(params)) = json_msg.get("params") { if let Some(result) = params.get("result") { let x: T = serde_json::from_value::(result.clone()).unwrap(); return Ok(x); } } // TODO: Add proper JSON RPC response/error handling... Err(PubsubClientError::UnexpectedMessageError(format!( "{:?}", json_msg ))) } pub fn shutdown(&mut self) -> std::thread::Result<()> { if self.t_cleanup.is_some() { info!("websocket thread - shutting down"); self.exit.store(true, Ordering::Relaxed); let x = self.t_cleanup.take().unwrap().join(); info!("websocket thread - shut down."); x } else { warn!("websocket thread - already shut down."); Ok(()) } } } pub type PubsubLogsClientSubscription = PubsubClientSubscription>; pub type LogsSubscription = ( PubsubLogsClientSubscription, Receiver>, ); pub type PubsubSlotClientSubscription = PubsubClientSubscription; pub type SlotsSubscription = (PubsubSlotClientSubscription, Receiver); pub type PubsubSignatureClientSubscription = PubsubClientSubscription>; pub type SignatureSubscription = ( PubsubSignatureClientSubscription, Receiver>, ); pub type PubsubBlockClientSubscription = PubsubClientSubscription>; pub type BlockSubscription = ( PubsubBlockClientSubscription, Receiver>, ); pub type PubsubProgramClientSubscription = PubsubClientSubscription>; pub type ProgramSubscription = ( PubsubProgramClientSubscription, Receiver>, ); pub type PubsubAccountClientSubscription = PubsubClientSubscription>; pub type AccountSubscription = ( PubsubAccountClientSubscription, Receiver>, ); pub type PubsubVoteClientSubscription = PubsubClientSubscription; pub type VoteSubscription = (PubsubVoteClientSubscription, Receiver); pub type PubsubRootClientSubscription = PubsubClientSubscription; pub type RootSubscription = (PubsubRootClientSubscription, Receiver); pub struct PubsubClient {} fn connect_with_retry( url: Url, ) -> Result>, tungstenite::Error> { let mut connection_retries = 5; loop { let result = connect(url.clone()).map(|(socket, _)| socket); if let Err(tungstenite::Error::Http(response)) = &result { if response.status() == reqwest::StatusCode::TOO_MANY_REQUESTS && connection_retries > 0 { let mut duration = Duration::from_millis(500); if let Some(retry_after) = response.headers().get(reqwest::header::RETRY_AFTER) { if let Ok(retry_after) = retry_after.to_str() { if let Ok(retry_after) = retry_after.parse::() { if retry_after < 120 { duration = Duration::from_secs(retry_after); } } } } connection_retries -= 1; debug!( "Too many requests: server responded with {:?}, {} retries left, pausing for {:?}", response, connection_retries, duration ); sleep(duration); continue; } } return result; } } impl PubsubClient { pub fn account_subscribe( url: &str, pubkey: &Pubkey, config: Option, ) -> Result { let url = Url::parse(url)?; let socket = connect_with_retry(url)?; let (sender, receiver) = unbounded(); let socket = Arc::new(RwLock::new(socket)); let socket_clone = socket.clone(); let exit = Arc::new(AtomicBool::new(false)); let exit_clone = exit.clone(); let body = json!({ "jsonrpc":"2.0", "id":1, "method":"accountSubscribe", "params":[ pubkey.to_string(), config ] }) .to_string(); let subscription_id = PubsubAccountClientSubscription::send_subscribe(&socket_clone, body)?; let t_cleanup = std::thread::spawn(move || { Self::cleanup_with_sender(exit_clone, &socket_clone, sender) }); let result = PubsubClientSubscription { message_type: PhantomData, operation: "account", socket, subscription_id, t_cleanup: Some(t_cleanup), exit, }; Ok((result, receiver)) } pub fn block_subscribe( url: &str, filter: RpcBlockSubscribeFilter, config: Option, ) -> Result { let url = Url::parse(url)?; let socket = connect_with_retry(url)?; let (sender, receiver) = unbounded(); let socket = Arc::new(RwLock::new(socket)); let socket_clone = socket.clone(); let exit = Arc::new(AtomicBool::new(false)); let exit_clone = exit.clone(); let body = json!({ "jsonrpc":"2.0", "id":1, "method":"blockSubscribe", "params":[filter, config] }) .to_string(); let subscription_id = PubsubBlockClientSubscription::send_subscribe(&socket_clone, body)?; let t_cleanup = std::thread::spawn(move || { Self::cleanup_with_sender(exit_clone, &socket_clone, sender) }); let result = PubsubClientSubscription { message_type: PhantomData, operation: "block", socket, subscription_id, t_cleanup: Some(t_cleanup), exit, }; Ok((result, receiver)) } pub fn logs_subscribe( url: &str, filter: RpcTransactionLogsFilter, config: RpcTransactionLogsConfig, ) -> Result { let url = Url::parse(url)?; let socket = connect_with_retry(url)?; let (sender, receiver) = unbounded(); let socket = Arc::new(RwLock::new(socket)); let socket_clone = socket.clone(); let exit = Arc::new(AtomicBool::new(false)); let exit_clone = exit.clone(); let body = json!({ "jsonrpc":"2.0", "id":1, "method":"logsSubscribe", "params":[filter, config] }) .to_string(); let subscription_id = PubsubLogsClientSubscription::send_subscribe(&socket_clone, body)?; let t_cleanup = std::thread::spawn(move || { Self::cleanup_with_sender(exit_clone, &socket_clone, sender) }); let result = PubsubClientSubscription { message_type: PhantomData, operation: "logs", socket, subscription_id, t_cleanup: Some(t_cleanup), exit, }; Ok((result, receiver)) } pub fn program_subscribe( url: &str, pubkey: &Pubkey, config: Option, ) -> Result { let url = Url::parse(url)?; let socket = connect_with_retry(url)?; let (sender, receiver) = unbounded(); let socket = Arc::new(RwLock::new(socket)); let socket_clone = socket.clone(); let exit = Arc::new(AtomicBool::new(false)); let exit_clone = exit.clone(); let body = json!({ "jsonrpc":"2.0", "id":1, "method":"programSubscribe", "params":[ pubkey.to_string(), config ] }) .to_string(); let subscription_id = PubsubProgramClientSubscription::send_subscribe(&socket_clone, body)?; let t_cleanup = std::thread::spawn(move || { Self::cleanup_with_sender(exit_clone, &socket_clone, sender) }); let result = PubsubClientSubscription { message_type: PhantomData, operation: "program", socket, subscription_id, t_cleanup: Some(t_cleanup), exit, }; Ok((result, receiver)) } pub fn vote_subscribe(url: &str) -> Result { let url = Url::parse(url)?; let socket = connect_with_retry(url)?; let (sender, receiver) = unbounded(); let socket = Arc::new(RwLock::new(socket)); let socket_clone = socket.clone(); let exit = Arc::new(AtomicBool::new(false)); let exit_clone = exit.clone(); let body = json!({ "jsonrpc":"2.0", "id":1, "method":"voteSubscribe", }) .to_string(); let subscription_id = PubsubVoteClientSubscription::send_subscribe(&socket_clone, body)?; let t_cleanup = std::thread::spawn(move || { Self::cleanup_with_sender(exit_clone, &socket_clone, sender) }); let result = PubsubClientSubscription { message_type: PhantomData, operation: "vote", socket, subscription_id, t_cleanup: Some(t_cleanup), exit, }; Ok((result, receiver)) } pub fn root_subscribe(url: &str) -> Result { let url = Url::parse(url)?; let socket = connect_with_retry(url)?; let (sender, receiver) = unbounded(); let socket = Arc::new(RwLock::new(socket)); let socket_clone = socket.clone(); let exit = Arc::new(AtomicBool::new(false)); let exit_clone = exit.clone(); let body = json!({ "jsonrpc":"2.0", "id":1, "method":"rootSubscribe", }) .to_string(); let subscription_id = PubsubRootClientSubscription::send_subscribe(&socket_clone, body)?; let t_cleanup = std::thread::spawn(move || { Self::cleanup_with_sender(exit_clone, &socket_clone, sender) }); let result = PubsubClientSubscription { message_type: PhantomData, operation: "root", socket, subscription_id, t_cleanup: Some(t_cleanup), exit, }; Ok((result, receiver)) } pub fn signature_subscribe( url: &str, signature: &Signature, config: Option, ) -> Result { let url = Url::parse(url)?; let socket = connect_with_retry(url)?; let (sender, receiver) = unbounded(); let socket = Arc::new(RwLock::new(socket)); let socket_clone = socket.clone(); let exit = Arc::new(AtomicBool::new(false)); let exit_clone = exit.clone(); let body = json!({ "jsonrpc":"2.0", "id":1, "method":"signatureSubscribe", "params":[ signature.to_string(), config ] }) .to_string(); let subscription_id = PubsubSignatureClientSubscription::send_subscribe(&socket_clone, body)?; let t_cleanup = std::thread::spawn(move || { Self::cleanup_with_sender(exit_clone, &socket_clone, sender) }); let result = PubsubClientSubscription { message_type: PhantomData, operation: "signature", socket, subscription_id, t_cleanup: Some(t_cleanup), exit, }; Ok((result, receiver)) } pub fn slot_subscribe(url: &str) -> Result { let url = Url::parse(url)?; let socket = connect_with_retry(url)?; let (sender, receiver) = unbounded::(); let socket = Arc::new(RwLock::new(socket)); let socket_clone = socket.clone(); let exit = Arc::new(AtomicBool::new(false)); let exit_clone = exit.clone(); let body = json!({ "jsonrpc":"2.0", "id":1, "method":"slotSubscribe", "params":[] }) .to_string(); let subscription_id = PubsubSlotClientSubscription::send_subscribe(&socket_clone, body)?; let t_cleanup = std::thread::spawn(move || { Self::cleanup_with_sender(exit_clone, &socket_clone, sender) }); let result = PubsubClientSubscription { message_type: PhantomData, operation: "slot", socket, subscription_id, t_cleanup: Some(t_cleanup), exit, }; Ok((result, receiver)) } pub fn slot_updates_subscribe( url: &str, handler: impl Fn(SlotUpdate) + Send + 'static, ) -> Result, PubsubClientError> { let url = Url::parse(url)?; let socket = connect_with_retry(url)?; let socket = Arc::new(RwLock::new(socket)); let socket_clone = socket.clone(); let exit = Arc::new(AtomicBool::new(false)); let exit_clone = exit.clone(); let body = json!({ "jsonrpc":"2.0", "id":1, "method":"slotsUpdatesSubscribe", "params":[] }) .to_string(); let subscription_id = PubsubSlotClientSubscription::send_subscribe(&socket, body)?; let t_cleanup = std::thread::spawn(move || { Self::cleanup_with_handler(exit_clone, &socket_clone, handler) }); Ok(PubsubClientSubscription { message_type: PhantomData, operation: "slotsUpdates", socket, subscription_id, t_cleanup: Some(t_cleanup), exit, }) } fn cleanup_with_sender( exit: Arc, socket: &Arc>>>, sender: Sender, ) where T: DeserializeOwned + Send + 'static, { let handler = move |message| match sender.send(message) { Ok(_) => (), Err(err) => { info!("receive error: {:?}", err); } }; Self::cleanup_with_handler(exit, socket, handler); } fn cleanup_with_handler( exit: Arc, socket: &Arc>>>, handler: F, ) where T: DeserializeOwned, F: Fn(T) + Send + 'static, { loop { if exit.load(Ordering::Relaxed) { break; } match PubsubClientSubscription::read_message(socket) { Ok(message) => handler(message), Err(err) => { info!("receive error: {:?}", err); break; } } } info!("websocket - exited receive loop"); } } #[cfg(test)] mod tests { // see client-test/test/client.rs }