use crate::p2p::testing::port_seed; use crate::tests::{DEFAULT_BLOCK_TIME, DEFAULT_MAX_PROTOCOL_WAIT_TIME, IntegrationTestSetup}; use crate::tracking::AutoAbortTask; use mpc_node_config::foreign_chains::RpcProviderName; use mpc_node_config::{ AuthConfig, ForeignChainConfig, ForeignChainProviderConfig, ForeignChainsConfig, }; use near_mpc_contract_interface::types::{ DomainConfig, DomainId, DomainPurpose, ForeignChain, Protocol, ReconstructionThreshold, }; use near_time::Clock; use std::collections::BTreeSet; use std::num::NonZeroU64; use std::time::Duration; #[tokio::test] #[test_log::test] #[expect(non_snake_case)] async fn foreign_chain_configuration_auto_registered_to_contract_on_startup__should_use_local_config() { // Given const GOVERNANCE_THRESHOLD: usize = 2; const RECONSTRUCTION_THRESHOLD: usize = 2; const TXN_DELAY_BLOCKS: u64 = 1; let temp_dir = tempfile::tempdir().unwrap(); let mut setup: IntegrationTestSetup = IntegrationTestSetup::new( Clock::real(), temp_dir.path(), vec!["test0".parse().unwrap(), "test1".parse().unwrap()], GOVERNANCE_THRESHOLD, TXN_DELAY_BLOCKS, port_seed::FOREIGN_CHAIN_POLICY_TEST, DEFAULT_BLOCK_TIME, ); let providers = near_mpc_bounded_collections::NonEmptyBTreeMap::new( RpcProviderName::from("public".to_string()), ForeignChainProviderConfig { rpc_url: "https://rpc.public.example.com".parse().unwrap(), auth: AuthConfig::None, }, ); let foreign_chains = ForeignChainsConfig { solana: Some(ForeignChainConfig { timeout_sec: NonZeroU64::new(30).unwrap(), max_retries: NonZeroU64::new(3).unwrap(), expected_network_fingerprint: None, providers, }), bitcoin: None, ethereum: None, abstract_chain: None, starknet: None, bnb: None, base: None, arbitrum: None, hyper_evm: None, polygon: None, aptos: None, sui: None, }; for config in &mut setup.configs { config.config.foreign_chains = foreign_chains.clone(); } let expected_foreign_chains: BTreeSet = foreign_chains .iter_chains() .map(|(chain, _)| chain) .collect(); { let mut contract = setup.indexer.contract_mut().await; contract.initialize(setup.participants.clone()); // Availability is computed based on ForeignTx domains threshold. contract.add_domains(vec![DomainConfig { id: DomainId(0), protocol: Protocol::CaitSith, reconstruction_threshold: ReconstructionThreshold::new(RECONSTRUCTION_THRESHOLD as u64), purpose: DomainPurpose::ForeignTx, }]); } let _runs = setup .configs .into_iter() .map(|config| AutoAbortTask::from(tokio::spawn(config.run()))) .collect::>(); // When let wait_result = tokio::time::timeout(DEFAULT_MAX_PROTOCOL_WAIT_TIME, async { loop { { let contract = setup.indexer.contract_mut().await; if **contract.available_foreign_chains() == expected_foreign_chains { break; } } tokio::time::sleep(Duration::from_millis(100)).await; } }) .await; // Then assert!(wait_result.is_ok(), "timed out waiting for policy update"); let contract = setup.indexer.contract_mut().await; assert_eq!( **contract.available_foreign_chains(), expected_foreign_chains ); let expected_node_config = BTreeSet::from([ForeignChain::Solana]); let configs = contract.foreign_chains_configs(); assert_eq!(configs.len(), setup.participants.participants.len()); assert!( configs .values() .all(|node_config| **node_config == expected_node_config) ); // The dual-write also feeds the legacy model the node still reads from. assert_eq!( **contract.supported_foreign_chains(), expected_foreign_chains ); let by_node = &contract .supported_foreign_chains_by_node() .foreign_chain_support_by_node; assert_eq!(by_node.len(), setup.participants.participants.len()); assert!( by_node .values() .all(|node_support| **node_support == expected_node_config) ); }