Skip to main content

alloy_chains/
spec.rs

1//! Specification of Ethereum EIP-155 chains.
2
3use crate::NamedChain;
4
5#[allow(unused_imports)]
6use alloc::{
7    collections::BTreeMap,
8    string::{String, ToString},
9};
10
11/// Ethereum EIP-155 chains.
12#[derive(Clone, Debug)]
13#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
14#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
15#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
16pub struct Chains {
17    /// Map of chain IDs to chain definitions.
18    pub chains: BTreeMap<u64, Chain>,
19}
20
21impl Default for Chains {
22    #[inline]
23    fn default() -> Self {
24        Self::new()
25    }
26}
27
28impl Chains {
29    /// Constructs an empty set of chains.
30    #[inline]
31    pub fn empty() -> Self {
32        Self { chains: Default::default() }
33    }
34
35    /// Returns the default chains.
36    pub fn new() -> Self {
37        Self { chains: NamedChain::iter().map(|c| (c as u64, Chain::new(c))).collect() }
38    }
39}
40
41/// Specification for a single chain.
42#[derive(Clone, Debug)]
43#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
44#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
45#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
46pub struct Chain {
47    /// The chain's internal ID. This is the Rust enum variant's name.
48    pub internal_id: String,
49    /// The chain's name. This is used in CLI argument parsing, TOML serialization etc.
50    pub name: String,
51    /// An optional hint for the average block time, in milliseconds.
52    pub average_blocktime_hint: Option<u64>,
53    /// Whether the chain is a legacy chain, which does not support EIP-1559.
54    pub is_legacy: bool,
55    /// Whether the chain supports the Shanghai hardfork.
56    pub supports_shanghai: bool,
57    /// Whether the chain is a testnet.
58    pub is_testnet: bool,
59    /// The chain's native currency symbol (e.g. `ETH`).
60    pub native_currency_symbol: Option<String>,
61    /// The chain's base block explorer API URL (e.g. `https://api.etherscan.io/v2/api?chainid=1`).
62    pub etherscan_api_url: Option<String>,
63    /// The chain's base block explorer base URL (e.g. `https://etherscan.io`).
64    pub etherscan_base_url: Option<String>,
65    /// The name of the environment variable that contains the Etherscan API key.
66    pub etherscan_api_key_name: Option<String>,
67}
68
69impl Chain {
70    /// Constructs a new chain specification from the given [`NamedChain`].
71    pub fn new(c: NamedChain) -> Self {
72        let (etherscan_api_url, etherscan_base_url) = c.etherscan_urls().unzip();
73        Self {
74            internal_id: format!("{c:?}"),
75            name: c.to_string(),
76            average_blocktime_hint: c
77                .average_blocktime_hint()
78                .map(|d| d.as_millis().try_into().unwrap_or(u64::MAX)),
79            is_legacy: c.is_legacy(),
80            supports_shanghai: c.supports_shanghai(),
81            is_testnet: c.is_testnet(),
82            native_currency_symbol: c.native_currency_symbol().map(Into::into),
83            etherscan_api_url: etherscan_api_url.map(Into::into),
84            etherscan_base_url: etherscan_base_url.map(Into::into),
85            etherscan_api_key_name: c.etherscan_api_key_name().map(Into::into),
86        }
87    }
88}
89
90#[cfg(all(test, feature = "std", feature = "serde", feature = "schema"))]
91mod tests {
92    use super::*;
93    use std::{fs, path::Path};
94
95    const JSON_PATH: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/assets/chains.json");
96    const SCHEMA_PATH: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/assets/chains.schema.json");
97
98    fn json_chains() -> String {
99        pretty_json(&Chains::new())
100    }
101
102    fn json_schema() -> String {
103        pretty_json(&schemars::schema_for!(Chains))
104    }
105
106    fn pretty_json<T: serde::Serialize>(value: &T) -> String {
107        let mut json = serde_json::to_string_pretty(value).unwrap();
108        json.push('\n');
109        json
110    }
111
112    #[test]
113    #[cfg_attr(miri, ignore = "no fs")]
114    fn spec_up_to_date() {
115        ensure_file_contents(Path::new(JSON_PATH), &json_chains());
116    }
117
118    #[test]
119    #[cfg_attr(miri, ignore = "no fs")]
120    fn schema_up_to_date() {
121        ensure_file_contents(Path::new(SCHEMA_PATH), &json_schema());
122    }
123
124    /// Checks that the `file` has the specified `contents`. If that is not the
125    /// case, updates the file and then fails the test.
126    fn ensure_file_contents(file: &Path, contents: &str) {
127        if let Ok(old_contents) = fs::read_to_string(file) {
128            if normalize_newlines(&old_contents) == normalize_newlines(contents) {
129                // File is already up to date.
130                return;
131            }
132        }
133
134        eprintln!("\n\x1b[31;1merror\x1b[0m: {} was not up-to-date, updating\n", file.display());
135        if std::env::var("CI").is_ok() {
136            eprintln!(
137                "    NOTE: run `cargo test --all-features` locally and commit the updated files\n"
138            );
139        }
140        if let Some(parent) = file.parent() {
141            let _ = fs::create_dir_all(parent);
142        }
143        fs::write(file, contents).unwrap();
144        panic!("some file was not up to date and has been updated, simply re-run the tests");
145    }
146
147    fn normalize_newlines(s: &str) -> String {
148        s.replace("\r\n", "\n")
149    }
150}