Skip to main content

ethrpc_rs/chains/
mod.rs

1//! Static metadata for known EVM-compatible chains.
2//!
3//! The full chain registry is embedded in the binary and parsed once on first
4//! access.
5//!
6//! ```
7//! let eth = ethrpc_rs::chains::get(1).unwrap();
8//! assert_eq!(eth.name, "Ethereum Mainnet");
9//! assert_eq!(eth.native_currency.as_ref().unwrap().symbol, "ETH");
10//! assert!(eth.has_feature("EIP1559"));
11//! ```
12
13use std::collections::HashMap;
14use std::sync::OnceLock;
15
16use serde::Deserialize;
17
18/// A feature supported by a chain (e.g. `EIP155`, `EIP1559`).
19#[derive(Debug, Clone, Deserialize)]
20pub struct ChainFeature {
21    /// The feature name.
22    pub name: String,
23}
24
25/// A chain's native currency.
26#[derive(Debug, Clone, Deserialize)]
27pub struct ChainCurrency {
28    /// The currency name (e.g. `Ether`).
29    pub name: String,
30    /// The currency symbol (e.g. `ETH`).
31    pub symbol: String,
32    /// The number of decimals.
33    pub decimals: i64,
34}
35
36/// ENS registry information for a chain.
37#[derive(Debug, Clone, Deserialize)]
38pub struct ChainEns {
39    /// The ENS registry contract address.
40    pub registry: String,
41}
42
43/// A block explorer for a chain.
44#[derive(Debug, Clone, Deserialize)]
45pub struct ChainExplorer {
46    /// The explorer name.
47    pub name: String,
48    /// The explorer base URL.
49    pub url: String,
50    /// The standard implemented (e.g. `EIP3091`).
51    #[serde(default)]
52    pub standard: String,
53}
54
55/// Metadata for an EVM-compatible chain.
56#[derive(Debug, Clone, Deserialize)]
57pub struct ChainInfo {
58    /// The chain's human-readable name.
59    pub name: String,
60    /// The short chain identifier (e.g. `ETH`).
61    #[serde(default)]
62    pub chain: String,
63    /// An icon identifier, if any.
64    #[serde(default)]
65    pub icon: String,
66    /// Known RPC endpoints.
67    #[serde(default)]
68    pub rpc: Vec<String>,
69    /// Supported features.
70    #[serde(default)]
71    pub features: Vec<ChainFeature>,
72    /// Faucet URLs.
73    #[serde(default)]
74    pub faucets: Vec<String>,
75    /// The native currency.
76    #[serde(rename = "nativeCurrency", default)]
77    pub native_currency: Option<ChainCurrency>,
78    /// An informational URL.
79    #[serde(rename = "infoURL", default)]
80    pub info_url: String,
81    /// A short name (e.g. `eth`).
82    #[serde(rename = "shortName", default)]
83    pub short_name: String,
84    /// The numeric chain id.
85    #[serde(rename = "chainId")]
86    pub chain_id: u64,
87    /// The network id.
88    #[serde(rename = "networkId", default)]
89    pub network_id: u64,
90    /// SLIP-44 coin type, if any.
91    #[serde(default)]
92    pub slip44: Option<i64>,
93    /// ENS registry information, if any.
94    #[serde(default)]
95    pub ens: Option<ChainEns>,
96    /// Known block explorers.
97    #[serde(default)]
98    pub explorers: Vec<ChainExplorer>,
99}
100
101impl ChainInfo {
102    /// Reports whether the chain supports the named feature.
103    pub fn has_feature(&self, feat: &str) -> bool {
104        self.features.iter().any(|f| f.name == feat)
105    }
106
107    /// Returns a URL to view `tx_hash` on the chain's first explorer, or `None`
108    /// if no explorer is configured.
109    pub fn transaction_url(&self, tx_hash: &str) -> Option<String> {
110        self.explorers
111            .first()
112            .map(|e| format!("{}/tx/{}", e.url, tx_hash))
113    }
114
115    /// Returns the URL of the chain's first block explorer, or `None`.
116    pub fn explorer_url(&self) -> Option<&str> {
117        self.explorers.first().map(|e| e.url.as_str())
118    }
119}
120
121/// The embedded chain registry, keyed by chain id as a string.
122const CHAINS_JSON: &str = include_str!("chains_data.json");
123
124fn registry() -> &'static HashMap<u64, ChainInfo> {
125    static REGISTRY: OnceLock<HashMap<u64, ChainInfo>> = OnceLock::new();
126    REGISTRY.get_or_init(|| {
127        let raw: HashMap<String, ChainInfo> =
128            serde_json::from_str(CHAINS_JSON).expect("embedded chains_data.json must be valid");
129        raw.into_iter()
130            .filter_map(|(k, v)| k.parse::<u64>().ok().map(|id| (id, v)))
131            .collect()
132    })
133}
134
135/// Returns the [`ChainInfo`] for the given chain id, or `None` if unknown.
136pub fn get(id: u64) -> Option<&'static ChainInfo> {
137    registry().get(&id)
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143
144    #[test]
145    fn ethereum_mainnet() {
146        let ci = get(1).expect("chain 1");
147        assert_eq!(ci.name, "Ethereum Mainnet");
148        assert_eq!(ci.chain_id, 1);
149        assert_eq!(ci.native_currency.as_ref().unwrap().symbol, "ETH");
150    }
151
152    #[test]
153    fn cached_same_pointer() {
154        let a = get(1).unwrap();
155        let b = get(1).unwrap();
156        assert!(std::ptr::eq(a, b));
157    }
158
159    #[test]
160    fn unknown_chain() {
161        assert!(get(0).is_none());
162    }
163
164    #[test]
165    fn features() {
166        let ci = get(1).unwrap();
167        assert!(ci.has_feature("EIP155"));
168        assert!(ci.has_feature("EIP1559"));
169        assert!(!ci.has_feature("nonexistent"));
170    }
171
172    #[test]
173    fn transaction_and_explorer_urls() {
174        let ci = get(1).unwrap();
175        assert_eq!(
176            ci.transaction_url("0xabc123").unwrap(),
177            "https://etherscan.io/tx/0xabc123"
178        );
179        assert_eq!(ci.explorer_url().unwrap(), "https://etherscan.io");
180    }
181
182    #[test]
183    fn no_explorer() {
184        let ci = ChainInfo {
185            name: "Test".to_string(),
186            chain: String::new(),
187            icon: String::new(),
188            rpc: vec![],
189            features: vec![],
190            faucets: vec![],
191            native_currency: None,
192            info_url: String::new(),
193            short_name: String::new(),
194            chain_id: 0,
195            network_id: 0,
196            slip44: None,
197            ens: None,
198            explorers: vec![],
199        };
200        assert!(ci.transaction_url("0xabc").is_none());
201        assert!(ci.explorer_url().is_none());
202    }
203}