diff --git a/Cargo.lock b/Cargo.lock index 017ccc64de..df7d2f4484 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3971,7 +3971,7 @@ dependencies = [ "bs58 0.5.1", "clap", "foreign-chain-inspector", - "foreign-chain-rpc-auth", + "foreign-chain-rpc-factory", "foreign-chain-rpc-interfaces", "futures", "hex", @@ -3990,16 +3990,19 @@ dependencies = [ name = "foreign-chain-inspector" version = "3.14.0" dependencies = [ + "anyhow", "assert_matches", "bs58 0.5.1", "derive_more 2.1.1", "ethereum-types", + "foreign-chain-rpc-factory", "foreign-chain-rpc-interfaces", "hex", "http", "httpmock", "jsonrpsee", "mockall", + "mpc-node-config", "mpc-primitives", "near-mpc-bounded-collections", "near-mpc-contract-interface", @@ -4013,14 +4016,17 @@ dependencies = [ ] [[package]] -name = "foreign-chain-rpc-auth" +name = "foreign-chain-rpc-factory" version = "3.14.0" dependencies = [ "anyhow", "assert_matches", "foreign-chain-inspector", + "foreign-chain-rpc-interfaces", "http", + "jsonrpsee", "mpc-node-config", + "near-mpc-contract-interface", "url", ] @@ -6328,7 +6334,7 @@ dependencies = [ "flume", "foreign-chain-health-check", "foreign-chain-inspector", - "foreign-chain-rpc-auth", + "foreign-chain-rpc-factory", "foreign-chain-rpc-interfaces", "futures", "gcloud-sdk", diff --git a/Cargo.toml b/Cargo.toml index c79a29bc1f..e8e00fa699 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ members = [ "crates/foreign-chain-config-tester", "crates/foreign-chain-health-check", "crates/foreign-chain-inspector", - "crates/foreign-chain-rpc-auth", + "crates/foreign-chain-rpc-factory", "crates/foreign-chain-rpc-interfaces", "crates/include-measurements", "crates/launcher-interface", @@ -57,7 +57,7 @@ chain-gateway-test-contract = { path = "crates/chain-gateway-test-contract" } contract-history = { path = "crates/contract-history" } foreign-chain-health-check = { path = "crates/foreign-chain-health-check" } foreign-chain-inspector = { path = "crates/foreign-chain-inspector" } -foreign-chain-rpc-auth = { path = "crates/foreign-chain-rpc-auth" } +foreign-chain-rpc-factory = { path = "crates/foreign-chain-rpc-factory" } foreign-chain-rpc-interfaces = { path = "crates/foreign-chain-rpc-interfaces" } include-measurements = { path = "crates/include-measurements" } launcher-interface = { path = "crates/launcher-interface" } diff --git a/crates/foreign-chain-health-check/Cargo.toml b/crates/foreign-chain-health-check/Cargo.toml index 137f30fdd7..c6b1505f03 100644 --- a/crates/foreign-chain-health-check/Cargo.toml +++ b/crates/foreign-chain-health-check/Cargo.toml @@ -12,7 +12,7 @@ anyhow = { workspace = true } bs58 = { workspace = true } clap = { workspace = true, optional = true } foreign-chain-inspector = { workspace = true } -foreign-chain-rpc-auth = { workspace = true } +foreign-chain-rpc-factory = { workspace = true } foreign-chain-rpc-interfaces = { workspace = true } futures = { workspace = true } hex = { workspace = true } @@ -24,6 +24,7 @@ tokio = { workspace = true } [dev-dependencies] assert_matches = { workspace = true } +foreign-chain-inspector = { workspace = true, features = ["test-utils"] } httpmock = { workspace = true } rstest = { workspace = true } serde_json = { workspace = true } diff --git a/crates/foreign-chain-health-check/src/lib.rs b/crates/foreign-chain-health-check/src/lib.rs index eb8e41e1df..c8a52fb670 100644 --- a/crates/foreign-chain-health-check/src/lib.rs +++ b/crates/foreign-chain-health-check/src/lib.rs @@ -22,13 +22,10 @@ use foreign_chain_inspector::base::inspector::Base; use foreign_chain_inspector::bnb::inspector::Bnb; use foreign_chain_inspector::ethereum::inspector::Ethereum; use foreign_chain_inspector::evm::inspector::EvmChain; -use foreign_chain_inspector::http_client::HttpClient; use foreign_chain_inspector::hyperevm::inspector::HyperEvm; use foreign_chain_inspector::polygon::inspector::Polygon; -use foreign_chain_inspector::{RpcAuthentication, build_http_client}; -use foreign_chain_rpc_auth::auth_config_to_rpc_auth; +use foreign_chain_rpc_factory::{build_http_client, resolve_provider_auth}; use foreign_chain_rpc_interfaces::sui::GrpcSuiClient; -use http::{HeaderName, HeaderValue}; use mpc_node_config::foreign_chains::RpcProviderName; use mpc_node_config::{ForeignChainConfig, ForeignChainProviderConfig, ForeignChainsConfig}; @@ -134,35 +131,10 @@ fn no_reference_reason(network: Network) -> String { ) } -fn timeout_of(cfg: &ForeignChainConfig) -> Duration { - Duration::from_secs(cfg.timeout_sec.get()) -} - fn provider_name(name: &RpcProviderName) -> String { name.as_str().to_owned() } -fn prepare_jsonrpc(provider: &ForeignChainProviderConfig) -> anyhow::Result { - let mut url = provider.rpc_url.clone(); - let auth = auth_config_to_rpc_auth(provider.auth.clone(), &mut url)?; - build_http_client(url, auth).map_err(|e| anyhow::anyhow!("failed to build HTTP client: {e}")) -} - -fn prepare_aptos( - provider: &ForeignChainProviderConfig, -) -> anyhow::Result<(String, Option<(HeaderName, HeaderValue)>)> { - let mut url = provider.rpc_url.clone(); - let auth = auth_config_to_rpc_auth(provider.auth.clone(), &mut url)?; - let header = match auth { - RpcAuthentication::KeyInUrl => None, - RpcAuthentication::CustomHeader { - header_name, - header_value, - } => Some((header_name, header_value)), - }; - Ok((url, header)) -} - async fn run_check(timeout: Duration, fut: impl Future>) -> Status { match tokio::time::timeout(timeout, fut).await { Ok(Ok(())) => Status::Passed, @@ -182,11 +154,11 @@ async fn run_evm( mark_skipped(chain, cfg, &no_reference_reason(network), out); return; }; - let timeout = timeout_of(cfg); + let timeout = cfg.timeout_duration(); let parsed = golden::hex32(vector.tx).and_then(|tx| golden::hex32(vector.block_hash).map(|bh| (tx, bh))); for (name, provider) in cfg.providers.iter() { - let status = match (&parsed, prepare_jsonrpc(provider)) { + let status = match (&parsed, build_http_client(provider)) { (Err(e), _) => Status::Failed(format!("invalid golden vector: {e:#}")), (Ok(_), Err(e)) => Status::Failed(format!("{e:#}")), (Ok((tx, bh)), Ok(client)) => { @@ -211,11 +183,11 @@ async fn run_bitcoin( mark_skipped("bitcoin", cfg, &no_reference_reason(network), out); return; }; - let timeout = timeout_of(cfg); + let timeout = cfg.timeout_duration(); let parsed = golden::hex32(vector.tx).and_then(|tx| golden::hex32(vector.block_hash).map(|bh| (tx, bh))); for (name, provider) in cfg.providers.iter() { - let status = match (&parsed, prepare_jsonrpc(provider)) { + let status = match (&parsed, build_http_client(provider)) { (Err(e), _) => Status::Failed(format!("invalid golden vector: {e:#}")), (Ok(_), Err(e)) => Status::Failed(format!("{e:#}")), (Ok((tx, bh)), Ok(client)) => { @@ -240,11 +212,11 @@ async fn run_starknet( mark_skipped("starknet", cfg, &no_reference_reason(network), out); return; }; - let timeout = timeout_of(cfg); + let timeout = cfg.timeout_duration(); let parsed = golden::felt32(vector.tx) .and_then(|tx| golden::felt32(vector.block_hash).map(|bh| (tx, bh))); for (name, provider) in cfg.providers.iter() { - let status = match (&parsed, prepare_jsonrpc(provider)) { + let status = match (&parsed, build_http_client(provider)) { (Err(e), _) => Status::Failed(format!("invalid golden vector: {e:#}")), (Ok(_), Err(e)) => Status::Failed(format!("{e:#}")), (Ok((tx, bh)), Ok(client)) => { @@ -269,10 +241,10 @@ async fn run_aptos( mark_skipped("aptos", cfg, &no_reference_reason(network), out); return; }; - let timeout = timeout_of(cfg); + let timeout = cfg.timeout_duration(); let parsed_tx = golden::hex32(vector.tx); for (name, provider) in cfg.providers.iter() { - let status = match (&parsed_tx, prepare_aptos(provider)) { + let status = match (&parsed_tx, resolve_provider_auth(provider)) { (Err(e), _) => Status::Failed(format!("invalid golden vector: {e:#}")), (Ok(_), Err(e)) => Status::Failed(format!("{e:#}")), (Ok(tx), Ok((url, header))) => { @@ -312,7 +284,7 @@ async fn run_sui( mark_skipped("sui", cfg, &no_reference_reason(network), out); return; }; - let timeout = timeout_of(cfg); + let timeout = cfg.timeout_duration(); for (name, provider) in cfg.providers.iter() { let status = match prepare_sui(provider, timeout) { Err(e) => Status::Failed(format!("{e:#}")), @@ -330,15 +302,7 @@ fn prepare_sui( provider: &ForeignChainProviderConfig, timeout: Duration, ) -> anyhow::Result { - let mut url = provider.rpc_url.clone(); - let auth = auth_config_to_rpc_auth(provider.auth.clone(), &mut url)?; - let header = match auth { - RpcAuthentication::KeyInUrl => None, - RpcAuthentication::CustomHeader { - header_name, - header_value, - } => Some((header_name, header_value)), - }; + let (url, header) = resolve_provider_auth(provider)?; GrpcSuiClient::new(url, header, timeout) .map_err(|e| anyhow::anyhow!("failed to build the Sui gRPC client: {e}")) } diff --git a/crates/foreign-chain-health-check/src/probe.rs b/crates/foreign-chain-health-check/src/probe.rs index 79dcc8c17c..670e760529 100644 --- a/crates/foreign-chain-health-check/src/probe.rs +++ b/crates/foreign-chain-health-check/src/probe.rs @@ -3,30 +3,13 @@ use std::collections::BTreeMap; -use foreign_chain_inspector::abstract_chain::inspector::Abstract; -use foreign_chain_inspector::adi::inspector::Adi; -use foreign_chain_inspector::aptos::inspector::AptosInspector; -use foreign_chain_inspector::arbitrum::inspector::Arbitrum; -use foreign_chain_inspector::avalanche::inspector::Avalanche; -use foreign_chain_inspector::base::inspector::Base; -use foreign_chain_inspector::bitcoin::inspector::BitcoinInspector; -use foreign_chain_inspector::bnb::inspector::Bnb; -use foreign_chain_inspector::ethereum::inspector::Ethereum; -use foreign_chain_inspector::evm::inspector::{EvmChain, EvmInspector}; -use foreign_chain_inspector::hyperevm::inspector::HyperEvm; -use foreign_chain_inspector::polygon::inspector::Polygon; -use foreign_chain_inspector::starknet::inspector::StarknetInspector; -use foreign_chain_inspector::sui::inspector::SuiInspector; use foreign_chain_inspector::{ - FanOut, ForeignChainInspectionError, NetworkFingerprint, ProviderFailure, + BuildInspectors, FanOut, ForeignChainInspectionError, NetworkFingerprint, ProviderFailure, }; -use foreign_chain_rpc_interfaces::aptos::ReqwestAptosClient; use mpc_node_config::{ForeignChainConfig, ForeignChainProviderConfig, ForeignChainsConfig}; use near_mpc_bounded_collections::NonEmptyVec; use near_mpc_contract_interface::types::{ForeignChain, ProviderId}; -use crate::{prepare_aptos, prepare_jsonrpc, prepare_sui, timeout_of}; - /// One provider's verdict. Anything other than [`ProviderStatus::Healthy`] is unhealthy. #[derive(Debug, Clone, PartialEq, Eq)] pub enum ProviderStatus { @@ -102,57 +85,22 @@ impl ProbeReport { /// Each provider is tried up to `max_retries` times, `timeout_sec` per try, and only for as long as /// the failures stay transient. This returns within the largest configured `timeout_sec * /// max_retries`, plus the [`foreign_chain_inspector::RETRY_BACKOFF`] between tries. -/// -/// TODO(#4043): take the inspectors as a dependency instead -pub async fn probe_all_providers(config: &ForeignChainsConfig) -> ProbeReport { - let probe_attempts = config - .iter_chains() - .map(|(chain, chain_config)| async move { - match chain { - ForeignChain::Starknet => { - probe_chain(chain, chain_config, |provider| { - Ok(StarknetInspector::new(prepare_jsonrpc(provider)?)) - }) - .await - } - ForeignChain::Abstract => probe_evm::(chain, chain_config).await, - ForeignChain::Adi => probe_evm::(chain, chain_config).await, - ForeignChain::Arbitrum => probe_evm::(chain, chain_config).await, - ForeignChain::Avalanche => probe_evm::(chain, chain_config).await, - ForeignChain::Base => probe_evm::(chain, chain_config).await, - ForeignChain::Bnb => probe_evm::(chain, chain_config).await, - ForeignChain::Ethereum => probe_evm::(chain, chain_config).await, - ForeignChain::HyperEvm => probe_evm::(chain, chain_config).await, - ForeignChain::Polygon => probe_evm::(chain, chain_config).await, - ForeignChain::Bitcoin => { - probe_chain(chain, chain_config, |provider| { - Ok(BitcoinInspector::new(prepare_jsonrpc(provider)?)) - }) - .await - } - ForeignChain::Aptos => { - let timeout = timeout_of(chain_config); - probe_chain(chain, chain_config, move |provider| { - let (url, auth_header) = prepare_aptos(provider)?; - Ok(AptosInspector::new(ReqwestAptosClient::new( - url, - auth_header, - timeout, - ))) - }) - .await - } - ForeignChain::Sui => { - let timeout = timeout_of(chain_config); - probe_chain(chain, chain_config, move |provider| { - Ok(SuiInspector::new(prepare_sui(provider, timeout)?)) - }) - .await - } - // Solana and Ton have no inspector to probe them with. - _ => rows_of(chain, chain_config, ProviderStatus::ProbeNotImplemented), - } - }); +pub async fn probe_all_providers( + config: &ForeignChainsConfig, + inspectors: &InspectorFactory, +) -> ProbeReport +where + InspectorFactory: BuildInspectors, +{ + let probe_attempts = config.iter_chains().map(|(chain, chain_config)| { + let timeout = chain_config.timeout_duration(); + async move { + probe_chain(chain, chain_config, |provider| { + inspectors.build(chain, provider, timeout) + }) + .await + } + }); futures::future::join_all(probe_attempts) .await @@ -160,35 +108,22 @@ pub async fn probe_all_providers(config: &ForeignChainsConfig) -> ProbeReport { .into() } -async fn probe_evm(chain: ForeignChain, config: &ForeignChainConfig) -> Vec -where - Chain: EvmChain + Clone + Send + Sync + 'static, -{ - probe_chain(chain, config, |provider| { - Ok(EvmInspector::<_, Chain>::new(prepare_jsonrpc(provider)?)) - }) - .await -} - async fn probe_chain( chain: ForeignChain, config: &ForeignChainConfig, - new_inspector: impl Fn(&ForeignChainProviderConfig) -> anyhow::Result, + build_new_inspector: impl Fn(&ForeignChainProviderConfig) -> anyhow::Result>, ) -> Vec where - I: foreign_chain_inspector::NetworkFingerprintInspector + Clone + Send + Sync + 'static, + I: foreign_chain_inspector::ChainInspector, { - let Some(expected) = &config.expected_network_fingerprint else { - return rows_of(chain, config, ProviderStatus::MissingExpectedFingerprint); - }; - let expected = I::canonical_fingerprint(expected); - let mut inspectors = Vec::new(); let mut rows = Vec::new(); for (name, provider) in config.providers.iter() { let provider_id = ProviderId(name.as_str().to_owned()); - match new_inspector(provider) { - Ok(inspector) => inspectors.push((provider_id, inspector)), + match build_new_inspector(provider) { + // Inspector not implemented for the chain + Ok(None) => return rows_of(chain, config, ProviderStatus::ProbeNotImplemented), + Ok(Some(inspector)) => inspectors.push((provider_id, inspector)), Err(error) => rows.push(ProviderHealth { chain, provider: provider_id, @@ -197,12 +132,19 @@ where } } + let Some(expected) = &config.expected_network_fingerprint else { + return rows_of(chain, config, ProviderStatus::MissingExpectedFingerprint); + }; + let Ok(inspectors) = NonEmptyVec::try_from(inspectors) else { return rows; }; + // Any of the chain's inspectors normalizes the same way; the first one that built is enough. + let (_, inspector) = inspectors.first(); + let expected = inspector.canonical_fingerprint(expected); let fingerprints = FanOut::new(inspectors) - .network_fingerprints(timeout_of(config), config.max_retries) + .network_fingerprints(config.timeout_duration(), config.max_retries) .await; for (provider, reported) in fingerprints { rows.push(ProviderHealth { @@ -265,12 +207,45 @@ fn classify( #[cfg(test)] #[expect(non_snake_case)] mod tests { + use foreign_chain_inspector::mock::{MockInspector, MockReply}; + + /// Hands the probe a mock inspector per provider URL. + struct MockInspectors(std::collections::BTreeMap); + + impl MockInspectors { + fn new<'a>(inspectors: impl IntoIterator) -> Self { + Self( + inspectors + .into_iter() + .map(|(url, inspector)| (url.to_string(), inspector)) + .collect(), + ) + } + } + + impl BuildInspectors for MockInspectors { + type Inspector = MockInspector; + + fn build( + &self, + _chain: ForeignChain, + provider: &ForeignChainProviderConfig, + _timeout: std::time::Duration, + ) -> anyhow::Result> { + let inspector = self + .0 + .get(&provider.rpc_url) + .unwrap_or_else(|| panic!("no mock inspector for `{}`", provider.rpc_url)); + Ok(Some(inspector.clone())) + } + } use super::*; use assert_matches::assert_matches; use foreign_chain_inspector::{ abstract_chain, adi, aptos, arbitrum, avalanche, base, bitcoin, bnb, ethereum, hyperevm, polygon, starknet, sui, }; + use foreign_chain_rpc_factory::inspectors::InspectorFactory; use foreign_chain_rpc_interfaces::sui::Status; use foreign_chain_rpc_interfaces::sui::proto::ledger_service_server::{ LedgerService, LedgerServiceServer, @@ -280,7 +255,6 @@ mod tests { use near_mpc_bounded_collections::NonEmptyBTreeMap; use rstest::rstest; use std::num::NonZeroU64; - use std::time::Duration; const MAINNET: &str = starknet::MAINNET_CHAIN_ID; const SEPOLIA: &str = starknet::SEPOLIA_CHAIN_ID; @@ -468,19 +442,10 @@ mod tests { .await } - async fn mock_bad_api_key(server: &httpmock::MockServer) -> httpmock::Mock<'_> { - mock_error_object(server, 401, -32600, "Must be authenticated!").await - } - async fn mock_unsupported_method(server: &httpmock::MockServer) -> httpmock::Mock<'_> { mock_error_object(server, 200, -32601, "Method not found").await } - /// Throttling over HTTP 200, so only the JSON-RPC code tells the caller to back off. - async fn mock_throttled_over_http_200(server: &httpmock::MockServer) -> httpmock::Mock<'_> { - mock_error_object(server, 200, -32005, "limit exceeded").await - } - async fn mock_non_jsonrpc_body(server: &httpmock::MockServer) -> httpmock::Mock<'_> { server .mock_async(|when, then| { @@ -490,16 +455,11 @@ mod tests { .await } - async fn mock_never_answers_in_time(server: &httpmock::MockServer) -> httpmock::Mock<'_> { - let body = serde_json::json!({"jsonrpc": "2.0", "result": MAINNET, "id": 0}); - server - .mock_async(|when, then| { - when.method(httpmock::Method::POST); - then.status(200) - .json_body(body) - .delay(Duration::from_secs(30)); - }) - .await + fn answering(fingerprint: &str) -> MockReply { + MockReply::Answer { + delay: std::time::Duration::ZERO, + fingerprint: fingerprint.to_string(), + } } /// Keyed by chain too: provider names repeat across chains in real configs. @@ -516,18 +476,16 @@ mod tests { #[tokio::test] async fn probe_all_providers__should_report_a_provider_on_the_expected_network_as_healthy() { // Given - let server = httpmock::MockServer::start_async().await; - let mock = mock_fingerprint(&server, MAINNET).await; - let config = starknet_only(chain_config( - Some(MAINNET), - one_provider("publicnode", &server.base_url()), - )); + let url = "http://mock.invalid/only"; + let config = starknet_only(chain_config(Some(MAINNET), one_provider("publicnode", url))); + let inspector = MockInspector::new([answering(MAINNET)]); + let inspectors = MockInspectors::new([(url, inspector.clone())]); // When - let report = probe_all_providers(&config).await; + let report = probe_all_providers(&config, &inspectors).await; // Then - mock.assert_async().await; + assert_eq!(inspector.calls(), 1); assert_eq!( status_of(&report, ForeignChain::Starknet, "publicnode"), ProviderStatus::Healthy @@ -537,15 +495,12 @@ mod tests { #[tokio::test] async fn probe_all_providers__should_report_a_provider_on_another_network_as_wrong_network() { // Given - let server = httpmock::MockServer::start_async().await; - mock_fingerprint(&server, SEPOLIA).await; - let config = starknet_only(chain_config( - Some(MAINNET), - one_provider("publicnode", &server.base_url()), - )); + let url = "http://mock.invalid/only"; + let config = starknet_only(chain_config(Some(MAINNET), one_provider("publicnode", url))); + let inspectors = MockInspectors::new([(url, MockInspector::new([answering(SEPOLIA)]))]); // When - let report = probe_all_providers(&config).await; + let report = probe_all_providers(&config, &inspectors).await; // Then assert_eq!( @@ -568,7 +523,7 @@ mod tests { )); // When - let report = probe_all_providers(&config).await; + let report = probe_all_providers(&config, &InspectorFactory).await; // Then assert_eq!( @@ -589,7 +544,7 @@ mod tests { )); // When - let report = probe_all_providers(&config).await; + let report = probe_all_providers(&config, &InspectorFactory).await; // Then assert_eq!( @@ -608,7 +563,7 @@ mod tests { )); // When - let report = probe_all_providers(&config).await; + let report = probe_all_providers(&config, &InspectorFactory).await; // Then assert_eq!( @@ -620,22 +575,23 @@ mod tests { #[tokio::test] async fn probe_all_providers__should_report_a_provider_refusing_the_request_without_retrying() { // Given - let server = httpmock::MockServer::start_async().await; - let mock = mock_bad_api_key(&server).await; + let url = "http://mock.invalid/only"; + let inspector = MockInspector::new([MockReply::refusal(std::time::Duration::ZERO)]); let config = starknet_only(with_retries( - chain_config(Some(MAINNET), one_provider("keyed", &server.base_url())), + chain_config(Some(MAINNET), one_provider("keyed", url)), 3, )); + let inspectors = MockInspectors::new([(url, inspector.clone())]); // When - let report = probe_all_providers(&config).await; + let report = probe_all_providers(&config, &inspectors).await; // Then assert_eq!( status_of(&report, ForeignChain::Starknet, "keyed"), ProviderStatus::RequestRejected ); - mock.assert_calls_async(1).await; + assert_eq!(inspector.calls(), 1, "a refusal must not be retried"); } #[tokio::test] @@ -649,7 +605,7 @@ mod tests { )); // When - let report = probe_all_providers(&config).await; + let report = probe_all_providers(&config, &InspectorFactory).await; // Then assert_eq!( @@ -669,7 +625,7 @@ mod tests { )); // When - let report = probe_all_providers(&config).await; + let report = probe_all_providers(&config, &InspectorFactory).await; // Then assert_eq!( @@ -678,18 +634,15 @@ mod tests { ); } - #[tokio::test] + #[tokio::test(start_paused = true)] async fn probe_all_providers__should_report_a_provider_that_does_not_answer_in_time() { // Given - let server = httpmock::MockServer::start_async().await; - mock_never_answers_in_time(&server).await; - let config = starknet_only(chain_config( - Some(MAINNET), - one_provider("slow", &server.base_url()), - )); + let url = "http://mock.invalid/slow"; + let config = starknet_only(chain_config(Some(MAINNET), one_provider("slow", url))); + let inspectors = MockInspectors::new([(url, MockInspector::new([MockReply::Hang]))]); // When - let report = probe_all_providers(&config).await; + let report = probe_all_providers(&config, &inspectors).await; // Then assert_eq!( @@ -709,7 +662,7 @@ mod tests { )); // When - let report = probe_all_providers(&config).await; + let report = probe_all_providers(&config, &InspectorFactory).await; // Then assert_eq!( @@ -739,7 +692,7 @@ mod tests { )); // When - let report = probe_all_providers(&config).await; + let report = probe_all_providers(&config, &InspectorFactory).await; // Then assert_eq!( @@ -757,7 +710,7 @@ mod tests { )); // When - let report = probe_all_providers(&config).await; + let report = probe_all_providers(&config, &InspectorFactory).await; // Then assert_eq!( @@ -776,7 +729,7 @@ mod tests { let config = starknet_only(chain_config(Some(MAINNET), providers)); // When - let report = probe_all_providers(&config).await; + let report = probe_all_providers(&config, &InspectorFactory).await; // Then assert_eq!( @@ -806,7 +759,7 @@ mod tests { )); // When - let report = probe_all_providers(&config).await; + let report = probe_all_providers(&config, &InspectorFactory).await; // Then assert_eq!( @@ -833,7 +786,7 @@ mod tests { }; // When - let report = probe_all_providers(&config).await; + let report = probe_all_providers(&config, &InspectorFactory).await; // Then assert_eq!( @@ -868,7 +821,7 @@ mod tests { } // When - let report = probe_all_providers(&config).await; + let report = probe_all_providers(&config, &InspectorFactory).await; // Then for EvmMainnet { chain, .. } in EVM_MAINNETS { @@ -894,7 +847,7 @@ mod tests { ); // When - let report = probe_all_providers(&config).await; + let report = probe_all_providers(&config, &InspectorFactory).await; // Then assert_eq!( @@ -917,7 +870,7 @@ mod tests { )); // When - let report = probe_all_providers(&config).await; + let report = probe_all_providers(&config, &InspectorFactory).await; // Then assert_eq!( @@ -937,7 +890,7 @@ mod tests { )); // When - let report = probe_all_providers(&config).await; + let report = probe_all_providers(&config, &InspectorFactory).await; // Then assert_eq!( @@ -976,7 +929,7 @@ mod tests { }; // When - let report = probe_all_providers(&config).await; + let report = probe_all_providers(&config, &InspectorFactory).await; // Then assert_eq!( @@ -999,7 +952,7 @@ mod tests { }; // When - let report = probe_all_providers(&config).await; + let report = probe_all_providers(&config, &InspectorFactory).await; // Then assert_eq!( @@ -1090,7 +1043,7 @@ mod tests { )); // When - let report = probe_all_providers(&config).await; + let report = probe_all_providers(&config, &InspectorFactory).await; // Then assert_eq!( @@ -1108,7 +1061,7 @@ mod tests { )); // When - let report = probe_all_providers(&config).await; + let report = probe_all_providers(&config, &InspectorFactory).await; // Then assert_eq!( @@ -1117,25 +1070,33 @@ mod tests { ); } - #[tokio::test] + #[tokio::test(start_paused = true)] async fn probe_all_providers__should_retry_a_provider_that_refused_with_a_rate_limit_code() { // Given - let server = httpmock::MockServer::start_async().await; - let mock = mock_throttled_over_http_200(&server).await; + let url = "http://mock.invalid/keyed"; + let inspector = MockInspector::new([ + MockReply::transient(std::time::Duration::from_millis(10)), + MockReply::transient(std::time::Duration::from_millis(10)), + ]); let config = starknet_only(with_retries( - chain_config(Some(MAINNET), one_provider("keyed", &server.base_url())), + chain_config(Some(MAINNET), one_provider("keyed", url)), 2, )); + let inspectors = MockInspectors::new([(url, inspector.clone())]); // When - let report = probe_all_providers(&config).await; + let report = probe_all_providers(&config, &inspectors).await; // Then assert_eq!( status_of(&report, ForeignChain::Starknet, "keyed"), ProviderStatus::Unreachable ); - mock.assert_calls_async(2).await; + assert_eq!( + inspector.calls(), + 2, + "the transient failure should be retried" + ); } #[tokio::test] @@ -1150,7 +1111,7 @@ mod tests { )); // When - let report = probe_all_providers(&config).await; + let report = probe_all_providers(&config, &InspectorFactory).await; // Then let ProviderStatus::WrongNetwork { observed, .. } = @@ -1188,7 +1149,7 @@ mod tests { let config = ForeignChainsConfig::default(); // When - let report = probe_all_providers(&config).await; + let report = probe_all_providers(&config, &InspectorFactory).await; // Then assert!(report.rows().is_empty()); @@ -1217,7 +1178,7 @@ mod tests { )); // When - let report = probe_all_providers(&config).await; + let report = probe_all_providers(&config, &InspectorFactory).await; // Then assert_matches!( diff --git a/crates/foreign-chain-inspector/Cargo.toml b/crates/foreign-chain-inspector/Cargo.toml index a22c514a34..6ada695690 100644 --- a/crates/foreign-chain-inspector/Cargo.toml +++ b/crates/foreign-chain-inspector/Cargo.toml @@ -4,14 +4,18 @@ version.workspace = true edition.workspace = true license.workspace = true +[features] +test-utils = [] + [dependencies] +anyhow = { workspace = true } bs58 = { workspace = true } derive_more = { workspace = true } ethereum-types = { workspace = true } foreign-chain-rpc-interfaces = { workspace = true } hex = { workspace = true } -http = { workspace = true } jsonrpsee = { workspace = true } +mpc-node-config = { workspace = true } mpc-primitives = { workspace = true } near-mpc-bounded-collections = { workspace = true } near-mpc-contract-interface = { workspace = true } @@ -22,6 +26,8 @@ tracing = { workspace = true } [dev-dependencies] assert_matches = { workspace = true } +foreign-chain-rpc-factory = { workspace = true } +http = { workspace = true } httpmock = { workspace = true } mockall = { workspace = true } rstest = { workspace = true } diff --git a/crates/foreign-chain-inspector/src/aptos/inspector.rs b/crates/foreign-chain-inspector/src/aptos/inspector.rs index 736cff9395..ec1abd4d43 100644 --- a/crates/foreign-chain-inspector/src/aptos/inspector.rs +++ b/crates/foreign-chain-inspector/src/aptos/inspector.rs @@ -39,12 +39,10 @@ where { async fn network_fingerprint(&self) -> Result { let ledger_info = self.client.get_ledger_info().await.classified()?; - Ok(Self::canonical_fingerprint( - &ledger_info.chain_id.to_string(), - )) + Ok(self.canonical_fingerprint(&ledger_info.chain_id.to_string())) } - fn canonical_fingerprint(fingerprint: &str) -> NetworkFingerprint { + fn canonical_fingerprint(&self, fingerprint: &str) -> NetworkFingerprint { NetworkFingerprint::new(canonical_chain_id_text(fingerprint)) } } diff --git a/crates/foreign-chain-inspector/src/bitcoin/inspector.rs b/crates/foreign-chain-inspector/src/bitcoin/inspector.rs index fe1621f837..4238c8d210 100644 --- a/crates/foreign-chain-inspector/src/bitcoin/inspector.rs +++ b/crates/foreign-chain-inspector/src/bitcoin/inspector.rs @@ -44,7 +44,7 @@ where Ok(NetworkFingerprint::new(genesis_hash.canonical_text())) } - fn canonical_fingerprint(fingerprint: &str) -> NetworkFingerprint { + fn canonical_fingerprint(&self, fingerprint: &str) -> NetworkFingerprint { NetworkFingerprint::new(GetBlockHashResponse(fingerprint.to_owned()).canonical_text()) } } diff --git a/crates/foreign-chain-inspector/src/evm/inspector.rs b/crates/foreign-chain-inspector/src/evm/inspector.rs index 00a9a3e405..ad88be1bc0 100644 --- a/crates/foreign-chain-inspector/src/evm/inspector.rs +++ b/crates/foreign-chain-inspector/src/evm/inspector.rs @@ -55,7 +55,7 @@ where Ok(NetworkFingerprint::new(chain_id.canonical_text())) } - fn canonical_fingerprint(fingerprint: &str) -> NetworkFingerprint { + fn canonical_fingerprint(&self, fingerprint: &str) -> NetworkFingerprint { NetworkFingerprint::new(ChainIdResponse(fingerprint.to_owned()).canonical_text()) } } diff --git a/crates/foreign-chain-inspector/src/lib.rs b/crates/foreign-chain-inspector/src/lib.rs index 4fe1d707ca..da4efff810 100644 --- a/crates/foreign-chain-inspector/src/lib.rs +++ b/crates/foreign-chain-inspector/src/lib.rs @@ -4,13 +4,12 @@ use std::time::Duration; use derive_more::{Deref, Display, From}; use ethereum_types::H256; -use http::{HeaderMap, HeaderName, HeaderValue}; use jsonrpsee::core::client::error::Error as RpcClientError; use jsonrpsee::core::http_helpers::HttpError; use jsonrpsee::http_client::transport::Error as HttpTransportError; -use jsonrpsee::http_client::{HttpClient, HttpClientBuilder}; +use mpc_node_config::ForeignChainProviderConfig; use near_mpc_bounded_collections::NonEmptyVec; -use near_mpc_contract_interface::types::ProviderId; +use near_mpc_contract_interface::types::{ForeignChain, ProviderId}; use thiserror::Error; pub use jsonrpsee::http_client; @@ -27,7 +26,10 @@ pub mod contract_interface_conversions; pub mod ethereum; pub mod evm; pub mod hyperevm; +#[cfg(any(test, feature = "test-utils"))] +pub mod mock; pub mod polygon; +pub mod rpc_inspector; pub mod starknet; pub mod sui; @@ -86,7 +88,7 @@ pub trait NetworkFingerprintInspector { /// Normalizes any spec-legal spelling of this chain's fingerprint into the single form the trait /// compares. Idempotent. - fn canonical_fingerprint(fingerprint: &str) -> NetworkFingerprint; + fn canonical_fingerprint(&self, fingerprint: &str) -> NetworkFingerprint; } /// Combines multiple inspectors that target the same chain into a single inspector. @@ -228,12 +230,27 @@ where } } +pub trait ChainInspector: NetworkFingerprintInspector + Clone + Send + Sync + 'static {} + +impl ChainInspector for T {} + +pub trait BuildInspectors: Sync { + type Inspector: ChainInspector; + + fn build( + &self, + chain: ForeignChain, + provider: &ForeignChainProviderConfig, + timeout: Duration, + ) -> anyhow::Result>; +} + /// Pause between two tries at the same provider. pub const RETRY_BACKOFF: Duration = Duration::from_millis(200); impl FanOut where - Inspector: NetworkFingerprintInspector + Clone + Send + Sync + 'static, + Inspector: ChainInspector, { /// Ask every provider for the network it serves concurrently, one result each. /// Unlike [`FanOut::extract`], disagreement is not an error: a diagnostic caller needs the @@ -272,19 +289,6 @@ where } } -#[derive(Debug, Clone)] -pub enum RpcAuthentication { - /// The key is in the URL (e.g., Alchemy, QuickNode). - /// Example: `https://eth-mainnet.alchemyapi.io/v2/your-api-key` - KeyInUrl, - /// Custom header for providers like NOWNodes or GetBlock. - /// Example: key="x-api-key", value="your-secret-token" - CustomHeader { - header_name: HeaderName, - header_value: HeaderValue, - }, -} - #[derive(From, Debug, Display, Clone, Copy, Deref, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct BlockConfirmations(u64); @@ -504,32 +508,6 @@ pub enum ProviderFailure { TimedOut, } -/// Builds an HTTP client with the specified authentication method. -/// This client can be used to construct a [`ForeignChainInspector`] such -/// as [`bitcoin::inspector::BitcoinInspector`]. -pub fn build_http_client( - base_url: String, - rpc_authentication: RpcAuthentication, -) -> Result { - let mut headers = HeaderMap::new(); - - match rpc_authentication { - RpcAuthentication::KeyInUrl => {} - RpcAuthentication::CustomHeader { - header_name, - header_value, - } => { - headers.insert(header_name, header_value); - } - } - - let client = HttpClientBuilder::default() - .set_headers(headers) - .build(&base_url)?; - - Ok(client) -} - #[cfg(test)] #[expect(non_snake_case)] mod tests { @@ -661,9 +639,9 @@ mod tests { fn classify_rpc_client_error__should_keep_the_rpc_url_out_of_the_message() { // Given let url_carrying_a_key = "http://provider.example/v2/super-secret".to_string(); - let error = transport(HttpTransportError::Url(url_carrying_a_key)); // When + let error = transport(HttpTransportError::Url(url_carrying_a_key)); let classified = ForeignChainInspectionError::classify_rpc_client_error(error); // Then diff --git a/crates/foreign-chain-inspector/src/mock.rs b/crates/foreign-chain-inspector/src/mock.rs new file mode 100644 index 0000000000..35a7f9a369 --- /dev/null +++ b/crates/foreign-chain-inspector/src/mock.rs @@ -0,0 +1,92 @@ +//! Test doubles for the Foreign Tx Inspectors. +//! +//! Use `#[tokio::test(start_paused = true)]` if test case simulates network latency. +//! Never use with real sockets, tokio paused clock will fire timeouts instantly. + +use std::collections::VecDeque; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::{Arc, Mutex}; +use std::time::Duration; + +use crate::{ForeignChainInspectionError, NetworkFingerprint, NetworkFingerprintInspector}; + +#[derive(Debug)] +pub enum MockReply { + Answer { + delay: Duration, + fingerprint: String, + }, + Fail { + delay: Duration, + error: ForeignChainInspectionError, + }, + /// Never resolves + Hang, +} + +impl MockReply { + pub fn fail(delay: Duration, error: ForeignChainInspectionError) -> Self { + Self::Fail { delay, error } + } + + pub fn transient(delay: Duration) -> Self { + Self::fail( + delay, + ForeignChainInspectionError::RpcRequestFailed("mock transient failure".to_string()), + ) + } + + pub fn refusal(delay: Duration) -> Self { + Self::fail( + delay, + ForeignChainInspectionError::RpcRequestRejected("mock refusal".to_string()), + ) + } +} + +/// Answers from a queue of [`MockReply`]s; panics on a call past the end of the queue. +#[derive(Clone)] +pub struct MockInspector { + replies: Arc>>, + calls: Arc, +} + +impl MockInspector { + pub fn new(replies: impl IntoIterator) -> Self { + Self { + replies: Arc::new(Mutex::new(replies.into_iter().collect())), + calls: Arc::new(AtomicUsize::new(0)), + } + } + + pub fn calls(&self) -> usize { + self.calls.load(Ordering::SeqCst) + } +} + +impl NetworkFingerprintInspector for MockInspector { + async fn network_fingerprint(&self) -> Result { + self.calls.fetch_add(1, Ordering::SeqCst); + let reply = self + .replies + .lock() + .expect("replies mutex poisoned") + .pop_front() + .expect("call beyond the queued replies"); + match reply { + MockReply::Answer { delay, fingerprint } => { + tokio::time::sleep(delay).await; + Ok(NetworkFingerprint::new(fingerprint)) + } + MockReply::Fail { delay, error } => { + tokio::time::sleep(delay).await; + Err(error) + } + MockReply::Hang => std::future::pending().await, + } + } + + fn canonical_fingerprint(&self, fingerprint: &str) -> NetworkFingerprint { + NetworkFingerprint::new(fingerprint) + } +} diff --git a/crates/foreign-chain-inspector/src/rpc_inspector.rs b/crates/foreign-chain-inspector/src/rpc_inspector.rs new file mode 100644 index 0000000000..0d0a9c1931 --- /dev/null +++ b/crates/foreign-chain-inspector/src/rpc_inspector.rs @@ -0,0 +1,73 @@ +use crate::abstract_chain::inspector::Abstract; +use crate::adi::inspector::Adi; +use crate::aptos::inspector::AptosInspector; +use crate::arbitrum::inspector::Arbitrum; +use crate::avalanche::inspector::Avalanche; +use crate::base::inspector::Base; +use crate::bitcoin::inspector::BitcoinInspector; +use crate::bnb::inspector::Bnb; +use crate::ethereum::inspector::Ethereum; +use crate::evm::inspector::EvmInspector; +use crate::http_client::HttpClient; +use crate::hyperevm::inspector::HyperEvm; +use crate::polygon::inspector::Polygon; +use crate::starknet::inspector::StarknetInspector; +use crate::sui::inspector::SuiInspector; +use crate::{ForeignChainInspectionError, NetworkFingerprint, NetworkFingerprintInspector}; +use foreign_chain_rpc_interfaces::aptos::ReqwestAptosClient; +use foreign_chain_rpc_interfaces::sui::GrpcSuiClient; + +#[derive(Clone)] +pub enum RpcInspector { + Abstract(EvmInspector), + Adi(EvmInspector), + Aptos(AptosInspector), + Arbitrum(EvmInspector), + Avalanche(EvmInspector), + Base(EvmInspector), + Bitcoin(BitcoinInspector), + Bnb(EvmInspector), + Ethereum(EvmInspector), + HyperEvm(EvmInspector), + Polygon(EvmInspector), + Starknet(StarknetInspector), + Sui(SuiInspector), +} + +impl NetworkFingerprintInspector for RpcInspector { + async fn network_fingerprint(&self) -> Result { + match self { + Self::Abstract(inspector) => inspector.network_fingerprint().await, + Self::Adi(inspector) => inspector.network_fingerprint().await, + Self::Aptos(inspector) => inspector.network_fingerprint().await, + Self::Arbitrum(inspector) => inspector.network_fingerprint().await, + Self::Avalanche(inspector) => inspector.network_fingerprint().await, + Self::Base(inspector) => inspector.network_fingerprint().await, + Self::Bitcoin(inspector) => inspector.network_fingerprint().await, + Self::Bnb(inspector) => inspector.network_fingerprint().await, + Self::Ethereum(inspector) => inspector.network_fingerprint().await, + Self::HyperEvm(inspector) => inspector.network_fingerprint().await, + Self::Polygon(inspector) => inspector.network_fingerprint().await, + Self::Starknet(inspector) => inspector.network_fingerprint().await, + Self::Sui(inspector) => inspector.network_fingerprint().await, + } + } + + fn canonical_fingerprint(&self, fingerprint: &str) -> NetworkFingerprint { + match self { + Self::Abstract(inspector) => inspector.canonical_fingerprint(fingerprint), + Self::Adi(inspector) => inspector.canonical_fingerprint(fingerprint), + Self::Aptos(inspector) => inspector.canonical_fingerprint(fingerprint), + Self::Arbitrum(inspector) => inspector.canonical_fingerprint(fingerprint), + Self::Avalanche(inspector) => inspector.canonical_fingerprint(fingerprint), + Self::Base(inspector) => inspector.canonical_fingerprint(fingerprint), + Self::Bitcoin(inspector) => inspector.canonical_fingerprint(fingerprint), + Self::Bnb(inspector) => inspector.canonical_fingerprint(fingerprint), + Self::Ethereum(inspector) => inspector.canonical_fingerprint(fingerprint), + Self::HyperEvm(inspector) => inspector.canonical_fingerprint(fingerprint), + Self::Polygon(inspector) => inspector.canonical_fingerprint(fingerprint), + Self::Starknet(inspector) => inspector.canonical_fingerprint(fingerprint), + Self::Sui(inspector) => inspector.canonical_fingerprint(fingerprint), + } + } +} diff --git a/crates/foreign-chain-inspector/src/starknet/inspector.rs b/crates/foreign-chain-inspector/src/starknet/inspector.rs index eee604bb61..4000b8db9b 100644 --- a/crates/foreign-chain-inspector/src/starknet/inspector.rs +++ b/crates/foreign-chain-inspector/src/starknet/inspector.rs @@ -36,10 +36,10 @@ where .request(CHAIN_ID_METHOD, NO_PARAMS) .await .map_err(ForeignChainInspectionError::classify_rpc_client_error)?; - Ok(Self::canonical_fingerprint(&chain_id.0)) + Ok(self.canonical_fingerprint(&chain_id.0)) } - fn canonical_fingerprint(fingerprint: &str) -> NetworkFingerprint { + fn canonical_fingerprint(&self, fingerprint: &str) -> NetworkFingerprint { NetworkFingerprint::new(ChainIdResponse(fingerprint.to_owned()).canonical_text()) } } diff --git a/crates/foreign-chain-inspector/src/sui/inspector.rs b/crates/foreign-chain-inspector/src/sui/inspector.rs index 3f75230412..db2703e46c 100644 --- a/crates/foreign-chain-inspector/src/sui/inspector.rs +++ b/crates/foreign-chain-inspector/src/sui/inspector.rs @@ -44,12 +44,12 @@ where "service info is missing the chain id".to_string(), )); }; - Ok(Self::canonical_fingerprint(&chain_id)) + Ok(self.canonical_fingerprint(&chain_id)) } /// Unlike inspectors for other chains, we do not need to normalize the input string here. /// Base58 is case sensitive and does not permit prefix or padding. - fn canonical_fingerprint(fingerprint: &str) -> NetworkFingerprint { + fn canonical_fingerprint(&self, fingerprint: &str) -> NetworkFingerprint { NetworkFingerprint::new(fingerprint) } } diff --git a/crates/foreign-chain-inspector/tests/abstract_rpc_manual.rs b/crates/foreign-chain-inspector/tests/abstract_rpc_manual.rs index 8dd92153fb..aed3e0230a 100644 --- a/crates/foreign-chain-inspector/tests/abstract_rpc_manual.rs +++ b/crates/foreign-chain-inspector/tests/abstract_rpc_manual.rs @@ -1,11 +1,12 @@ use assert_matches::assert_matches; use foreign_chain_inspector::{ - EthereumFinality, ForeignChainInspector, NetworkFingerprintInspector, RpcAuthentication, + EthereumFinality, ForeignChainInspector, NetworkFingerprintInspector, abstract_chain::{ AbstractBlockHash, AbstractTransactionHash, TESTNET_CHAIN_ID, inspector::{AbstractExtractedValue, AbstractExtractor, AbstractInspector}, }, }; +use foreign_chain_rpc_factory::build_http_client; const ABSTRACT_RPC_URL: &str = "https://api.testnet.abs.xyz"; @@ -26,10 +27,10 @@ async fn inspector_extracts_block_hash_against_live_rpc_provider() { .parse() .unwrap(); - let http_client = foreign_chain_inspector::build_http_client( - ABSTRACT_RPC_URL.to_string(), - RpcAuthentication::KeyInUrl, - ) + let http_client = build_http_client(&mpc_node_config::ForeignChainProviderConfig { + rpc_url: ABSTRACT_RPC_URL.to_string(), + auth: mpc_node_config::AuthConfig::None, + }) .unwrap(); let inspector = AbstractInspector::new(http_client); @@ -62,10 +63,10 @@ const EXPECTED_NETWORK_FINGERPRINT: u64 = TESTNET_CHAIN_ID; #[ignore = "manual test to sanity check against live Abstract RPC provider"] async fn network_fingerprint_matches_the_shipped_config_value_against_live_rpc_provider() { // given - let http_client = foreign_chain_inspector::build_http_client( - ABSTRACT_RPC_URL.to_string(), - RpcAuthentication::KeyInUrl, - ) + let http_client = build_http_client(&mpc_node_config::ForeignChainProviderConfig { + rpc_url: ABSTRACT_RPC_URL.to_string(), + auth: mpc_node_config::AuthConfig::None, + }) .unwrap(); let inspector = AbstractInspector::new(http_client); diff --git a/crates/foreign-chain-inspector/tests/adi_rpc_manual.rs b/crates/foreign-chain-inspector/tests/adi_rpc_manual.rs index eddb016a6b..b0c00bcb04 100644 --- a/crates/foreign-chain-inspector/tests/adi_rpc_manual.rs +++ b/crates/foreign-chain-inspector/tests/adi_rpc_manual.rs @@ -1,11 +1,12 @@ use assert_matches::assert_matches; use foreign_chain_inspector::{ - EthereumFinality, ForeignChainInspector, NetworkFingerprintInspector, RpcAuthentication, + EthereumFinality, ForeignChainInspector, NetworkFingerprintInspector, adi::{ AdiBlockHash, AdiTransactionHash, MAINNET_CHAIN_ID, inspector::{AdiExtractedValue, AdiExtractor, AdiInspector}, }, }; +use foreign_chain_rpc_factory::build_http_client; const ADI_RPC_URL: &str = "https://rpc.adifoundation.ai"; @@ -26,10 +27,10 @@ async fn inspector_extracts_block_hash_against_live_rpc_provider() { .parse() .unwrap(); - let http_client = foreign_chain_inspector::build_http_client( - ADI_RPC_URL.to_string(), - RpcAuthentication::KeyInUrl, - ) + let http_client = build_http_client(&mpc_node_config::ForeignChainProviderConfig { + rpc_url: ADI_RPC_URL.to_string(), + auth: mpc_node_config::AuthConfig::None, + }) .unwrap(); let inspector = AdiInspector::new(http_client); @@ -66,10 +67,10 @@ const EXPECTED_NETWORK_FINGERPRINT: u64 = MAINNET_CHAIN_ID; #[ignore = "manual test to sanity check against live ADI Chain RPC provider"] async fn network_fingerprint_matches_the_shipped_config_value_against_live_rpc_provider() { // given - let http_client = foreign_chain_inspector::build_http_client( - ADI_RPC_URL.to_string(), - RpcAuthentication::KeyInUrl, - ) + let http_client = build_http_client(&mpc_node_config::ForeignChainProviderConfig { + rpc_url: ADI_RPC_URL.to_string(), + auth: mpc_node_config::AuthConfig::None, + }) .unwrap(); let inspector = AdiInspector::new(http_client); diff --git a/crates/foreign-chain-inspector/tests/arbitrum_rpc_manual.rs b/crates/foreign-chain-inspector/tests/arbitrum_rpc_manual.rs index 2a3811e78a..500ad81db5 100644 --- a/crates/foreign-chain-inspector/tests/arbitrum_rpc_manual.rs +++ b/crates/foreign-chain-inspector/tests/arbitrum_rpc_manual.rs @@ -1,11 +1,12 @@ use assert_matches::assert_matches; use foreign_chain_inspector::{ - EthereumFinality, ForeignChainInspector, NetworkFingerprintInspector, RpcAuthentication, + EthereumFinality, ForeignChainInspector, NetworkFingerprintInspector, arbitrum::{ ArbitrumBlockHash, ArbitrumTransactionHash, MAINNET_CHAIN_ID, inspector::{ArbitrumExtractedValue, ArbitrumExtractor, ArbitrumInspector}, }, }; +use foreign_chain_rpc_factory::build_http_client; const ARBITRUM_RPC_URL: &str = "https://arb1.arbitrum.io/rpc"; @@ -26,10 +27,10 @@ async fn inspector_extracts_block_hash_against_live_rpc_provider() { .parse() .unwrap(); - let http_client = foreign_chain_inspector::build_http_client( - ARBITRUM_RPC_URL.to_string(), - RpcAuthentication::KeyInUrl, - ) + let http_client = build_http_client(&mpc_node_config::ForeignChainProviderConfig { + rpc_url: ARBITRUM_RPC_URL.to_string(), + auth: mpc_node_config::AuthConfig::None, + }) .unwrap(); let inspector = ArbitrumInspector::new(http_client); @@ -66,10 +67,10 @@ const EXPECTED_NETWORK_FINGERPRINT: u64 = MAINNET_CHAIN_ID; #[ignore = "manual test to sanity check against live Arbitrum RPC provider"] async fn network_fingerprint_matches_the_shipped_config_value_against_live_rpc_provider() { // given - let http_client = foreign_chain_inspector::build_http_client( - ARBITRUM_RPC_URL.to_string(), - RpcAuthentication::KeyInUrl, - ) + let http_client = build_http_client(&mpc_node_config::ForeignChainProviderConfig { + rpc_url: ARBITRUM_RPC_URL.to_string(), + auth: mpc_node_config::AuthConfig::None, + }) .unwrap(); let inspector = ArbitrumInspector::new(http_client); diff --git a/crates/foreign-chain-inspector/tests/avalanche_rpc_manual.rs b/crates/foreign-chain-inspector/tests/avalanche_rpc_manual.rs index b5120b3af2..cc594ac8d8 100644 --- a/crates/foreign-chain-inspector/tests/avalanche_rpc_manual.rs +++ b/crates/foreign-chain-inspector/tests/avalanche_rpc_manual.rs @@ -1,11 +1,12 @@ use assert_matches::assert_matches; use foreign_chain_inspector::{ - EthereumFinality, ForeignChainInspector, NetworkFingerprintInspector, RpcAuthentication, + EthereumFinality, ForeignChainInspector, NetworkFingerprintInspector, avalanche::{ AvalancheBlockHash, AvalancheTransactionHash, MAINNET_CHAIN_ID, inspector::{AvalancheExtractedValue, AvalancheExtractor, AvalancheInspector}, }, }; +use foreign_chain_rpc_factory::build_http_client; const AVALANCHE_RPC_URL: &str = "https://api.avax.network/ext/bc/C/rpc"; @@ -26,10 +27,10 @@ async fn inspector_extracts_block_hash_against_live_rpc_provider() { .parse() .unwrap(); - let http_client = foreign_chain_inspector::build_http_client( - AVALANCHE_RPC_URL.to_string(), - RpcAuthentication::KeyInUrl, - ) + let http_client = build_http_client(&mpc_node_config::ForeignChainProviderConfig { + rpc_url: AVALANCHE_RPC_URL.to_string(), + auth: mpc_node_config::AuthConfig::None, + }) .unwrap(); let inspector = AvalancheInspector::new(http_client); @@ -62,10 +63,10 @@ const EXPECTED_NETWORK_FINGERPRINT: u64 = MAINNET_CHAIN_ID; #[ignore = "manual test to sanity check against live Avalanche C-Chain RPC provider"] async fn network_fingerprint_matches_the_shipped_config_value_against_live_rpc_provider() { // given - let http_client = foreign_chain_inspector::build_http_client( - AVALANCHE_RPC_URL.to_string(), - RpcAuthentication::KeyInUrl, - ) + let http_client = build_http_client(&mpc_node_config::ForeignChainProviderConfig { + rpc_url: AVALANCHE_RPC_URL.to_string(), + auth: mpc_node_config::AuthConfig::None, + }) .unwrap(); let inspector = AvalancheInspector::new(http_client); diff --git a/crates/foreign-chain-inspector/tests/base_rpc_manual.rs b/crates/foreign-chain-inspector/tests/base_rpc_manual.rs index 0976e3324e..92b3182994 100644 --- a/crates/foreign-chain-inspector/tests/base_rpc_manual.rs +++ b/crates/foreign-chain-inspector/tests/base_rpc_manual.rs @@ -1,11 +1,12 @@ use assert_matches::assert_matches; use foreign_chain_inspector::{ - EthereumFinality, ForeignChainInspector, NetworkFingerprintInspector, RpcAuthentication, + EthereumFinality, ForeignChainInspector, NetworkFingerprintInspector, base::{ BaseBlockHash, BaseTransactionHash, MAINNET_CHAIN_ID, inspector::{BaseExtractedValue, BaseExtractor, BaseInspector}, }, }; +use foreign_chain_rpc_factory::build_http_client; const BASE_RPC_URL: &str = "https://mainnet.base.org"; @@ -26,10 +27,10 @@ async fn inspector_extracts_block_hash_against_live_rpc_provider() { .parse() .unwrap(); - let http_client = foreign_chain_inspector::build_http_client( - BASE_RPC_URL.to_string(), - RpcAuthentication::KeyInUrl, - ) + let http_client = build_http_client(&mpc_node_config::ForeignChainProviderConfig { + rpc_url: BASE_RPC_URL.to_string(), + auth: mpc_node_config::AuthConfig::None, + }) .unwrap(); let inspector = BaseInspector::new(http_client); @@ -66,10 +67,10 @@ const EXPECTED_NETWORK_FINGERPRINT: u64 = MAINNET_CHAIN_ID; #[ignore = "manual test to sanity check against live Base RPC provider"] async fn network_fingerprint_matches_the_shipped_config_value_against_live_rpc_provider() { // given - let http_client = foreign_chain_inspector::build_http_client( - BASE_RPC_URL.to_string(), - RpcAuthentication::KeyInUrl, - ) + let http_client = build_http_client(&mpc_node_config::ForeignChainProviderConfig { + rpc_url: BASE_RPC_URL.to_string(), + auth: mpc_node_config::AuthConfig::None, + }) .unwrap(); let inspector = BaseInspector::new(http_client); diff --git a/crates/foreign-chain-inspector/tests/bitcoin_inspector.rs b/crates/foreign-chain-inspector/tests/bitcoin_inspector.rs index 39ce5c033d..b2b32ee4bf 100644 --- a/crates/foreign-chain-inspector/tests/bitcoin_inspector.rs +++ b/crates/foreign-chain-inspector/tests/bitcoin_inspector.rs @@ -8,14 +8,15 @@ use crate::common::{ use foreign_chain_inspector::{ BlockConfirmations, ForeignChainInspectionError, ForeignChainInspector, - NetworkFingerprintInspector, RpcAuthentication, + NetworkFingerprintInspector, bitcoin::{ BitcoinBlockHash, BitcoinExtractedValue, BitcoinTransactionHash, MAINNET_GENESIS_BLOCK_HASH, inspector::{BitcoinExtractor, BitcoinInspector}, }, - build_http_client, }; +use foreign_chain_rpc_factory::build_http_client; +use mpc_node_config::{AuthConfig, ForeignChainProviderConfig}; use assert_matches::assert_matches; use foreign_chain_rpc_interfaces::bitcoin::{ @@ -361,7 +362,11 @@ async fn inspector_extracts_block_hash_via_http_rpc_client() { }); }); - let client = build_http_client(server.url("/"), RpcAuthentication::KeyInUrl).unwrap(); + let client = build_http_client(&ForeignChainProviderConfig { + rpc_url: server.url("/"), + auth: AuthConfig::None, + }) + .unwrap(); let inspector = BitcoinInspector::new(client); // when @@ -391,7 +396,11 @@ async fn network_fingerprint__should_ask_the_provider_for_the_hash_at_height_zer })); }) .await; - let client = build_http_client(server.url("/"), RpcAuthentication::KeyInUrl).unwrap(); + let client = build_http_client(&ForeignChainProviderConfig { + rpc_url: server.url("/"), + auth: AuthConfig::None, + }) + .unwrap(); let inspector = BitcoinInspector::new(client); // When diff --git a/crates/foreign-chain-inspector/tests/bitcoin_rpc_manual.rs b/crates/foreign-chain-inspector/tests/bitcoin_rpc_manual.rs index 7561013b0a..8649d35f7d 100644 --- a/crates/foreign-chain-inspector/tests/bitcoin_rpc_manual.rs +++ b/crates/foreign-chain-inspector/tests/bitcoin_rpc_manual.rs @@ -1,11 +1,12 @@ use foreign_chain_inspector::{ - BlockConfirmations, ForeignChainInspector, NetworkFingerprintInspector, RpcAuthentication, + BlockConfirmations, ForeignChainInspector, NetworkFingerprintInspector, bitcoin::{ BitcoinBlockHash, BitcoinExtractedValue, BitcoinTransactionHash, MAINNET_GENESIS_BLOCK_HASH, inspector::{BitcoinExtractor, BitcoinInspector}, }, }; +use foreign_chain_rpc_factory::build_http_client; use jsonrpsee::{core::client::ClientT, http_client::HttpClient}; use rstest::rstest; use serde::Deserialize; @@ -35,10 +36,10 @@ async fn inspector_extracts_block_hash_against_live_rpc_provider( #[case] expected_block_hash: Option<&'static str>, ) { // given - let http_client = foreign_chain_inspector::build_http_client( - PUBLIC_NODE_URL.to_string(), - RpcAuthentication::KeyInUrl, - ) + let http_client = build_http_client(&mpc_node_config::ForeignChainProviderConfig { + rpc_url: PUBLIC_NODE_URL.to_string(), + auth: mpc_node_config::AuthConfig::None, + }) .unwrap(); let (transaction_id, expected_block_hash) = resolve_input(&http_client, tx_hash, expected_block_hash).await; @@ -127,10 +128,10 @@ const EXPECTED_NETWORK_FINGERPRINT: &str = MAINNET_GENESIS_BLOCK_HASH; #[ignore = "manual test to sanity check against live Bitcoin RPC provider"] async fn network_fingerprint_matches_the_shipped_config_value_against_live_rpc_provider() { // given - let http_client = foreign_chain_inspector::build_http_client( - PUBLIC_NODE_URL.to_string(), - RpcAuthentication::KeyInUrl, - ) + let http_client = build_http_client(&mpc_node_config::ForeignChainProviderConfig { + rpc_url: PUBLIC_NODE_URL.to_string(), + auth: mpc_node_config::AuthConfig::None, + }) .unwrap(); let inspector = BitcoinInspector::new(http_client); diff --git a/crates/foreign-chain-inspector/tests/bnb_rpc_manual.rs b/crates/foreign-chain-inspector/tests/bnb_rpc_manual.rs index accb8d7658..cf21c82696 100644 --- a/crates/foreign-chain-inspector/tests/bnb_rpc_manual.rs +++ b/crates/foreign-chain-inspector/tests/bnb_rpc_manual.rs @@ -1,11 +1,12 @@ use assert_matches::assert_matches; use foreign_chain_inspector::{ - EthereumFinality, ForeignChainInspector, NetworkFingerprintInspector, RpcAuthentication, + EthereumFinality, ForeignChainInspector, NetworkFingerprintInspector, bnb::{ BnbBlockHash, BnbTransactionHash, MAINNET_CHAIN_ID, inspector::{BnbExtractedValue, BnbExtractor, BnbInspector}, }, }; +use foreign_chain_rpc_factory::build_http_client; const BNB_RPC_URL: &str = "https://bsc-rpc.publicnode.com"; @@ -26,10 +27,10 @@ async fn inspector_extracts_block_hash_against_live_rpc_provider() { .parse() .unwrap(); - let http_client = foreign_chain_inspector::build_http_client( - BNB_RPC_URL.to_string(), - RpcAuthentication::KeyInUrl, - ) + let http_client = build_http_client(&mpc_node_config::ForeignChainProviderConfig { + rpc_url: BNB_RPC_URL.to_string(), + auth: mpc_node_config::AuthConfig::None, + }) .unwrap(); let inspector = BnbInspector::new(http_client); @@ -66,10 +67,10 @@ const EXPECTED_NETWORK_FINGERPRINT: u64 = MAINNET_CHAIN_ID; #[ignore = "manual test to sanity check against live BNB RPC provider"] async fn network_fingerprint_matches_the_shipped_config_value_against_live_rpc_provider() { // given - let http_client = foreign_chain_inspector::build_http_client( - BNB_RPC_URL.to_string(), - RpcAuthentication::KeyInUrl, - ) + let http_client = build_http_client(&mpc_node_config::ForeignChainProviderConfig { + rpc_url: BNB_RPC_URL.to_string(), + auth: mpc_node_config::AuthConfig::None, + }) .unwrap(); let inspector = BnbInspector::new(http_client); diff --git a/crates/foreign-chain-inspector/tests/ethereum_rpc_manual.rs b/crates/foreign-chain-inspector/tests/ethereum_rpc_manual.rs index 4174cbf65d..b615ff3bd9 100644 --- a/crates/foreign-chain-inspector/tests/ethereum_rpc_manual.rs +++ b/crates/foreign-chain-inspector/tests/ethereum_rpc_manual.rs @@ -1,11 +1,12 @@ use assert_matches::assert_matches; use foreign_chain_inspector::{ - EthereumFinality, ForeignChainInspector, NetworkFingerprintInspector, RpcAuthentication, + EthereumFinality, ForeignChainInspector, NetworkFingerprintInspector, ethereum::{ EthereumBlockHash, EthereumTransactionHash, MAINNET_CHAIN_ID, inspector::{EthereumExtractedValue, EthereumExtractor, EthereumInspector}, }, }; +use foreign_chain_rpc_factory::build_http_client; const ETHEREUM_RPC_URL: &str = "https://ethereum-rpc.publicnode.com"; @@ -26,10 +27,10 @@ async fn inspector_extracts_block_hash_against_live_rpc_provider() { .parse() .unwrap(); - let http_client = foreign_chain_inspector::build_http_client( - ETHEREUM_RPC_URL.to_string(), - RpcAuthentication::KeyInUrl, - ) + let http_client = build_http_client(&mpc_node_config::ForeignChainProviderConfig { + rpc_url: ETHEREUM_RPC_URL.to_string(), + auth: mpc_node_config::AuthConfig::None, + }) .unwrap(); let inspector = EthereumInspector::new(http_client); @@ -62,10 +63,10 @@ const EXPECTED_NETWORK_FINGERPRINT: u64 = MAINNET_CHAIN_ID; #[ignore = "manual test to sanity check against live Ethereum RPC provider"] async fn network_fingerprint_matches_the_shipped_config_value_against_live_rpc_provider() { // given - let http_client = foreign_chain_inspector::build_http_client( - ETHEREUM_RPC_URL.to_string(), - RpcAuthentication::KeyInUrl, - ) + let http_client = build_http_client(&mpc_node_config::ForeignChainProviderConfig { + rpc_url: ETHEREUM_RPC_URL.to_string(), + auth: mpc_node_config::AuthConfig::None, + }) .unwrap(); let inspector = EthereumInspector::new(http_client); diff --git a/crates/foreign-chain-inspector/tests/evm_inspector.rs b/crates/foreign-chain-inspector/tests/evm_inspector.rs index f082f37b5d..dfcc7a9adc 100644 --- a/crates/foreign-chain-inspector/tests/evm_inspector.rs +++ b/crates/foreign-chain-inspector/tests/evm_inspector.rs @@ -8,11 +8,12 @@ use crate::common::{ use foreign_chain_inspector::{ EthereumFinality, ForeignChainInspectionError, ForeignChainInspector, - NetworkFingerprintInspector, RpcAuthentication, + NetworkFingerprintInspector, base::inspector::Base, - build_http_client, evm::inspector::{EvmChain, EvmExtractedValue, EvmExtractor, EvmInspector}, }; +use foreign_chain_rpc_factory::build_http_client; +use mpc_node_config::{AuthConfig, ForeignChainProviderConfig}; use assert_matches::assert_matches; use foreign_chain_rpc_interfaces::evm::{ @@ -383,8 +384,11 @@ macro_rules! evm_inspector_tests { }); }); - let client = - build_http_client(server.url("/"), RpcAuthentication::KeyInUrl).unwrap(); + let client = build_http_client(&ForeignChainProviderConfig { + rpc_url: server.url("/"), + auth: AuthConfig::None, + }) + .unwrap(); let inspector = Inspector::new(client); // when @@ -764,7 +768,11 @@ async fn network_fingerprint__should_ask_the_provider_for_its_chain_id() { })); }) .await; - let client = build_http_client(server.url("/"), RpcAuthentication::KeyInUrl).unwrap(); + let client = build_http_client(&ForeignChainProviderConfig { + rpc_url: server.url("/"), + auth: AuthConfig::None, + }) + .unwrap(); let inspector = EvmInspector::<_, Base>::new(client); // When diff --git a/crates/foreign-chain-inspector/tests/hyperevm_rpc_manual.rs b/crates/foreign-chain-inspector/tests/hyperevm_rpc_manual.rs index 329c1bf276..32cedf8475 100644 --- a/crates/foreign-chain-inspector/tests/hyperevm_rpc_manual.rs +++ b/crates/foreign-chain-inspector/tests/hyperevm_rpc_manual.rs @@ -1,11 +1,12 @@ use assert_matches::assert_matches; use foreign_chain_inspector::{ - EthereumFinality, ForeignChainInspector, NetworkFingerprintInspector, RpcAuthentication, + EthereumFinality, ForeignChainInspector, NetworkFingerprintInspector, hyperevm::{ HyperEvmBlockHash, HyperEvmTransactionHash, MAINNET_CHAIN_ID, inspector::{HyperEvmExtractedValue, HyperEvmExtractor, HyperEvmInspector}, }, }; +use foreign_chain_rpc_factory::build_http_client; const HYPEREVM_RPC_URL: &str = "https://rpc.hyperliquid.xyz/evm"; @@ -26,10 +27,10 @@ async fn inspector_extracts_block_hash_against_live_rpc_provider() { .parse() .unwrap(); - let http_client = foreign_chain_inspector::build_http_client( - HYPEREVM_RPC_URL.to_string(), - RpcAuthentication::KeyInUrl, - ) + let http_client = build_http_client(&mpc_node_config::ForeignChainProviderConfig { + rpc_url: HYPEREVM_RPC_URL.to_string(), + auth: mpc_node_config::AuthConfig::None, + }) .unwrap(); let inspector = HyperEvmInspector::new(http_client); @@ -66,10 +67,10 @@ const EXPECTED_NETWORK_FINGERPRINT: u64 = MAINNET_CHAIN_ID; #[ignore = "manual test to sanity check against live HyperEVM RPC provider"] async fn network_fingerprint_matches_the_shipped_config_value_against_live_rpc_provider() { // given - let http_client = foreign_chain_inspector::build_http_client( - HYPEREVM_RPC_URL.to_string(), - RpcAuthentication::KeyInUrl, - ) + let http_client = build_http_client(&mpc_node_config::ForeignChainProviderConfig { + rpc_url: HYPEREVM_RPC_URL.to_string(), + auth: mpc_node_config::AuthConfig::None, + }) .unwrap(); let inspector = HyperEvmInspector::new(http_client); diff --git a/crates/foreign-chain-inspector/tests/polygon_rpc_manual.rs b/crates/foreign-chain-inspector/tests/polygon_rpc_manual.rs index 6654e83225..454737e099 100644 --- a/crates/foreign-chain-inspector/tests/polygon_rpc_manual.rs +++ b/crates/foreign-chain-inspector/tests/polygon_rpc_manual.rs @@ -1,11 +1,12 @@ use assert_matches::assert_matches; use foreign_chain_inspector::{ - EthereumFinality, ForeignChainInspector, NetworkFingerprintInspector, RpcAuthentication, + EthereumFinality, ForeignChainInspector, NetworkFingerprintInspector, polygon::{ MAINNET_CHAIN_ID, PolygonBlockHash, PolygonTransactionHash, inspector::{PolygonExtractedValue, PolygonExtractor, PolygonInspector}, }, }; +use foreign_chain_rpc_factory::build_http_client; const POLYGON_RPC_URL: &str = "https://polygon.drpc.org"; @@ -26,10 +27,10 @@ async fn inspector_extracts_block_hash_against_live_rpc_provider() { .parse() .unwrap(); - let http_client = foreign_chain_inspector::build_http_client( - POLYGON_RPC_URL.to_string(), - RpcAuthentication::KeyInUrl, - ) + let http_client = build_http_client(&mpc_node_config::ForeignChainProviderConfig { + rpc_url: POLYGON_RPC_URL.to_string(), + auth: mpc_node_config::AuthConfig::None, + }) .unwrap(); let inspector = PolygonInspector::new(http_client); @@ -66,10 +67,10 @@ const EXPECTED_NETWORK_FINGERPRINT: u64 = MAINNET_CHAIN_ID; #[ignore = "manual test to sanity check against live Polygon RPC provider"] async fn network_fingerprint_matches_the_shipped_config_value_against_live_rpc_provider() { // given - let http_client = foreign_chain_inspector::build_http_client( - POLYGON_RPC_URL.to_string(), - RpcAuthentication::KeyInUrl, - ) + let http_client = build_http_client(&mpc_node_config::ForeignChainProviderConfig { + rpc_url: POLYGON_RPC_URL.to_string(), + auth: mpc_node_config::AuthConfig::None, + }) .unwrap(); let inspector = PolygonInspector::new(http_client); diff --git a/crates/foreign-chain-inspector/tests/starknet_inspector.rs b/crates/foreign-chain-inspector/tests/starknet_inspector.rs index 3c748a9baf..f12a6a1691 100644 --- a/crates/foreign-chain-inspector/tests/starknet_inspector.rs +++ b/crates/foreign-chain-inspector/tests/starknet_inspector.rs @@ -8,12 +8,13 @@ use crate::common::{ use foreign_chain_inspector::{ FanOut, ForeignChainInspectionError, ForeignChainInspector, NetworkFingerprintInspector, - RpcAuthentication, build_http_client, starknet::{ MAINNET_CHAIN_ID, StarknetBlockHash, StarknetExtractedValue, StarknetTransactionHash, inspector::{StarknetExtractor, StarknetFinality, StarknetInspector}, }, }; +use foreign_chain_rpc_factory::build_http_client; +use mpc_node_config::{AuthConfig, ForeignChainProviderConfig}; use assert_matches::assert_matches; use foreign_chain_rpc_interfaces::starknet::{ @@ -376,7 +377,11 @@ async fn extract__should_return_block_hash_via_http_rpc_client() { setup_starknet_rpc_mock(&server); let tx_id = StarknetTransactionHash::from([9; 32]); - let client = build_http_client(server.url("/"), RpcAuthentication::KeyInUrl).unwrap(); + let client = build_http_client(&ForeignChainProviderConfig { + rpc_url: server.url("/"), + auth: AuthConfig::None, + }) + .unwrap(); let inspector = StarknetInspector::new(client); // when @@ -487,7 +492,11 @@ async fn extract__should_return_event_log_for_specific_index_via_http_rpc_client setup_starknet_rpc_mock(&server); let tx_id = StarknetTransactionHash::from([9; 32]); - let client = build_http_client(server.url("/"), RpcAuthentication::KeyInUrl).unwrap(); + let client = build_http_client(&ForeignChainProviderConfig { + rpc_url: server.url("/"), + auth: AuthConfig::None, + }) + .unwrap(); let inspector = StarknetInspector::new(client); // when diff --git a/crates/foreign-chain-inspector/tests/starknet_rpc_manual.rs b/crates/foreign-chain-inspector/tests/starknet_rpc_manual.rs index c4e51e4b2a..3f495e401b 100644 --- a/crates/foreign-chain-inspector/tests/starknet_rpc_manual.rs +++ b/crates/foreign-chain-inspector/tests/starknet_rpc_manual.rs @@ -1,10 +1,11 @@ use foreign_chain_inspector::{ - ForeignChainInspector, RpcAuthentication, + ForeignChainInspector, starknet::{ MAINNET_CHAIN_ID, StarknetBlockHash, StarknetExtractedValue, StarknetTransactionHash, inspector::{StarknetExtractor, StarknetFinality, StarknetInspector}, }, }; +use foreign_chain_rpc_factory::build_http_client; use jsonrpsee::core::client::ClientT; use jsonrpsee::http_client::HttpClient; use rstest::rstest; @@ -36,10 +37,10 @@ async fn inspector_extracts_block_hash_against_live_rpc_provider( #[case] finality: StarknetFinality, ) { // given - let http_client = foreign_chain_inspector::build_http_client( - PUBLIC_NODE_URL.to_string(), - RpcAuthentication::KeyInUrl, - ) + let http_client = build_http_client(&mpc_node_config::ForeignChainProviderConfig { + rpc_url: PUBLIC_NODE_URL.to_string(), + auth: mpc_node_config::AuthConfig::None, + }) .unwrap(); let (transaction_id, expected_block_hash) = resolve_input(&http_client, tx_hash, expected_block_hash).await; @@ -116,10 +117,10 @@ const EXPECTED_NETWORK_FINGERPRINT: &str = MAINNET_CHAIN_ID; #[ignore = "manual test to sanity check against live Starknet RPC provider"] async fn network_fingerprint_matches_the_shipped_config_value_against_live_rpc_provider() { // given - let http_client = foreign_chain_inspector::build_http_client( - PUBLIC_NODE_URL.to_string(), - RpcAuthentication::KeyInUrl, - ) + let http_client = build_http_client(&mpc_node_config::ForeignChainProviderConfig { + rpc_url: PUBLIC_NODE_URL.to_string(), + auth: mpc_node_config::AuthConfig::None, + }) .unwrap(); let inspector = StarknetInspector::new(http_client); diff --git a/crates/foreign-chain-rpc-auth/Cargo.toml b/crates/foreign-chain-rpc-factory/Cargo.toml similarity index 67% rename from crates/foreign-chain-rpc-auth/Cargo.toml rename to crates/foreign-chain-rpc-factory/Cargo.toml index 2be3f5abbf..72fb10b1c4 100644 --- a/crates/foreign-chain-rpc-auth/Cargo.toml +++ b/crates/foreign-chain-rpc-factory/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "foreign-chain-rpc-auth" +name = "foreign-chain-rpc-factory" version.workspace = true edition.workspace = true license.workspace = true @@ -7,8 +7,11 @@ license.workspace = true [dependencies] anyhow = { workspace = true } foreign-chain-inspector = { workspace = true } +foreign-chain-rpc-interfaces = { workspace = true } http = { workspace = true } +jsonrpsee = { workspace = true } mpc-node-config = { workspace = true } +near-mpc-contract-interface = { workspace = true } url = { workspace = true } [dev-dependencies] diff --git a/crates/foreign-chain-rpc-factory/src/inspectors.rs b/crates/foreign-chain-rpc-factory/src/inspectors.rs new file mode 100644 index 0000000000..3c757e1d89 --- /dev/null +++ b/crates/foreign-chain-rpc-factory/src/inspectors.rs @@ -0,0 +1,79 @@ +use std::time::Duration; + +use foreign_chain_inspector::BuildInspectors; +use foreign_chain_inspector::aptos::inspector::AptosInspector; +use foreign_chain_inspector::bitcoin::inspector::BitcoinInspector; +use foreign_chain_inspector::evm::inspector::EvmInspector; +use foreign_chain_inspector::rpc_inspector::RpcInspector; +use foreign_chain_inspector::starknet::inspector::StarknetInspector; +use foreign_chain_inspector::sui::inspector::SuiInspector; +use foreign_chain_rpc_interfaces::aptos::ReqwestAptosClient; +use foreign_chain_rpc_interfaces::sui::GrpcSuiClient; +use mpc_node_config::ForeignChainProviderConfig; +use near_mpc_contract_interface::types::ForeignChain; + +use crate::{build_http_client, resolve_provider_auth}; + +#[derive(Clone, Copy)] +pub struct InspectorFactory; + +impl BuildInspectors for InspectorFactory { + type Inspector = RpcInspector; + + fn build( + &self, + chain: ForeignChain, + provider: &ForeignChainProviderConfig, + timeout: Duration, + ) -> anyhow::Result> { + Ok(Some(match chain { + ForeignChain::Abstract => { + RpcInspector::Abstract(EvmInspector::new(build_http_client(provider)?)) + } + ForeignChain::Adi => RpcInspector::Adi(EvmInspector::new(build_http_client(provider)?)), + ForeignChain::Aptos => { + let (url, auth_header) = resolve_provider_auth(provider)?; + RpcInspector::Aptos(AptosInspector::new(ReqwestAptosClient::new( + url, + auth_header, + timeout, + ))) + } + ForeignChain::Arbitrum => { + RpcInspector::Arbitrum(EvmInspector::new(build_http_client(provider)?)) + } + ForeignChain::Avalanche => { + RpcInspector::Avalanche(EvmInspector::new(build_http_client(provider)?)) + } + ForeignChain::Base => { + RpcInspector::Base(EvmInspector::new(build_http_client(provider)?)) + } + ForeignChain::Bitcoin => { + RpcInspector::Bitcoin(BitcoinInspector::new(build_http_client(provider)?)) + } + ForeignChain::Bnb => RpcInspector::Bnb(EvmInspector::new(build_http_client(provider)?)), + ForeignChain::Ethereum => { + RpcInspector::Ethereum(EvmInspector::new(build_http_client(provider)?)) + } + ForeignChain::HyperEvm => { + RpcInspector::HyperEvm(EvmInspector::new(build_http_client(provider)?)) + } + ForeignChain::Polygon => { + RpcInspector::Polygon(EvmInspector::new(build_http_client(provider)?)) + } + ForeignChain::Starknet => { + RpcInspector::Starknet(StarknetInspector::new(build_http_client(provider)?)) + } + ForeignChain::Sui => { + let (url, auth_header) = resolve_provider_auth(provider)?; + RpcInspector::Sui(SuiInspector::new( + GrpcSuiClient::new(url, auth_header, timeout) + .map_err(|e| anyhow::anyhow!("failed to build the Sui gRPC client: {e}"))?, + )) + } + // `ForeignChain` is `non_exhaustive`, so the chains left without an inspector cannot + // be listed here. + _ => return Ok(None), + })) + } +} diff --git a/crates/foreign-chain-rpc-auth/src/lib.rs b/crates/foreign-chain-rpc-factory/src/lib.rs similarity index 77% rename from crates/foreign-chain-rpc-auth/src/lib.rs rename to crates/foreign-chain-rpc-factory/src/lib.rs index 5d1549c3ed..04ef9382b1 100644 --- a/crates/foreign-chain-rpc-auth/src/lib.rs +++ b/crates/foreign-chain-rpc-factory/src/lib.rs @@ -1,15 +1,26 @@ use anyhow::Context; -use foreign_chain_inspector::RpcAuthentication; -use http::HeaderValue; -use mpc_node_config::AuthConfig; - -/// Convert an [`AuthConfig`] into a [`foreign_chain_inspector::RpcAuthentication`]. -/// -/// Shared by the MPC node and the foreign-chain config tester so both exercise the -/// exact same URL/auth handling. It lives in its own crate (rather than the -/// lightweight `mpc-node-config`) to keep `foreign-chain-inspector` out of the -/// config crate's dependency tree. -pub fn auth_config_to_rpc_auth( +use http::{HeaderMap, HeaderName, HeaderValue}; +use jsonrpsee::http_client::{HttpClient, HttpClientBuilder}; +use mpc_node_config::{AuthConfig, ForeignChainProviderConfig}; + +pub mod inspectors; + +#[derive(Debug, Clone)] +pub(crate) enum RpcAuthentication { + /// The key is in the URL (e.g., Alchemy, QuickNode). + /// Example: `https://eth-mainnet.alchemyapi.io/v2/your-api-key` + KeyInUrl, + /// Custom header for providers like NOWNodes or GetBlock. + /// Example: key="x-api-key", value="your-secret-token" + CustomHeader { + header_name: HeaderName, + header_value: HeaderValue, + }, +} + +/// Convert an [`AuthConfig`] into a [`RpcAuthentication`], substituting `Path`/`Query` tokens +/// into `rpc_url` as it goes. +fn auth_config_to_rpc_auth( auth: AuthConfig, rpc_url: &mut String, ) -> anyhow::Result { @@ -25,7 +36,8 @@ pub fn auth_config_to_rpc_auth( Some(scheme) => format!("{scheme} {token_value}"), None => token_value, }; - let mut header_value = HeaderValue::from_str(&header_value_str)?; + let mut header_value = HeaderValue::from_str(&header_value_str) + .map_err(|e| anyhow::anyhow!("invalid header value: {e}"))?; // Redacts the token from `Debug` output and excludes it from HPACK // dynamic-table indexing on h2 connections. header_value.set_sensitive(true); @@ -52,6 +64,40 @@ pub fn auth_config_to_rpc_auth( } } +pub fn resolve_provider_auth( + provider: &ForeignChainProviderConfig, +) -> anyhow::Result<(String, Option<(HeaderName, HeaderValue)>)> { + let mut url = provider.rpc_url.clone(); + let auth = auth_config_to_rpc_auth(provider.auth.clone(), &mut url)?; + Ok(( + url, + match auth { + RpcAuthentication::KeyInUrl => None, + RpcAuthentication::CustomHeader { + header_name, + header_value, + } => Some((header_name, header_value)), + }, + )) +} + +/// Builds an HTTP client for a configured provider, resolving its URL and authentication. +/// This client can be used to construct a [`foreign_chain_inspector::ForeignChainInspector`]. +pub fn build_http_client(provider: &ForeignChainProviderConfig) -> anyhow::Result { + let (url, auth) = resolve_provider_auth(provider)?; + let mut headers = HeaderMap::new(); + + if let Some((header_name, header_value)) = auth { + headers.insert(header_name, header_value); + } + + let client = HttpClientBuilder::default() + .set_headers(headers) + .build(&url)?; + + Ok(client) +} + #[cfg(test)] #[expect(non_snake_case)] mod tests { @@ -96,7 +142,7 @@ mod tests { fn auth_config_to_rpc_auth__header_auth_leaves_url_unchanged() { // Given let auth = AuthConfig::Header { - name: http::HeaderName::from_static("authorization"), + name: HeaderName::from_static("authorization"), scheme: Some("Bearer".to_string()), token: TokenConfig::Val { val: "secret".to_string(), @@ -116,7 +162,7 @@ mod tests { fn auth_config_to_rpc_auth__header_auth_with_scheme_prepends_scheme() { // Given let auth = AuthConfig::Header { - name: http::HeaderName::from_static("authorization"), + name: HeaderName::from_static("authorization"), scheme: Some("Bearer".to_string()), token: TokenConfig::Val { val: "secret".to_string(), @@ -136,10 +182,9 @@ mod tests { #[test] fn auth_config_to_rpc_auth__header_auth_without_scheme_uses_raw_token() { - // Given: providers like Tatum (`x-api-key`) and NowNodes (`api-key`) use - // the raw token as the header value, with no scheme prefix. + // Given let auth = AuthConfig::Header { - name: http::HeaderName::from_static("x-api-key"), + name: HeaderName::from_static("x-api-key"), scheme: None, token: TokenConfig::Val { val: "raw-token-value".to_string(), @@ -161,7 +206,7 @@ mod tests { fn auth_config_to_rpc_auth__should_mark_header_value_sensitive() { // Given let auth = AuthConfig::Header { - name: http::HeaderName::from_static("authorization"), + name: HeaderName::from_static("authorization"), scheme: Some("Bearer".to_string()), token: TokenConfig::Val { val: "secret".to_string(), @@ -182,7 +227,7 @@ mod tests { #[test] fn auth_config_to_rpc_auth__query_auth_appends_param_to_url_without_query() { - // Given: providers like Helius use `?api-key=` on a URL with no query. + // Given let auth = AuthConfig::Query { name: "api-key".to_string(), token: TokenConfig::Val { @@ -201,8 +246,7 @@ mod tests { #[test] fn auth_config_to_rpc_auth__query_auth_appends_param_to_url_with_existing_query() { - // Given: dRPC's `?network=ethereum&dkey=` form — the URL already has - // query parameters and the auth key must be appended with `&`. + // Given let auth = AuthConfig::Query { name: "dkey".to_string(), token: TokenConfig::Val { @@ -224,7 +268,7 @@ mod tests { #[test] fn auth_config_to_rpc_auth__query_auth_url_encodes_special_characters() { - // Given: tokens may contain characters that must be URL-encoded. + // Given let auth = AuthConfig::Query { name: "api-key".to_string(), token: TokenConfig::Val { diff --git a/crates/foreign-chain-rpc-interfaces/src/aptos.rs b/crates/foreign-chain-rpc-interfaces/src/aptos.rs index 8bd1a0dab8..200187bb03 100644 --- a/crates/foreign-chain-rpc-interfaces/src/aptos.rs +++ b/crates/foreign-chain-rpc-interfaces/src/aptos.rs @@ -219,7 +219,7 @@ mod tests { #[test] fn build_request_url__preserves_query_auth_param() { - // Given a base carrying a query-auth param (as produced by `auth_config_to_rpc_auth`). + // Given a base carrying a query-auth param. let base = Url::parse("https://host/v1?api_key=secret").unwrap(); // When diff --git a/crates/node-config/src/foreign_chains.rs b/crates/node-config/src/foreign_chains.rs index c15e66d4c3..d8b9c6db20 100644 --- a/crates/node-config/src/foreign_chains.rs +++ b/crates/node-config/src/foreign_chains.rs @@ -60,6 +60,12 @@ pub struct ForeignChainConfig { pub providers: NonEmptyBTreeMap, } +impl ForeignChainConfig { + pub fn timeout_duration(&self) -> std::time::Duration { + std::time::Duration::from_secs(self.timeout_sec.get()) + } +} + #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct ForeignChainProviderConfig { pub rpc_url: String, diff --git a/crates/node/Cargo.toml b/crates/node/Cargo.toml index b622feadb8..2b0d9208af 100644 --- a/crates/node/Cargo.toml +++ b/crates/node/Cargo.toml @@ -25,7 +25,7 @@ ed25519-dalek = { workspace = true } flume = { workspace = true } foreign-chain-health-check = { workspace = true } foreign-chain-inspector = { workspace = true } -foreign-chain-rpc-auth = { workspace = true } +foreign-chain-rpc-factory = { workspace = true } foreign-chain-rpc-interfaces = { workspace = true } futures = { workspace = true } gcloud-sdk = { workspace = true } diff --git a/crates/node/src/foreign_chain_probe.rs b/crates/node/src/foreign_chain_probe.rs index fc3b57d332..b6c6789e94 100644 --- a/crates/node/src/foreign_chain_probe.rs +++ b/crates/node/src/foreign_chain_probe.rs @@ -7,6 +7,7 @@ use std::future::Future; use foreign_chain_health_check::probe::{ ProbeReport, ProviderHealth, ProviderStatus, probe_all_providers, }; +use foreign_chain_rpc_factory::inspectors::InspectorFactory; use mpc_node_config::ForeignChainsConfig; use near_mpc_contract_interface::types as dtos; use tracing::{info, warn}; @@ -22,7 +23,11 @@ pub async fn run_periodic_probe(foreign_chains: ForeignChainsConfig, ticker: imp return; } - probe_periodically(|| probe_all_providers(&foreign_chains), ticker).await; + probe_periodically( + || probe_all_providers(&foreign_chains, &InspectorFactory), + ticker, + ) + .await; } async fn probe_periodically>( diff --git a/crates/node/src/providers/verify_foreign_tx.rs b/crates/node/src/providers/verify_foreign_tx.rs index 412febefd6..988988f80d 100644 --- a/crates/node/src/providers/verify_foreign_tx.rs +++ b/crates/node/src/providers/verify_foreign_tx.rs @@ -7,6 +7,7 @@ use crate::providers::EcdsaSignatureProvider; use crate::storage::VerifyForeignTransactionRequestStorage; use crate::types::VerifyForeignTxId; use borsh::{BorshDeserialize, BorshSerialize}; +use foreign_chain_inspector::FanOut; use foreign_chain_inspector::abstract_chain::inspector::AbstractInspector; use foreign_chain_inspector::adi::inspector::AdiInspector; use foreign_chain_inspector::aptos::inspector::AptosInspector; @@ -21,11 +22,12 @@ use foreign_chain_inspector::hyperevm::inspector::HyperEvmInspector; use foreign_chain_inspector::polygon::inspector::PolygonInspector; use foreign_chain_inspector::starknet::inspector::StarknetInspector; use foreign_chain_inspector::sui::inspector::SuiInspector; -use foreign_chain_inspector::{FanOut, RpcAuthentication}; -use foreign_chain_rpc_auth::auth_config_to_rpc_auth; +use foreign_chain_rpc_factory::{build_http_client, resolve_provider_auth}; use foreign_chain_rpc_interfaces::aptos::ReqwestAptosClient; use foreign_chain_rpc_interfaces::sui::GrpcSuiClient; -use mpc_node_config::{ConfigFile, ForeignChainConfig, ForeignChainsConfig}; +use mpc_node_config::{ + ConfigFile, ForeignChainConfig, ForeignChainProviderConfig, ForeignChainsConfig, +}; use mpc_primitives::ReconstructionThreshold; use near_mpc_contract_interface::types::ProviderId; use std::sync::Arc; @@ -57,18 +59,14 @@ impl ForeignChainInspectors { fn build(config: &ForeignChainsConfig) -> anyhow::Result { fn build_fanout( chain_config: Option<&ForeignChainConfig>, - new_inspector: impl Fn(String, RpcAuthentication, Duration) -> anyhow::Result, + new_inspector: impl Fn(&ForeignChainProviderConfig, Duration) -> anyhow::Result, ) -> anyhow::Result>> { let Some(c) = chain_config else { return Ok(None); }; let timeout = Duration::from_secs(c.timeout_sec.get()); let inspectors = c.providers.try_map_to_vec(|name, p| { - // `Path`/`Query` auth is substituted into `url`; `Header` auth is returned - // as `RpcAuthentication::CustomHeader` for the client to install. - let mut url = p.rpc_url.clone(); - let rpc_auth = auth_config_to_rpc_auth(p.auth.clone(), &mut url)?; - let inspector = new_inspector(url, rpc_auth, timeout)?; + let inspector = new_inspector(p, timeout)?; anyhow::Ok((ProviderId(name.as_str().to_owned()), inspector)) })?; Ok(Some(FanOut::new(inspectors))) @@ -79,42 +77,28 @@ impl ForeignChainInspectors { /// deadline in the signing flow, as they did before this adapter existed. fn with_http_client( new_inspector: impl Fn(HttpClient) -> I, - ) -> impl Fn(String, RpcAuthentication, Duration) -> anyhow::Result { - move |url, rpc_auth, _timeout| { - let client = foreign_chain_inspector::build_http_client(url, rpc_auth)?; + ) -> impl Fn(&ForeignChainProviderConfig, Duration) -> anyhow::Result { + move |provider, _timeout| { + let client = build_http_client(provider)?; Ok(new_inspector(client)) } } fn new_sui_inspector( - url: String, - rpc_auth: RpcAuthentication, + provider: &ForeignChainProviderConfig, timeout: Duration, ) -> anyhow::Result> { - let auth_header = match rpc_auth { - RpcAuthentication::KeyInUrl => None, - RpcAuthentication::CustomHeader { - header_name, - header_value, - } => Some((header_name, header_value)), - }; + let (url, auth_header) = resolve_provider_auth(provider)?; let client = GrpcSuiClient::new(url, auth_header, timeout) .map_err(|e| anyhow::anyhow!("failed to build the Sui gRPC client: {e}"))?; Ok(SuiInspector::new(client)) } fn new_aptos_inspector( - url: String, - rpc_auth: RpcAuthentication, + provider: &ForeignChainProviderConfig, timeout: Duration, ) -> anyhow::Result> { - let auth_header = match rpc_auth { - RpcAuthentication::KeyInUrl => None, - RpcAuthentication::CustomHeader { - header_name, - header_value, - } => Some((header_name, header_value)), - }; + let (url, auth_header) = resolve_provider_auth(provider)?; Ok(AptosInspector::new(ReqwestAptosClient::new( url, auth_header,