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        let (etherscan_api_url, etherscan_base_url) = c.etherscan_urls().unzip();
74        Self {
75            internal_id: format!("{c:?}"),
76            name: c.to_string(),
77            average_blocktime_hint: c
78                .average_blocktime_hint()
79                .map(|d| d.as_millis().try_into().unwrap_or(u64::MAX)),
80            is_legacy: c.is_legacy(),
81            supports_shanghai: c.supports_shanghai(),
82            is_testnet: c.is_testnet(),
83            native_currency_symbol: c.native_currency_symbol().map(Into::into),
84            etherscan_api_url: etherscan_api_url.map(Into::into),
85            etherscan_base_url: etherscan_base_url.map(Into::into),
86            etherscan_api_key_name: c.etherscan_api_key_name().map(Into::into),
87        }
88    }
89}
90
91#[cfg(all(test, feature = "std", feature = "serde", feature = "schema"))]
92mod tests {
93    use super::*;
94    use std::{fs, path::Path};
95
96    const JSON_PATH: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/assets/chains.json");
97    const SCHEMA_PATH: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/assets/chains.schema.json");
98
99    fn json_chains() -> String {
100        serde_json::to_string_pretty(&Chains::new()).unwrap()
101    }
102
103    fn json_schema() -> String {
104        serde_json::to_string_pretty(&schemars::schema_for!(Chains)).unwrap()
105    }
106
107    #[test]
108    #[cfg_attr(miri, ignore = "no fs")]
109    fn spec_up_to_date() {
110        ensure_file_contents(Path::new(JSON_PATH), &json_chains());
111    }
112
113    #[test]
114    #[cfg_attr(miri, ignore = "no fs")]
115    fn schema_up_to_date() {
116        ensure_file_contents(Path::new(SCHEMA_PATH), &json_schema());
117    }
118
119    /// Checks that the `file` has the specified `contents`. If that is not the
120    /// case, updates the file and then fails the test.
121    fn ensure_file_contents(file: &Path, contents: &str) {
122        if let Ok(old_contents) = fs::read_to_string(file) {
123            if normalize_newlines(&old_contents) == normalize_newlines(contents) {
124                // File is already up to date.
125                return;
126            }
127        }
128
129        eprintln!("\n\x1b[31;1merror\x1b[0m: {} was not up-to-date, updating\n", file.display());
130        if std::env::var("CI").is_ok() {
131            eprintln!(
132                "    NOTE: run `cargo test --all-features` locally and commit the updated files\n"
133            );
134        }
135        if let Some(parent) = file.parent() {
136            let _ = fs::create_dir_all(parent);
137        }
138        fs::write(file, contents).unwrap();
139        panic!("some file was not up to date and has been updated, simply re-run the tests");
140    }
141
142    fn normalize_newlines(s: &str) -> String {
143        s.replace("\r\n", "\n")
144    }
145}