1use std::collections::HashMap;
8
9use crate::chain::{
10 parse_block_number, ChainExtra, ChainId, ChainState, ChainStatus, REQ_ID_PARA_DB_SAVE,
11 REQ_ID_RELAY_DB_SAVE,
12};
13use crate::store::ChainStore;
14
15const INITIAL_HEALTH_ID: u64 = 1000;
17
18struct ChainEntry {
20 status: ChainStatus,
21 relay_db_key: String,
22 para_db_key: String,
23 chain_specs: Option<(String, String)>,
24 health_id: u64,
25}
26
27pub struct ChainStateMachine<S: ChainStore> {
32 chains: HashMap<ChainId, ChainEntry>,
33 store: S,
34}
35
36impl<S: ChainStore> ChainStateMachine<S> {
37 pub fn new(store: S) -> Self {
38 Self {
39 chains: HashMap::new(),
40 store,
41 }
42 }
43
44 pub fn register_chain(&mut self, chain: ChainId) {
46 self.chains.insert(
47 chain,
48 ChainEntry {
49 status: ChainStatus {
50 id: chain,
51 name: chain.display_name(),
52 state: ChainState::Connecting,
53 extra: ChainExtra::None,
54 },
55 relay_db_key: chain.relay_db_key().to_string(),
56 para_db_key: chain.para_db_key(),
57 chain_specs: chain
58 .chain_specs()
59 .map(|(r, p)| (r.to_string(), p.to_string())),
60 health_id: INITIAL_HEALTH_ID,
61 },
62 );
63 }
64
65 pub fn register_chain_entry(&mut self, entry: &crate::registry::ChainRegistryEntry) {
68 let chain = entry.id;
69 self.chains.insert(
70 chain,
71 ChainEntry {
72 status: ChainStatus {
73 id: chain,
74 name: chain.display_name(),
75 state: ChainState::Connecting,
76 extra: ChainExtra::None,
77 },
78 relay_db_key: entry.relay_db_key.clone(),
79 para_db_key: entry.para_db_key.clone(),
80 chain_specs: entry.chain_specs.clone(),
81 health_id: INITIAL_HEALTH_ID,
82 },
83 );
84 }
85
86 pub fn unregister_chain(&mut self, chain: ChainId) {
88 if let Some(entry) = self.chains.get_mut(&chain) {
89 entry.status.state = ChainState::Disconnected;
90 }
91 }
92
93 pub fn set_state(&mut self, chain: ChainId, state: ChainState) {
95 if let Some(entry) = self.chains.get_mut(&chain) {
96 entry.status.state = state;
97 }
98 }
99
100 pub fn set_state_with_extra(&mut self, chain: ChainId, state: ChainState, extra: ChainExtra) {
102 if let Some(entry) = self.chains.get_mut(&chain) {
103 entry.status.state = state;
104 entry.status.extra = extra;
105 }
106 }
107
108 pub fn store(&self) -> &S {
110 &self.store
111 }
112
113 pub fn process_response(&mut self, chain: ChainId, text: &str) {
115 let v: serde_json::Value = match serde_json::from_str(text) {
116 Ok(v) => v,
117 Err(_) => return,
118 };
119
120 let entry = match self.chains.get_mut(&chain) {
121 Some(e) => e,
122 None => return,
123 };
124
125 if let Some(id) = v.get("id").and_then(|i| i.as_u64()) {
127 if id == REQ_ID_PARA_DB_SAVE {
128 if let Some(db) = v.get("result").and_then(|r| r.as_str()) {
129 self.store.save(&entry.para_db_key, db);
130 log::info!("{chain:?}: saved para DB ({} bytes)", db.len());
131 } else if let Some(err) = v.get("error") {
132 log::warn!("{chain:?}: para DB save returned error: {err}");
133 }
134 return;
135 }
136 }
137
138 if let Some(result) = v.get("result") {
140 if let (Some(peers), Some(is_syncing)) = (
141 result.get("peers").and_then(|p| p.as_u64()),
142 result.get("isSyncing").and_then(|s| s.as_bool()),
143 ) {
144 let current_block = match &entry.status.state {
145 ChainState::Live { best_block, .. }
146 | ChainState::Syncing { best_block, .. } => *best_block,
147 _ => 0,
148 };
149
150 entry.status.state = if is_syncing {
151 ChainState::Syncing {
152 best_block: current_block,
153 peers: peers as u32,
154 }
155 } else {
156 ChainState::Live {
157 best_block: current_block,
158 peers: peers as u32,
159 }
160 };
161 return;
162 }
163 }
164
165 if let Some(block) = parse_block_number(text) {
167 let (current_peers, current_syncing) = match &entry.status.state {
168 ChainState::Live { peers, .. } => (*peers, false),
169 ChainState::Syncing { peers, .. } => (*peers, true),
170 ChainState::Connecting => (0, true),
171 _ => (0, false),
172 };
173
174 entry.status.state = if current_syncing && current_peers > 0 {
175 ChainState::Syncing {
176 best_block: block,
177 peers: current_peers,
178 }
179 } else {
180 ChainState::Live {
181 best_block: block,
182 peers: current_peers,
183 }
184 };
185 }
186 }
187
188 pub fn process_relay_response(&mut self, chain: ChainId, text: &str) {
190 if let Ok(v) = serde_json::from_str::<serde_json::Value>(text) {
191 if v.get("id").and_then(|i| i.as_u64()) == Some(REQ_ID_RELAY_DB_SAVE) {
192 if let Some(db) = v.get("result").and_then(|r| r.as_str()) {
193 let key = self
194 .chains
195 .get(&chain)
196 .map(|e| e.relay_db_key.as_str())
197 .unwrap_or_else(|| chain.relay_db_key());
198 self.store.save(key, db);
199 log::info!("{chain:?}: saved relay DB ({} bytes)", db.len());
200 } else if let Some(err) = v.get("error") {
201 log::warn!("{chain:?}: relay DB save returned error: {err}");
202 }
203 }
204 }
205 }
206
207 pub fn set_error(&mut self, chain: ChainId, msg: String) {
209 if let Some(entry) = self.chains.get_mut(&chain) {
210 entry.status.state = ChainState::Error(msg);
211 }
212 }
213
214 pub fn status(&self, chain: ChainId) -> ChainStatus {
215 self.chains
216 .get(&chain)
217 .map(|e| e.status.clone())
218 .unwrap_or_else(|| ChainStatus::disconnected(chain))
219 }
220
221 pub fn all_statuses(&self) -> Vec<ChainStatus> {
222 ChainId::all().iter().map(|&id| self.status(id)).collect()
223 }
224
225 pub fn subscribe_new_heads_request() -> String {
227 serde_json::json!({
228 "jsonrpc": "2.0",
229 "id": 1,
230 "method": "chain_subscribeNewHeads",
231 "params": []
232 })
233 .to_string()
234 }
235
236 pub fn health_check_request(&mut self, chain: ChainId) -> Option<String> {
239 let entry = self.chains.get_mut(&chain)?;
240 entry.health_id += 1;
241 Some(
242 serde_json::json!({
243 "jsonrpc": "2.0",
244 "id": entry.health_id,
245 "method": "system_health",
246 "params": []
247 })
248 .to_string(),
249 )
250 }
251
252 pub fn para_db_save_request() -> String {
254 serde_json::json!({
255 "jsonrpc": "2.0",
256 "id": REQ_ID_PARA_DB_SAVE,
257 "method": "chainHead_unstable_finalizedDatabase",
258 "params": []
259 })
260 .to_string()
261 }
262
263 pub fn relay_db_save_request() -> String {
265 serde_json::json!({
266 "jsonrpc": "2.0",
267 "id": REQ_ID_RELAY_DB_SAVE,
268 "method": "chainHead_unstable_finalizedDatabase",
269 "params": []
270 })
271 .to_string()
272 }
273
274 pub fn load_relay_db(&self, chain: ChainId) -> String {
276 match self.chains.get(&chain) {
277 Some(e) => self.store.load(&e.relay_db_key),
278 None => self.store.load(chain.relay_db_key()),
279 }
280 }
281
282 pub fn load_para_db(&self, chain: ChainId) -> String {
284 match self.chains.get(&chain) {
285 Some(e) => self.store.load(&e.para_db_key),
286 None => self.store.load(&chain.para_db_key()),
287 }
288 }
289
290 pub fn chain_specs(chain: ChainId) -> Option<(&'static str, &'static str)> {
292 chain.chain_specs()
293 }
294
295 pub fn chain_specs_owned(&self, chain: ChainId) -> Option<(String, String)> {
298 self.chains.get(&chain).and_then(|e| e.chain_specs.clone())
299 }
300
301 pub fn clear_chain_dbs(&self, chain: ChainId) {
303 let relay_key = self
304 .chains
305 .get(&chain)
306 .map(|e| e.relay_db_key.clone())
307 .unwrap_or_else(|| chain.relay_db_key().to_string());
308 let para_key = self
309 .chains
310 .get(&chain)
311 .map(|e| e.para_db_key.clone())
312 .unwrap_or_else(|| chain.para_db_key());
313 self.store.save(&relay_key, "");
314 self.store.save(¶_key, "");
315 }
316
317 pub fn statement_submit_request(encoded_hex: &str, request_id: u64) -> String {
321 serde_json::json!({
322 "jsonrpc": "2.0",
323 "id": request_id,
324 "method": "statement_submit",
325 "params": [encoded_hex]
326 })
327 .to_string()
328 }
329
330 pub fn statement_subscribe_request(request_id: u64) -> String {
332 serde_json::json!({
333 "jsonrpc": "2.0",
334 "id": request_id,
335 "method": "statement_subscribeStatement",
336 "params": ["any"]
337 })
338 .to_string()
339 }
340
341 pub fn statement_unsubscribe_request(sub_id: &str, request_id: u64) -> String {
343 serde_json::json!({
344 "jsonrpc": "2.0",
345 "id": request_id,
346 "method": "statement_unsubscribeStatement",
347 "params": [sub_id]
348 })
349 .to_string()
350 }
351
352 pub fn statement_broadcasts_request(topic_hexes: &[String], request_id: u64) -> String {
354 serde_json::json!({
355 "jsonrpc": "2.0",
356 "id": request_id,
357 "method": "statement_broadcastsStatement",
358 "params": [topic_hexes]
359 })
360 .to_string()
361 }
362
363 pub fn parse_statement_notification(text: &str) -> Vec<String> {
367 let v: serde_json::Value = match serde_json::from_str(text) {
368 Ok(v) => v,
369 Err(_) => return Vec::new(),
370 };
371
372 if v.get("method").and_then(|m| m.as_str()) != Some("statement_subscribeStatement") {
373 return Vec::new();
374 }
375
376 let result = match v.pointer("/params/result") {
377 Some(r) => r,
378 None => return Vec::new(),
379 };
380
381 let stmts = result
382 .pointer("/data/statements")
383 .or_else(|| result.pointer("/newStatements/statements"))
384 .or_else(|| result.get("statements"));
385
386 match stmts.and_then(|s| s.as_array()) {
387 Some(arr) => arr
388 .iter()
389 .filter_map(|v| v.as_str().map(String::from))
390 .collect(),
391 None => Vec::new(),
392 }
393 }
394}
395
396#[cfg(test)]
397mod tests {
398 use super::*;
399 use std::cell::RefCell;
400
401 struct InMemoryStore {
402 data: RefCell<HashMap<String, String>>,
403 }
404
405 impl InMemoryStore {
406 fn new() -> Self {
407 Self {
408 data: RefCell::new(HashMap::new()),
409 }
410 }
411 }
412
413 impl ChainStore for InMemoryStore {
414 fn load(&self, key: &str) -> String {
415 self.data.borrow().get(key).cloned().unwrap_or_default()
416 }
417
418 fn save(&self, key: &str, data: &str) {
419 self.data
420 .borrow_mut()
421 .insert(key.to_string(), data.to_string());
422 }
423 }
424
425 fn make_sm() -> ChainStateMachine<InMemoryStore> {
426 let store = InMemoryStore::new();
427 let mut sm = ChainStateMachine::new(store);
428 sm.register_chain(ChainId::PaseoAssetHub);
429 sm
430 }
431
432 #[test]
433 fn register_sets_connecting() {
434 let sm = make_sm();
435 let status = sm.status(ChainId::PaseoAssetHub);
436 assert!(matches!(status.state, ChainState::Connecting));
437 }
438
439 #[test]
440 fn unregister_sets_disconnected() {
441 let mut sm = make_sm();
442 sm.unregister_chain(ChainId::PaseoAssetHub);
443 let status = sm.status(ChainId::PaseoAssetHub);
444 assert!(matches!(status.state, ChainState::Disconnected));
445 }
446
447 #[test]
448 fn unregistered_chain_returns_disconnected() {
449 let sm = make_sm();
450 let status = sm.status(ChainId::Ethereum);
451 assert!(matches!(status.state, ChainState::Disconnected));
452 }
453
454 #[test]
455 fn health_response_sets_live() {
456 let mut sm = make_sm();
457 let resp = r#"{"jsonrpc":"2.0","id":1001,"result":{"peers":5,"isSyncing":false}}"#;
458 sm.process_response(ChainId::PaseoAssetHub, resp);
459 let status = sm.status(ChainId::PaseoAssetHub);
460 assert!(matches!(status.state, ChainState::Live { peers: 5, .. }));
461 }
462
463 #[test]
464 fn health_response_sets_syncing() {
465 let mut sm = make_sm();
466 let resp = r#"{"jsonrpc":"2.0","id":1001,"result":{"peers":3,"isSyncing":true}}"#;
467 sm.process_response(ChainId::PaseoAssetHub, resp);
468 let status = sm.status(ChainId::PaseoAssetHub);
469 assert!(matches!(status.state, ChainState::Syncing { peers: 3, .. }));
470 }
471
472 #[test]
473 fn new_head_updates_block_number() {
474 let mut sm = make_sm();
475 let health = r#"{"jsonrpc":"2.0","id":1001,"result":{"peers":5,"isSyncing":false}}"#;
477 sm.process_response(ChainId::PaseoAssetHub, health);
478 let head =
480 r#"{"jsonrpc":"2.0","method":"chain_newHead","params":{"result":{"number":"0x1a4"}}}"#;
481 sm.process_response(ChainId::PaseoAssetHub, head);
482 let status = sm.status(ChainId::PaseoAssetHub);
483 match status.state {
484 ChainState::Live {
485 best_block, peers, ..
486 } => {
487 assert_eq!(best_block, 0x1a4);
488 assert_eq!(peers, 5);
489 }
490 other => panic!("expected Live, got {other:?}"),
491 }
492 }
493
494 #[test]
495 fn para_db_save_stores_to_store() {
496 let mut sm = make_sm();
497 let resp = format!(
498 r#"{{"jsonrpc":"2.0","id":{},"result":"saved-db-content"}}"#,
499 REQ_ID_PARA_DB_SAVE,
500 );
501 sm.process_response(ChainId::PaseoAssetHub, &resp);
502 assert_eq!(sm.store().load("PaseoAssetHub"), "saved-db-content");
503 }
504
505 #[test]
506 fn relay_db_save_stores_to_store() {
507 let mut sm = make_sm();
508 let resp = format!(
509 r#"{{"jsonrpc":"2.0","id":{},"result":"relay-db-content"}}"#,
510 REQ_ID_RELAY_DB_SAVE,
511 );
512 sm.process_relay_response(ChainId::PaseoAssetHub, &resp);
513 assert_eq!(
514 sm.store().load(ChainId::PaseoAssetHub.relay_db_key()),
515 "relay-db-content"
516 );
517 }
518
519 #[test]
520 fn set_state_preserves_extra() {
521 let mut sm = make_sm();
522 sm.set_state_with_extra(
523 ChainId::PaseoAssetHub,
524 ChainState::Live {
525 best_block: 100,
526 peers: 5,
527 },
528 ChainExtra::Eth {
529 finalized_block: 50,
530 gas_price_gwei: 20,
531 },
532 );
533 sm.set_state(
535 ChainId::PaseoAssetHub,
536 ChainState::Live {
537 best_block: 200,
538 peers: 3,
539 },
540 );
541 let status = sm.status(ChainId::PaseoAssetHub);
542 assert!(matches!(
543 status.extra,
544 ChainExtra::Eth {
545 finalized_block: 50,
546 gas_price_gwei: 20
547 }
548 ));
549 assert!(matches!(
550 status.state,
551 ChainState::Live {
552 best_block: 200,
553 peers: 3
554 }
555 ));
556 }
557
558 #[test]
559 fn set_state_with_extra_updates_both() {
560 let mut sm = make_sm();
561 sm.set_state_with_extra(
562 ChainId::PaseoAssetHub,
563 ChainState::Live {
564 best_block: 100,
565 peers: 5,
566 },
567 ChainExtra::Btc {
568 tip_height: 800000,
569 fee_rate_sat_vb: 10,
570 },
571 );
572 let status = sm.status(ChainId::PaseoAssetHub);
573 assert!(matches!(
574 status.state,
575 ChainState::Live {
576 best_block: 100,
577 peers: 5
578 }
579 ));
580 assert!(matches!(
581 status.extra,
582 ChainExtra::Btc {
583 tip_height: 800000,
584 fee_rate_sat_vb: 10
585 }
586 ));
587 }
588
589 #[test]
590 fn process_response_unregistered_chain_is_noop() {
591 let mut sm = make_sm();
592 let resp = r#"{"jsonrpc":"2.0","id":1001,"result":{"peers":5,"isSyncing":false}}"#;
593 sm.process_response(ChainId::Ethereum, resp);
595 let status = sm.status(ChainId::Ethereum);
596 assert!(matches!(status.state, ChainState::Disconnected));
597 }
598
599 #[test]
600 fn health_check_request_unregistered_returns_none() {
601 let mut sm = make_sm();
602 assert!(sm.health_check_request(ChainId::Ethereum).is_none());
603 }
604
605 #[test]
606 fn health_check_request_id_starts_above_1000_and_increments() {
607 let mut sm = make_sm();
608 let req1 = sm.health_check_request(ChainId::PaseoAssetHub).unwrap();
609 let req2 = sm.health_check_request(ChainId::PaseoAssetHub).unwrap();
610
611 let v1: serde_json::Value = serde_json::from_str(&req1).unwrap();
612 let v2: serde_json::Value = serde_json::from_str(&req2).unwrap();
613
614 let id1 = v1["id"].as_u64().unwrap();
615 let id2 = v2["id"].as_u64().unwrap();
616
617 assert!(id1 > 1000);
618 assert_eq!(id2, id1 + 1);
619 }
620
621 #[test]
622 fn all_statuses_includes_registered_chains() {
623 let sm = make_sm();
624 let statuses = sm.all_statuses();
625 let paseo = statuses
626 .iter()
627 .find(|s| s.id == ChainId::PaseoAssetHub)
628 .expect("PaseoAssetHub should be in all_statuses");
629 assert!(matches!(paseo.state, ChainState::Connecting));
630 }
631
632 #[test]
633 fn all_statuses_returns_disconnected_for_unregistered() {
634 let sm = make_sm();
635 let statuses = sm.all_statuses();
636 let eth = statuses
637 .iter()
638 .find(|s| s.id == ChainId::Ethereum)
639 .expect("Ethereum should be in all_statuses (Disconnected)");
640 assert!(matches!(eth.state, ChainState::Disconnected));
641 }
642
643 #[test]
644 fn parse_statement_notification_data_statements_path() {
645 let text = r#"{"jsonrpc":"2.0","method":"statement_subscribeStatement","params":{"result":{"data":{"statements":["0xab","0xcd"]}}}}"#;
646 let stmts = ChainStateMachine::<InMemoryStore>::parse_statement_notification(text);
647 assert_eq!(stmts, vec!["0xab", "0xcd"]);
648 }
649
650 #[test]
651 fn parse_statement_notification_new_statements_path() {
652 let text = r#"{"jsonrpc":"2.0","method":"statement_subscribeStatement","params":{"result":{"newStatements":{"statements":["0xef"]}}}}"#;
653 let stmts = ChainStateMachine::<InMemoryStore>::parse_statement_notification(text);
654 assert_eq!(stmts, vec!["0xef"]);
655 }
656
657 #[test]
658 fn parse_statement_notification_plain_statements_path() {
659 let text = r#"{"jsonrpc":"2.0","method":"statement_subscribeStatement","params":{"result":{"statements":["0x11","0x22","0x33"]}}}"#;
660 let stmts = ChainStateMachine::<InMemoryStore>::parse_statement_notification(text);
661 assert_eq!(stmts, vec!["0x11", "0x22", "0x33"]);
662 }
663
664 #[test]
665 fn parse_statement_notification_wrong_method_returns_empty() {
666 let text = r#"{"jsonrpc":"2.0","method":"chain_newHead","params":{"result":{"statements":["0x11"]}}}"#;
667 let stmts = ChainStateMachine::<InMemoryStore>::parse_statement_notification(text);
668 assert!(stmts.is_empty());
669 }
670
671 #[test]
672 fn parse_statement_notification_no_statements_returns_empty() {
673 let text =
674 r#"{"jsonrpc":"2.0","method":"statement_subscribeStatement","params":{"result":{}}}"#;
675 let stmts = ChainStateMachine::<InMemoryStore>::parse_statement_notification(text);
676 assert!(stmts.is_empty());
677 }
678
679 #[test]
680 fn para_db_save_error_does_not_change_state() {
681 let mut sm = make_sm();
682 let health = r#"{"jsonrpc":"2.0","id":1001,"result":{"peers":5,"isSyncing":false}}"#;
684 sm.process_response(ChainId::PaseoAssetHub, health);
685
686 let error_resp = format!(
688 r#"{{"jsonrpc":"2.0","id":{},"error":{{"code":-32000,"message":"db error"}}}}"#,
689 REQ_ID_PARA_DB_SAVE,
690 );
691 sm.process_response(ChainId::PaseoAssetHub, &error_resp);
692
693 let status = sm.status(ChainId::PaseoAssetHub);
695 assert!(matches!(status.state, ChainState::Live { peers: 5, .. }));
696 assert_eq!(sm.store().load("PaseoAssetHub"), "");
698 }
699
700 #[test]
701 fn register_chain_entry_sets_connecting() {
702 use crate::chain::ConnectionBackend;
703 use crate::registry::ChainRegistryEntry;
704 let store = InMemoryStore::new();
705 let mut sm = ChainStateMachine::new(store);
706 sm.register_chain_entry(&ChainRegistryEntry {
707 id: ChainId::PaseoAssetHub,
708 genesis_hash: [0u8; 32],
709 display_name: "Paseo Asset Hub".to_string(),
710 endpoint: String::new(),
711 backend: ConnectionBackend::Smoldot,
712 relay_db_key: "custom-relay".to_string(),
713 para_db_key: "custom-para".to_string(),
714 chain_specs: None,
715 });
716 let status = sm.status(ChainId::PaseoAssetHub);
717 assert!(matches!(status.state, ChainState::Connecting));
718 }
719
720 #[test]
721 fn register_chain_entry_uses_entry_db_keys() {
722 use crate::chain::ConnectionBackend;
723 use crate::registry::ChainRegistryEntry;
724 let store = InMemoryStore::new();
725 let mut sm = ChainStateMachine::new(store);
726 sm.register_chain_entry(&ChainRegistryEntry {
727 id: ChainId::PaseoAssetHub,
728 genesis_hash: [0u8; 32],
729 display_name: "Paseo Asset Hub".to_string(),
730 endpoint: String::new(),
731 backend: ConnectionBackend::Smoldot,
732 relay_db_key: "custom-relay".to_string(),
733 para_db_key: "custom-para".to_string(),
734 chain_specs: None,
735 });
736
737 let resp = format!(
739 r#"{{"jsonrpc":"2.0","id":{},"result":"relay-data"}}"#,
740 REQ_ID_RELAY_DB_SAVE,
741 );
742 sm.process_relay_response(ChainId::PaseoAssetHub, &resp);
743 assert_eq!(sm.store().load("custom-relay"), "relay-data");
744
745 let resp2 = format!(
747 r#"{{"jsonrpc":"2.0","id":{},"result":"para-data"}}"#,
748 REQ_ID_PARA_DB_SAVE,
749 );
750 sm.process_response(ChainId::PaseoAssetHub, &resp2);
751 assert_eq!(sm.store().load("custom-para"), "para-data");
752 }
753
754 #[test]
755 fn chain_specs_owned_returns_entry_specs() {
756 use crate::chain::ConnectionBackend;
757 use crate::registry::ChainRegistryEntry;
758 let store = InMemoryStore::new();
759 let mut sm = ChainStateMachine::new(store);
760 sm.register_chain_entry(&ChainRegistryEntry {
761 id: ChainId::PaseoAssetHub,
762 genesis_hash: [0u8; 32],
763 display_name: "Paseo Asset Hub".to_string(),
764 endpoint: String::new(),
765 backend: ConnectionBackend::Rpc,
766 relay_db_key: "r".to_string(),
767 para_db_key: "p".to_string(),
768 chain_specs: Some(("relay-spec".to_string(), "para-spec".to_string())),
769 });
770 let specs = sm.chain_specs_owned(ChainId::PaseoAssetHub);
771 assert_eq!(
772 specs,
773 Some(("relay-spec".to_string(), "para-spec".to_string()))
774 );
775
776 assert!(sm.chain_specs_owned(ChainId::Ethereum).is_none());
778 }
779
780 #[test]
781 fn register_chain_entry_backward_compat_with_register_chain() {
782 use crate::chain::ConnectionBackend;
785 use crate::registry::ChainRegistryEntry;
786
787 let store1 = InMemoryStore::new();
788 let mut sm1 = ChainStateMachine::new(store1);
789 sm1.register_chain(ChainId::PaseoAssetHub);
790
791 let store2 = InMemoryStore::new();
792 let mut sm2 = ChainStateMachine::new(store2);
793 sm2.register_chain_entry(&ChainRegistryEntry {
794 id: ChainId::PaseoAssetHub,
795 genesis_hash: [0u8; 32],
796 display_name: ChainId::PaseoAssetHub.display_name().to_string(),
797 endpoint: ChainId::PaseoAssetHub.endpoint().to_string(),
798 backend: ConnectionBackend::Smoldot,
799 relay_db_key: ChainId::PaseoAssetHub.relay_db_key().to_string(),
800 para_db_key: ChainId::PaseoAssetHub.para_db_key(),
801 chain_specs: ChainId::PaseoAssetHub
802 .chain_specs()
803 .map(|(r, p)| (r.to_string(), p.to_string())),
804 });
805
806 let s1 = sm1.status(ChainId::PaseoAssetHub);
808 let s2 = sm2.status(ChainId::PaseoAssetHub);
809 assert_eq!(s1.name, s2.name);
810 assert!(matches!(s1.state, ChainState::Connecting));
811 assert!(matches!(s2.state, ChainState::Connecting));
812
813 let resp = format!(
815 r#"{{"jsonrpc":"2.0","id":{},"result":"db-content"}}"#,
816 REQ_ID_PARA_DB_SAVE,
817 );
818 sm1.process_response(ChainId::PaseoAssetHub, &resp);
819 sm2.process_response(ChainId::PaseoAssetHub, &resp);
820 assert_eq!(
821 sm1.store().load(&ChainId::PaseoAssetHub.para_db_key()),
822 sm2.store().load(&ChainId::PaseoAssetHub.para_db_key()),
823 );
824 }
825}