1use alloy_primitives::U256;
24use serde::{de, Deserialize, Deserializer};
25use std::time::Duration;
26use thiserror::Error;
27
28pub mod eip;
29pub mod schema;
30
31include!(concat!(env!("OUT_DIR"), "/chain_generated.rs"));
32
33#[cfg(test)]
34mod test {
35 use super::{all_chains, Chain};
36 use crate::schema;
37 use serde_json::Value;
38 use std::collections::HashSet;
39 use strum::IntoEnumIterator;
40
41 #[test]
42 fn test_chain_count_vs_json() {
43 use std::fs;
44 use std::path::PathBuf;
45
46 let enum_count = Chain::iter().count();
47 println!("enum_count: {enum_count}");
48
49 let local_chain_ids: HashSet<u64> = Chain::iter().map(|chain| chain.id()).collect();
50
51 let path = option_env!("CHAINS_JSON_PATH")
53 .map(PathBuf::from)
54 .expect("CHAINS_JSON_PATH not set; build script should have downloaded chains.json");
55 let json_text = fs::read_to_string(&path)
56 .unwrap_or_else(|e| panic!("Failed to read {:?}: {}", path, e));
57 let chains: Vec<Value> = serde_json::from_str(&json_text).expect("Failed to parse JSON");
58 let json_chain_count = chains.len();
59
60 println!("chains.json contains {json_chain_count} chains");
61 assert_eq!(
62 enum_count, json_chain_count,
63 "enum_count and json_chain_count should be equal"
64 );
65
66 if enum_count != json_chain_count {
67 for chain_data in chains {
68 if let Some(chain_id) = chain_data.get("chainId").and_then(|id| id.as_u64()) {
69 if !local_chain_ids.contains(&chain_id) {
70 let name = chain_data
71 .get("name")
72 .and_then(|n| n.as_str())
73 .unwrap_or("Unknown");
74 let short_name = chain_data
75 .get("shortName")
76 .and_then(|n| n.as_str())
77 .unwrap_or("Unknown");
78 println!(
79 "Missing chain: ID={chain_id}, Name={name}, ShortName={short_name}"
80 );
81 }
82 }
83 }
84 }
85 }
86
87 #[test]
88 fn test_chain_properties() {
89 let mainnet = Chain::Mainnet;
91 assert_eq!(mainnet.id(), 1);
92 assert!(mainnet.name().contains("Ethereum"));
93
94 let (_name, symbol, decimals) = mainnet.native_currency();
96 assert_eq!(symbol.to_uppercase(), "ETH");
97 assert_eq!(decimals, 18);
98
99 assert!(!Chain::Mainnet.rpc_urls().is_empty());
101
102 assert!(Chain::Mainnet.info_url().starts_with("http"));
104
105 assert_eq!(Chain::Mainnet.short_name().to_uppercase(), "ETH");
107
108 assert_eq!(Chain::Mainnet.slip44(), Some(60));
110 }
111
112 #[test]
113 fn test_blocks_in() {
114 const TARGET_AGE: u64 = 6 * 60 * 60 * 1000; assert_eq!(Chain::Mainnet.blocks_in(TARGET_AGE).round(), 1800.0);
117 assert_eq!(Chain::Sepolia.blocks_in(TARGET_AGE).round(), 1800.0);
118 }
120
121 #[test]
122 fn test_deserialize_from_str() {
123 let json_data = "\"1\""; let network: Chain = serde_json::from_str(json_data).unwrap();
126 assert_eq!(network, Chain::Mainnet);
127
128 let json_data = "\"11155111\""; let network: Chain = serde_json::from_str(json_data).unwrap();
130 assert_eq!(network, Chain::Sepolia);
131
132 let json_data = "\"invalid\""; let result: Result<Chain, _> = serde_json::from_str(json_data);
137 assert!(result.is_err());
138 }
139
140 #[test]
141 fn chains_sorted_and_unique() {
142 let ids: Vec<u64> = Chain::iter().map(|c| c.id()).collect();
143 assert!(!ids.is_empty(), "Chain list should not be empty");
144
145 let mut sorted = ids.clone();
146 sorted.sort_unstable();
147 assert_eq!(
148 ids, sorted,
149 "Chain variants should stay ordered by chain id"
150 );
151
152 let unique: HashSet<u64> = ids.iter().copied().collect();
153 assert_eq!(
154 ids.len(),
155 unique.len(),
156 "Chain ids should be unique across the enum"
157 );
158 }
159
160 #[test]
161 fn chain_records_have_basic_fields() {
162 let mut short_names = HashSet::new();
163
164 for record in all_chains() {
165 assert!(
166 Chain::try_from(record.chain_id).is_ok(),
167 "Chain::try_from should cover chain_id {}",
168 record.chain_id
169 );
170 assert!(
171 record.chain_id > 0,
172 "chain_id should be positive for {}",
173 record.name
174 );
175 assert!(
176 !record.name.trim().is_empty(),
177 "name should not be empty for chain_id {}",
178 record.chain_id
179 );
180 assert!(
181 !record.chain.trim().is_empty(),
182 "chain slug should not be empty for chain_id {}",
183 record.chain_id
184 );
185 assert!(
186 !record.short_name.trim().is_empty(),
187 "short_name should not be empty for chain_id {}",
188 record.chain_id
189 );
190 assert!(
191 short_names.insert(record.short_name.as_str()),
192 "short_name {} reused for chain_id {}",
193 record.short_name,
194 record.chain_id
195 );
196 assert!(
197 !record.native_currency.name.trim().is_empty(),
198 "native currency name missing for chain_id {}",
199 record.chain_id
200 );
201 assert!(
202 !record.native_currency.symbol.trim().is_empty(),
203 "native currency symbol missing for chain_id {}",
204 record.chain_id
205 );
206 assert!(
207 record.native_currency.decimals > 0,
208 "native currency decimals must be >0 for chain_id {}",
209 record.chain_id
210 );
211 }
212 }
213
214 #[test]
215 fn schema_loader_matches_bundled_data() {
216 let loaded = schema::load_chains().expect("schema::load_chains should succeed");
217 let bundled = all_chains();
218
219 assert_eq!(
220 loaded.len(),
221 bundled.len(),
222 "schema loader should match bundled chain count"
223 );
224
225 let loaded_ids: HashSet<u64> = loaded.iter().map(|c| c.chain_id).collect();
226 let bundled_ids: HashSet<u64> = bundled.iter().map(|c| c.chain_id).collect();
227 assert_eq!(
228 loaded_ids, bundled_ids,
229 "schema loader and bundled data should agree on chain ids"
230 );
231 }
232}