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) = 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 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 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}