stratus/eth/rpc/
rpc_client_app.rs

1use std::fmt::Display;
2
3#[cfg(feature = "metrics")]
4use crate::infra::metrics::MetricLabelValue;
5
6// Include the build-time generated client scopes matcher
7include!(concat!(env!("OUT_DIR"), "/client_scopes.rs"));
8
9#[derive(Debug, Clone, strum::EnumIs, PartialEq, Eq, Hash)]
10pub enum RpcClientApp {
11    /// Client application identified itself.
12    Identified(String),
13
14    /// Client application is unknown.
15    Unknown,
16}
17
18impl Display for RpcClientApp {
19    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
20        match self {
21            RpcClientApp::Identified(name) => write!(f, "{name}"),
22            RpcClientApp::Unknown => write!(f, "unknown"),
23        }
24    }
25}
26
27impl RpcClientApp {
28    /// Parse known client application name to groups.
29    pub fn parse(name: &str) -> RpcClientApp {
30        let name = name.trim().trim_start_matches('/').trim_end_matches('/').to_ascii_lowercase().replace('_', "-");
31        if name.is_empty() {
32            return RpcClientApp::Unknown;
33        }
34
35        RpcClientApp::Identified(create_client_scope(&name))
36    }
37}
38
39// -----------------------------------------------------------------------------
40// Serialization / Deserialization
41// -----------------------------------------------------------------------------
42impl serde::Serialize for RpcClientApp {
43    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
44    where
45        S: serde::Serializer,
46    {
47        match self {
48            RpcClientApp::Identified(client) => serializer.serialize_str(client.as_ref()),
49            RpcClientApp::Unknown => serializer.serialize_str("unknown"),
50        }
51    }
52}
53
54impl<'de> serde::Deserialize<'de> for RpcClientApp {
55    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
56    where
57        D: serde::Deserializer<'de>,
58    {
59        let value = String::deserialize(deserializer)?;
60        match value.as_str() {
61            "unknown" => Ok(Self::Unknown),
62            _ => Ok(Self::Identified(value)),
63        }
64    }
65}
66
67// -----------------------------------------------------------------------------
68// Conversions: Self -> Other
69// -----------------------------------------------------------------------------
70#[cfg(feature = "metrics")]
71impl From<&RpcClientApp> for MetricLabelValue {
72    fn from(value: &RpcClientApp) -> Self {
73        match value {
74            RpcClientApp::Identified(name) => Self::Some(name.to_string()),
75            RpcClientApp::Unknown => Self::Some("unknown".to_string()),
76        }
77    }
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83
84    #[test]
85    fn test_scope_parsing() {
86        // Test stratus scope (stratus*/ - prefix trimmed)
87        assert_eq!(RpcClientApp::parse("stratus-node").to_string(), "stratus::-node");
88        assert_eq!(RpcClientApp::parse("stratus").to_string(), "stratus::");
89
90        // Test acquiring scope (exact match: authorizer)
91        assert_eq!(RpcClientApp::parse("authorizer").to_string(), "acquiring::authorizer");
92
93        // Test banking scope (balance* and banking* - prefix NOT trimmed)
94        assert_eq!(RpcClientApp::parse("banking-service").to_string(), "banking::banking-service");
95        assert_eq!(RpcClientApp::parse("balance-checker").to_string(), "banking::balance-checker");
96
97        // Test issuing scope (issuing* and infinitecard* - prefix NOT trimmed)
98        assert_eq!(RpcClientApp::parse("issuing-api").to_string(), "issuing::issuing-api");
99        assert_eq!(RpcClientApp::parse("infinitecard-service").to_string(), "issuing::infinitecard-service");
100
101        // Test lending scope (lending* - prefix NOT trimmed)
102        assert_eq!(RpcClientApp::parse("lending-core").to_string(), "lending::lending-core");
103
104        // Test infra scope (exact matches: blockscout, golani, tx-replayer)
105        assert_eq!(RpcClientApp::parse("blockscout").to_string(), "infra::blockscout");
106        assert_eq!(RpcClientApp::parse("golani").to_string(), "infra::golani");
107        assert_eq!(RpcClientApp::parse("tx-replayer").to_string(), "infra::tx-replayer");
108
109        // Test user scope (user-*/ - prefix trimmed, OR exact match: insomnia)
110        assert_eq!(RpcClientApp::parse("user-service").to_string(), "user::service");
111        assert_eq!(RpcClientApp::parse("insomnia").to_string(), "user::insomnia");
112
113        // Test exact match edge cases - these should fall through to "other" scope
114        // Names similar to exact matches but with extra information
115        assert_eq!(RpcClientApp::parse("authorizer-v2").to_string(), "other::authorizer-v2");
116        assert_eq!(RpcClientApp::parse("authorizer.staging").to_string(), "other::authorizer.staging");
117        assert_eq!(RpcClientApp::parse("super-authorizer").to_string(), "other::super-authorizer");
118
119        assert_eq!(RpcClientApp::parse("blockscout-api").to_string(), "other::blockscout-api");
120        assert_eq!(RpcClientApp::parse("blockscout-staging").to_string(), "other::blockscout-staging");
121        assert_eq!(RpcClientApp::parse("mini-blockscout").to_string(), "other::mini-blockscout");
122
123        assert_eq!(RpcClientApp::parse("golani-dev").to_string(), "other::golani-dev");
124        assert_eq!(RpcClientApp::parse("golani.local").to_string(), "other::golani.local");
125        assert_eq!(RpcClientApp::parse("pre-golani").to_string(), "other::pre-golani");
126
127        assert_eq!(RpcClientApp::parse("tx-replayer-v3").to_string(), "other::tx-replayer-v3");
128        assert_eq!(RpcClientApp::parse("tx-replayer.backup").to_string(), "other::tx-replayer.backup");
129        assert_eq!(RpcClientApp::parse("new-tx-replayer").to_string(), "other::new-tx-replayer");
130
131        assert_eq!(RpcClientApp::parse("insomnia-client").to_string(), "other::insomnia-client");
132        assert_eq!(RpcClientApp::parse("insomnia.v8").to_string(), "other::insomnia.v8");
133        assert_eq!(RpcClientApp::parse("custom-insomnia").to_string(), "other::custom-insomnia");
134
135        // Test other scope (fallback)
136        assert_eq!(RpcClientApp::parse("random-service").to_string(), "other::random-service");
137    }
138}