1use alloy_json_rpc::RpcError;
2use alloy_transport::{BoxTransport, TransportConnect, TransportError, TransportErrorKind};
3use std::{str::FromStr, time::Duration};
4
5#[cfg(any(feature = "ws", feature = "ipc"))]
6use alloy_pubsub::PubSubConnect;
7
8#[derive(Clone, Debug, PartialEq, Eq)]
10#[non_exhaustive]
11pub enum BuiltInConnectionString {
12 #[cfg(any(feature = "reqwest", feature = "hyper"))]
14 Http(url::Url),
15 #[cfg(feature = "ws")]
17 Ws(url::Url, Option<alloy_transport::Authorization>),
18 #[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 pub async fn connect(s: &str) -> Result<BoxTransport, TransportError> {
63 let connection = Self::from_str(s)?;
64 connection.connect_boxed().await
65 }
66
67 pub async fn connect_with(
93 s: &str,
94 config: ConnectionConfig,
95 ) -> Result<BoxTransport, TransportError> {
96 let connection = Self::from_str(s)?;
97 connection.connect_boxed_with(config).await
98 }
99
100 pub async fn connect_boxed(&self) -> Result<BoxTransport, TransportError> {
107 self.connect_boxed_with(ConnectionConfig::default()).await
108 }
109
110 pub async fn connect_boxed_with(
121 &self,
122 config: ConnectionConfig,
123 ) -> Result<BoxTransport, TransportError> {
124 let _ = &config; match self {
128 #[cfg(all(not(feature = "hyper"), feature = "reqwest"))]
130 Self::Http(url) => {
131 Ok(alloy_transport::Transport::boxed(
132 alloy_transport_http::Http::<reqwest::Client>::new(url.clone()),
133 ))
134 }
135
136 #[cfg(feature = "hyper")]
138 Self::Http(url) => Ok(alloy_transport::Transport::boxed(
139 alloy_transport_http::HyperTransport::new_hyper(url.clone()),
140 )),
141
142 #[cfg(feature = "ws")]
143 Self::Ws(url, existing_auth) => {
144 let mut ws_connect = alloy_transport_ws::WsConnect::new(url.clone());
145
146 let auth = config.auth.as_ref().or(existing_auth.as_ref());
148 #[cfg(not(target_family = "wasm"))]
149 if let Some(auth) = auth {
150 ws_connect = ws_connect.with_auth(auth.clone());
151 }
152 #[cfg(target_family = "wasm")]
153 let _ = auth; #[cfg(not(target_family = "wasm"))]
157 if let Some(ws_config) = config.ws_config {
158 ws_connect = ws_connect.with_config(ws_config);
159 }
160
161 if let Some(max_retries) = config.max_retries {
163 ws_connect = ws_connect.with_max_retries(max_retries);
164 }
165 if let Some(retry_interval) = config.retry_interval {
166 ws_connect = ws_connect.with_retry_interval(retry_interval);
167 }
168
169 ws_connect.into_service().await.map(alloy_transport::Transport::boxed)
170 }
171
172 #[cfg(feature = "ipc")]
173 Self::Ipc(path) => alloy_transport_ipc::IpcConnect::new(path.to_owned())
174 .into_service()
175 .await
176 .map(alloy_transport::Transport::boxed),
177
178 #[cfg(not(any(
179 feature = "reqwest",
180 feature = "hyper",
181 feature = "ws",
182 feature = "ipc"
183 )))]
184 _ => Err(TransportErrorKind::custom_str(
185 "No transports enabled. Enable one of: reqwest, hyper, ws, ipc",
186 )),
187 }
188 }
189
190 #[cfg(any(feature = "reqwest", feature = "hyper"))]
192 pub fn try_as_http(s: &str) -> Result<Self, TransportError> {
193 let url = if s.starts_with("localhost:") || s.parse::<std::net::SocketAddr>().is_ok() {
194 let s = format!("http://{s}");
195 url::Url::parse(&s)
196 } else {
197 url::Url::parse(s)
198 }
199 .map_err(TransportErrorKind::custom)?;
200
201 let scheme = url.scheme();
202 if scheme != "http" && scheme != "https" {
203 let msg = format!("invalid URL scheme: {scheme}; expected `http` or `https`");
204 return Err(TransportErrorKind::custom_str(&msg));
205 }
206
207 Ok(Self::Http(url))
208 }
209
210 #[cfg(feature = "ws")]
212 pub fn try_as_ws(s: &str) -> Result<Self, TransportError> {
213 let url = if s.starts_with("localhost:") || s.parse::<std::net::SocketAddr>().is_ok() {
214 let s = format!("ws://{s}");
215 url::Url::parse(&s)
216 } else {
217 url::Url::parse(s)
218 }
219 .map_err(TransportErrorKind::custom)?;
220
221 let scheme = url.scheme();
222 if scheme != "ws" && scheme != "wss" {
223 let msg = format!("invalid URL scheme: {scheme}; expected `ws` or `wss`");
224 return Err(TransportErrorKind::custom_str(&msg));
225 }
226
227 let auth = alloy_transport::Authorization::extract_from_url(&url);
228
229 Ok(Self::Ws(url, auth))
230 }
231
232 #[cfg(feature = "ipc")]
235 pub fn try_as_ipc(s: &str) -> Result<Self, TransportError> {
236 let s = s.strip_prefix("file://").or_else(|| s.strip_prefix("ipc://")).unwrap_or(s);
237
238 let path = std::path::Path::new(s);
240 let _meta = path.metadata().map_err(|e| {
241 let msg = format!("failed to read IPC path {}: {e}", path.display());
242 TransportErrorKind::custom_str(&msg)
243 })?;
244
245 Ok(Self::Ipc(path.to_path_buf()))
246 }
247}
248
249impl FromStr for BuiltInConnectionString {
250 type Err = RpcError<TransportErrorKind>;
251
252 #[allow(clippy::let_and_return)]
253 fn from_str(s: &str) -> Result<Self, Self::Err> {
254 let res = Err(TransportErrorKind::custom_str(&format!(
255 "No transports enabled. Enable one of: reqwest, hyper, ws, ipc. Connection info: '{s}'"
256 )));
257 #[cfg(any(feature = "reqwest", feature = "hyper"))]
258 let res = res.or_else(|_| Self::try_as_http(s));
259 #[cfg(feature = "ws")]
260 let res = res.or_else(|_| Self::try_as_ws(s));
261 #[cfg(feature = "ipc")]
262 let res = res.or_else(|_| Self::try_as_ipc(s));
263 res
264 }
265}
266
267#[derive(Clone, Debug, Default)]
272#[non_exhaustive]
273pub struct ConnectionConfig {
274 pub auth: Option<alloy_transport::Authorization>,
276 pub max_retries: Option<u32>,
278 pub retry_interval: Option<Duration>,
280 #[cfg(all(feature = "ws", not(target_family = "wasm")))]
282 pub ws_config: Option<alloy_transport_ws::WebSocketConfig>,
283}
284
285impl ConnectionConfig {
286 pub const fn new() -> Self {
288 Self {
289 auth: None,
290 max_retries: None,
291 retry_interval: None,
292 #[cfg(all(feature = "ws", not(target_family = "wasm")))]
293 ws_config: None,
294 }
295 }
296
297 pub fn with_auth(mut self, auth: alloy_transport::Authorization) -> Self {
299 self.auth = Some(auth);
300 self
301 }
302
303 pub const fn with_max_retries(mut self, max_retries: u32) -> Self {
305 self.max_retries = Some(max_retries);
306 self
307 }
308
309 pub const fn with_retry_interval(mut self, retry_interval: Duration) -> Self {
311 self.retry_interval = Some(retry_interval);
312 self
313 }
314
315 #[cfg(all(feature = "ws", not(target_family = "wasm")))]
317 pub const fn with_ws_config(mut self, config: alloy_transport_ws::WebSocketConfig) -> Self {
318 self.ws_config = Some(config);
319 self
320 }
321}
322
323#[cfg(test)]
324mod test {
325 use super::*;
326 use similar_asserts::assert_eq;
327 use url::Url;
328
329 #[test]
330 fn test_parsing_urls() {
331 assert_eq!(
332 BuiltInConnectionString::from_str("http://localhost:8545").unwrap(),
333 BuiltInConnectionString::Http("http://localhost:8545".parse::<Url>().unwrap())
334 );
335 assert_eq!(
336 BuiltInConnectionString::from_str("localhost:8545").unwrap(),
337 BuiltInConnectionString::Http("http://localhost:8545".parse::<Url>().unwrap())
338 );
339 assert_eq!(
340 BuiltInConnectionString::from_str("https://localhost:8545").unwrap(),
341 BuiltInConnectionString::Http("https://localhost:8545".parse::<Url>().unwrap())
342 );
343 assert_eq!(
344 BuiltInConnectionString::from_str("http://127.0.0.1:8545").unwrap(),
345 BuiltInConnectionString::Http("http://127.0.0.1:8545".parse::<Url>().unwrap())
346 );
347
348 assert_eq!(
349 BuiltInConnectionString::from_str("http://localhost").unwrap(),
350 BuiltInConnectionString::Http("http://localhost".parse::<Url>().unwrap())
351 );
352 assert_eq!(
353 BuiltInConnectionString::from_str("127.0.0.1:8545").unwrap(),
354 BuiltInConnectionString::Http("http://127.0.0.1:8545".parse::<Url>().unwrap())
355 );
356 assert_eq!(
357 BuiltInConnectionString::from_str("http://user:pass@example.com").unwrap(),
358 BuiltInConnectionString::Http("http://user:pass@example.com".parse::<Url>().unwrap())
359 );
360 }
361
362 #[test]
363 #[cfg(feature = "ws")]
364 fn test_parsing_ws() {
365 use alloy_transport::Authorization;
366
367 assert_eq!(
368 BuiltInConnectionString::from_str("ws://localhost:8545").unwrap(),
369 BuiltInConnectionString::Ws("ws://localhost:8545".parse::<Url>().unwrap(), None)
370 );
371 assert_eq!(
372 BuiltInConnectionString::from_str("wss://localhost:8545").unwrap(),
373 BuiltInConnectionString::Ws("wss://localhost:8545".parse::<Url>().unwrap(), None)
374 );
375 assert_eq!(
376 BuiltInConnectionString::from_str("ws://127.0.0.1:8545").unwrap(),
377 BuiltInConnectionString::Ws("ws://127.0.0.1:8545".parse::<Url>().unwrap(), None)
378 );
379
380 assert_eq!(
381 BuiltInConnectionString::from_str("ws://alice:pass@127.0.0.1:8545").unwrap(),
382 BuiltInConnectionString::Ws(
383 "ws://alice:pass@127.0.0.1:8545".parse::<Url>().unwrap(),
384 Some(Authorization::basic("alice", "pass"))
385 )
386 );
387 }
388
389 #[test]
390 #[cfg(feature = "ipc")]
391 #[cfg_attr(windows, ignore = "TODO: windows IPC")]
392 fn test_parsing_ipc() {
393 use alloy_node_bindings::Anvil;
394
395 let temp_dir = tempfile::tempdir().unwrap();
397 let ipc_path = temp_dir.path().join("anvil.ipc");
398 let ipc_arg = format!("--ipc={}", ipc_path.display());
399 let _anvil = Anvil::new().arg(ipc_arg).spawn();
400 let path_str = ipc_path.to_str().unwrap();
401
402 assert_eq!(
403 BuiltInConnectionString::from_str(&format!("ipc://{path_str}")).unwrap(),
404 BuiltInConnectionString::Ipc(ipc_path.clone())
405 );
406
407 assert_eq!(
408 BuiltInConnectionString::from_str(&format!("file://{path_str}")).unwrap(),
409 BuiltInConnectionString::Ipc(ipc_path.clone())
410 );
411
412 assert_eq!(
413 BuiltInConnectionString::from_str(ipc_path.to_str().unwrap()).unwrap(),
414 BuiltInConnectionString::Ipc(ipc_path.clone())
415 );
416 }
417
418 #[test]
419 #[cfg(feature = "ws")]
420 fn test_ws_config_auth_priority() {
421 use alloy_transport::Authorization;
422
423 let config_auth = Authorization::bearer("config-token");
425 let url_auth = Some(Authorization::basic("user", "pass"));
426
427 let _ws_connection =
428 BuiltInConnectionString::Ws("ws://user:pass@localhost:8545".parse().unwrap(), url_auth);
429
430 let config = ConnectionConfig::new().with_auth(config_auth.clone());
431
432 assert_eq!(config.auth.as_ref().unwrap().to_string(), config_auth.to_string());
436 }
437
438 #[test]
439 fn test_backward_compatibility() {
440 let default_config = ConnectionConfig::default();
442 assert!(default_config.auth.is_none());
443 assert!(default_config.max_retries.is_none());
444
445 }
447}