use crate::chain::{ChainId, ConnectionBackend, GenesisHash};
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct ChainRegistryEntry {
pub id: ChainId,
pub genesis_hash: GenesisHash,
pub display_name: String,
pub endpoint: String,
pub backend: ConnectionBackend,
pub relay_db_key: String,
pub para_db_key: String,
pub chain_specs: Option<(String, String)>,
}
#[derive(Debug, Default)]
pub struct ChainRegistry {
entries: Vec<ChainRegistryEntry>,
by_hash: HashMap<GenesisHash, ChainId>,
}
impl ChainRegistry {
pub fn from_known_chains() -> Self {
let mut registry = Self::default();
for &id in ChainId::all() {
if let Some(hash) = id.genesis_hash() {
registry.insert_entry(ChainRegistryEntry {
id,
genesis_hash: hash,
display_name: id.display_name().to_string(),
endpoint: id.endpoint().to_string(),
backend: id.backend(),
relay_db_key: id.relay_db_key().to_string(),
para_db_key: id.para_db_key().to_string(),
chain_specs: id
.chain_specs()
.map(|(r, p)| (r.to_string(), p.to_string())),
});
}
}
registry
}
pub fn insert_entry(&mut self, entry: ChainRegistryEntry) {
let id = entry.id;
let genesis_hash = entry.genesis_hash;
self.by_hash.retain(|_, v| *v != id);
self.entries
.retain(|e| e.id != id && e.genesis_hash != genesis_hash);
self.entries.push(entry);
self.by_hash.insert(genesis_hash, id);
}
pub fn insert(&mut self, id: ChainId, genesis_hash: GenesisHash) {
self.insert_entry(ChainRegistryEntry {
id,
genesis_hash,
display_name: id.display_name().to_string(),
endpoint: id.endpoint().to_string(),
backend: id.backend(),
relay_db_key: id.relay_db_key().to_string(),
para_db_key: id.para_db_key().to_string(),
chain_specs: id
.chain_specs()
.map(|(r, p)| (r.to_string(), p.to_string())),
});
}
pub fn by_genesis_hash(&self, hash: &GenesisHash) -> Option<ChainId> {
self.by_hash.get(hash).copied()
}
pub fn genesis_hashes(&self) -> Vec<GenesisHash> {
self.entries.iter().map(|e| e.genesis_hash).collect()
}
pub fn entries(&self) -> &[ChainRegistryEntry] {
&self.entries
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::chain::GENESIS_POLKADOT_ASSET_HUB;
#[test]
fn test_from_known_chains_includes_polkadot_asset_hub() {
let registry = ChainRegistry::from_known_chains();
let ids: Vec<ChainId> = registry.entries().iter().map(|e| e.id).collect();
assert!(
ids.contains(&ChainId::PolkadotAssetHub),
"registry must contain PolkadotAssetHub"
);
}
#[test]
fn test_by_genesis_hash_returns_correct_chain() {
let registry = ChainRegistry::from_known_chains();
let result = registry.by_genesis_hash(&GENESIS_POLKADOT_ASSET_HUB);
assert_eq!(result, Some(ChainId::PolkadotAssetHub));
}
#[test]
fn test_by_genesis_hash_returns_none_for_unknown() {
let registry = ChainRegistry::from_known_chains();
let unknown: GenesisHash = [0xde; 32];
assert_eq!(registry.by_genesis_hash(&unknown), None);
}
#[test]
fn test_insert_and_lookup() {
let mut registry = ChainRegistry::default();
let hash: GenesisHash = [0x01; 32];
registry.insert(ChainId::PaseoAssetHub, hash);
assert_eq!(
registry.by_genesis_hash(&hash),
Some(ChainId::PaseoAssetHub)
);
assert_eq!(registry.entries().len(), 1);
registry.insert(ChainId::PaseoPeople, hash);
assert_eq!(registry.entries().len(), 1);
assert_eq!(registry.by_genesis_hash(&hash), Some(ChainId::PaseoPeople));
}
#[test]
fn test_insert_same_chain_id_different_hash_replaces() {
let mut registry = ChainRegistry::default();
let hash_a: GenesisHash = [0xAA; 32];
let hash_b: GenesisHash = [0xBB; 32];
registry.insert(ChainId::PaseoAssetHub, hash_a);
assert_eq!(
registry.by_genesis_hash(&hash_a),
Some(ChainId::PaseoAssetHub)
);
registry.insert(ChainId::PaseoAssetHub, hash_b);
assert_eq!(registry.entries().len(), 1, "old entry must be removed");
assert_eq!(
registry.by_genesis_hash(&hash_b),
Some(ChainId::PaseoAssetHub)
);
assert_eq!(
registry.by_genesis_hash(&hash_a),
None,
"old hash must be gone"
);
}
#[test]
fn test_genesis_hashes_returns_all() {
let mut registry = ChainRegistry::default();
let hash_a: GenesisHash = [0xAA; 32];
let hash_b: GenesisHash = [0xBB; 32];
registry.insert(ChainId::PaseoAssetHub, hash_a);
registry.insert(ChainId::PaseoPeople, hash_b);
let hashes = registry.genesis_hashes();
assert_eq!(hashes.len(), 2);
assert!(hashes.contains(&hash_a));
assert!(hashes.contains(&hash_b));
}
#[test]
fn test_empty_registry() {
let registry = ChainRegistry::default();
assert!(registry.entries().is_empty());
assert!(registry.genesis_hashes().is_empty());
assert_eq!(registry.by_genesis_hash(&[0u8; 32]), None);
}
#[test]
fn test_entry_has_display_name_and_endpoint() {
let registry = ChainRegistry::from_known_chains();
let entry = registry
.entries()
.iter()
.find(|e| e.id == ChainId::PolkadotAssetHub)
.expect("PolkadotAssetHub must be present in from_known_chains()");
assert_eq!(entry.display_name, "Polkadot Asset Hub");
assert_eq!(entry.endpoint, "wss://polkadot-asset-hub-rpc.polkadot.io");
}
#[test]
fn test_entry_has_chain_specs_for_smoldot_chains() {
let registry = ChainRegistry::from_known_chains();
let entry = registry
.entries()
.iter()
.find(|e| e.id == ChainId::PolkadotAssetHub)
.expect("PolkadotAssetHub must be present in from_known_chains()");
assert!(
entry.chain_specs.is_some(),
"PolkadotAssetHub entry must carry chain_specs for smoldot backend"
);
let (relay_spec, para_spec) = entry.chain_specs.as_ref().unwrap();
assert!(!relay_spec.is_empty(), "relay chain spec must not be empty");
assert!(!para_spec.is_empty(), "para chain spec must not be empty");
}
#[test]
fn test_entry_relay_db_key_matches_legacy() {
let registry = ChainRegistry::from_known_chains();
let entry = registry
.entries()
.iter()
.find(|e| e.id == ChainId::PolkadotAssetHub)
.expect("PolkadotAssetHub must be present in from_known_chains()");
assert_eq!(
entry.relay_db_key,
ChainId::PolkadotAssetHub.relay_db_key(),
"ChainRegistryEntry.relay_db_key must match ChainId::relay_db_key()"
);
assert_eq!(entry.relay_db_key, "polkadot-relay");
}
}