alloy_rpc_client/
builtin.rs1use 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#[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_boxed(&self) -> Result<BoxTransport, TransportError> {
55 match self {
59 #[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 #[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_family = "wasm"), 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 #[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 #[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 #[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 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: '{s}'"
170 )));
171 #[cfg(any(feature = "reqwest", feature = "hyper"))]
172 let res = res.or_else(|_| Self::try_as_http(s));
173 #[cfg(feature = "ws")]
174 let res = res.or_else(|_| Self::try_as_ws(s));
175 #[cfg(feature = "ipc")]
176 let res = res.or_else(|_| Self::try_as_ipc(s));
177 res
178 }
179}
180
181#[cfg(test)]
182mod test {
183 use super::*;
184 use similar_asserts::assert_eq;
185 use url::Url;
186
187 #[test]
188 fn test_parsing_urls() {
189 assert_eq!(
190 BuiltInConnectionString::from_str("http://localhost:8545").unwrap(),
191 BuiltInConnectionString::Http("http://localhost:8545".parse::<Url>().unwrap())
192 );
193 assert_eq!(
194 BuiltInConnectionString::from_str("localhost:8545").unwrap(),
195 BuiltInConnectionString::Http("http://localhost:8545".parse::<Url>().unwrap())
196 );
197 assert_eq!(
198 BuiltInConnectionString::from_str("https://localhost:8545").unwrap(),
199 BuiltInConnectionString::Http("https://localhost:8545".parse::<Url>().unwrap())
200 );
201 assert_eq!(
202 BuiltInConnectionString::from_str("localhost:8545").unwrap(),
203 BuiltInConnectionString::Http("http://localhost:8545".parse::<Url>().unwrap())
204 );
205 assert_eq!(
206 BuiltInConnectionString::from_str("http://127.0.0.1:8545").unwrap(),
207 BuiltInConnectionString::Http("http://127.0.0.1:8545".parse::<Url>().unwrap())
208 );
209
210 assert_eq!(
211 BuiltInConnectionString::from_str("http://localhost").unwrap(),
212 BuiltInConnectionString::Http("http://localhost".parse::<Url>().unwrap())
213 );
214 assert_eq!(
215 BuiltInConnectionString::from_str("127.0.0.1:8545").unwrap(),
216 BuiltInConnectionString::Http("http://127.0.0.1:8545".parse::<Url>().unwrap())
217 );
218 assert_eq!(
219 BuiltInConnectionString::from_str("http://user:pass@example.com").unwrap(),
220 BuiltInConnectionString::Http("http://user:pass@example.com".parse::<Url>().unwrap())
221 );
222 }
223
224 #[test]
225 #[cfg(feature = "ws")]
226 fn test_parsing_ws() {
227 use alloy_transport::Authorization;
228
229 assert_eq!(
230 BuiltInConnectionString::from_str("ws://localhost:8545").unwrap(),
231 BuiltInConnectionString::Ws("ws://localhost:8545".parse::<Url>().unwrap(), None)
232 );
233 assert_eq!(
234 BuiltInConnectionString::from_str("wss://localhost:8545").unwrap(),
235 BuiltInConnectionString::Ws("wss://localhost:8545".parse::<Url>().unwrap(), None)
236 );
237 assert_eq!(
238 BuiltInConnectionString::from_str("ws://127.0.0.1:8545").unwrap(),
239 BuiltInConnectionString::Ws("ws://127.0.0.1:8545".parse::<Url>().unwrap(), None)
240 );
241
242 assert_eq!(
243 BuiltInConnectionString::from_str("ws://alice:pass@127.0.0.1:8545").unwrap(),
244 BuiltInConnectionString::Ws(
245 "ws://alice:pass@127.0.0.1:8545".parse::<Url>().unwrap(),
246 Some(Authorization::basic("alice", "pass"))
247 )
248 );
249 }
250
251 #[test]
252 #[cfg(feature = "ipc")]
253 #[cfg_attr(windows, ignore = "TODO: windows IPC")]
254 fn test_parsing_ipc() {
255 use alloy_node_bindings::Anvil;
256
257 let temp_dir = tempfile::tempdir().unwrap();
259 let ipc_path = temp_dir.path().join("anvil.ipc");
260 let ipc_arg = format!("--ipc={}", ipc_path.display());
261 let _anvil = Anvil::new().arg(ipc_arg).spawn();
262 let path_str = ipc_path.to_str().unwrap();
263
264 assert_eq!(
265 BuiltInConnectionString::from_str(&format!("ipc://{path_str}")).unwrap(),
266 BuiltInConnectionString::Ipc(ipc_path.clone())
267 );
268
269 assert_eq!(
270 BuiltInConnectionString::from_str(&format!("file://{path_str}")).unwrap(),
271 BuiltInConnectionString::Ipc(ipc_path.clone())
272 );
273
274 assert_eq!(
275 BuiltInConnectionString::from_str(ipc_path.to_str().unwrap()).unwrap(),
276 BuiltInConnectionString::Ipc(ipc_path.clone())
277 );
278 }
279}