foundry_common/provider/
curl_transport.rs1use alloy_json_rpc::{RequestPacket, ResponsePacket};
4use alloy_transport::{TransportError, TransportFut};
5use serde_json::Value;
6use tower::Service;
7use url::Url;
8
9fn shell_escape(s: &str) -> String {
11 s.replace('\'', "'\"'\"'")
12}
13
14pub 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#[derive(Clone, Debug)]
58pub struct CurlTransport {
59 url: Url,
61 headers: Vec<String>,
63 jwt: Option<String>,
65}
66
67impl CurlTransport {
68 pub fn new(url: Url) -> Self {
70 Self { url, headers: vec![], jwt: None }
71 }
72
73 pub fn with_headers(mut self, headers: Vec<String>) -> Self {
75 self.headers = headers;
76 self
77 }
78
79 pub fn with_jwt(mut self, jwt: Option<String>) -> Self {
81 self.jwt = jwt;
82 self
83 }
84
85 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 pub fn request(&self, req: RequestPacket) -> TransportFut<'static> {
109 let curl_cmd = self.generate_curl_command(&req);
110
111 Box::pin(async move {
112 let _ = crate::sh_println!("{curl_cmd}");
114
115 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}