Skip to main content

anvil_server/
lib.rs

1//! Bootstrap [axum] RPC servers.
2
3#![cfg_attr(not(test), warn(unused_crate_dependencies))]
4#![cfg_attr(docsrs, feature(doc_cfg))]
5
6#[macro_use]
7extern crate tracing;
8
9use anvil_rpc::{
10    error::RpcError,
11    request::{Id, RpcMethodCall},
12    response::{ResponseResult, RpcResponse},
13};
14use axum::{
15    Router,
16    extract::DefaultBodyLimit,
17    http::{HeaderValue, Method, header},
18    routing::{MethodRouter, post},
19};
20use serde::de::DeserializeOwned;
21use std::fmt;
22use tower_http::{cors::CorsLayer, trace::TraceLayer};
23
24/// Redacts sensitive information from RPC request parameters for TRACE logging.
25///
26/// This function redacts:
27/// - Fork URLs in `anvil_reset`, `hardhat_reset`, and `anvil_setRpcUrl` methods.
28/// - Signed transaction data in submission, signing, classification, and tracing methods.
29fn redact_params(method: &str, params: &serde_json::Value) -> serde_json::Value {
30    match method {
31        // Redact signed transaction data, but keep transaction hashes visible
32        "eth_sendRawTransaction"
33        | "eth_sendRawTransactionSync"
34        | "eth_sendRawTransactionConditional"
35        | "eth_signRawTransaction"
36        | "anvil_classifyTransaction"
37        | "trace_rawTransaction" => {
38            if let Some(array) = params.as_array() {
39                if array.is_empty() {
40                    params.clone()
41                } else {
42                    // Keep the first param as "[SIGNED_TX]" for visibility that a tx was sent
43                    let mut redacted = vec![serde_json::Value::String("[SIGNED_TX]".to_string())];
44                    // Keep other params (e.g., timeout, conditional) visible as they're not
45                    // sensitive
46                    redacted.extend(array.iter().skip(1).cloned());
47                    serde_json::Value::Array(redacted)
48                }
49            } else {
50                params.clone()
51            }
52        }
53        _ => {
54            let mut redacted = params.clone();
55            redact_fork_credentials(method, &mut redacted);
56            redacted
57        }
58    }
59}
60
61/// Redacts fork credentials while preserving the rest of a failed request dump.
62/// Signed transactions remain available in failed dumps for reproduction.
63fn redact_fork_credentials(method: &str, params: &mut serde_json::Value) {
64    match method {
65        "anvil_reset" | "hardhat_reset" => redact_fork_params(params),
66        "anvil_setRpcUrl" => {
67            if let Some(serde_json::Value::String(url)) = params.get_mut(0) {
68                *url = redact_url_credentials(url);
69            }
70        }
71        _ => {}
72    }
73}
74
75/// Follows reset wrappers and positional URL slots while preserving other fields.
76fn redact_fork_params(param: &mut serde_json::Value) {
77    match param {
78        serde_json::Value::Array(array) => {
79            // Serde accepts Params, tagged forking, and ForkOpts as sequences. Their first
80            // element is the next wrapper or the URL; the block number follows the URL.
81            if let Some(first) = array.first_mut() {
82                redact_fork_params(first);
83            }
84        }
85        serde_json::Value::Object(object) => {
86            for (key, value) in object {
87                if matches!(key.as_str(), "params" | "forking" | "jsonRpcUrl") {
88                    redact_fork_params(value);
89                }
90            }
91        }
92        serde_json::Value::String(url) => *url = redact_url_credentials(url),
93        _ => {}
94    }
95}
96
97/// Keeps only the URL's scheme, host, and port, or fully redacts URLs without a host.
98fn redact_url_credentials(url: &str) -> String {
99    let Ok(mut parsed) = url::Url::parse(url) else { return "[REDACTED_URL]".to_string() };
100    // A scheme-less credential string can parse with its username as the scheme.
101    if parsed.host().is_none() {
102        return "[REDACTED_URL]".to_string();
103    }
104    // Credentials can occur in userinfo, paths, queries, or fragments regardless of length.
105    let _ = parsed.set_username("");
106    let _ = parsed.set_password(None);
107    parsed.set_path("");
108    parsed.set_query(None);
109    parsed.set_fragment(None);
110    parsed.to_string()
111}
112
113mod config;
114pub use config::ServerConfig;
115
116mod error;
117mod handler;
118
119mod pubsub;
120pub use pubsub::{PubSubContext, PubSubRpcHandler};
121
122mod ws;
123
124#[cfg(feature = "ipc")]
125pub mod ipc;
126
127/// Helper trait that is used to execute ethereum rpc calls
128pub trait RpcHandler: Clone + Send + Sync + 'static {
129    /// The request type to expect
130    type Request: DeserializeOwned + Send + Sync + fmt::Debug;
131
132    /// Invoked when the request was received
133    fn on_request(&self, request: Self::Request) -> impl Future<Output = ResponseResult> + Send;
134
135    /// Invoked for every incoming [`RpcMethodCall`]. Notifications are adapted to method calls
136    /// with an [`anvil_rpc::request::Id::Null`] identifier, and their responses are discarded.
137    ///
138    /// This will attempt to deserialize a `{ "method" : "<name>", "params": "<params>" }` message
139    /// into the `Request` type of this handler. If a `Request` instance was deserialized
140    /// successfully, [`Self::on_request`] will be invoked.
141    ///
142    /// **Note**: override this function if the expected `Request` deviates from `{ "method" :
143    /// "<name>", "params": "<params>" }`
144    fn on_call(&self, call: RpcMethodCall) -> impl Future<Output = RpcResponse> + Send {
145        async move {
146            let RpcMethodCall { method, params, id, .. } = call;
147
148            let mut params: serde_json::Value = params.into();
149
150            // Log with redacted sensitive parameters
151            trace!(target: "rpc", id = ?id, method = ?method, params = ?redact_params(&method, &params), "received method call");
152
153            let call = serde_json::json!({
154                "method": &method,
155                "params": params
156            });
157
158            match serde_json::from_value::<Self::Request>(call) {
159                Ok(req) => {
160                    let result = self.on_request(req).await;
161                    if let ResponseResult::Error(error) = &result {
162                        redact_fork_credentials(&method, &mut params);
163                        self.on_request_error(&method, &id, &params, error);
164                    }
165                    RpcResponse::new(id, result)
166                }
167                Err(err) => {
168                    let err = err.to_string();
169                    let method_not_found = serde_json::from_value::<Self::Request>(
170                        serde_json::json!({ "method": &method }),
171                    )
172                    .is_err_and(|err| err.to_string().contains("unknown variant"));
173                    if method_not_found {
174                        error!(target: "rpc", ?id, ?method, "failed to deserialize method due to unknown variant");
175                        RpcResponse::new(id, RpcError::method_not_found())
176                    } else {
177                        error!(target: "rpc", ?id, ?method, ?err, "failed to deserialize method");
178                        RpcResponse::new(id, RpcError::invalid_params(err))
179                    }
180                }
181            }
182        }
183    }
184
185    /// Invoked when a deserialized request returns an error from [`Self::on_request`].
186    /// Parameters retain their original shape with fork URL credentials redacted.
187    /// Deserialization errors are excluded because their messages can contain request parameters.
188    fn on_request_error(
189        &self,
190        _method: &str,
191        _id: &Id,
192        _params: &serde_json::Value,
193        _error: &RpcError,
194    ) {
195    }
196}
197
198/// Configures an [`axum::Router`] that handles JSON-RPC calls via both HTTP and WS.
199pub fn http_ws_router<Http, Ws>(config: ServerConfig, http: Http, ws: Ws) -> Router
200where
201    Http: RpcHandler,
202    Ws: PubSubRpcHandler,
203{
204    router_inner(config, post(handler::handle).get(ws::handle_ws), (http, ws))
205}
206
207/// Configures an [`axum::Router`] that handles JSON-RPC calls via HTTP.
208pub fn http_router<Http>(config: ServerConfig, http: Http) -> Router
209where
210    Http: RpcHandler,
211{
212    router_inner(config, post(handler::handle), (http, ()))
213}
214
215fn router_inner<S: Clone + Send + Sync + 'static>(
216    config: ServerConfig,
217    root_method_router: MethodRouter<S>,
218    state: S,
219) -> Router {
220    let ServerConfig { allow_origin, no_cors, no_request_size_limit } = config;
221
222    let mut router = Router::new()
223        .route("/", root_method_router)
224        .with_state(state)
225        .layer(TraceLayer::new_for_http());
226    if !no_cors {
227        // See [`tower_http::cors`](https://docs.rs/tower-http/latest/tower_http/cors/index.html)
228        // for more details.
229        router = router.layer(
230            CorsLayer::new()
231                .allow_origin(allow_origin.0)
232                .allow_headers([header::CONTENT_TYPE])
233                .allow_methods([Method::GET, Method::POST]),
234        );
235    }
236    if no_request_size_limit {
237        router = router.layer(DefaultBodyLimit::disable());
238    }
239    router
240}
241
242#[cfg(test)]
243mod tests {
244    use super::*;
245    use serde_json::json;
246
247    #[test]
248    fn redacts_url_credentials() {
249        for (url, expected) in [
250            ("https://user:pass@example.invalid:8545/", "https://example.invalid:8545/"),
251            ("https://example.invalid:8545/key?token=x#secret", "https://example.invalid:8545/"),
252            (
253                "https://example.invalid:8545/abcdefghijklmnop?api_key=abcdefghijklmnop",
254                "https://example.invalid:8545/",
255            ),
256            ("invalid secret URL", "[REDACTED_URL]"),
257            ("https://user:pass@invalid host/secret", "[REDACTED_URL]"),
258            ("mykey:secret@example.invalid/rpc/key", "[REDACTED_URL]"),
259            ("mykey:/secret@example.invalid/rpc/key", "[REDACTED_URL]"),
260        ] {
261            assert_eq!(redact_url_credentials(url), expected, "{url}");
262        }
263    }
264
265    #[test]
266    fn redacts_fork_parameter_shapes_without_losing_context() {
267        let url = "https://user:pass@example.invalid:8545/key?token=x#secret";
268        let safe = "https://example.invalid:8545/";
269        for (params, expected) in [
270            (json!([{"forking": {"jsonRpcUrl": url}}]), json!([{"forking": {"jsonRpcUrl": safe}}])),
271            (json!({"forking": {"jsonRpcUrl": url}}), json!({"forking": {"jsonRpcUrl": safe}})),
272            (json!({"jsonRpcUrl": url}), json!({"jsonRpcUrl": safe})),
273            (
274                json!({"params": {"forking": {"jsonRpcUrl": url, "blockNumber": 42}}}),
275                json!({"params": {"forking": {"jsonRpcUrl": safe, "blockNumber": 42}}}),
276            ),
277            (json!([[url, "0x2a"]]), json!([[safe, "0x2a"]])),
278            (json!([{"forking": [url, 42]}]), json!([{"forking": [safe, 42]}])),
279            (json!({"params": [url, 42]}), json!({"params": [safe, 42]})),
280            (json!([[[url, 42]]]), json!([[[safe, 42]]])),
281            (json!({"params": [[url, 42]]}), json!({"params": [[safe, 42]]})),
282            (json!([[url]]), json!([[safe]])),
283            (json!([[null, "0x2a"]]), json!([[null, "0x2a"]])),
284            (
285                json!({"params": {"blockNumber": "0x2a", "extra": ["diagnostic"]}}),
286                json!({"params": {"blockNumber": "0x2a", "extra": ["diagnostic"]}}),
287            ),
288            (
289                json!({"params": {"forking": [null, 42], "extra": ["diagnostic"]}}),
290                json!({"params": {"forking": [null, 42], "extra": ["diagnostic"]}}),
291            ),
292        ] {
293            for method in ["anvil_reset", "hardhat_reset"] {
294                assert_eq!(redact_params(method, &params), expected, "{method}: {params}");
295                let mut failed = params.clone();
296                redact_fork_credentials(method, &mut failed);
297                assert_eq!(failed, expected, "{method}: {params}");
298            }
299        }
300        for (mut params, expected) in [
301            (json!([url]), json!([safe])),
302            (json!(["invalid secret URL"]), json!(["[REDACTED_URL]"])),
303        ] {
304            assert_eq!(redact_params("anvil_setRpcUrl", &params), expected);
305            redact_fork_credentials("anvil_setRpcUrl", &mut params);
306            assert_eq!(params, expected);
307        }
308    }
309
310    #[test]
311    fn preserves_failure_params_and_redacts_only_trace_transactions() {
312        for method in [
313            "eth_sendRawTransaction",
314            "eth_sendRawTransactionSync",
315            "eth_sendRawTransactionConditional",
316            "eth_signRawTransaction",
317            "anvil_classifyTransaction",
318            "trace_rawTransaction",
319        ] {
320            for (params, expected) in [
321                (json!(["0x12345678"]), json!(["[SIGNED_TX]"])),
322                (json!(["0x12345678", 5000]), json!(["[SIGNED_TX]", 5000])),
323                (
324                    json!(["0x12345678", ["trace", "stateDiff"], "0x2a"]),
325                    json!(["[SIGNED_TX]", ["trace", "stateDiff"], "0x2a"]),
326                ),
327                (json!([]), json!([])),
328                (json!(null), json!(null)),
329            ] {
330                assert_eq!(redact_params(method, &params), expected, "{method}");
331                let mut failed = params.clone();
332                redact_fork_credentials(method, &mut failed);
333                assert_eq!(failed, params, "{method}");
334            }
335        }
336        let params = json!([{"to": "0x1111111111111111111111111111111111111111", "data": "0x12345678"}, "0x100000"]);
337        assert_eq!(redact_params("eth_call", &params), params);
338        let mut failed = params.clone();
339        redact_fork_credentials("eth_call", &mut failed);
340        assert_eq!(failed, params);
341    }
342}