use crate::{provider::ProviderError, JsonRpcClient}; use async_trait::async_trait; use async_tungstenite::tungstenite::{self, protocol::Message}; use futures_util::{ lock::Mutex, sink::{Sink, SinkExt}, stream::{Stream, StreamExt}, }; use serde::{Deserialize, Serialize}; use std::fmt::Debug; use std::sync::atomic::{AtomicU64, Ordering}; use thiserror::Error; use super::common::{JsonRpcError, Request, ResponseData}; // Convenience methods for connecting with async-std/tokio: #[cfg(any(feature = "tokio-runtime", feature = "async-std-runtime"))] use async_tungstenite::WebSocketStream; // connect_async #[cfg(all(feature = "async-std-runtime", not(feature = "tokio-runtime")))] use async_tungstenite::async_std::connect_async; #[cfg(feature = "tokio-runtime")] use async_tungstenite::tokio::{connect_async, TokioAdapter}; #[cfg(feature = "tokio-runtime")] type TcpStream = TokioAdapter; #[cfg(all(feature = "async-std-runtime", not(feature = "tokio-runtime")))] type TcpStream = async_std::net::TcpStream; // If there is no TLS, just use the TCP Stream #[cfg(all(feature = "tokio-runtime", not(feature = "tokio-tls")))] pub type MaybeTlsStream = TcpStream; #[cfg(all(feature = "async-std-runtime", not(feature = "async-std-tls")))] pub type MaybeTlsStream = TcpStream; // Use either #[cfg(feature = "tokio-tls")] type TlsStream = real_tokio_native_tls::TlsStream; #[cfg(all(feature = "async-std-tls", not(feature = "tokio-tls")))] type TlsStream = async_tls::client::TlsStream; #[cfg(any(feature = "tokio-tls", feature = "async-std-tls"))] pub use async_tungstenite::stream::Stream as StreamSwitcher; #[cfg(feature = "tokio-tls")] pub type MaybeTlsStream = StreamSwitcher>>>; #[cfg(all(feature = "async-std-tls", not(feature = "tokio-tls")))] pub type MaybeTlsStream = StreamSwitcher>; /// A JSON-RPC Client over Websockets. /// /// If the library is not compiled with any runtime support, then you will have /// to manually instantiate a websocket connection and call `Provider::new` on it. /// /// ```ignore /// use ethers::providers::Ws; /// /// let ws = Ws::new(...) /// ``` /// /// If you have compiled the library with any of the following features, you may /// instantiate the websocket instance with the `connect` call and your URL: /// - `tokio-runtime`: Uses `tokio` as the runtime /// - `tokio-tls`: Same as `tokio-runtime` but with TLS support /// - `async-std-runtime`: Uses `async-std-runtime` /// - `async-tls`: Same as `async-std-runtime` but with TLS support /// /// ```no_run /// # #[cfg(any( /// # feature = "tokio-runtime", /// # feature = "tokio-tls", /// # feature = "async-std-runtime", /// # feature = "async-std-tls", /// # ))] /// # async fn foo() -> Result<(), Box> { /// use ethers::providers::Ws; /// /// let ws = Ws::connect("ws://localhost:8545").await?; /// /// // If built with TLS support (otherwise will get a "TLS Support not compiled in" error) /// let ws = Ws::connect("wss://localhost:8545").await?; /// # Ok(()) /// # } /// ``` /// /// This feature is built using [`async-tungstenite`](https://docs.rs/async-tungstenite). If you need other runtimes, /// consider importing `async-tungstenite` with the [corresponding feature /// flag](https://github.com/sdroege/async-tungstenite/blob/master/Cargo.toml#L15-L22) /// for your runtime. pub struct Provider { id: AtomicU64, ws: Mutex, } #[cfg(any(feature = "tokio-runtime", feature = "async-std-runtime"))] impl Provider> { /// Initializes a new WebSocket Client. /// separately. pub async fn connect( url: impl tungstenite::client::IntoClientRequest + Unpin, ) -> Result { let (ws, _) = connect_async(url).await?; Ok(Self::new(ws)) } } impl Provider where S: Send + Sync + Stream> + Sink + Unpin, { /// Initializes a new WebSocket Client. The websocket connection must be initiated /// separately. pub fn new(ws: S) -> Self { Self { id: AtomicU64::new(0), ws: Mutex::new(ws), } } } #[derive(Error, Debug)] /// Error thrown when sending a WS message pub enum ClientError { /// Thrown if deserialization failed #[error(transparent)] JsonError(#[from] serde_json::Error), #[error(transparent)] /// Thrown if the response could not be parsed JsonRpcError(#[from] JsonRpcError), /// Thrown if the websocket didn't respond to our message #[error("Websocket connection did not respond with data")] NoResponse, /// Thrown if there's an error over the WS connection #[error(transparent)] TungsteniteError(#[from] tungstenite::Error), } impl From for ProviderError { fn from(src: ClientError) -> Self { ProviderError::JsonRpcClientError(Box::new(src)) } } #[async_trait] impl JsonRpcClient for Provider where S: Send + Sync + Stream> + Sink + Unpin, { type Error = ClientError; /// Sends a POST request with the provided method and the params serialized as JSON /// over WebSockets async fn request Deserialize<'a>>( &self, method: &str, params: T, ) -> Result { // we get a lock on the websocket to avoid race conditions with multiple borrows let mut lock = self.ws.lock().await; let next_id = self.id.load(Ordering::SeqCst) + 1; self.id.store(next_id, Ordering::SeqCst); // send the message let payload = serde_json::to_string(&Request::new(next_id, method, params))?; lock.send(Message::text(payload)).await?; // get the response bytes let resp = lock.next().await.ok_or(ClientError::NoResponse)??; let data: ResponseData = match resp { Message::Text(inner) => serde_json::from_str(&inner)?, Message::Binary(inner) => serde_json::from_slice(&inner)?, // TODO: Should we do something if we receive a Ping, Pong or Close? _ => return Err(ClientError::NoResponse), }; Ok(data.into_result()?) } }