1use crate::NamedChain;
4use strum::IntoEnumIterator;
5
6#[allow(unused_imports)]
7use alloc::{
8 collections::BTreeMap,
9 string::{String, ToString},
10};
11
12#[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 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 #[inline]
32 pub fn empty() -> Self {
33 Self { chains: Default::default() }
34 }
35
36 pub fn new() -> Self {
38 Self { chains: NamedChain::iter().map(|c| (c as u64, Chain::new(c))).collect() }
39 }
40}
41
42#[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 pub internal_id: String,
50 pub name: String,
52 pub average_blocktime_hint: Option<u64>,
54 pub is_legacy: bool,
56 pub supports_shanghai: bool,
58 pub is_testnet: bool,
60 pub native_currency_symbol: Option<String>,
62 pub etherscan_api_url: Option<String>,
64 pub etherscan_base_url: Option<String>,
66 pub etherscan_api_key_name: Option<String>,
68}
69
70impl Chain {
71 pub fn new(c: NamedChain) -> Self {
73 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 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 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}