1use crate::NamedChain;
4
5#[allow(unused_imports)]
6use alloc::{
7 collections::BTreeMap,
8 string::{String, ToString},
9};
10
11#[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 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 #[inline]
31 pub fn empty() -> Self {
32 Self { chains: Default::default() }
33 }
34
35 pub fn new() -> Self {
37 Self { chains: NamedChain::iter().map(|c| (c as u64, Chain::new(c))).collect() }
38 }
39}
40
41#[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 pub internal_id: String,
49 pub name: String,
51 pub average_blocktime_hint: Option<u64>,
53 pub is_legacy: bool,
55 pub supports_shanghai: bool,
57 pub is_testnet: bool,
59 pub native_currency_symbol: Option<String>,
61 pub etherscan_api_url: Option<String>,
63 pub etherscan_base_url: Option<String>,
65 pub etherscan_api_key_name: Option<String>,
67}
68
69impl Chain {
70 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 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 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}