1pub type GenesisHash = [u8; 32];
5
6pub 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#[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,
22 Ethereum,
23 EthereumSepolia,
24 Bitcoin,
25 BitcoinSignet,
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum ConnectionBackend {
31 Smoldot,
33 Rpc,
35 Kyoto,
37 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 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 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 pub fn backend(self) -> ConnectionBackend {
102 match self {
103 ChainId::PolkadotAssetHub | ChainId::PaseoAssetHub | ChainId::PolkadotPeople => {
104 ConnectionBackend::Smoldot
105 }
106 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 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 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 pub fn relay_db_key(self) -> &'static str {
144 match self {
145 ChainId::PolkadotAssetHub | ChainId::PolkadotPeople => "polkadot-relay",
147 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 pub fn para_db_key(self) -> String {
159 format!("{self:?}")
160 }
161
162 pub fn genesis_hash(self) -> Option<GenesisHash> {
168 match self {
169 ChainId::PolkadotAssetHub => Some(GENESIS_POLKADOT_ASSET_HUB),
171 _ => None,
174 }
175 }
176}
177
178#[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#[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#[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
223pub 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
233pub 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 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 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 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 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 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
340 #[test]
341 fn test_paseo_smoldot_specs_parse_and_include_required_fields() {
342 let (relay_spec, para_spec) = ChainId::PaseoAssetHub
343 .chain_specs()
344 .expect("PaseoAssetHub must include smoldot chain specs");
345
346 let relay: serde_json::Value =
347 serde_json::from_str(relay_spec).expect("relay spec must be valid JSON");
348 let para: serde_json::Value =
349 serde_json::from_str(para_spec).expect("parachain spec must be valid JSON");
350
351 assert!(
352 relay
353 .get("bootNodes")
354 .and_then(|nodes| nodes.as_array())
355 .is_some_and(|nodes| !nodes.is_empty()),
356 "relay spec must include bootnodes"
357 );
358 assert!(
359 relay.get("lightSyncState").is_some(),
360 "relay spec must include lightSyncState"
361 );
362 assert_eq!(
363 para.get("relay_chain").and_then(|value| value.as_str()),
364 Some("paseo"),
365 "parachain spec must target the paseo relay"
366 );
367 assert_eq!(
368 para.get("para_id").and_then(|value| value.as_u64()),
369 Some(1000),
370 "parachain spec must target Asset Hub paseo para_id 1000"
371 );
372 }
373}