pub type GenesisHash = [u8; 32];
pub const GENESIS_POLKADOT_ASSET_HUB: GenesisHash = [
0x68, 0xd5, 0x6f, 0x15, 0xf8, 0x5d, 0x31, 0x36, 0x97, 0x0e, 0xc1, 0x69, 0x46, 0x04, 0x0b, 0xc1,
0x75, 0x26, 0x54, 0xe9, 0x06, 0x14, 0x7f, 0x7e, 0x43, 0xe9, 0xd5, 0x39, 0xd7, 0xc3, 0xde, 0x2f,
];
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub enum ChainId {
PolkadotAssetHub,
PaseoAssetHub,
PolkadotPeople,
PaseoPeople,
Previewnet,
Individuality,
Ethereum,
EthereumSepolia,
Bitcoin,
BitcoinSignet,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConnectionBackend {
Smoldot,
Rpc,
Kyoto,
Helios,
}
impl ChainId {
pub fn endpoint(self) -> &'static str {
match self {
ChainId::PolkadotAssetHub => "wss://polkadot-asset-hub-rpc.polkadot.io",
ChainId::PaseoAssetHub => "wss://asset-hub-paseo.dotters.network",
ChainId::PolkadotPeople => "wss://polkadot-people-rpc.polkadot.io",
ChainId::PaseoPeople => "wss://people-paseo.dotters.network",
ChainId::Previewnet => "wss://previewnet.dotsamalabs.com/asset-hub",
ChainId::Individuality => "wss://pop3-testnet.parity-lab.parity.io/people",
ChainId::Ethereum => "",
ChainId::EthereumSepolia => "",
ChainId::Bitcoin => "",
ChainId::BitcoinSignet => "",
}
}
pub fn display_name(self) -> &'static str {
match self {
ChainId::PolkadotAssetHub => "Polkadot Asset Hub",
ChainId::PaseoAssetHub => "Paseo Asset Hub",
ChainId::PolkadotPeople => "Polkadot People",
ChainId::PaseoPeople => "Paseo People",
ChainId::Previewnet => "Previewnet",
ChainId::Individuality => "Individuality",
ChainId::Ethereum => "Ethereum",
ChainId::EthereumSepolia => "Ethereum Sepolia",
ChainId::Bitcoin => "Bitcoin",
ChainId::BitcoinSignet => "Bitcoin Signet",
}
}
pub fn all() -> &'static [ChainId] {
&[
ChainId::PolkadotAssetHub,
ChainId::PaseoAssetHub,
ChainId::PolkadotPeople,
ChainId::PaseoPeople,
ChainId::Previewnet,
ChainId::Individuality,
ChainId::Ethereum,
ChainId::EthereumSepolia,
ChainId::Bitcoin,
ChainId::BitcoinSignet,
]
}
pub fn substrate_chains() -> &'static [ChainId] {
&[
ChainId::PolkadotAssetHub,
ChainId::PaseoAssetHub,
ChainId::PolkadotPeople,
ChainId::PaseoPeople,
ChainId::Previewnet,
ChainId::Individuality,
]
}
pub fn backend(self) -> ConnectionBackend {
match self {
ChainId::PolkadotAssetHub | ChainId::PaseoAssetHub | ChainId::PolkadotPeople => {
ConnectionBackend::Smoldot
}
ChainId::PaseoPeople | ChainId::Previewnet | ChainId::Individuality => {
ConnectionBackend::Rpc
}
ChainId::Ethereum | ChainId::EthereumSepolia => ConnectionBackend::Helios,
ChainId::Bitcoin | ChainId::BitcoinSignet => ConnectionBackend::Kyoto,
}
}
pub fn chain_specs(self) -> Option<(&'static str, &'static str)> {
match self {
ChainId::PolkadotAssetHub => Some((
include_str!("../chain-specs/polkadot.json"),
include_str!("../chain-specs/polkadot_asset_hub.json"),
)),
ChainId::PaseoAssetHub => Some((
include_str!("../chain-specs/paseo.json"),
include_str!("../chain-specs/paseo_asset_hub.json"),
)),
ChainId::PolkadotPeople => Some((
include_str!("../chain-specs/polkadot.json"),
include_str!("../chain-specs/polkadot_people.json"),
)),
ChainId::PaseoPeople
| ChainId::Previewnet
| ChainId::Individuality
| ChainId::Ethereum
| ChainId::EthereumSepolia
| ChainId::Bitcoin
| ChainId::BitcoinSignet => None,
}
}
pub fn relay_db_key(self) -> &'static str {
match self {
ChainId::PolkadotAssetHub | ChainId::PolkadotPeople => "polkadot-relay",
ChainId::PaseoAssetHub | ChainId::PaseoPeople | ChainId::Individuality => "paseo-relay",
ChainId::Previewnet
| ChainId::Ethereum
| ChainId::EthereumSepolia
| ChainId::Bitcoin
| ChainId::BitcoinSignet => "unknown-relay",
}
}
pub fn para_db_key(self) -> String {
format!("{self:?}")
}
pub fn genesis_hash(self) -> Option<GenesisHash> {
match self {
ChainId::PolkadotAssetHub => Some(GENESIS_POLKADOT_ASSET_HUB),
_ => None,
}
}
}
#[derive(Debug, Clone)]
pub enum ChainState {
Disconnected,
Connecting,
Syncing { best_block: u64, peers: u32 },
Live { best_block: u64, peers: u32 },
Error(String),
}
#[derive(Debug, Clone, Default)]
pub enum ChainExtra {
#[default]
None,
Eth {
finalized_block: u64,
gas_price_gwei: u64,
},
Btc {
tip_height: u64,
fee_rate_sat_vb: u32,
},
}
#[derive(Debug, Clone)]
pub struct ChainStatus {
pub id: ChainId,
pub name: &'static str,
pub state: ChainState,
pub extra: ChainExtra,
}
impl ChainStatus {
pub fn disconnected(id: ChainId) -> Self {
Self {
id,
name: id.display_name(),
state: ChainState::Disconnected,
extra: ChainExtra::None,
}
}
}
pub fn parse_block_number(text: &str) -> Option<u64> {
let v: serde_json::Value = serde_json::from_str(text).ok()?;
let num_str = v
.pointer("/params/result/number")
.and_then(|n| n.as_str())?;
let hex = num_str.strip_prefix("0x").unwrap_or(num_str);
u64::from_str_radix(hex, 16).ok()
}
pub const REQ_ID_PARA_DB_SAVE: u64 = u64::MAX - 1;
pub const REQ_ID_RELAY_DB_SAVE: u64 = u64::MAX;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_all_returns_all_variants() {
let all = ChainId::all();
assert!(all.contains(&ChainId::PolkadotAssetHub));
assert!(all.contains(&ChainId::PaseoAssetHub));
assert!(all.contains(&ChainId::PolkadotPeople));
assert!(all.contains(&ChainId::PaseoPeople));
assert!(all.contains(&ChainId::Previewnet));
assert!(all.contains(&ChainId::Individuality));
assert!(all.contains(&ChainId::Ethereum));
assert!(all.contains(&ChainId::EthereumSepolia));
assert!(all.contains(&ChainId::Bitcoin));
assert!(all.contains(&ChainId::BitcoinSignet));
assert_eq!(all.len(), 10);
}
#[test]
fn test_substrate_chains_is_subset_of_all() {
let all = ChainId::all();
for chain in ChainId::substrate_chains() {
assert!(
all.contains(chain),
"{:?} is in substrate_chains() but not in all()",
chain
);
}
}
#[test]
fn test_endpoint_returns_nonempty_string() {
let chains_with_endpoints = [
ChainId::PolkadotAssetHub,
ChainId::PaseoAssetHub,
ChainId::PolkadotPeople,
ChainId::PaseoPeople,
ChainId::Previewnet,
ChainId::Individuality,
];
for chain in chains_with_endpoints {
assert!(
!chain.endpoint().is_empty(),
"{:?}.endpoint() must be non-empty",
chain
);
}
}
#[test]
fn test_display_name_returns_nonempty_string() {
for chain in ChainId::all() {
assert!(
!chain.display_name().is_empty(),
"{:?}.display_name() must be non-empty",
chain
);
}
}
#[test]
fn test_genesis_hash_known_for_polkadot_asset_hub() {
let hash = ChainId::PolkadotAssetHub.genesis_hash();
assert!(
hash.is_some(),
"PolkadotAssetHub must have a known genesis hash"
);
let bytes = hash.unwrap();
assert_eq!(bytes.len(), 32);
assert_eq!(
bytes[0], 0x68,
"genesis hash byte[0] must match known value"
);
}
#[test]
fn test_genesis_hash_unknown_for_testnets() {
assert!(
ChainId::PaseoAssetHub.genesis_hash().is_none(),
"PaseoAssetHub genesis hash must be None (discovered at runtime)"
);
}
#[test]
fn test_relay_db_key_shared_for_same_relay() {
assert_eq!(
ChainId::PolkadotAssetHub.relay_db_key(),
ChainId::PolkadotPeople.relay_db_key(),
"PolkadotAssetHub and PolkadotPeople must share the polkadot relay DB key"
);
assert_eq!(ChainId::PolkadotAssetHub.relay_db_key(), "polkadot-relay");
}
}