alloy_chains/
spec.rs

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