alloy_rpc_client/
builtin.rs

1use alloy_json_rpc::RpcError;
2use alloy_transport::{BoxTransport, TransportConnect, TransportError, TransportErrorKind};
3use std::str::FromStr;
4
5#[cfg(any(feature = "ws", feature = "ipc"))]
6use alloy_pubsub::PubSubConnect;
7
8/// Connection string for built-in transports.
9#[derive(Clone, Debug, PartialEq, Eq)]
10#[non_exhaustive]
11pub enum BuiltInConnectionString {
12    /// HTTP transport.
13    #[cfg(any(feature = "reqwest", feature = "hyper"))]
14    Http(url::Url),
15    /// WebSocket transport.
16    #[cfg(feature = "ws")]
17    Ws(url::Url, Option<alloy_transport::Authorization>),
18    /// IPC transport.
19    #[cfg(feature = "ipc")]
20    Ipc(std::path::PathBuf),
21}
22
23impl TransportConnect for BuiltInConnectionString {
24    fn is_local(&self) -> bool {
25        match self {
26            #[cfg(any(feature = "reqwest", feature = "hyper"))]
27            Self::Http(url) => alloy_transport::utils::guess_local_url(url),
28            #[cfg(feature = "ws")]
29            Self::Ws(url, _) => alloy_transport::utils::guess_local_url(url),
30            #[cfg(feature = "ipc")]
31            Self::Ipc(_) => true,
32            #[cfg(not(any(
33                feature = "reqwest",
34                feature = "hyper",
35                feature = "ws",
36                feature = "ipc"
37            )))]
38            _ => false,
39        }
40    }
41
42    async fn get_transport(&self) -> Result<BoxTransport, TransportError> {
43        self.connect_boxed().await
44    }
45}
46
47impl BuiltInConnectionString {
48    /// Connect with the given connection string.
49    ///
50    /// # Notes
51    ///
52    /// - If `hyper` feature is enabled
53    /// - WS will extract auth, however, auth is disabled for wasm.
54    pub async fn connect_boxed(&self) -> Result<BoxTransport, TransportError> {
55        // NB:
56        // HTTP match will always produce hyper if the feature is enabled.
57        // WS match arms are fall-through. Auth arm is disabled for wasm.
58        match self {
59            // reqwest is enabled, hyper is not
60            #[cfg(all(not(feature = "hyper"), feature = "reqwest"))]
61            Self::Http(url) => {
62                Ok(alloy_transport::Transport::boxed(
63                    alloy_transport_http::Http::<reqwest::Client>::new(url.clone()),
64                ))
65            }
66
67            // hyper is enabled, reqwest is not
68            #[cfg(feature = "hyper")]
69            Self::Http(url) => Ok(alloy_transport::Transport::boxed(
70                alloy_transport_http::HyperTransport::new_hyper(url.clone()),
71            )),
72
73            #[cfg(all(not(target_arch = "wasm32"), feature = "ws"))]
74            Self::Ws(url, Some(auth)) => alloy_transport_ws::WsConnect::new(url.clone())
75                .with_auth(auth.clone())
76                .into_service()
77                .await
78                .map(alloy_transport::Transport::boxed),
79
80            #[cfg(feature = "ws")]
81            Self::Ws(url, _) => alloy_transport_ws::WsConnect::new(url.clone())
82                .into_service()
83                .await
84                .map(alloy_transport::Transport::boxed),
85
86            #[cfg(feature = "ipc")]
87            Self::Ipc(path) => alloy_transport_ipc::IpcConnect::new(path.to_owned())
88                .into_service()
89                .await
90                .map(alloy_transport::Transport::boxed),
91
92            #[cfg(not(any(
93                feature = "reqwest",
94                feature = "hyper",
95                feature = "ws",
96                feature = "ipc"
97            )))]
98            _ => Err(TransportErrorKind::custom_str(
99                "No transports enabled. Enable one of: reqwest, hyper, ws, ipc",
100            )),
101        }
102    }
103
104    /// Tries to parse the given string as an HTTP URL.
105    #[cfg(any(feature = "reqwest", feature = "hyper"))]
106    pub fn try_as_http(s: &str) -> Result<Self, TransportError> {
107        let url = if s.starts_with("localhost:") || s.parse::<std::net::SocketAddr>().is_ok() {
108            let s = format!("http://{s}");
109            url::Url::parse(&s)
110        } else {
111            url::Url::parse(s)
112        }
113        .map_err(TransportErrorKind::custom)?;
114
115        let scheme = url.scheme();
116        if scheme != "http" && scheme != "https" {
117            let msg = format!("invalid URL scheme: {scheme}; expected `http` or `https`");
118            return Err(TransportErrorKind::custom_str(&msg));
119        }
120
121        Ok(Self::Http(url))
122    }
123
124    /// Tries to parse the given string as a WebSocket URL.
125    #[cfg(feature = "ws")]
126    pub fn try_as_ws(s: &str) -> Result<Self, TransportError> {
127        let url = if s.starts_with("localhost:") || s.parse::<std::net::SocketAddr>().is_ok() {
128            let s = format!("ws://{}", s);
129            url::Url::parse(&s)
130        } else {
131            url::Url::parse(s)
132        }
133        .map_err(TransportErrorKind::custom)?;
134
135        let scheme = url.scheme();
136        if scheme != "ws" && scheme != "wss" {
137            let msg = format!("invalid URL scheme: {scheme}; expected `ws` or `wss`");
138            return Err(TransportErrorKind::custom_str(&msg));
139        }
140
141        let auth = alloy_transport::Authorization::extract_from_url(&url);
142
143        Ok(Self::Ws(url, auth))
144    }
145
146    /// Tries to parse the given string as an IPC path, returning an error if
147    /// the path does not exist.
148    #[cfg(feature = "ipc")]
149    pub fn try_as_ipc(s: &str) -> Result<Self, TransportError> {
150        let s = s.strip_prefix("file://").or_else(|| s.strip_prefix("ipc://")).unwrap_or(s);
151
152        // Check if it exists.
153        let path = std::path::Path::new(s);
154        let _meta = path.metadata().map_err(|e| {
155            let msg = format!("failed to read IPC path {}: {e}", path.display());
156            TransportErrorKind::custom_str(&msg)
157        })?;
158
159        Ok(Self::Ipc(path.to_path_buf()))
160    }
161}
162
163impl FromStr for BuiltInConnectionString {
164    type Err = RpcError<TransportErrorKind>;
165
166    #[allow(clippy::let_and_return)]
167    fn from_str(s: &str) -> Result<Self, Self::Err> {
168        let res = Err(TransportErrorKind::custom_str(&format!(
169            "No transports enabled. Enable one of: reqwest, hyper, ws, ipc. Connection info: '{}'",
170            s
171        )));
172        #[cfg(any(feature = "reqwest", feature = "hyper"))]
173        let res = res.or_else(|_| Self::try_as_http(s));
174        #[cfg(feature = "ws")]
175        let res = res.or_else(|_| Self::try_as_ws(s));
176        #[cfg(feature = "ipc")]
177        let res = res.or_else(|_| Self::try_as_ipc(s));
178        res
179    }
180}
181
182#[cfg(test)]
183mod test {
184    use super::*;
185    use similar_asserts::assert_eq;
186    use url::Url;
187
188    #[test]
189    fn test_parsing_urls() {
190        assert_eq!(
191            BuiltInConnectionString::from_str("http://localhost:8545").unwrap(),
192            BuiltInConnectionString::Http("http://localhost:8545".parse::<Url>().unwrap())
193        );
194        assert_eq!(
195            BuiltInConnectionString::from_str("localhost:8545").unwrap(),
196            BuiltInConnectionString::Http("http://localhost:8545".parse::<Url>().unwrap())
197        );
198        assert_eq!(
199            BuiltInConnectionString::from_str("https://localhost:8545").unwrap(),
200            BuiltInConnectionString::Http("https://localhost:8545".parse::<Url>().unwrap())
201        );
202        assert_eq!(
203            BuiltInConnectionString::from_str("localhost:8545").unwrap(),
204            BuiltInConnectionString::Http("http://localhost:8545".parse::<Url>().unwrap())
205        );
206        assert_eq!(
207            BuiltInConnectionString::from_str("http://127.0.0.1:8545").unwrap(),
208            BuiltInConnectionString::Http("http://127.0.0.1:8545".parse::<Url>().unwrap())
209        );
210
211        assert_eq!(
212            BuiltInConnectionString::from_str("http://localhost").unwrap(),
213            BuiltInConnectionString::Http("http://localhost".parse::<Url>().unwrap())
214        );
215        assert_eq!(
216            BuiltInConnectionString::from_str("127.0.0.1:8545").unwrap(),
217            BuiltInConnectionString::Http("http://127.0.0.1:8545".parse::<Url>().unwrap())
218        );
219        assert_eq!(
220            BuiltInConnectionString::from_str("http://user:pass@example.com").unwrap(),
221            BuiltInConnectionString::Http("http://user:pass@example.com".parse::<Url>().unwrap())
222        );
223    }
224
225    #[test]
226    #[cfg(feature = "ws")]
227    fn test_parsing_ws() {
228        use alloy_transport::Authorization;
229
230        assert_eq!(
231            BuiltInConnectionString::from_str("ws://localhost:8545").unwrap(),
232            BuiltInConnectionString::Ws("ws://localhost:8545".parse::<Url>().unwrap(), None)
233        );
234        assert_eq!(
235            BuiltInConnectionString::from_str("wss://localhost:8545").unwrap(),
236            BuiltInConnectionString::Ws("wss://localhost:8545".parse::<Url>().unwrap(), None)
237        );
238        assert_eq!(
239            BuiltInConnectionString::from_str("ws://127.0.0.1:8545").unwrap(),
240            BuiltInConnectionString::Ws("ws://127.0.0.1:8545".parse::<Url>().unwrap(), None)
241        );
242
243        assert_eq!(
244            BuiltInConnectionString::from_str("ws://alice:pass@127.0.0.1:8545").unwrap(),
245            BuiltInConnectionString::Ws(
246                "ws://alice:pass@127.0.0.1:8545".parse::<Url>().unwrap(),
247                Some(Authorization::basic("alice", "pass"))
248            )
249        );
250    }
251
252    #[test]
253    #[cfg(feature = "ipc")]
254    #[cfg_attr(windows, ignore = "TODO: windows IPC")]
255    fn test_parsing_ipc() {
256        use alloy_node_bindings::Anvil;
257
258        // Spawn an Anvil instance to create an IPC socket, as it's different from a normal file.
259        let temp_dir = tempfile::tempdir().unwrap();
260        let ipc_path = temp_dir.path().join("anvil.ipc");
261        let ipc_arg = format!("--ipc={}", ipc_path.display());
262        let _anvil = Anvil::new().arg(ipc_arg).spawn();
263        let path_str = ipc_path.to_str().unwrap();
264
265        assert_eq!(
266            BuiltInConnectionString::from_str(&format!("ipc://{}", path_str)).unwrap(),
267            BuiltInConnectionString::Ipc(ipc_path.clone())
268        );
269
270        assert_eq!(
271            BuiltInConnectionString::from_str(&format!("file://{}", path_str)).unwrap(),
272            BuiltInConnectionString::Ipc(ipc_path.clone())
273        );
274
275        assert_eq!(
276            BuiltInConnectionString::from_str(ipc_path.to_str().unwrap()).unwrap(),
277            BuiltInConnectionString::Ipc(ipc_path.clone())
278        );
279    }
280}