Skip to main content

chainrpc_core/
method_safety.rs

1//! Method safety classification — prevents retrying unsafe (write) methods.
2
3use std::collections::HashSet;
4use std::sync::OnceLock;
5
6/// Classification of JSON-RPC method safety for retry/dedup decisions.
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum MethodSafety {
9    /// Safe to retry, deduplicate, and cache. Read-only methods.
10    Safe,
11    /// Idempotent — same tx hash is safe to re-submit, but NEVER auto-retry
12    /// with different parameters. e.g. eth_sendRawTransaction (same raw tx = same hash).
13    Idempotent,
14    /// NEVER auto-retry. Retrying could cause double-spend or duplicate side effects.
15    /// e.g. eth_sendTransaction (node signs, different nonce possible).
16    Unsafe,
17}
18
19/// Classify a JSON-RPC method by its safety level.
20pub fn classify_method(method: &str) -> MethodSafety {
21    if unsafe_methods().contains(method) {
22        MethodSafety::Unsafe
23    } else if idempotent_methods().contains(method) {
24        MethodSafety::Idempotent
25    } else {
26        MethodSafety::Safe
27    }
28}
29
30/// Returns true if the method is safe to retry on transient failure.
31pub fn is_safe_to_retry(method: &str) -> bool {
32    classify_method(method) == MethodSafety::Safe
33}
34
35/// Returns true if the method is safe to deduplicate (coalesce concurrent identical requests).
36pub fn is_safe_to_dedup(method: &str) -> bool {
37    classify_method(method) == MethodSafety::Safe
38}
39
40/// Returns true if the method result can be cached.
41pub fn is_cacheable(method: &str) -> bool {
42    classify_method(method) == MethodSafety::Safe
43}
44
45fn unsafe_methods() -> &'static HashSet<&'static str> {
46    static UNSAFE: OnceLock<HashSet<&'static str>> = OnceLock::new();
47    UNSAFE.get_or_init(|| {
48        [
49            "eth_sendTransaction",
50            "personal_sendTransaction",
51            "eth_sign",
52            "personal_sign",
53            "eth_signTransaction",
54        ]
55        .into_iter()
56        .collect()
57    })
58}
59
60fn idempotent_methods() -> &'static HashSet<&'static str> {
61    static IDEMPOTENT: OnceLock<HashSet<&'static str>> = OnceLock::new();
62    IDEMPOTENT.get_or_init(|| ["eth_sendRawTransaction"].into_iter().collect())
63}
64
65// All other methods are Safe by default (eth_call, eth_getBalance, eth_blockNumber, etc.)
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70
71    #[test]
72    fn safe_read_methods() {
73        assert_eq!(classify_method("eth_blockNumber"), MethodSafety::Safe);
74        assert_eq!(classify_method("eth_getBalance"), MethodSafety::Safe);
75        assert_eq!(classify_method("eth_call"), MethodSafety::Safe);
76        assert_eq!(classify_method("eth_getLogs"), MethodSafety::Safe);
77        assert_eq!(
78            classify_method("eth_getTransactionReceipt"),
79            MethodSafety::Safe
80        );
81        assert_eq!(classify_method("eth_getBlockByNumber"), MethodSafety::Safe);
82        assert_eq!(classify_method("eth_chainId"), MethodSafety::Safe);
83        assert_eq!(classify_method("net_version"), MethodSafety::Safe);
84    }
85
86    #[test]
87    fn idempotent_methods_test() {
88        assert_eq!(
89            classify_method("eth_sendRawTransaction"),
90            MethodSafety::Idempotent
91        );
92    }
93
94    #[test]
95    fn unsafe_methods_test() {
96        assert_eq!(classify_method("eth_sendTransaction"), MethodSafety::Unsafe);
97        assert_eq!(
98            classify_method("personal_sendTransaction"),
99            MethodSafety::Unsafe
100        );
101        assert_eq!(classify_method("eth_sign"), MethodSafety::Unsafe);
102    }
103
104    #[test]
105    fn retry_safety() {
106        assert!(is_safe_to_retry("eth_blockNumber"));
107        assert!(!is_safe_to_retry("eth_sendRawTransaction"));
108        assert!(!is_safe_to_retry("eth_sendTransaction"));
109    }
110
111    #[test]
112    fn dedup_safety() {
113        assert!(is_safe_to_dedup("eth_getBalance"));
114        assert!(!is_safe_to_dedup("eth_sendRawTransaction"));
115    }
116
117    #[test]
118    fn unknown_methods_are_safe() {
119        assert_eq!(classify_method("custom_rpc_method"), MethodSafety::Safe);
120    }
121}