foundry_common/provider/
curl_transport.rs1use alloy_json_rpc::{RequestPacket, ResponsePacket};
4use alloy_rpc_types_engine::{Claims, JwtSecret};
5use alloy_transport::{TransportError, TransportFut};
6use eyre::Context;
7use serde_json::Value;
8use tower::Service;
9use url::Url;
10
11fn shell_escape(s: &str) -> String {
13 s.replace('\'', "'\"'\"'")
14}
15
16fn build_jwt(jwt_secret: &str) -> eyre::Result<String> {
18 let secret = JwtSecret::from_hex(jwt_secret)?;
20 let claims = Claims::default();
21 let token = secret.encode(&claims)?;
22 Ok(token)
23}
24
25fn append_jwt_auth(cmd: &mut String, jwt_secret: &str) -> eyre::Result<()> {
27 let jwt = build_jwt(jwt_secret).wrap_err("Invalid --jwt-secret provided")?;
28
29 cmd.push_str(&format!(" -H 'Authorization: Bearer {}'", shell_escape(jwt.as_str())));
30
31 Ok(())
32}
33
34pub fn generate_curl_command(
39 url: &str,
40 method: &str,
41 params: Value,
42 headers: Option<&[String]>,
43 jwt_secret: Option<&str>,
44) -> eyre::Result<String> {
45 let payload = serde_json::json!({
46 "jsonrpc": "2.0",
47 "method": method,
48 "params": params,
49 "id": 1
50 });
51 let payload_str = serde_json::to_string(&payload).unwrap_or_default();
52 let escaped_payload = shell_escape(&payload_str);
53
54 let mut cmd = String::from("curl -X POST");
55 cmd.push_str(" -H 'Content-Type: application/json'");
56
57 if let Some(secret) = jwt_secret {
58 append_jwt_auth(&mut cmd, secret)?;
59 }
60
61 if let Some(hdrs) = headers {
62 for h in hdrs {
63 cmd.push_str(&format!(" -H '{}'", shell_escape(h)));
64 }
65 }
66
67 cmd.push_str(&format!(" --data-raw '{escaped_payload}'"));
68 cmd.push_str(&format!(" '{}'", shell_escape(url)));
69
70 Ok(cmd)
71}
72
73#[derive(Clone, Debug)]
78pub struct CurlTransport {
79 url: Url,
81 headers: Vec<String>,
83 jwt: Option<String>,
85}
86
87impl CurlTransport {
88 pub const fn new(url: Url) -> Self {
90 Self { url, headers: vec![], jwt: None }
91 }
92
93 pub fn with_headers(mut self, headers: Vec<String>) -> Self {
95 self.headers = headers;
96 self
97 }
98
99 pub fn with_jwt(mut self, jwt: Option<String>) -> Self {
101 self.jwt = jwt;
102 self
103 }
104
105 fn generate_curl_command(&self, req: &RequestPacket) -> eyre::Result<String> {
107 let payload_str = serde_json::to_string(req).unwrap_or_default();
108 let escaped_payload = shell_escape(&payload_str);
109
110 let mut cmd = String::from("curl -X POST");
111 cmd.push_str(" -H 'Content-Type: application/json'");
112
113 if let Some(jwt_secret) = &self.jwt {
114 append_jwt_auth(&mut cmd, jwt_secret)?;
115 }
116
117 for h in &self.headers {
118 cmd.push_str(&format!(" -H '{}'", shell_escape(h)));
119 }
120
121 cmd.push_str(&format!(" --data-raw '{escaped_payload}'"));
122 cmd.push_str(&format!(" '{}'", shell_escape(self.url.as_str())));
123
124 Ok(cmd)
125 }
126
127 pub fn request(&self, req: RequestPacket) -> TransportFut<'static> {
129 let curl_cmd_result = self.generate_curl_command(&req);
130
131 Box::pin(async move {
132 match curl_cmd_result {
133 Ok(curl_cmd) => {
134 let _ = crate::sh_println!("{curl_cmd}");
135 std::process::exit(0);
136 }
137 Err(e) => {
138 let _ = crate::sh_eprintln!("Error: {e:?}");
139 std::process::exit(1);
140 }
141 }
142 })
143 }
144}
145
146impl Service<RequestPacket> for CurlTransport {
147 type Response = ResponsePacket;
148 type Error = TransportError;
149 type Future = TransportFut<'static>;
150
151 #[inline]
152 fn poll_ready(
153 &mut self,
154 _cx: &mut std::task::Context<'_>,
155 ) -> std::task::Poll<Result<(), Self::Error>> {
156 std::task::Poll::Ready(Ok(()))
157 }
158
159 #[inline]
160 fn call(&mut self, req: RequestPacket) -> Self::Future {
161 self.request(req)
162 }
163}
164
165impl Service<RequestPacket> for &CurlTransport {
166 type Response = ResponsePacket;
167 type Error = TransportError;
168 type Future = TransportFut<'static>;
169
170 #[inline]
171 fn poll_ready(
172 &mut self,
173 _cx: &mut std::task::Context<'_>,
174 ) -> std::task::Poll<Result<(), Self::Error>> {
175 std::task::Poll::Ready(Ok(()))
176 }
177
178 #[inline]
179 fn call(&mut self, req: RequestPacket) -> Self::Future {
180 self.request(req)
181 }
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187 use alloy_json_rpc::{Id, Request};
188
189 fn make_test_request() -> RequestPacket {
190 let req: Request<Vec<()>> = Request::new("eth_blockNumber", Id::Number(1), vec![]);
191 let serialized = req.serialize().unwrap();
192 RequestPacket::Single(serialized)
193 }
194
195 #[test]
196 fn test_basic_curl_command() {
197 let transport = CurlTransport::new("https://eth.example.com".parse().unwrap());
198 let req = make_test_request();
199 let cmd = transport.generate_curl_command(&req).unwrap();
200 assert!(cmd.contains("eth_blockNumber"));
201 assert!(cmd.contains("https://eth.example.com"));
202 assert!(cmd.contains("jsonrpc"));
203 }
204
205 #[test]
206 fn test_curl_with_headers() {
207 let transport = CurlTransport::new("https://eth.example.com".parse().unwrap())
208 .with_headers(vec!["X-Custom: value".to_string()]);
209 let req = make_test_request();
210 let cmd = transport.generate_curl_command(&req).unwrap();
211 assert!(cmd.contains("X-Custom: value"));
212 }
213
214 #[test]
215 fn test_curl_with_jwt() {
216 let jwt_secret = "5c43996d0d150a81f06ae452fce38120d97a4156650aec7487b3384bfe32edae";
217 let transport = CurlTransport::new("https://eth.example.com".parse().unwrap())
218 .with_jwt(Some(jwt_secret.to_string()));
219 let req = make_test_request();
220 let cmd = transport.generate_curl_command(&req).unwrap();
221
222 let jwt = cmd
223 .split("Authorization: Bearer ")
224 .nth(1)
225 .expect("missing Authorization header")
226 .split('\'')
227 .next()
228 .expect("malformed Authorization header");
229
230 let secret = JwtSecret::from_hex(jwt_secret).unwrap();
231 secret.validate(jwt).unwrap();
232 }
233
234 #[test]
235 fn test_shell_escape() {
236 let escaped = shell_escape("it's a test");
237 assert_eq!(escaped, "it'\"'\"'s a test");
238 }
239}