Skip to main content

host_chain_core/
chain.rs

1//! Shared chain data types — WASM-compatible, no platform-specific deps.
2
3/// 32-byte genesis block hash identifying a chain.
4pub type GenesisHash = [u8; 32];
5
6/// Polkadot Asset Hub genesis hash (stable mainnet).
7pub const GENESIS_POLKADOT_ASSET_HUB: GenesisHash = [
8    0x68, 0xd5, 0x6f, 0x15, 0xf8, 0x5d, 0x31, 0x36, 0x97, 0x0e, 0xc1, 0x69, 0x46, 0x04, 0x0b, 0xc1,
9    0x75, 0x26, 0x54, 0xe9, 0x06, 0x14, 0x7f, 0x7e, 0x43, 0xe9, 0xd5, 0x39, 0xd7, 0xc3, 0xde, 0x2f,
10];
11
12/// Chain identifier — all supported chains in the host ecosystem.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
14pub enum ChainId {
15    PolkadotAssetHub,
16    PaseoAssetHub,
17    PolkadotPeople,
18    PaseoPeople,
19    Previewnet,
20    /// Individuality parachain — hosts the `Resources` pallet with lite usernames.
21    Individuality,
22    Ethereum,
23    EthereumSepolia,
24    Bitcoin,
25    BitcoinSignet,
26}
27
28/// Which backend to use for a given chain.
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum ConnectionBackend {
31    /// Embedded smoldot light client (trustless, peer-to-peer).
32    Smoldot,
33    /// Direct WebSocket RPC to a public endpoint (centralized).
34    Rpc,
35    /// Kyoto BIP-157/158 compact block filter light client (Bitcoin P2P).
36    Kyoto,
37    /// Helios Ethereum light client (consensus + execution verification).
38    Helios,
39}
40
41impl ChainId {
42    pub fn endpoint(self) -> &'static str {
43        match self {
44            ChainId::PolkadotAssetHub => "wss://polkadot-asset-hub-rpc.polkadot.io",
45            ChainId::PaseoAssetHub => "wss://asset-hub-paseo.dotters.network",
46            ChainId::PolkadotPeople => "wss://polkadot-people-rpc.polkadot.io",
47            ChainId::PaseoPeople => "wss://people-paseo.dotters.network",
48            ChainId::Previewnet => "wss://previewnet.dotsamalabs.com/asset-hub",
49            ChainId::Individuality => "wss://pop3-testnet.parity-lab.parity.io/people",
50            ChainId::Ethereum => "",
51            ChainId::EthereumSepolia => "",
52            ChainId::Bitcoin => "",
53            ChainId::BitcoinSignet => "",
54        }
55    }
56
57    pub fn display_name(self) -> &'static str {
58        match self {
59            ChainId::PolkadotAssetHub => "Polkadot Asset Hub",
60            ChainId::PaseoAssetHub => "Paseo Asset Hub",
61            ChainId::PolkadotPeople => "Polkadot People",
62            ChainId::PaseoPeople => "Paseo People",
63            ChainId::Previewnet => "Previewnet",
64            ChainId::Individuality => "Individuality",
65            ChainId::Ethereum => "Ethereum",
66            ChainId::EthereumSepolia => "Ethereum Sepolia",
67            ChainId::Bitcoin => "Bitcoin",
68            ChainId::BitcoinSignet => "Bitcoin Signet",
69        }
70    }
71
72    /// All chain IDs including ETH/BTC. UI should gate display on settings flags.
73    pub fn all() -> &'static [ChainId] {
74        &[
75            ChainId::PolkadotAssetHub,
76            ChainId::PaseoAssetHub,
77            ChainId::PolkadotPeople,
78            ChainId::PaseoPeople,
79            ChainId::Previewnet,
80            ChainId::Individuality,
81            ChainId::Ethereum,
82            ChainId::EthereumSepolia,
83            ChainId::Bitcoin,
84            ChainId::BitcoinSignet,
85        ]
86    }
87
88    /// Substrate/Polkadot chains only (for existing chain settings UI).
89    pub fn substrate_chains() -> &'static [ChainId] {
90        &[
91            ChainId::PolkadotAssetHub,
92            ChainId::PaseoAssetHub,
93            ChainId::PolkadotPeople,
94            ChainId::PaseoPeople,
95            ChainId::Previewnet,
96            ChainId::Individuality,
97        ]
98    }
99
100    /// Determine the connection backend for this chain.
101    pub fn backend(self) -> ConnectionBackend {
102        match self {
103            ChainId::PolkadotAssetHub | ChainId::PaseoAssetHub | ChainId::PolkadotPeople => {
104                ConnectionBackend::Smoldot
105            }
106            // PaseoPeople, Previewnet, and Individuality use direct RPC — no smoldot
107            // chain spec is available for these parachains.
108            ChainId::PaseoPeople | ChainId::Previewnet | ChainId::Individuality => {
109                ConnectionBackend::Rpc
110            }
111            ChainId::Ethereum | ChainId::EthereumSepolia => ConnectionBackend::Helios,
112            ChainId::Bitcoin | ChainId::BitcoinSignet => ConnectionBackend::Kyoto,
113        }
114    }
115
116    /// Return (relay_chain_spec, parachain_spec) for smoldot-backed chains.
117    pub fn chain_specs(self) -> Option<(&'static str, &'static str)> {
118        match self {
119            ChainId::PolkadotAssetHub => Some((
120                include_str!("../chain-specs/polkadot.json"),
121                include_str!("../chain-specs/polkadot_asset_hub.json"),
122            )),
123            ChainId::PaseoAssetHub => Some((
124                include_str!("../chain-specs/paseo.json"),
125                include_str!("../chain-specs/paseo_asset_hub.json"),
126            )),
127            // People chains share the relay spec with their respective Asset Hub variant.
128            ChainId::PolkadotPeople => Some((
129                include_str!("../chain-specs/polkadot.json"),
130                include_str!("../chain-specs/polkadot_people.json"),
131            )),
132            ChainId::PaseoPeople
133            | ChainId::Previewnet
134            | ChainId::Individuality
135            | ChainId::Ethereum
136            | ChainId::EthereumSepolia
137            | ChainId::Bitcoin
138            | ChainId::BitcoinSignet => None,
139        }
140    }
141
142    /// Database key for the relay chain of this chain.
143    pub fn relay_db_key(self) -> &'static str {
144        match self {
145            // PolkadotPeople shares the Polkadot relay chain with PolkadotAssetHub.
146            ChainId::PolkadotAssetHub | ChainId::PolkadotPeople => "polkadot-relay",
147            // PaseoPeople and Individuality share the Paseo relay chain with PaseoAssetHub.
148            ChainId::PaseoAssetHub | ChainId::PaseoPeople | ChainId::Individuality => "paseo-relay",
149            ChainId::Previewnet
150            | ChainId::Ethereum
151            | ChainId::EthereumSepolia
152            | ChainId::Bitcoin
153            | ChainId::BitcoinSignet => "unknown-relay",
154        }
155    }
156
157    /// Database key for this chain's parachain database.
158    pub fn para_db_key(self) -> String {
159        format!("{self:?}")
160    }
161
162    /// Return the 32-byte genesis block hash for this chain, or `None` for
163    /// chains where the genesis hash is not known at compile time (e.g. testnets
164    /// that may fork).
165    ///
166    /// Sources: Subscan / Polkadot.js Apps
167    pub fn genesis_hash(self) -> Option<GenesisHash> {
168        match self {
169            // Polkadot Asset Hub (formerly Statemint) — stable mainnet genesis.
170            ChainId::PolkadotAssetHub => Some(GENESIS_POLKADOT_ASSET_HUB),
171            // Paseo Asset Hub is a testnet whose genesis may change with network
172            // resets — discover at runtime rather than hardcoding.
173            _ => None,
174        }
175    }
176}
177
178/// Connection state for a chain.
179#[derive(Debug, Clone)]
180pub enum ChainState {
181    Disconnected,
182    Connecting,
183    Syncing { best_block: u64, peers: u32 },
184    Live { best_block: u64, peers: u32 },
185    Error(String),
186}
187
188/// Chain-specific extra data surfaced to the UI.
189#[derive(Debug, Clone, Default)]
190pub enum ChainExtra {
191    #[default]
192    None,
193    Eth {
194        finalized_block: u64,
195        gas_price_gwei: u64,
196    },
197    Btc {
198        tip_height: u64,
199        fee_rate_sat_vb: u32,
200    },
201}
202
203/// Full status snapshot for a chain.
204#[derive(Debug, Clone)]
205pub struct ChainStatus {
206    pub id: ChainId,
207    pub name: &'static str,
208    pub state: ChainState,
209    pub extra: ChainExtra,
210}
211
212impl ChainStatus {
213    pub fn disconnected(id: ChainId) -> Self {
214        Self {
215            id,
216            name: id.display_name(),
217            state: ChainState::Disconnected,
218            extra: ChainExtra::None,
219        }
220    }
221}
222
223/// Parse a Substrate JSON-RPC new head notification and extract the block number.
224pub fn parse_block_number(text: &str) -> Option<u64> {
225    let v: serde_json::Value = serde_json::from_str(text).ok()?;
226    let num_str = v
227        .pointer("/params/result/number")
228        .and_then(|n| n.as_str())?;
229    let hex = num_str.strip_prefix("0x").unwrap_or(num_str);
230    u64::from_str_radix(hex, 16).ok()
231}
232
233/// Request IDs for DB save responses — use high values to avoid collision
234/// with health_id (starts at 1000, increments by 1 every ~2s).
235pub const REQ_ID_PARA_DB_SAVE: u64 = u64::MAX - 1;
236pub const REQ_ID_RELAY_DB_SAVE: u64 = u64::MAX;
237
238#[cfg(test)]
239mod tests {
240    use super::*;
241
242    #[test]
243    fn test_all_returns_all_variants() {
244        let all = ChainId::all();
245        // Every declared variant must appear exactly once.
246        assert!(all.contains(&ChainId::PolkadotAssetHub));
247        assert!(all.contains(&ChainId::PaseoAssetHub));
248        assert!(all.contains(&ChainId::PolkadotPeople));
249        assert!(all.contains(&ChainId::PaseoPeople));
250        assert!(all.contains(&ChainId::Previewnet));
251        assert!(all.contains(&ChainId::Individuality));
252        assert!(all.contains(&ChainId::Ethereum));
253        assert!(all.contains(&ChainId::EthereumSepolia));
254        assert!(all.contains(&ChainId::Bitcoin));
255        assert!(all.contains(&ChainId::BitcoinSignet));
256        assert_eq!(all.len(), 10);
257    }
258
259    #[test]
260    fn test_substrate_chains_is_subset_of_all() {
261        let all = ChainId::all();
262        for chain in ChainId::substrate_chains() {
263            assert!(
264                all.contains(chain),
265                "{:?} is in substrate_chains() but not in all()",
266                chain
267            );
268        }
269    }
270
271    #[test]
272    fn test_endpoint_returns_nonempty_string() {
273        // Substrate chains with known public endpoints must have a non-empty URL.
274        let chains_with_endpoints = [
275            ChainId::PolkadotAssetHub,
276            ChainId::PaseoAssetHub,
277            ChainId::PolkadotPeople,
278            ChainId::PaseoPeople,
279            ChainId::Previewnet,
280            ChainId::Individuality,
281        ];
282        for chain in chains_with_endpoints {
283            assert!(
284                !chain.endpoint().is_empty(),
285                "{:?}.endpoint() must be non-empty",
286                chain
287            );
288        }
289    }
290
291    #[test]
292    fn test_display_name_returns_nonempty_string() {
293        for chain in ChainId::all() {
294            assert!(
295                !chain.display_name().is_empty(),
296                "{:?}.display_name() must be non-empty",
297                chain
298            );
299        }
300    }
301
302    #[test]
303    fn test_genesis_hash_known_for_polkadot_asset_hub() {
304        let hash = ChainId::PolkadotAssetHub.genesis_hash();
305        assert!(
306            hash.is_some(),
307            "PolkadotAssetHub must have a known genesis hash"
308        );
309        let bytes = hash.unwrap();
310        assert_eq!(bytes.len(), 32);
311        // First byte of the well-known Polkadot Asset Hub genesis hash.
312        assert_eq!(
313            bytes[0], 0x68,
314            "genesis hash byte[0] must match known value"
315        );
316    }
317
318    #[test]
319    fn test_genesis_hash_unknown_for_testnets() {
320        // Paseo is a testnet that resets — genesis hash must not be hardcoded.
321        assert!(
322            ChainId::PaseoAssetHub.genesis_hash().is_none(),
323            "PaseoAssetHub genesis hash must be None (discovered at runtime)"
324        );
325    }
326
327    #[test]
328    fn test_relay_db_key_shared_for_same_relay() {
329        // PolkadotAssetHub and PolkadotPeople both connect through the Polkadot
330        // relay chain, so they must share the same relay DB key to avoid
331        // spinning up a duplicate relay chain database entry.
332        assert_eq!(
333            ChainId::PolkadotAssetHub.relay_db_key(),
334            ChainId::PolkadotPeople.relay_db_key(),
335            "PolkadotAssetHub and PolkadotPeople must share the polkadot relay DB key"
336        );
337        assert_eq!(ChainId::PolkadotAssetHub.relay_db_key(), "polkadot-relay");
338    }
339}