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(s: &str) -> Result<BoxTransport, TransportError> {
63 let connection = Self::from_str(s)?;
64 connection.connect_boxed().await
65 }
66
67 pub async fn connect_boxed(&self) -> Result<BoxTransport, TransportError> {
74 match self {
78 #[cfg(all(not(feature = "hyper"), feature = "reqwest"))]
80 Self::Http(url) => {
81 Ok(alloy_transport::Transport::boxed(
82 alloy_transport_http::Http::<reqwest::Client>::new(url.clone()),
83 ))
84 }
85
86 #[cfg(feature = "hyper")]
88 Self::Http(url) => Ok(alloy_transport::Transport::boxed(
89 alloy_transport_http::HyperTransport::new_hyper(url.clone()),
90 )),
91
92 #[cfg(all(not(target_family = "wasm"), feature = "ws"))]
93 Self::Ws(url, Some(auth)) => alloy_transport_ws::WsConnect::new(url.clone())
94 .with_auth(auth.clone())
95 .into_service()
96 .await
97 .map(alloy_transport::Transport::boxed),
98
99 #[cfg(feature = "ws")]
100 Self::Ws(url, _) => alloy_transport_ws::WsConnect::new(url.clone())
101 .into_service()
102 .await
103 .map(alloy_transport::Transport::boxed),
104
105 #[cfg(feature = "ipc")]
106 Self::Ipc(path) => alloy_transport_ipc::IpcConnect::new(path.to_owned())
107 .into_service()
108 .await
109 .map(alloy_transport::Transport::boxed),
110
111 #[cfg(not(any(
112 feature = "reqwest",
113 feature = "hyper",
114 feature = "ws",
115 feature = "ipc"
116 )))]
117 _ => Err(TransportErrorKind::custom_str(
118 "No transports enabled. Enable one of: reqwest, hyper, ws, ipc",
119 )),
120 }
121 }
122
123 #[cfg(any(feature = "reqwest", feature = "hyper"))]
125 pub fn try_as_http(s: &str) -> Result<Self, TransportError> {
126 let url = if s.starts_with("localhost:") || s.parse::<std::net::SocketAddr>().is_ok() {
127 let s = format!("http://{s}");
128 url::Url::parse(&s)
129 } else {
130 url::Url::parse(s)
131 }
132 .map_err(TransportErrorKind::custom)?;
133
134 let scheme = url.scheme();
135 if scheme != "http" && scheme != "https" {
136 let msg = format!("invalid URL scheme: {scheme}; expected `http` or `https`");
137 return Err(TransportErrorKind::custom_str(&msg));
138 }
139
140 Ok(Self::Http(url))
141 }
142
143 #[cfg(feature = "ws")]
145 pub fn try_as_ws(s: &str) -> Result<Self, TransportError> {
146 let url = if s.starts_with("localhost:") || s.parse::<std::net::SocketAddr>().is_ok() {
147 let s = format!("ws://{s}");
148 url::Url::parse(&s)
149 } else {
150 url::Url::parse(s)
151 }
152 .map_err(TransportErrorKind::custom)?;
153
154 let scheme = url.scheme();
155 if scheme != "ws" && scheme != "wss" {
156 let msg = format!("invalid URL scheme: {scheme}; expected `ws` or `wss`");
157 return Err(TransportErrorKind::custom_str(&msg));
158 }
159
160 let auth = alloy_transport::Authorization::extract_from_url(&url);
161
162 Ok(Self::Ws(url, auth))
163 }
164
165 #[cfg(feature = "ipc")]
168 pub fn try_as_ipc(s: &str) -> Result<Self, TransportError> {
169 let s = s.strip_prefix("file://").or_else(|| s.strip_prefix("ipc://")).unwrap_or(s);
170
171 let path = std::path::Path::new(s);
173 let _meta = path.metadata().map_err(|e| {
174 let msg = format!("failed to read IPC path {}: {e}", path.display());
175 TransportErrorKind::custom_str(&msg)
176 })?;
177
178 Ok(Self::Ipc(path.to_path_buf()))
179 }
180}
181
182impl FromStr for BuiltInConnectionString {
183 type Err = RpcError<TransportErrorKind>;
184
185 #[allow(clippy::let_and_return)]
186 fn from_str(s: &str) -> Result<Self, Self::Err> {
187 let res = Err(TransportErrorKind::custom_str(&format!(
188 "No transports enabled. Enable one of: reqwest, hyper, ws, ipc. Connection info: '{s}'"
189 )));
190 #[cfg(any(feature = "reqwest", feature = "hyper"))]
191 let res = res.or_else(|_| Self::try_as_http(s));
192 #[cfg(feature = "ws")]
193 let res = res.or_else(|_| Self::try_as_ws(s));
194 #[cfg(feature = "ipc")]
195 let res = res.or_else(|_| Self::try_as_ipc(s));
196 res
197 }
198}
199
200#[cfg(test)]
201mod test {
202 use super::*;
203 use similar_asserts::assert_eq;
204 use url::Url;
205
206 #[test]
207 fn test_parsing_urls() {
208 assert_eq!(
209 BuiltInConnectionString::from_str("http://localhost:8545").unwrap(),
210 BuiltInConnectionString::Http("http://localhost:8545".parse::<Url>().unwrap())
211 );
212 assert_eq!(
213 BuiltInConnectionString::from_str("localhost:8545").unwrap(),
214 BuiltInConnectionString::Http("http://localhost:8545".parse::<Url>().unwrap())
215 );
216 assert_eq!(
217 BuiltInConnectionString::from_str("https://localhost:8545").unwrap(),
218 BuiltInConnectionString::Http("https://localhost:8545".parse::<Url>().unwrap())
219 );
220 assert_eq!(
221 BuiltInConnectionString::from_str("http://127.0.0.1:8545").unwrap(),
222 BuiltInConnectionString::Http("http://127.0.0.1:8545".parse::<Url>().unwrap())
223 );
224
225 assert_eq!(
226 BuiltInConnectionString::from_str("http://localhost").unwrap(),
227 BuiltInConnectionString::Http("http://localhost".parse::<Url>().unwrap())
228 );
229 assert_eq!(
230 BuiltInConnectionString::from_str("127.0.0.1:8545").unwrap(),
231 BuiltInConnectionString::Http("http://127.0.0.1:8545".parse::<Url>().unwrap())
232 );
233 assert_eq!(
234 BuiltInConnectionString::from_str("http://user:pass@example.com").unwrap(),
235 BuiltInConnectionString::Http("http://user:pass@example.com".parse::<Url>().unwrap())
236 );
237 }
238
239 #[test]
240 #[cfg(feature = "ws")]
241 fn test_parsing_ws() {
242 use alloy_transport::Authorization;
243
244 assert_eq!(
245 BuiltInConnectionString::from_str("ws://localhost:8545").unwrap(),
246 BuiltInConnectionString::Ws("ws://localhost:8545".parse::<Url>().unwrap(), None)
247 );
248 assert_eq!(
249 BuiltInConnectionString::from_str("wss://localhost:8545").unwrap(),
250 BuiltInConnectionString::Ws("wss://localhost:8545".parse::<Url>().unwrap(), None)
251 );
252 assert_eq!(
253 BuiltInConnectionString::from_str("ws://127.0.0.1:8545").unwrap(),
254 BuiltInConnectionString::Ws("ws://127.0.0.1:8545".parse::<Url>().unwrap(), None)
255 );
256
257 assert_eq!(
258 BuiltInConnectionString::from_str("ws://alice:pass@127.0.0.1:8545").unwrap(),
259 BuiltInConnectionString::Ws(
260 "ws://alice:pass@127.0.0.1:8545".parse::<Url>().unwrap(),
261 Some(Authorization::basic("alice", "pass"))
262 )
263 );
264 }
265
266 #[test]
267 #[cfg(feature = "ipc")]
268 #[cfg_attr(windows, ignore = "TODO: windows IPC")]
269 fn test_parsing_ipc() {
270 use alloy_node_bindings::Anvil;
271
272 let temp_dir = tempfile::tempdir().unwrap();
274 let ipc_path = temp_dir.path().join("anvil.ipc");
275 let ipc_arg = format!("--ipc={}", ipc_path.display());
276 let _anvil = Anvil::new().arg(ipc_arg).spawn();
277 let path_str = ipc_path.to_str().unwrap();
278
279 assert_eq!(
280 BuiltInConnectionString::from_str(&format!("ipc://{path_str}")).unwrap(),
281 BuiltInConnectionString::Ipc(ipc_path.clone())
282 );
283
284 assert_eq!(
285 BuiltInConnectionString::from_str(&format!("file://{path_str}")).unwrap(),
286 BuiltInConnectionString::Ipc(ipc_path.clone())
287 );
288
289 assert_eq!(
290 BuiltInConnectionString::from_str(ipc_path.to_str().unwrap()).unwrap(),
291 BuiltInConnectionString::Ipc(ipc_path.clone())
292 );
293 }
294}