foundry_common/provider/
curl_transport.rs

1//! Transport that outputs equivalent curl commands instead of making RPC requests.
2
3use alloy_json_rpc::{RequestPacket, ResponsePacket};
4use alloy_transport::{TransportError, TransportFut};
5use serde_json::Value;
6use tower::Service;
7use url::Url;
8
9/// Escapes a string for use in a single-quoted shell argument.
10fn shell_escape(s: &str) -> String {
11    s.replace('\'', "'\"'\"'")
12}
13
14/// Generates a curl command for an RPC request.
15///
16/// This is a standalone helper that can be used to generate curl commands
17/// without going through the transport layer.
18pub fn generate_curl_command(
19    url: &str,
20    method: &str,
21    params: Value,
22    headers: Option<&[String]>,
23    jwt: Option<&str>,
24) -> String {
25    let payload = serde_json::json!({
26        "jsonrpc": "2.0",
27        "method": method,
28        "params": params,
29        "id": 1
30    });
31    let payload_str = serde_json::to_string(&payload).unwrap_or_default();
32    let escaped_payload = shell_escape(&payload_str);
33
34    let mut cmd = String::from("curl -X POST");
35    cmd.push_str(" -H 'Content-Type: application/json'");
36
37    if let Some(jwt) = jwt {
38        cmd.push_str(&format!(" -H 'Authorization: Bearer {}'", shell_escape(jwt)));
39    }
40
41    if let Some(hdrs) = headers {
42        for h in hdrs {
43            cmd.push_str(&format!(" -H '{}'", shell_escape(h)));
44        }
45    }
46
47    cmd.push_str(&format!(" --data-raw '{escaped_payload}'"));
48    cmd.push_str(&format!(" '{}'", shell_escape(url)));
49
50    cmd
51}
52
53/// A transport that prints curl commands instead of executing RPC requests.
54///
55/// When a request is made through this transport, it will print the equivalent
56/// curl command to stderr and return a dummy successful response.
57#[derive(Clone, Debug)]
58pub struct CurlTransport {
59    /// The URL to connect to.
60    url: Url,
61    /// The headers to use for requests.
62    headers: Vec<String>,
63    /// The JWT to use for requests.
64    jwt: Option<String>,
65}
66
67impl CurlTransport {
68    /// Create a new curl transport with the given URL.
69    pub fn new(url: Url) -> Self {
70        Self { url, headers: vec![], jwt: None }
71    }
72
73    /// Set the headers for the transport.
74    pub fn with_headers(mut self, headers: Vec<String>) -> Self {
75        self.headers = headers;
76        self
77    }
78
79    /// Set the JWT for the transport.
80    pub fn with_jwt(mut self, jwt: Option<String>) -> Self {
81        self.jwt = jwt;
82        self
83    }
84
85    /// Generate a curl command for a request.
86    fn generate_curl_command(&self, req: &RequestPacket) -> String {
87        let payload_str = serde_json::to_string(req).unwrap_or_default();
88        let escaped_payload = shell_escape(&payload_str);
89
90        let mut cmd = String::from("curl -X POST");
91        cmd.push_str(" -H 'Content-Type: application/json'");
92
93        if let Some(jwt) = &self.jwt {
94            cmd.push_str(&format!(" -H 'Authorization: Bearer {}'", shell_escape(jwt)));
95        }
96
97        for h in &self.headers {
98            cmd.push_str(&format!(" -H '{}'", shell_escape(h)));
99        }
100
101        cmd.push_str(&format!(" --data-raw '{escaped_payload}'"));
102        cmd.push_str(&format!(" '{}'", shell_escape(self.url.as_str())));
103
104        cmd
105    }
106
107    /// Handle a request by printing the curl command.
108    pub fn request(&self, req: RequestPacket) -> TransportFut<'static> {
109        let curl_cmd = self.generate_curl_command(&req);
110
111        Box::pin(async move {
112            // Print the curl command to stdout
113            let _ = crate::sh_println!("{curl_cmd}");
114
115            // Exit cleanly after printing the curl command
116            std::process::exit(0);
117        })
118    }
119}
120
121impl Service<RequestPacket> for CurlTransport {
122    type Response = ResponsePacket;
123    type Error = TransportError;
124    type Future = TransportFut<'static>;
125
126    #[inline]
127    fn poll_ready(
128        &mut self,
129        _cx: &mut std::task::Context<'_>,
130    ) -> std::task::Poll<Result<(), Self::Error>> {
131        std::task::Poll::Ready(Ok(()))
132    }
133
134    #[inline]
135    fn call(&mut self, req: RequestPacket) -> Self::Future {
136        self.request(req)
137    }
138}
139
140impl Service<RequestPacket> for &CurlTransport {
141    type Response = ResponsePacket;
142    type Error = TransportError;
143    type Future = TransportFut<'static>;
144
145    #[inline]
146    fn poll_ready(
147        &mut self,
148        _cx: &mut std::task::Context<'_>,
149    ) -> std::task::Poll<Result<(), Self::Error>> {
150        std::task::Poll::Ready(Ok(()))
151    }
152
153    #[inline]
154    fn call(&mut self, req: RequestPacket) -> Self::Future {
155        self.request(req)
156    }
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162    use alloy_json_rpc::{Id, Request};
163
164    fn make_test_request() -> RequestPacket {
165        let req: Request<Vec<()>> = Request::new("eth_blockNumber", Id::Number(1), vec![]);
166        let serialized = req.serialize().unwrap();
167        RequestPacket::Single(serialized)
168    }
169
170    #[test]
171    fn test_basic_curl_command() {
172        let transport = CurlTransport::new("https://eth.example.com".parse().unwrap());
173        let req = make_test_request();
174        let cmd = transport.generate_curl_command(&req);
175        assert!(cmd.contains("eth_blockNumber"));
176        assert!(cmd.contains("https://eth.example.com"));
177        assert!(cmd.contains("jsonrpc"));
178    }
179
180    #[test]
181    fn test_curl_with_headers() {
182        let transport = CurlTransport::new("https://eth.example.com".parse().unwrap())
183            .with_headers(vec!["X-Custom: value".to_string()]);
184        let req = make_test_request();
185        let cmd = transport.generate_curl_command(&req);
186        assert!(cmd.contains("X-Custom: value"));
187    }
188
189    #[test]
190    fn test_curl_with_jwt() {
191        let transport = CurlTransport::new("https://eth.example.com".parse().unwrap())
192            .with_jwt(Some("my-jwt-token".to_string()));
193        let req = make_test_request();
194        let cmd = transport.generate_curl_command(&req);
195        assert!(cmd.contains("Authorization: Bearer my-jwt-token"));
196    }
197
198    #[test]
199    fn test_shell_escape() {
200        let escaped = shell_escape("it's a test");
201        assert_eq!(escaped, "it'\"'\"'s a test");
202    }
203}