use std::collections::HashMap;
use std::sync::OnceLock;
use serde::Deserialize;
#[derive(Debug, Clone, Deserialize)]
pub struct ChainFeature {
pub name: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ChainCurrency {
pub name: String,
pub symbol: String,
pub decimals: i64,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ChainEns {
pub registry: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ChainExplorer {
pub name: String,
pub url: String,
#[serde(default)]
pub standard: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ChainInfo {
pub name: String,
#[serde(default)]
pub chain: String,
#[serde(default)]
pub icon: String,
#[serde(default)]
pub rpc: Vec<String>,
#[serde(default)]
pub features: Vec<ChainFeature>,
#[serde(default)]
pub faucets: Vec<String>,
#[serde(rename = "nativeCurrency", default)]
pub native_currency: Option<ChainCurrency>,
#[serde(rename = "infoURL", default)]
pub info_url: String,
#[serde(rename = "shortName", default)]
pub short_name: String,
#[serde(rename = "chainId")]
pub chain_id: u64,
#[serde(rename = "networkId", default)]
pub network_id: u64,
#[serde(default)]
pub slip44: Option<i64>,
#[serde(default)]
pub ens: Option<ChainEns>,
#[serde(default)]
pub explorers: Vec<ChainExplorer>,
}
impl ChainInfo {
pub fn has_feature(&self, feat: &str) -> bool {
self.features.iter().any(|f| f.name == feat)
}
pub fn transaction_url(&self, tx_hash: &str) -> Option<String> {
self.explorers
.first()
.map(|e| format!("{}/tx/{}", e.url, tx_hash))
}
pub fn explorer_url(&self) -> Option<&str> {
self.explorers.first().map(|e| e.url.as_str())
}
}
const CHAINS_JSON: &str = include_str!("chains_data.json");
fn registry() -> &'static HashMap<u64, ChainInfo> {
static REGISTRY: OnceLock<HashMap<u64, ChainInfo>> = OnceLock::new();
REGISTRY.get_or_init(|| {
let raw: HashMap<String, ChainInfo> =
serde_json::from_str(CHAINS_JSON).expect("embedded chains_data.json must be valid");
raw.into_iter()
.filter_map(|(k, v)| k.parse::<u64>().ok().map(|id| (id, v)))
.collect()
})
}
pub fn get(id: u64) -> Option<&'static ChainInfo> {
registry().get(&id)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ethereum_mainnet() {
let ci = get(1).expect("chain 1");
assert_eq!(ci.name, "Ethereum Mainnet");
assert_eq!(ci.chain_id, 1);
assert_eq!(ci.native_currency.as_ref().unwrap().symbol, "ETH");
}
#[test]
fn cached_same_pointer() {
let a = get(1).unwrap();
let b = get(1).unwrap();
assert!(std::ptr::eq(a, b));
}
#[test]
fn unknown_chain() {
assert!(get(0).is_none());
}
#[test]
fn features() {
let ci = get(1).unwrap();
assert!(ci.has_feature("EIP155"));
assert!(ci.has_feature("EIP1559"));
assert!(!ci.has_feature("nonexistent"));
}
#[test]
fn transaction_and_explorer_urls() {
let ci = get(1).unwrap();
assert_eq!(
ci.transaction_url("0xabc123").unwrap(),
"https://etherscan.io/tx/0xabc123"
);
assert_eq!(ci.explorer_url().unwrap(), "https://etherscan.io");
}
#[test]
fn no_explorer() {
let ci = ChainInfo {
name: "Test".to_string(),
chain: String::new(),
icon: String::new(),
rpc: vec![],
features: vec![],
faucets: vec![],
native_currency: None,
info_url: String::new(),
short_name: String::new(),
chain_id: 0,
network_id: 0,
slip44: None,
ens: None,
explorers: vec![],
};
assert!(ci.transaction_url("0xabc").is_none());
assert!(ci.explorer_url().is_none());
}
}