From f2557e6c3c43eab5a52de9acb951e07f921073e8 Mon Sep 17 00:00:00 2001 From: Haiyue Chen Date: Thu, 27 Aug 2026 19:45:21 +0200 Subject: [PATCH 01/14] feat: inject the clients a chain's inspector is built over BuildRpcClients is the seam: choosing it chooses the transport, and by extension every inspector built over it, so a caller can put the whole set on clients it controls. RpcClients reaches the real providers. Nothing uses it yet. The probe and transaction verification each build their own inspectors today, and move across separately. --- Cargo.lock | 2 + crates/foreign-chain-rpc-auth/Cargo.toml | 2 + .../foreign-chain-rpc-auth/src/inspectors.rs | 142 ++++++++++++++++++ crates/foreign-chain-rpc-auth/src/lib.rs | 2 + 4 files changed, 148 insertions(+) create mode 100644 crates/foreign-chain-rpc-auth/src/inspectors.rs diff --git a/Cargo.lock b/Cargo.lock index 017ccc64de..28edca07c7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4019,7 +4019,9 @@ dependencies = [ "anyhow", "assert_matches", "foreign-chain-inspector", + "foreign-chain-rpc-interfaces", "http", + "jsonrpsee", "mpc-node-config", "url", ] diff --git a/crates/foreign-chain-rpc-auth/Cargo.toml b/crates/foreign-chain-rpc-auth/Cargo.toml index 2be3f5abbf..22f0999d27 100644 --- a/crates/foreign-chain-rpc-auth/Cargo.toml +++ b/crates/foreign-chain-rpc-auth/Cargo.toml @@ -7,7 +7,9 @@ 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 } url = { workspace = true } diff --git a/crates/foreign-chain-rpc-auth/src/inspectors.rs b/crates/foreign-chain-rpc-auth/src/inspectors.rs new file mode 100644 index 0000000000..579f35241c --- /dev/null +++ b/crates/foreign-chain-rpc-auth/src/inspectors.rs @@ -0,0 +1,142 @@ +//! Building a chain's inspector for one of its providers. +//! +//! What a caller injects is [`BuildRpcClients`]. Choosing it chooses the transport, and by +//! extension every inspector built over it, so a caller can put the whole set on clients it +//! controls without the code that uses the inspectors knowing. + +use std::time::Duration; + +use foreign_chain_inspector::aptos::inspector::AptosInspector; +use foreign_chain_inspector::bitcoin::inspector::BitcoinInspector; +use foreign_chain_inspector::evm::inspector::{EvmChain, EvmInspector}; +use foreign_chain_inspector::starknet::inspector::StarknetInspector; +use foreign_chain_inspector::sui::inspector::SuiInspector; +use foreign_chain_inspector::{RpcAuthentication, build_http_client}; +use foreign_chain_rpc_interfaces::aptos::{AptosRpcClient, ReqwestAptosClient}; +use foreign_chain_rpc_interfaces::sui::{GrpcSuiClient, SuiRpcClient}; +use jsonrpsee::core::client::ClientT; +use jsonrpsee::http_client::HttpClient; +use mpc_node_config::ForeignChainProviderConfig; + +use crate::auth_config_to_rpc_auth; + +/// The transports inspectors are built over. [`RpcClients`] reaches the real providers; a caller +/// that needs inspectors on something else implements this instead. +pub trait BuildRpcClients { + type JsonRpc: ClientT + Clone + Send + Sync + 'static; + type Aptos: AptosRpcClient + Clone + Send + Sync + 'static; + type Sui: SuiRpcClient + Clone + Send + Sync + 'static; + + fn json_rpc(&self, provider: &ForeignChainProviderConfig) -> anyhow::Result; + + fn aptos( + &self, + provider: &ForeignChainProviderConfig, + timeout: Duration, + ) -> anyhow::Result; + + fn sui( + &self, + provider: &ForeignChainProviderConfig, + timeout: Duration, + ) -> anyhow::Result; +} + +/// Resolves the provider's credentials and reaches it over the network. +#[derive(Clone, Copy)] +pub struct RpcClients; + +impl RpcClients { + /// Applies the provider's credentials, leaving them in the URL or in a header as its auth + /// config asks. + fn authenticate( + provider: &ForeignChainProviderConfig, + ) -> anyhow::Result<(String, RpcAuthentication)> { + let mut url = provider.rpc_url.clone(); + let auth = auth_config_to_rpc_auth(provider.auth.clone(), &mut url)?; + Ok((url, auth)) + } + + /// The gRPC and REST clients carry credentials in request metadata rather than the URL. + fn auth_header(auth: RpcAuthentication) -> Option<(http::HeaderName, http::HeaderValue)> { + match auth { + RpcAuthentication::KeyInUrl => None, + RpcAuthentication::CustomHeader { + header_name, + header_value, + } => Some((header_name, header_value)), + } + } +} + +impl BuildRpcClients for RpcClients { + type Aptos = ReqwestAptosClient; + type JsonRpc = HttpClient; + type Sui = GrpcSuiClient; + + fn json_rpc(&self, provider: &ForeignChainProviderConfig) -> anyhow::Result { + let (url, auth) = Self::authenticate(provider)?; + Ok(build_http_client(url, auth)?) + } + + fn aptos( + &self, + provider: &ForeignChainProviderConfig, + timeout: Duration, + ) -> anyhow::Result { + let (url, auth) = Self::authenticate(provider)?; + Ok(ReqwestAptosClient::new( + url, + Self::auth_header(auth), + timeout, + )) + } + + fn sui( + &self, + provider: &ForeignChainProviderConfig, + timeout: Duration, + ) -> anyhow::Result { + let (url, auth) = Self::authenticate(provider)?; + GrpcSuiClient::new(url, Self::auth_header(auth), timeout) + .map_err(|e| anyhow::anyhow!("failed to build the Sui gRPC client: {e}")) + } +} + +/// The EVM chains differ only in their marker type, which the caller fixes. +pub fn evm_inspector( + clients: &Clients, + provider: &ForeignChainProviderConfig, +) -> anyhow::Result> { + Ok(EvmInspector::new(clients.json_rpc(provider)?)) +} + +pub fn starknet_inspector( + clients: &Clients, + provider: &ForeignChainProviderConfig, +) -> anyhow::Result> { + Ok(StarknetInspector::new(clients.json_rpc(provider)?)) +} + +pub fn bitcoin_inspector( + clients: &Clients, + provider: &ForeignChainProviderConfig, +) -> anyhow::Result> { + Ok(BitcoinInspector::new(clients.json_rpc(provider)?)) +} + +pub fn aptos_inspector( + clients: &Clients, + provider: &ForeignChainProviderConfig, + timeout: Duration, +) -> anyhow::Result> { + Ok(AptosInspector::new(clients.aptos(provider, timeout)?)) +} + +pub fn sui_inspector( + clients: &Clients, + provider: &ForeignChainProviderConfig, + timeout: Duration, +) -> anyhow::Result> { + Ok(SuiInspector::new(clients.sui(provider, timeout)?)) +} diff --git a/crates/foreign-chain-rpc-auth/src/lib.rs b/crates/foreign-chain-rpc-auth/src/lib.rs index 5d1549c3ed..97f2a70d66 100644 --- a/crates/foreign-chain-rpc-auth/src/lib.rs +++ b/crates/foreign-chain-rpc-auth/src/lib.rs @@ -1,3 +1,5 @@ +pub mod inspectors; + use anyhow::Context; use foreign_chain_inspector::RpcAuthentication; use http::HeaderValue; From 17025c179b0c09237fc37e10913880069aca3803 Mon Sep 17 00:00:00 2001 From: Haiyue Chen Date: Thu, 27 Aug 2026 20:01:06 +0200 Subject: [PATCH 02/14] refactor: split client construction from inspector construction Building the client that talks to a provider and building the inspector over that client are separate concerns, so they get a module each. The crate resolved credentials and nothing else, so it is renamed for what it builds now. --- Cargo.lock | 6 +- Cargo.toml | 4 +- crates/foreign-chain-health-check/Cargo.toml | 2 +- crates/foreign-chain-health-check/src/lib.rs | 2 +- .../Cargo.toml | 2 +- .../src/clients.rs} | 56 ++----------------- .../src/inspectors.rs | 51 +++++++++++++++++ .../src/lib.rs | 5 ++ crates/node/Cargo.toml | 2 +- .../node/src/providers/verify_foreign_tx.rs | 2 +- 10 files changed, 72 insertions(+), 60 deletions(-) rename crates/{foreign-chain-rpc-auth => foreign-chain-rpc-factory}/Cargo.toml (92%) rename crates/{foreign-chain-rpc-auth/src/inspectors.rs => foreign-chain-rpc-factory/src/clients.rs} (58%) create mode 100644 crates/foreign-chain-rpc-factory/src/inspectors.rs rename crates/{foreign-chain-rpc-auth => foreign-chain-rpc-factory}/src/lib.rs (97%) diff --git a/Cargo.lock b/Cargo.lock index 28edca07c7..368cb3d649 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", @@ -4013,7 +4013,7 @@ dependencies = [ ] [[package]] -name = "foreign-chain-rpc-auth" +name = "foreign-chain-rpc-factory" version = "3.14.0" dependencies = [ "anyhow", @@ -6330,7 +6330,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..be28166f7f 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 } diff --git a/crates/foreign-chain-health-check/src/lib.rs b/crates/foreign-chain-health-check/src/lib.rs index eb8e41e1df..c517c9bc02 100644 --- a/crates/foreign-chain-health-check/src/lib.rs +++ b/crates/foreign-chain-health-check/src/lib.rs @@ -26,7 +26,7 @@ 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::auth_config_to_rpc_auth; use foreign_chain_rpc_interfaces::sui::GrpcSuiClient; use http::{HeaderName, HeaderValue}; use mpc_node_config::foreign_chains::RpcProviderName; diff --git a/crates/foreign-chain-rpc-auth/Cargo.toml b/crates/foreign-chain-rpc-factory/Cargo.toml similarity index 92% rename from crates/foreign-chain-rpc-auth/Cargo.toml rename to crates/foreign-chain-rpc-factory/Cargo.toml index 22f0999d27..0a8f6e94b8 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 diff --git a/crates/foreign-chain-rpc-auth/src/inspectors.rs b/crates/foreign-chain-rpc-factory/src/clients.rs similarity index 58% rename from crates/foreign-chain-rpc-auth/src/inspectors.rs rename to crates/foreign-chain-rpc-factory/src/clients.rs index 579f35241c..05316b1f75 100644 --- a/crates/foreign-chain-rpc-auth/src/inspectors.rs +++ b/crates/foreign-chain-rpc-factory/src/clients.rs @@ -1,16 +1,7 @@ -//! Building a chain's inspector for one of its providers. -//! -//! What a caller injects is [`BuildRpcClients`]. Choosing it chooses the transport, and by -//! extension every inspector built over it, so a caller can put the whole set on clients it -//! controls without the code that uses the inspectors knowing. +//! Building the client that talks to one provider, with the provider's credentials applied. use std::time::Duration; -use foreign_chain_inspector::aptos::inspector::AptosInspector; -use foreign_chain_inspector::bitcoin::inspector::BitcoinInspector; -use foreign_chain_inspector::evm::inspector::{EvmChain, EvmInspector}; -use foreign_chain_inspector::starknet::inspector::StarknetInspector; -use foreign_chain_inspector::sui::inspector::SuiInspector; use foreign_chain_inspector::{RpcAuthentication, build_http_client}; use foreign_chain_rpc_interfaces::aptos::{AptosRpcClient, ReqwestAptosClient}; use foreign_chain_rpc_interfaces::sui::{GrpcSuiClient, SuiRpcClient}; @@ -20,8 +11,11 @@ use mpc_node_config::ForeignChainProviderConfig; use crate::auth_config_to_rpc_auth; -/// The transports inspectors are built over. [`RpcClients`] reaches the real providers; a caller -/// that needs inspectors on something else implements this instead. +/// The transports a chain is reached over. [`RpcClients`] reaches the real providers; a caller that +/// needs something else, a test most of all, implements this instead. +/// +/// Injecting it also settles which inspectors get built, since +/// [`crate::inspectors`] puts them on whatever this returns. pub trait BuildRpcClients { type JsonRpc: ClientT + Clone + Send + Sync + 'static; type Aptos: AptosRpcClient + Clone + Send + Sync + 'static; @@ -102,41 +96,3 @@ impl BuildRpcClients for RpcClients { .map_err(|e| anyhow::anyhow!("failed to build the Sui gRPC client: {e}")) } } - -/// The EVM chains differ only in their marker type, which the caller fixes. -pub fn evm_inspector( - clients: &Clients, - provider: &ForeignChainProviderConfig, -) -> anyhow::Result> { - Ok(EvmInspector::new(clients.json_rpc(provider)?)) -} - -pub fn starknet_inspector( - clients: &Clients, - provider: &ForeignChainProviderConfig, -) -> anyhow::Result> { - Ok(StarknetInspector::new(clients.json_rpc(provider)?)) -} - -pub fn bitcoin_inspector( - clients: &Clients, - provider: &ForeignChainProviderConfig, -) -> anyhow::Result> { - Ok(BitcoinInspector::new(clients.json_rpc(provider)?)) -} - -pub fn aptos_inspector( - clients: &Clients, - provider: &ForeignChainProviderConfig, - timeout: Duration, -) -> anyhow::Result> { - Ok(AptosInspector::new(clients.aptos(provider, timeout)?)) -} - -pub fn sui_inspector( - clients: &Clients, - provider: &ForeignChainProviderConfig, - timeout: Duration, -) -> anyhow::Result> { - Ok(SuiInspector::new(clients.sui(provider, timeout)?)) -} 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..98e9285869 --- /dev/null +++ b/crates/foreign-chain-rpc-factory/src/inspectors.rs @@ -0,0 +1,51 @@ +//! Building a chain's inspector for one of its providers, over whichever clients the caller +//! injects. + +use std::time::Duration; + +use foreign_chain_inspector::aptos::inspector::AptosInspector; +use foreign_chain_inspector::bitcoin::inspector::BitcoinInspector; +use foreign_chain_inspector::evm::inspector::{EvmChain, EvmInspector}; +use foreign_chain_inspector::starknet::inspector::StarknetInspector; +use foreign_chain_inspector::sui::inspector::SuiInspector; +use mpc_node_config::ForeignChainProviderConfig; + +use crate::clients::BuildRpcClients; + +/// The EVM chains differ only in their marker type, which the caller fixes. +pub fn evm_inspector( + clients: &Clients, + provider: &ForeignChainProviderConfig, +) -> anyhow::Result> { + Ok(EvmInspector::new(clients.json_rpc(provider)?)) +} + +pub fn starknet_inspector( + clients: &Clients, + provider: &ForeignChainProviderConfig, +) -> anyhow::Result> { + Ok(StarknetInspector::new(clients.json_rpc(provider)?)) +} + +pub fn bitcoin_inspector( + clients: &Clients, + provider: &ForeignChainProviderConfig, +) -> anyhow::Result> { + Ok(BitcoinInspector::new(clients.json_rpc(provider)?)) +} + +pub fn aptos_inspector( + clients: &Clients, + provider: &ForeignChainProviderConfig, + timeout: Duration, +) -> anyhow::Result> { + Ok(AptosInspector::new(clients.aptos(provider, timeout)?)) +} + +pub fn sui_inspector( + clients: &Clients, + provider: &ForeignChainProviderConfig, + timeout: Duration, +) -> anyhow::Result> { + Ok(SuiInspector::new(clients.sui(provider, timeout)?)) +} diff --git a/crates/foreign-chain-rpc-auth/src/lib.rs b/crates/foreign-chain-rpc-factory/src/lib.rs similarity index 97% rename from crates/foreign-chain-rpc-auth/src/lib.rs rename to crates/foreign-chain-rpc-factory/src/lib.rs index 97f2a70d66..7c419fa54f 100644 --- a/crates/foreign-chain-rpc-auth/src/lib.rs +++ b/crates/foreign-chain-rpc-factory/src/lib.rs @@ -1,3 +1,8 @@ +//! Building what talks to a foreign chain, from one provider's configuration: its credentials +//! resolved here, the client that carries them in [`clients`], and the inspector over that client +//! in [`inspectors`]. + +pub mod clients; pub mod inspectors; use anyhow::Context; 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/providers/verify_foreign_tx.rs b/crates/node/src/providers/verify_foreign_tx.rs index 412febefd6..6e8bc42ab5 100644 --- a/crates/node/src/providers/verify_foreign_tx.rs +++ b/crates/node/src/providers/verify_foreign_tx.rs @@ -22,7 +22,7 @@ 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::auth_config_to_rpc_auth; use foreign_chain_rpc_interfaces::aptos::ReqwestAptosClient; use foreign_chain_rpc_interfaces::sui::GrpcSuiClient; use mpc_node_config::{ConfigFile, ForeignChainConfig, ForeignChainsConfig}; From d25a1ac6fdbaa288b871b809ffcaf4b10f1a2c9b Mon Sep 17 00:00:00 2001 From: Haiyue Chen Date: Thu, 27 Aug 2026 20:11:15 +0200 Subject: [PATCH 03/14] feat: inject the inspectors, not just the clients under them Free functions can only be called. As trait methods they can be implemented, so a caller can answer for the inspectors themselves rather than only for the transport beneath them. A blanket implementation over BuildRpcClients keeps both levels open: choosing the clients settles the inspectors, and a caller that needs to supply its own implements BuildInspectors directly and builds no client at all. --- .../src/inspectors.rs | 135 +++++++++++++----- 1 file changed, 102 insertions(+), 33 deletions(-) diff --git a/crates/foreign-chain-rpc-factory/src/inspectors.rs b/crates/foreign-chain-rpc-factory/src/inspectors.rs index 98e9285869..4217fbbcd8 100644 --- a/crates/foreign-chain-rpc-factory/src/inspectors.rs +++ b/crates/foreign-chain-rpc-factory/src/inspectors.rs @@ -1,8 +1,13 @@ -//! Building a chain's inspector for one of its providers, over whichever clients the caller -//! injects. +//! Building a chain's inspector for one of its providers. +//! +//! Injecting [`BuildInspectors`] settles which inspectors a caller gets. Anything that builds +//! clients gets the real ones for free, through the blanket implementation over +//! [`BuildRpcClients`]; a caller that needs to answer for the inspectors themselves, a test most of +//! all, implements this instead and never builds a client at all. use std::time::Duration; +use foreign_chain_inspector::NetworkFingerprintInspector; use foreign_chain_inspector::aptos::inspector::AptosInspector; use foreign_chain_inspector::bitcoin::inspector::BitcoinInspector; use foreign_chain_inspector::evm::inspector::{EvmChain, EvmInspector}; @@ -12,40 +17,104 @@ use mpc_node_config::ForeignChainProviderConfig; use crate::clients::BuildRpcClients; -/// The EVM chains differ only in their marker type, which the caller fixes. -pub fn evm_inspector( - clients: &Clients, - provider: &ForeignChainProviderConfig, -) -> anyhow::Result> { - Ok(EvmInspector::new(clients.json_rpc(provider)?)) -} +/// One method per inspector shape rather than per chain: the EVM chains differ only in the marker +/// type the caller fixes. +/// +/// `timeout` is the provider's configured deadline. The chains reached over JSON-RPC take it in the +/// inspection deadline instead, so their implementations may ignore it. +pub trait BuildInspectors { + type Evm: NetworkFingerprintInspector + + Clone + + Send + + Sync + + 'static; + type Starknet: NetworkFingerprintInspector + Clone + Send + Sync + 'static; + type Bitcoin: NetworkFingerprintInspector + Clone + Send + Sync + 'static; + type Aptos: NetworkFingerprintInspector + Clone + Send + Sync + 'static; + type Sui: NetworkFingerprintInspector + Clone + Send + Sync + 'static; -pub fn starknet_inspector( - clients: &Clients, - provider: &ForeignChainProviderConfig, -) -> anyhow::Result> { - Ok(StarknetInspector::new(clients.json_rpc(provider)?)) -} + fn evm( + &self, + provider: &ForeignChainProviderConfig, + timeout: Duration, + ) -> anyhow::Result>; -pub fn bitcoin_inspector( - clients: &Clients, - provider: &ForeignChainProviderConfig, -) -> anyhow::Result> { - Ok(BitcoinInspector::new(clients.json_rpc(provider)?)) -} + fn starknet( + &self, + provider: &ForeignChainProviderConfig, + timeout: Duration, + ) -> anyhow::Result; + + fn bitcoin( + &self, + provider: &ForeignChainProviderConfig, + timeout: Duration, + ) -> anyhow::Result; + + fn aptos( + &self, + provider: &ForeignChainProviderConfig, + timeout: Duration, + ) -> anyhow::Result; -pub fn aptos_inspector( - clients: &Clients, - provider: &ForeignChainProviderConfig, - timeout: Duration, -) -> anyhow::Result> { - Ok(AptosInspector::new(clients.aptos(provider, timeout)?)) + fn sui( + &self, + provider: &ForeignChainProviderConfig, + timeout: Duration, + ) -> anyhow::Result; } -pub fn sui_inspector( - clients: &Clients, - provider: &ForeignChainProviderConfig, - timeout: Duration, -) -> anyhow::Result> { - Ok(SuiInspector::new(clients.sui(provider, timeout)?)) +/// Choosing the clients is enough to settle the inspectors: each is its chain's inspector over the +/// client that reaches it. +impl BuildInspectors for Clients { + type Aptos = AptosInspector; + type Bitcoin = BitcoinInspector; + type Evm = + EvmInspector; + type Starknet = StarknetInspector; + type Sui = SuiInspector; + + fn evm( + &self, + provider: &ForeignChainProviderConfig, + _timeout: Duration, + ) -> anyhow::Result> { + Ok(EvmInspector::new(self.json_rpc(provider)?)) + } + + fn starknet( + &self, + provider: &ForeignChainProviderConfig, + _timeout: Duration, + ) -> anyhow::Result { + Ok(StarknetInspector::new(self.json_rpc(provider)?)) + } + + fn bitcoin( + &self, + provider: &ForeignChainProviderConfig, + _timeout: Duration, + ) -> anyhow::Result { + Ok(BitcoinInspector::new(self.json_rpc(provider)?)) + } + + fn aptos( + &self, + provider: &ForeignChainProviderConfig, + timeout: Duration, + ) -> anyhow::Result { + Ok(AptosInspector::new(BuildRpcClients::aptos( + self, provider, timeout, + )?)) + } + + fn sui( + &self, + provider: &ForeignChainProviderConfig, + timeout: Duration, + ) -> anyhow::Result { + Ok(SuiInspector::new(BuildRpcClients::sui( + self, provider, timeout, + )?)) + } } From 87909f52848f6b4f9be6d360d8c3f505d87390a9 Mon Sep 17 00:00:00 2001 From: Haiyue Chen Date: Thu, 27 Aug 2026 20:28:55 +0200 Subject: [PATCH 04/14] feat: build one inspector type a caller can hold across chains A caller that spans chains, as the probe does, needs a single type to hold them behind. BuildInspectors::any answers with it, or with None where no inspector exists, and its match is the only list of covered chains. Reaching that needed canonical_fingerprint to take self: with no receiver there is nothing to dispatch on, so no erased inspector could implement the trait. Every implementation already ignored the distinction. --- Cargo.lock | 1 + .../foreign-chain-health-check/src/probe.rs | 4 +- .../src/aptos/inspector.rs | 6 +- .../src/bitcoin/inspector.rs | 2 +- .../src/evm/inspector.rs | 2 +- crates/foreign-chain-inspector/src/lib.rs | 3 +- .../src/rpc_inspector.rs | 82 +++++++++++++++++++ .../src/starknet/inspector.rs | 4 +- .../src/sui/inspector.rs | 4 +- crates/foreign-chain-rpc-factory/Cargo.toml | 1 + .../src/inspectors.rs | 60 ++++++++++++-- 11 files changed, 147 insertions(+), 22 deletions(-) create mode 100644 crates/foreign-chain-inspector/src/rpc_inspector.rs diff --git a/Cargo.lock b/Cargo.lock index 368cb3d649..78f56264ba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4023,6 +4023,7 @@ dependencies = [ "http", "jsonrpsee", "mpc-node-config", + "near-mpc-contract-interface", "url", ] diff --git a/crates/foreign-chain-health-check/src/probe.rs b/crates/foreign-chain-health-check/src/probe.rs index 79dcc8c17c..300ef9bb18 100644 --- a/crates/foreign-chain-health-check/src/probe.rs +++ b/crates/foreign-chain-health-check/src/probe.rs @@ -181,8 +181,6 @@ where 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() { @@ -200,6 +198,8 @@ where 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 expected = inspectors.first().1.canonical_fingerprint(expected); let fingerprints = FanOut::new(inspectors) .network_fingerprints(timeout_of(config), config.max_retries) 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..a91ecc9c18 100644 --- a/crates/foreign-chain-inspector/src/lib.rs +++ b/crates/foreign-chain-inspector/src/lib.rs @@ -28,6 +28,7 @@ pub mod ethereum; pub mod evm; pub mod hyperevm; pub mod polygon; +pub mod rpc_inspector; pub mod starknet; pub mod sui; @@ -86,7 +87,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. 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..d6a6daaa3d --- /dev/null +++ b/crates/foreign-chain-inspector/src/rpc_inspector.rs @@ -0,0 +1,82 @@ +//! Holding any chain's inspector behind one type. + +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::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::AptosRpcClient; +use foreign_chain_rpc_interfaces::sui::SuiRpcClient; +use jsonrpsee::core::client::ClientT; + +/// One provider's inspector, whichever chain it serves. Lets a caller that spans chains, as the +/// probe does, hold them behind a single type. +#[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 +where + JsonRpc: ClientT + Send + Sync, + Aptos: AptosRpcClient + Send + Sync, + Sui: SuiRpcClient + Send + Sync, +{ + 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-rpc-factory/Cargo.toml b/crates/foreign-chain-rpc-factory/Cargo.toml index 0a8f6e94b8..72fb10b1c4 100644 --- a/crates/foreign-chain-rpc-factory/Cargo.toml +++ b/crates/foreign-chain-rpc-factory/Cargo.toml @@ -11,6 +11,7 @@ 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 index 4217fbbcd8..62e1fdbd8c 100644 --- a/crates/foreign-chain-rpc-factory/src/inspectors.rs +++ b/crates/foreign-chain-rpc-factory/src/inspectors.rs @@ -11,27 +11,33 @@ use foreign_chain_inspector::NetworkFingerprintInspector; use foreign_chain_inspector::aptos::inspector::AptosInspector; use foreign_chain_inspector::bitcoin::inspector::BitcoinInspector; use foreign_chain_inspector::evm::inspector::{EvmChain, EvmInspector}; +use foreign_chain_inspector::rpc_inspector::RpcInspector; use foreign_chain_inspector::starknet::inspector::StarknetInspector; use foreign_chain_inspector::sui::inspector::SuiInspector; use mpc_node_config::ForeignChainProviderConfig; +use near_mpc_contract_interface::types::ForeignChain; use crate::clients::BuildRpcClients; +/// What every inspector this builds must satisfy: probe a provider, and survive being held and +/// shared for as long as the caller keeps it. +pub trait ChainInspector: NetworkFingerprintInspector + Clone + Send + Sync + 'static {} + +impl ChainInspector for T {} + /// One method per inspector shape rather than per chain: the EVM chains differ only in the marker /// type the caller fixes. /// /// `timeout` is the provider's configured deadline. The chains reached over JSON-RPC take it in the /// inspection deadline instead, so their implementations may ignore it. pub trait BuildInspectors { - type Evm: NetworkFingerprintInspector - + Clone - + Send - + Sync - + 'static; - type Starknet: NetworkFingerprintInspector + Clone + Send + Sync + 'static; - type Bitcoin: NetworkFingerprintInspector + Clone + Send + Sync + 'static; - type Aptos: NetworkFingerprintInspector + Clone + Send + Sync + 'static; - type Sui: NetworkFingerprintInspector + Clone + Send + Sync + 'static; + type Evm: ChainInspector; + type Starknet: ChainInspector; + type Bitcoin: ChainInspector; + type Aptos: ChainInspector; + type Sui: ChainInspector; + /// One type covering every chain, for a caller that holds inspectors of several at once. + type Any: ChainInspector; fn evm( &self, @@ -62,6 +68,14 @@ pub trait BuildInspectors { provider: &ForeignChainProviderConfig, timeout: Duration, ) -> anyhow::Result; + + /// The inspector for whichever chain `chain` is, or `None` when none exists to probe it. + fn any( + &self, + chain: ForeignChain, + provider: &ForeignChainProviderConfig, + timeout: Duration, + ) -> anyhow::Result>; } /// Choosing the clients is enough to settle the inspectors: each is its chain's inspector over the @@ -72,6 +86,7 @@ impl BuildInspectors for Clients { type Evm = EvmInspector; type Starknet = StarknetInspector; + type Any = RpcInspector; type Sui = SuiInspector; fn evm( @@ -117,4 +132,31 @@ impl BuildInspectors for Clients { self, provider, timeout, )?)) } + + fn any( + &self, + chain: ForeignChain, + provider: &ForeignChainProviderConfig, + timeout: Duration, + ) -> anyhow::Result> { + Ok(Some(match chain { + ForeignChain::Abstract => RpcInspector::Abstract(self.evm(provider, timeout)?), + ForeignChain::Adi => RpcInspector::Adi(self.evm(provider, timeout)?), + ForeignChain::Aptos => { + RpcInspector::Aptos(BuildInspectors::aptos(self, provider, timeout)?) + } + ForeignChain::Arbitrum => RpcInspector::Arbitrum(self.evm(provider, timeout)?), + ForeignChain::Avalanche => RpcInspector::Avalanche(self.evm(provider, timeout)?), + ForeignChain::Base => RpcInspector::Base(self.evm(provider, timeout)?), + ForeignChain::Bitcoin => RpcInspector::Bitcoin(self.bitcoin(provider, timeout)?), + ForeignChain::Bnb => RpcInspector::Bnb(self.evm(provider, timeout)?), + ForeignChain::Ethereum => RpcInspector::Ethereum(self.evm(provider, timeout)?), + ForeignChain::HyperEvm => RpcInspector::HyperEvm(self.evm(provider, timeout)?), + ForeignChain::Polygon => RpcInspector::Polygon(self.evm(provider, timeout)?), + ForeignChain::Starknet => RpcInspector::Starknet(self.starknet(provider, timeout)?), + ForeignChain::Sui => RpcInspector::Sui(BuildInspectors::sui(self, provider, timeout)?), + // Solana, Ton and Fogo have no inspector to probe them with. + _ => return Ok(None), + })) + } } From 0356062130c2fa4e5a0a15c7cfe4d46ce20708a1 Mon Sep 17 00:00:00 2001 From: Haiyue Chen Date: Thu, 27 Aug 2026 21:56:43 +0200 Subject: [PATCH 05/14] refactor: give the probe its inspectors instead of building them probe_all_providers takes a BuildInspectors, so the chain dispatch it used to carry moves behind the factory and its coverage becomes one list rather than two. InspectorFactory holds the clients it builds over, so choosing the clients and choosing the inspectors stay separate decisions. The tests that were bounded by the wall clock now script an inspector under paused time, taking the timeout case from a second to microseconds. The ones that still stand up a server keep covering what a script cannot: real request shapes, auth splicing and client setup. --- Cargo.lock | 2 + crates/foreign-chain-health-check/Cargo.toml | 1 + .../foreign-chain-health-check/src/probe.rs | 296 ++++++++---------- crates/foreign-chain-inspector/Cargo.toml | 3 + crates/foreign-chain-inspector/src/lib.rs | 10 +- crates/foreign-chain-inspector/src/mock.rs | 96 ++++++ crates/foreign-chain-rpc-factory/Cargo.toml | 2 + .../foreign-chain-rpc-factory/src/clients.rs | 31 +- .../src/inspectors.rs | 263 ++++++++-------- crates/node/src/foreign_chain_probe.rs | 5 +- 10 files changed, 412 insertions(+), 297 deletions(-) create mode 100644 crates/foreign-chain-inspector/src/mock.rs diff --git a/Cargo.lock b/Cargo.lock index 78f56264ba..6b52e72119 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4023,7 +4023,9 @@ dependencies = [ "http", "jsonrpsee", "mpc-node-config", + "near-mpc-bounded-collections", "near-mpc-contract-interface", + "tokio", "url", ] diff --git a/crates/foreign-chain-health-check/Cargo.toml b/crates/foreign-chain-health-check/Cargo.toml index be28166f7f..c6b1505f03 100644 --- a/crates/foreign-chain-health-check/Cargo.toml +++ b/crates/foreign-chain-health-check/Cargo.toml @@ -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/probe.rs b/crates/foreign-chain-health-check/src/probe.rs index 300ef9bb18..dd5ea7afbb 100644 --- a/crates/foreign-chain-health-check/src/probe.rs +++ b/crates/foreign-chain-health-check/src/probe.rs @@ -3,29 +3,15 @@ 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, }; -use foreign_chain_rpc_interfaces::aptos::ReqwestAptosClient; +use foreign_chain_rpc_factory::inspectors::BuildInspectors; 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}; +use crate::timeout_of; /// One provider's verdict. Anything other than [`ProviderStatus::Healthy`] is unhealthy. #[derive(Debug, Clone, PartialEq, Eq)] @@ -97,62 +83,27 @@ impl ProbeReport { } } -/// Probe every configured provider concurrently. +/// Probe every configured provider concurrently, with the inspectors `inspectors` builds. /// /// 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 = timeout_of(chain_config); + async move { + probe_chain(chain, chain_config, |provider| { + inspectors.build(chain, provider, timeout) + }) + .await + } + }); futures::future::join_all(probe_attempts) .await @@ -160,33 +111,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 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) { + // No inspector covers the chain, so none of its providers can be asked. + 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, @@ -195,6 +135,10 @@ 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; }; @@ -265,12 +209,48 @@ fn classify( #[cfg(test)] #[expect(non_snake_case)] mod tests { + use foreign_chain_inspector::mock::{ScriptedInspector, ScriptedReply}; + + /// Hands the probe a scripted inspector per provider URL, so nothing builds a client and + /// every delay is a virtual timer. Never combine this with the httpmock tests above under + /// paused time: the runtime advances the clock while a real socket is silent. + struct ScriptedInspectors(std::collections::BTreeMap); + + impl ScriptedInspectors { + fn new<'a>(scripts: impl IntoIterator) -> Self { + Self( + scripts + .into_iter() + .map(|(url, inspector)| (url.to_string(), inspector)) + .collect(), + ) + } + } + + impl BuildInspectors for ScriptedInspectors { + type Inspector = ScriptedInspector; + + 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 inspector scripted 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::clients::RpcClientFactory; + 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 +260,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 +447,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 +460,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) -> ScriptedReply { + ScriptedReply::Answer { + delay: std::time::Duration::ZERO, + fingerprint: fingerprint.to_string(), + } } /// Keyed by chain too: provider names repeat across chains in real configs. @@ -516,18 +481,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://scripted.invalid/only"; + let config = starknet_only(chain_config(Some(MAINNET), one_provider("publicnode", url))); + let inspector = ScriptedInspector::new([answering(MAINNET)]); + let inspectors = ScriptedInspectors::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 +500,13 @@ 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://scripted.invalid/only"; + let config = starknet_only(chain_config(Some(MAINNET), one_provider("publicnode", url))); + let inspectors = + ScriptedInspectors::new([(url, ScriptedInspector::new([answering(SEPOLIA)]))]); // When - let report = probe_all_providers(&config).await; + let report = probe_all_providers(&config, &inspectors).await; // Then assert_eq!( @@ -568,7 +529,7 @@ mod tests { )); // When - let report = probe_all_providers(&config).await; + let report = probe_all_providers(&config, &InspectorFactory::new(RpcClientFactory)).await; // Then assert_eq!( @@ -589,7 +550,7 @@ mod tests { )); // When - let report = probe_all_providers(&config).await; + let report = probe_all_providers(&config, &InspectorFactory::new(RpcClientFactory)).await; // Then assert_eq!( @@ -608,7 +569,7 @@ mod tests { )); // When - let report = probe_all_providers(&config).await; + let report = probe_all_providers(&config, &InspectorFactory::new(RpcClientFactory)).await; // Then assert_eq!( @@ -620,22 +581,25 @@ 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://scripted.invalid/only"; + let inspector = ScriptedInspector::new([ScriptedReply::Refusal { + delay: 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 = ScriptedInspectors::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 +613,7 @@ mod tests { )); // When - let report = probe_all_providers(&config).await; + let report = probe_all_providers(&config, &InspectorFactory::new(RpcClientFactory)).await; // Then assert_eq!( @@ -669,7 +633,7 @@ mod tests { )); // When - let report = probe_all_providers(&config).await; + let report = probe_all_providers(&config, &InspectorFactory::new(RpcClientFactory)).await; // Then assert_eq!( @@ -678,18 +642,16 @@ 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://scripted.invalid/slow"; + let config = starknet_only(chain_config(Some(MAINNET), one_provider("slow", url))); + let inspectors = + ScriptedInspectors::new([(url, ScriptedInspector::new([ScriptedReply::Hang]))]); // When - let report = probe_all_providers(&config).await; + let report = probe_all_providers(&config, &inspectors).await; // Then assert_eq!( @@ -709,7 +671,7 @@ mod tests { )); // When - let report = probe_all_providers(&config).await; + let report = probe_all_providers(&config, &InspectorFactory::new(RpcClientFactory)).await; // Then assert_eq!( @@ -739,7 +701,7 @@ mod tests { )); // When - let report = probe_all_providers(&config).await; + let report = probe_all_providers(&config, &InspectorFactory::new(RpcClientFactory)).await; // Then assert_eq!( @@ -757,7 +719,7 @@ mod tests { )); // When - let report = probe_all_providers(&config).await; + let report = probe_all_providers(&config, &InspectorFactory::new(RpcClientFactory)).await; // Then assert_eq!( @@ -776,7 +738,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::new(RpcClientFactory)).await; // Then assert_eq!( @@ -806,7 +768,7 @@ mod tests { )); // When - let report = probe_all_providers(&config).await; + let report = probe_all_providers(&config, &InspectorFactory::new(RpcClientFactory)).await; // Then assert_eq!( @@ -833,7 +795,7 @@ mod tests { }; // When - let report = probe_all_providers(&config).await; + let report = probe_all_providers(&config, &InspectorFactory::new(RpcClientFactory)).await; // Then assert_eq!( @@ -868,7 +830,7 @@ mod tests { } // When - let report = probe_all_providers(&config).await; + let report = probe_all_providers(&config, &InspectorFactory::new(RpcClientFactory)).await; // Then for EvmMainnet { chain, .. } in EVM_MAINNETS { @@ -894,7 +856,7 @@ mod tests { ); // When - let report = probe_all_providers(&config).await; + let report = probe_all_providers(&config, &InspectorFactory::new(RpcClientFactory)).await; // Then assert_eq!( @@ -917,7 +879,7 @@ mod tests { )); // When - let report = probe_all_providers(&config).await; + let report = probe_all_providers(&config, &InspectorFactory::new(RpcClientFactory)).await; // Then assert_eq!( @@ -937,7 +899,7 @@ mod tests { )); // When - let report = probe_all_providers(&config).await; + let report = probe_all_providers(&config, &InspectorFactory::new(RpcClientFactory)).await; // Then assert_eq!( @@ -976,7 +938,7 @@ mod tests { }; // When - let report = probe_all_providers(&config).await; + let report = probe_all_providers(&config, &InspectorFactory::new(RpcClientFactory)).await; // Then assert_eq!( @@ -999,7 +961,7 @@ mod tests { }; // When - let report = probe_all_providers(&config).await; + let report = probe_all_providers(&config, &InspectorFactory::new(RpcClientFactory)).await; // Then assert_eq!( @@ -1090,7 +1052,7 @@ mod tests { )); // When - let report = probe_all_providers(&config).await; + let report = probe_all_providers(&config, &InspectorFactory::new(RpcClientFactory)).await; // Then assert_eq!( @@ -1108,7 +1070,7 @@ mod tests { )); // When - let report = probe_all_providers(&config).await; + let report = probe_all_providers(&config, &InspectorFactory::new(RpcClientFactory)).await; // Then assert_eq!( @@ -1117,25 +1079,37 @@ 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://scripted.invalid/keyed"; + let inspector = ScriptedInspector::new([ + ScriptedReply::TransientFailure { + delay: std::time::Duration::from_millis(10), + }, + ScriptedReply::TransientFailure { + delay: 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 = ScriptedInspectors::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 +1124,7 @@ mod tests { )); // When - let report = probe_all_providers(&config).await; + let report = probe_all_providers(&config, &InspectorFactory::new(RpcClientFactory)).await; // Then let ProviderStatus::WrongNetwork { observed, .. } = @@ -1188,7 +1162,7 @@ mod tests { let config = ForeignChainsConfig::default(); // When - let report = probe_all_providers(&config).await; + let report = probe_all_providers(&config, &InspectorFactory::new(RpcClientFactory)).await; // Then assert!(report.rows().is_empty()); @@ -1217,7 +1191,7 @@ mod tests { )); // When - let report = probe_all_providers(&config).await; + let report = probe_all_providers(&config, &InspectorFactory::new(RpcClientFactory)).await; // Then assert_matches!( diff --git a/crates/foreign-chain-inspector/Cargo.toml b/crates/foreign-chain-inspector/Cargo.toml index a22c514a34..6cea4507bf 100644 --- a/crates/foreign-chain-inspector/Cargo.toml +++ b/crates/foreign-chain-inspector/Cargo.toml @@ -4,6 +4,9 @@ version.workspace = true edition.workspace = true license.workspace = true +[features] +test-utils = [] + [dependencies] bs58 = { workspace = true } derive_more = { workspace = true } diff --git a/crates/foreign-chain-inspector/src/lib.rs b/crates/foreign-chain-inspector/src/lib.rs index a91ecc9c18..99a94e4fb0 100644 --- a/crates/foreign-chain-inspector/src/lib.rs +++ b/crates/foreign-chain-inspector/src/lib.rs @@ -27,6 +27,8 @@ 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; @@ -229,12 +231,18 @@ where } } +/// What a caller that holds an inspector needs of it: probe a provider, and survive being held and +/// shared for as long as the caller keeps it. +pub trait ChainInspector: NetworkFingerprintInspector + Clone + Send + Sync + 'static {} + +impl ChainInspector for T {} + /// 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 diff --git a/crates/foreign-chain-inspector/src/mock.rs b/crates/foreign-chain-inspector/src/mock.rs new file mode 100644 index 0000000000..aa3e59a3ab --- /dev/null +++ b/crates/foreign-chain-inspector/src/mock.rs @@ -0,0 +1,96 @@ +//! Scripted test doubles for the network fingerprint probe. +//! +//! Under `#[tokio::test(start_paused = true)]` every scripted delay is a virtual timer, so +//! retry, backoff and timeout behavior runs deterministically and in microseconds of wall +//! time. Never combine paused time with a real socket (httpmock, tonic): the runtime +//! advances the clock automatically while the socket is silent, firing timeouts before any +//! real response can land. + +use std::collections::VecDeque; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::{Arc, Mutex}; +use std::time::Duration; + +use crate::{ForeignChainInspectionError, NetworkFingerprint, NetworkFingerprintInspector}; + +/// One scripted attempt: a virtual delay, then an outcome. Outcomes are constructed fresh +/// per attempt because [`ForeignChainInspectionError`] is not `Clone`. +#[derive(Debug)] +pub enum ScriptedReply { + /// Answer `fingerprint` after `delay`. Keep the string under + /// [`NetworkFingerprint`]'s length cap, or it is truncated on the way out. + Answer { + delay: Duration, + fingerprint: String, + }, + /// A transient failure ([`ForeignChainInspectionError::RpcRequestFailed`]) after + /// `delay`; [`FanOut`](crate::FanOut) retries it. + TransientFailure { delay: Duration }, + /// A refusal ([`ForeignChainInspectionError::RpcRequestRejected`]) after `delay`; + /// [`FanOut`](crate::FanOut) does not retry it. + Refusal { delay: Duration }, + /// Never resolves; only the caller's timeout ends the attempt. + Hang, +} + +/// A [`NetworkFingerprintInspector`] that answers each call from a queue of +/// [`ScriptedReply`]s and panics on a call beyond the script, so an unexpected extra +/// attempt fails loudly. Clones share the queue and the call counter — +/// [`FanOut`](crate::FanOut) clones its inspector into the task it spawns for each +/// provider — so use one instance per provider and keep a clone in the test for +/// [`ScriptedInspector::calls`]. +#[derive(Clone)] +pub struct ScriptedInspector { + script: Arc>>, + calls: Arc, +} + +impl ScriptedInspector { + pub fn new(replies: impl IntoIterator) -> Self { + Self { + script: Arc::new(Mutex::new(replies.into_iter().collect())), + calls: Arc::new(AtomicUsize::new(0)), + } + } + + /// How many attempts reached this inspector so far. + pub fn calls(&self) -> usize { + self.calls.load(Ordering::SeqCst) + } +} + +impl NetworkFingerprintInspector for ScriptedInspector { + async fn network_fingerprint(&self) -> Result { + self.calls.fetch_add(1, Ordering::SeqCst); + let reply = self + .script + .lock() + .expect("script mutex poisoned") + .pop_front() + .expect("call beyond the script"); + match reply { + ScriptedReply::Answer { delay, fingerprint } => { + tokio::time::sleep(delay).await; + Ok(NetworkFingerprint::new(fingerprint)) + } + ScriptedReply::TransientFailure { delay } => { + tokio::time::sleep(delay).await; + Err(ForeignChainInspectionError::RpcRequestFailed( + "scripted transient failure".to_string(), + )) + } + ScriptedReply::Refusal { delay } => { + tokio::time::sleep(delay).await; + Err(ForeignChainInspectionError::RpcRequestRejected( + "scripted refusal".to_string(), + )) + } + ScriptedReply::Hang => std::future::pending().await, + } + } + + /// Identity: tests script the exact canonical string they assert. + fn canonical_fingerprint(&self, fingerprint: &str) -> NetworkFingerprint { + NetworkFingerprint::new(fingerprint) + } +} diff --git a/crates/foreign-chain-rpc-factory/Cargo.toml b/crates/foreign-chain-rpc-factory/Cargo.toml index 72fb10b1c4..e5f36bdd36 100644 --- a/crates/foreign-chain-rpc-factory/Cargo.toml +++ b/crates/foreign-chain-rpc-factory/Cargo.toml @@ -16,6 +16,8 @@ url = { workspace = true } [dev-dependencies] assert_matches = { workspace = true } +near-mpc-bounded-collections = { workspace = true } +tokio = { workspace = true } [lints] workspace = true diff --git a/crates/foreign-chain-rpc-factory/src/clients.rs b/crates/foreign-chain-rpc-factory/src/clients.rs index 05316b1f75..a27ca0ed00 100644 --- a/crates/foreign-chain-rpc-factory/src/clients.rs +++ b/crates/foreign-chain-rpc-factory/src/clients.rs @@ -11,17 +11,21 @@ use mpc_node_config::ForeignChainProviderConfig; use crate::auth_config_to_rpc_auth; -/// The transports a chain is reached over. [`RpcClients`] reaches the real providers; a caller that -/// needs something else, a test most of all, implements this instead. +/// The transports a chain is reached over. [`RpcClientFactory`] reaches the real providers; a +/// caller that needs something else, a test most of all, implements this instead. /// -/// Injecting it also settles which inspectors get built, since -/// [`crate::inspectors`] puts them on whatever this returns. +/// An [`crate::inspectors::InspectorFactory`] is built over one of these, so the choice also +/// settles what the inspectors it hands out are talking to. pub trait BuildRpcClients { type JsonRpc: ClientT + Clone + Send + Sync + 'static; type Aptos: AptosRpcClient + Clone + Send + Sync + 'static; type Sui: SuiRpcClient + Clone + Send + Sync + 'static; - fn json_rpc(&self, provider: &ForeignChainProviderConfig) -> anyhow::Result; + fn json_rpc( + &self, + provider: &ForeignChainProviderConfig, + timeout: Duration, + ) -> anyhow::Result; fn aptos( &self, @@ -36,11 +40,11 @@ pub trait BuildRpcClients { ) -> anyhow::Result; } -/// Resolves the provider's credentials and reaches it over the network. +/// Reaches the real providers over the network, with each one's credentials applied. #[derive(Clone, Copy)] -pub struct RpcClients; +pub struct RpcClientFactory; -impl RpcClients { +impl RpcClientFactory { /// Applies the provider's credentials, leaving them in the URL or in a header as its auth /// config asks. fn authenticate( @@ -63,12 +67,19 @@ impl RpcClients { } } -impl BuildRpcClients for RpcClients { +impl BuildRpcClients for RpcClientFactory { type Aptos = ReqwestAptosClient; type JsonRpc = HttpClient; type Sui = GrpcSuiClient; - fn json_rpc(&self, provider: &ForeignChainProviderConfig) -> anyhow::Result { + /// The deadline is not handed to jsonrpsee: these chains are bounded by the caller's own + /// deadline, as they were before this factory existed. It stays in the signature so a caller + /// building its own clients can honour it. + fn json_rpc( + &self, + provider: &ForeignChainProviderConfig, + _timeout: Duration, + ) -> anyhow::Result { let (url, auth) = Self::authenticate(provider)?; Ok(build_http_client(url, auth)?) } diff --git a/crates/foreign-chain-rpc-factory/src/inspectors.rs b/crates/foreign-chain-rpc-factory/src/inspectors.rs index 62e1fdbd8c..5de9275191 100644 --- a/crates/foreign-chain-rpc-factory/src/inspectors.rs +++ b/crates/foreign-chain-rpc-factory/src/inspectors.rs @@ -1,16 +1,16 @@ //! Building a chain's inspector for one of its providers. //! -//! Injecting [`BuildInspectors`] settles which inspectors a caller gets. Anything that builds -//! clients gets the real ones for free, through the blanket implementation over -//! [`BuildRpcClients`]; a caller that needs to answer for the inspectors themselves, a test most of -//! all, implements this instead and never builds a client at all. +//! Injecting [`BuildInspectors`] settles which inspectors a caller gets. [`InspectorFactory`] is +//! the one that builds real inspectors, and is itself injected with the clients to build them +//! over; a caller that needs to answer for the inspectors themselves, a test most of all, +//! implements [`BuildInspectors`] on its own type and builds no client at all. use std::time::Duration; -use foreign_chain_inspector::NetworkFingerprintInspector; +use foreign_chain_inspector::ChainInspector; use foreign_chain_inspector::aptos::inspector::AptosInspector; use foreign_chain_inspector::bitcoin::inspector::BitcoinInspector; -use foreign_chain_inspector::evm::inspector::{EvmChain, EvmInspector}; +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; @@ -19,144 +19,159 @@ use near_mpc_contract_interface::types::ForeignChain; use crate::clients::BuildRpcClients; -/// What every inspector this builds must satisfy: probe a provider, and survive being held and -/// shared for as long as the caller keeps it. -pub trait ChainInspector: NetworkFingerprintInspector + Clone + Send + Sync + 'static {} - -impl ChainInspector for T {} - -/// One method per inspector shape rather than per chain: the EVM chains differ only in the marker -/// type the caller fixes. -/// -/// `timeout` is the provider's configured deadline. The chains reached over JSON-RPC take it in the -/// inspection deadline instead, so their implementations may ignore it. -pub trait BuildInspectors { - type Evm: ChainInspector; - type Starknet: ChainInspector; - type Bitcoin: ChainInspector; - type Aptos: ChainInspector; - type Sui: ChainInspector; - /// One type covering every chain, for a caller that holds inspectors of several at once. - type Any: ChainInspector; - - fn evm( - &self, - provider: &ForeignChainProviderConfig, - timeout: Duration, - ) -> anyhow::Result>; - - fn starknet( - &self, - provider: &ForeignChainProviderConfig, - timeout: Duration, - ) -> anyhow::Result; - - fn bitcoin( - &self, - provider: &ForeignChainProviderConfig, - timeout: Duration, - ) -> anyhow::Result; - - fn aptos( - &self, - provider: &ForeignChainProviderConfig, - timeout: Duration, - ) -> anyhow::Result; - - fn sui( - &self, - provider: &ForeignChainProviderConfig, - timeout: Duration, - ) -> anyhow::Result; +/// `Sync` because a caller that probes several chains at once shares one factory across them. +pub trait BuildInspectors: Sync { + /// One type covering every chain, so a caller that spans them can hold their inspectors + /// together. + type Inspector: ChainInspector; /// The inspector for whichever chain `chain` is, or `None` when none exists to probe it. - fn any( + /// + /// `timeout` is the provider's configured deadline. The chains reached over JSON-RPC take it in + /// the inspection deadline instead, so an implementation may ignore it for those. + fn build( &self, chain: ForeignChain, provider: &ForeignChainProviderConfig, timeout: Duration, - ) -> anyhow::Result>; + ) -> anyhow::Result>; } -/// Choosing the clients is enough to settle the inspectors: each is its chain's inspector over the -/// client that reaches it. -impl BuildInspectors for Clients { - type Aptos = AptosInspector; - type Bitcoin = BitcoinInspector; - type Evm = - EvmInspector; - type Starknet = StarknetInspector; - type Any = RpcInspector; - type Sui = SuiInspector; - - fn evm( - &self, - provider: &ForeignChainProviderConfig, - _timeout: Duration, - ) -> anyhow::Result> { - Ok(EvmInspector::new(self.json_rpc(provider)?)) - } - - fn starknet( - &self, - provider: &ForeignChainProviderConfig, - _timeout: Duration, - ) -> anyhow::Result { - Ok(StarknetInspector::new(self.json_rpc(provider)?)) - } - - fn bitcoin( - &self, - provider: &ForeignChainProviderConfig, - _timeout: Duration, - ) -> anyhow::Result { - Ok(BitcoinInspector::new(self.json_rpc(provider)?)) - } +/// Builds each chain's inspector over the client that reaches it, whichever clients it was given. +pub struct InspectorFactory { + clients: Clients, +} - fn aptos( - &self, - provider: &ForeignChainProviderConfig, - timeout: Duration, - ) -> anyhow::Result { - Ok(AptosInspector::new(BuildRpcClients::aptos( - self, provider, timeout, - )?)) +impl InspectorFactory { + pub fn new(clients: Clients) -> Self { + Self { clients } } +} - fn sui( - &self, - provider: &ForeignChainProviderConfig, - timeout: Duration, - ) -> anyhow::Result { - Ok(SuiInspector::new(BuildRpcClients::sui( - self, provider, timeout, - )?)) - } +impl BuildInspectors for InspectorFactory { + type Inspector = RpcInspector; - fn any( + fn build( &self, chain: ForeignChain, provider: &ForeignChainProviderConfig, timeout: Duration, - ) -> anyhow::Result> { + ) -> anyhow::Result> { Ok(Some(match chain { - ForeignChain::Abstract => RpcInspector::Abstract(self.evm(provider, timeout)?), - ForeignChain::Adi => RpcInspector::Adi(self.evm(provider, timeout)?), + ForeignChain::Abstract => { + RpcInspector::Abstract(EvmInspector::new(self.clients.json_rpc(provider, timeout)?)) + } + ForeignChain::Adi => { + RpcInspector::Adi(EvmInspector::new(self.clients.json_rpc(provider, timeout)?)) + } ForeignChain::Aptos => { - RpcInspector::Aptos(BuildInspectors::aptos(self, provider, timeout)?) + RpcInspector::Aptos(AptosInspector::new(self.clients.aptos(provider, timeout)?)) + } + ForeignChain::Arbitrum => { + RpcInspector::Arbitrum(EvmInspector::new(self.clients.json_rpc(provider, timeout)?)) + } + ForeignChain::Avalanche => RpcInspector::Avalanche(EvmInspector::new( + self.clients.json_rpc(provider, timeout)?, + )), + ForeignChain::Base => { + RpcInspector::Base(EvmInspector::new(self.clients.json_rpc(provider, timeout)?)) + } + ForeignChain::Bitcoin => RpcInspector::Bitcoin(BitcoinInspector::new( + self.clients.json_rpc(provider, timeout)?, + )), + ForeignChain::Bnb => { + RpcInspector::Bnb(EvmInspector::new(self.clients.json_rpc(provider, timeout)?)) + } + ForeignChain::Ethereum => { + RpcInspector::Ethereum(EvmInspector::new(self.clients.json_rpc(provider, timeout)?)) + } + ForeignChain::HyperEvm => { + RpcInspector::HyperEvm(EvmInspector::new(self.clients.json_rpc(provider, timeout)?)) + } + ForeignChain::Polygon => { + RpcInspector::Polygon(EvmInspector::new(self.clients.json_rpc(provider, timeout)?)) } - ForeignChain::Arbitrum => RpcInspector::Arbitrum(self.evm(provider, timeout)?), - ForeignChain::Avalanche => RpcInspector::Avalanche(self.evm(provider, timeout)?), - ForeignChain::Base => RpcInspector::Base(self.evm(provider, timeout)?), - ForeignChain::Bitcoin => RpcInspector::Bitcoin(self.bitcoin(provider, timeout)?), - ForeignChain::Bnb => RpcInspector::Bnb(self.evm(provider, timeout)?), - ForeignChain::Ethereum => RpcInspector::Ethereum(self.evm(provider, timeout)?), - ForeignChain::HyperEvm => RpcInspector::HyperEvm(self.evm(provider, timeout)?), - ForeignChain::Polygon => RpcInspector::Polygon(self.evm(provider, timeout)?), - ForeignChain::Starknet => RpcInspector::Starknet(self.starknet(provider, timeout)?), - ForeignChain::Sui => RpcInspector::Sui(BuildInspectors::sui(self, provider, timeout)?), - // Solana, Ton and Fogo have no inspector to probe them with. + ForeignChain::Starknet => RpcInspector::Starknet(StarknetInspector::new( + self.clients.json_rpc(provider, timeout)?, + )), + ForeignChain::Sui => { + RpcInspector::Sui(SuiInspector::new(self.clients.sui(provider, timeout)?)) + } + // No inspector exists for other chains. _ => return Ok(None), })) } } + +#[cfg(test)] +#[expect(non_snake_case)] +mod tests { + use std::num::NonZeroU64; + + use mpc_node_config::{ + AuthConfig, ForeignChainConfig, ForeignChainProviderConfig, ForeignChainsConfig, + }; + use near_mpc_bounded_collections::NonEmptyBTreeMap; + + use super::*; + use crate::clients::RpcClientFactory; + + /// Every chain a config can hold is set, so a chain added later has to be listed here too, and + /// whoever adds it has to say whether an inspector covers it. + fn every_configurable_chain() -> ForeignChainsConfig { + let section = || { + Some(ForeignChainConfig { + timeout_sec: NonZeroU64::new(1).unwrap(), + max_retries: NonZeroU64::new(1).unwrap(), + expected_network_fingerprint: None, + providers: NonEmptyBTreeMap::new( + "only".to_string().into(), + ForeignChainProviderConfig { + rpc_url: "http://127.0.0.1:9".to_string(), + auth: AuthConfig::None, + }, + ), + }) + }; + ForeignChainsConfig { + solana: section(), + bitcoin: section(), + ethereum: section(), + abstract_chain: section(), + starknet: section(), + bnb: section(), + base: section(), + arbitrum: section(), + hyper_evm: section(), + polygon: section(), + aptos: section(), + sui: section(), + avalanche: section(), + adi: section(), + } + } + + // The Sui client is built on a gRPC channel, which needs a reactor to exist. + #[tokio::test] + async fn build__should_cover_every_configurable_chain_that_has_an_inspector() { + // Given + let config = every_configurable_chain(); + let factory = InspectorFactory::new(RpcClientFactory); + + // When + let uncovered: Vec<_> = config + .iter_chains() + .filter(|(chain, chain_config)| { + let provider = chain_config.providers.iter().next().expect("a provider").1; + factory + .build(*chain, provider, Duration::from_secs(1)) + .expect("the provider is well formed") + .is_none() + }) + .map(|(chain, _)| chain) + .collect(); + + // Then + assert_eq!(uncovered, vec![ForeignChain::Solana]); + } +} diff --git a/crates/node/src/foreign_chain_probe.rs b/crates/node/src/foreign_chain_probe.rs index fc3b57d332..cd884845fc 100644 --- a/crates/node/src/foreign_chain_probe.rs +++ b/crates/node/src/foreign_chain_probe.rs @@ -7,6 +7,8 @@ use std::future::Future; use foreign_chain_health_check::probe::{ ProbeReport, ProviderHealth, ProviderStatus, probe_all_providers, }; +use foreign_chain_rpc_factory::clients::RpcClientFactory; +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 +24,8 @@ pub async fn run_periodic_probe(foreign_chains: ForeignChainsConfig, ticker: imp return; } - probe_periodically(|| probe_all_providers(&foreign_chains), ticker).await; + let inspectors = InspectorFactory::new(RpcClientFactory); + probe_periodically(|| probe_all_providers(&foreign_chains, &inspectors), ticker).await; } async fn probe_periodically>( From d5446b6152120d0c7141fe7acad1be4b6e488249 Mon Sep 17 00:00:00 2001 From: Haiyue Chen Date: Thu, 27 Aug 2026 23:24:07 +0200 Subject: [PATCH 06/14] chore: trim the inspector injection docs to the load-bearing points --- .../foreign-chain-health-check/src/probe.rs | 13 ++++---- crates/foreign-chain-inspector/src/lib.rs | 5 ++- crates/foreign-chain-inspector/src/mock.rs | 33 +++++++------------ .../src/rpc_inspector.rs | 6 ++-- .../foreign-chain-rpc-factory/src/clients.rs | 12 ++----- .../src/inspectors.rs | 23 +++++-------- crates/foreign-chain-rpc-factory/src/lib.rs | 10 +++--- 7 files changed, 40 insertions(+), 62 deletions(-) diff --git a/crates/foreign-chain-health-check/src/probe.rs b/crates/foreign-chain-health-check/src/probe.rs index dd5ea7afbb..68d8d52726 100644 --- a/crates/foreign-chain-health-check/src/probe.rs +++ b/crates/foreign-chain-health-check/src/probe.rs @@ -83,7 +83,7 @@ impl ProbeReport { } } -/// Probe every configured provider concurrently, with the inspectors `inspectors` builds. +/// Probe every configured provider concurrently. /// /// 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 * @@ -124,7 +124,7 @@ where for (name, provider) in config.providers.iter() { let provider_id = ProviderId(name.as_str().to_owned()); match build_new_inspector(provider) { - // No inspector covers the chain, so none of its providers can be asked. + // No inspector for the chain, so none of its providers can be asked. Ok(None) => return rows_of(chain, config, ProviderStatus::ProbeNotImplemented), Ok(Some(inspector)) => inspectors.push((provider_id, inspector)), Err(error) => rows.push(ProviderHealth { @@ -143,7 +143,8 @@ where return rows; }; // Any of the chain's inspectors normalizes the same way; the first one that built is enough. - let expected = inspectors.first().1.canonical_fingerprint(expected); + let (_, inspector) = inspectors.first(); + let expected = inspector.canonical_fingerprint(expected); let fingerprints = FanOut::new(inspectors) .network_fingerprints(timeout_of(config), config.max_retries) @@ -211,9 +212,9 @@ fn classify( mod tests { use foreign_chain_inspector::mock::{ScriptedInspector, ScriptedReply}; - /// Hands the probe a scripted inspector per provider URL, so nothing builds a client and - /// every delay is a virtual timer. Never combine this with the httpmock tests above under - /// paused time: the runtime advances the clock while a real socket is silent. + /// Hands the probe a scripted inspector per provider URL, so nothing builds a client. Do not + /// put the httpmock tests above under paused time: the clock jumps while a real socket is + /// silent. struct ScriptedInspectors(std::collections::BTreeMap); impl ScriptedInspectors { diff --git a/crates/foreign-chain-inspector/src/lib.rs b/crates/foreign-chain-inspector/src/lib.rs index 99a94e4fb0..8d3a392e88 100644 --- a/crates/foreign-chain-inspector/src/lib.rs +++ b/crates/foreign-chain-inspector/src/lib.rs @@ -231,8 +231,7 @@ where } } -/// What a caller that holds an inspector needs of it: probe a provider, and survive being held and -/// shared for as long as the caller keeps it. +/// What a caller needs of an inspector to hold it, clone it into tasks and keep it alive. pub trait ChainInspector: NetworkFingerprintInspector + Clone + Send + Sync + 'static {} impl ChainInspector for T {} @@ -670,9 +669,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 index aa3e59a3ab..da196cd1f9 100644 --- a/crates/foreign-chain-inspector/src/mock.rs +++ b/crates/foreign-chain-inspector/src/mock.rs @@ -1,10 +1,8 @@ //! Scripted test doubles for the network fingerprint probe. //! -//! Under `#[tokio::test(start_paused = true)]` every scripted delay is a virtual timer, so -//! retry, backoff and timeout behavior runs deterministically and in microseconds of wall -//! time. Never combine paused time with a real socket (httpmock, tonic): the runtime -//! advances the clock automatically while the socket is silent, firing timeouts before any -//! real response can land. +//! Scripted delays are virtual timers under `#[tokio::test(start_paused = true)]`, so retry, +//! backoff and timeout run in microseconds. Never mix paused time with a real socket (httpmock, +//! tonic): the runtime advances the clock while the socket is silent and fires the timeout first. use std::collections::VecDeque; use std::sync::atomic::{AtomicUsize, Ordering}; @@ -13,32 +11,27 @@ use std::time::Duration; use crate::{ForeignChainInspectionError, NetworkFingerprint, NetworkFingerprintInspector}; -/// One scripted attempt: a virtual delay, then an outcome. Outcomes are constructed fresh -/// per attempt because [`ForeignChainInspectionError`] is not `Clone`. +/// One scripted attempt. Outcomes are built per attempt because [`ForeignChainInspectionError`] +/// is not `Clone`. #[derive(Debug)] pub enum ScriptedReply { - /// Answer `fingerprint` after `delay`. Keep the string under - /// [`NetworkFingerprint`]'s length cap, or it is truncated on the way out. + /// Keep `fingerprint` under [`NetworkFingerprint`]'s length cap, or it is truncated on the + /// way out. Answer { delay: Duration, fingerprint: String, }, - /// A transient failure ([`ForeignChainInspectionError::RpcRequestFailed`]) after - /// `delay`; [`FanOut`](crate::FanOut) retries it. + /// [`FanOut`](crate::FanOut) retries a transient failure. TransientFailure { delay: Duration }, - /// A refusal ([`ForeignChainInspectionError::RpcRequestRejected`]) after `delay`; - /// [`FanOut`](crate::FanOut) does not retry it. + /// [`FanOut`](crate::FanOut) does not retry a refusal. Refusal { delay: Duration }, /// Never resolves; only the caller's timeout ends the attempt. Hang, } -/// A [`NetworkFingerprintInspector`] that answers each call from a queue of -/// [`ScriptedReply`]s and panics on a call beyond the script, so an unexpected extra -/// attempt fails loudly. Clones share the queue and the call counter — -/// [`FanOut`](crate::FanOut) clones its inspector into the task it spawns for each -/// provider — so use one instance per provider and keep a clone in the test for -/// [`ScriptedInspector::calls`]. +/// Answers from a queue of [`ScriptedReply`]s and panics past the end of the script, so an +/// unexpected extra attempt fails loudly. Clones share the queue and the counter, so give each +/// provider its own and keep a clone for [`ScriptedInspector::calls`]. #[derive(Clone)] pub struct ScriptedInspector { script: Arc>>, @@ -53,7 +46,6 @@ impl ScriptedInspector { } } - /// How many attempts reached this inspector so far. pub fn calls(&self) -> usize { self.calls.load(Ordering::SeqCst) } @@ -89,7 +81,6 @@ impl NetworkFingerprintInspector for ScriptedInspector { } } - /// Identity: tests script the exact canonical string they assert. 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 index d6a6daaa3d..43e1a84452 100644 --- a/crates/foreign-chain-inspector/src/rpc_inspector.rs +++ b/crates/foreign-chain-inspector/src/rpc_inspector.rs @@ -1,4 +1,4 @@ -//! Holding any chain's inspector behind one type. +//! One type holding any chain's inspector. use crate::abstract_chain::inspector::Abstract; use crate::adi::inspector::Adi; @@ -19,8 +19,8 @@ use foreign_chain_rpc_interfaces::aptos::AptosRpcClient; use foreign_chain_rpc_interfaces::sui::SuiRpcClient; use jsonrpsee::core::client::ClientT; -/// One provider's inspector, whichever chain it serves. Lets a caller that spans chains, as the -/// probe does, hold them behind a single type. +/// [`NetworkFingerprintInspector`] is not dyn compatible, so a caller that spans chains needs an +/// enum rather than a trait object. #[derive(Clone)] pub enum RpcInspector { Abstract(EvmInspector), diff --git a/crates/foreign-chain-rpc-factory/src/clients.rs b/crates/foreign-chain-rpc-factory/src/clients.rs index a27ca0ed00..93f055e066 100644 --- a/crates/foreign-chain-rpc-factory/src/clients.rs +++ b/crates/foreign-chain-rpc-factory/src/clients.rs @@ -13,9 +13,6 @@ use crate::auth_config_to_rpc_auth; /// The transports a chain is reached over. [`RpcClientFactory`] reaches the real providers; a /// caller that needs something else, a test most of all, implements this instead. -/// -/// An [`crate::inspectors::InspectorFactory`] is built over one of these, so the choice also -/// settles what the inspectors it hands out are talking to. pub trait BuildRpcClients { type JsonRpc: ClientT + Clone + Send + Sync + 'static; type Aptos: AptosRpcClient + Clone + Send + Sync + 'static; @@ -45,8 +42,6 @@ pub trait BuildRpcClients { pub struct RpcClientFactory; impl RpcClientFactory { - /// Applies the provider's credentials, leaving them in the URL or in a header as its auth - /// config asks. fn authenticate( provider: &ForeignChainProviderConfig, ) -> anyhow::Result<(String, RpcAuthentication)> { @@ -55,7 +50,6 @@ impl RpcClientFactory { Ok((url, auth)) } - /// The gRPC and REST clients carry credentials in request metadata rather than the URL. fn auth_header(auth: RpcAuthentication) -> Option<(http::HeaderName, http::HeaderValue)> { match auth { RpcAuthentication::KeyInUrl => None, @@ -72,9 +66,9 @@ impl BuildRpcClients for RpcClientFactory { type JsonRpc = HttpClient; type Sui = GrpcSuiClient; - /// The deadline is not handed to jsonrpsee: these chains are bounded by the caller's own - /// deadline, as they were before this factory existed. It stays in the signature so a caller - /// building its own clients can honour it. + /// jsonrpsee never sees the deadline: these chains stay bounded by the caller's own, as they + /// were before this factory existed. It stays in the signature for an implementation that + /// does want it. fn json_rpc( &self, provider: &ForeignChainProviderConfig, diff --git a/crates/foreign-chain-rpc-factory/src/inspectors.rs b/crates/foreign-chain-rpc-factory/src/inspectors.rs index 5de9275191..f1aa461de5 100644 --- a/crates/foreign-chain-rpc-factory/src/inspectors.rs +++ b/crates/foreign-chain-rpc-factory/src/inspectors.rs @@ -1,9 +1,7 @@ //! Building a chain's inspector for one of its providers. //! -//! Injecting [`BuildInspectors`] settles which inspectors a caller gets. [`InspectorFactory`] is -//! the one that builds real inspectors, and is itself injected with the clients to build them -//! over; a caller that needs to answer for the inspectors themselves, a test most of all, -//! implements [`BuildInspectors`] on its own type and builds no client at all. +//! A caller that wants to answer for the inspectors themselves, a test most of all, implements +//! [`BuildInspectors`] and builds no client at all. use std::time::Duration; @@ -21,14 +19,12 @@ use crate::clients::BuildRpcClients; /// `Sync` because a caller that probes several chains at once shares one factory across them. pub trait BuildInspectors: Sync { - /// One type covering every chain, so a caller that spans them can hold their inspectors - /// together. type Inspector: ChainInspector; - /// The inspector for whichever chain `chain` is, or `None` when none exists to probe it. + /// `None` when no inspector exists to probe the chain. /// - /// `timeout` is the provider's configured deadline. The chains reached over JSON-RPC take it in - /// the inspection deadline instead, so an implementation may ignore it for those. + /// `timeout` reaches the transports that can hold one; the JSON-RPC chains take their deadline + /// from the caller instead. fn build( &self, chain: ForeignChain, @@ -37,7 +33,7 @@ pub trait BuildInspectors: Sync { ) -> anyhow::Result>; } -/// Builds each chain's inspector over the client that reaches it, whichever clients it was given. +/// Builds each chain's inspector over the clients it was given. pub struct InspectorFactory { clients: Clients, } @@ -97,7 +93,8 @@ impl BuildInspectors for InspectorFactory { RpcInspector::Sui(SuiInspector::new(self.clients.sui(provider, timeout)?)) } - // No inspector exists for other chains. + // `ForeignChain` is `non_exhaustive`, so the chains left without an inspector cannot + // be listed here. _ => return Ok(None), })) } @@ -116,8 +113,7 @@ mod tests { use super::*; use crate::clients::RpcClientFactory; - /// Every chain a config can hold is set, so a chain added later has to be listed here too, and - /// whoever adds it has to say whether an inspector covers it. + /// Set exhaustively, so a chain added to the config has to be answered for here too. fn every_configurable_chain() -> ForeignChainsConfig { let section = || { Some(ForeignChainConfig { @@ -151,7 +147,6 @@ mod tests { } } - // The Sui client is built on a gRPC channel, which needs a reactor to exist. #[tokio::test] async fn build__should_cover_every_configurable_chain_that_has_an_inspector() { // Given diff --git a/crates/foreign-chain-rpc-factory/src/lib.rs b/crates/foreign-chain-rpc-factory/src/lib.rs index 7c419fa54f..2b47af3d06 100644 --- a/crates/foreign-chain-rpc-factory/src/lib.rs +++ b/crates/foreign-chain-rpc-factory/src/lib.rs @@ -143,8 +143,7 @@ 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"), scheme: None, @@ -189,7 +188,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 { @@ -208,8 +207,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 { @@ -231,7 +229,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 { From 67a64fba4b919216780f3fdb0c25948c145092dc Mon Sep 17 00:00:00 2001 From: Haiyue Chen Date: Fri, 28 Aug 2026 01:22:12 +0200 Subject: [PATCH 07/14] refactor: drop the client seam and make the inspector concrete BuildInspectors is now the only seam: it lives in foreign-chain-inspector, and RpcInspector is a concrete enum over the real transports. The BuildRpcClients trait and RpcClientFactory had no consumers and only made the transport a detail the caller had to pick; InspectorFactory becomes a unit struct. The verify path's duplicated construction is left for a follow-up. --- Cargo.lock | 3 +- .../foreign-chain-health-check/src/probe.rs | 52 ++++---- crates/foreign-chain-inspector/Cargo.toml | 2 + crates/foreign-chain-inspector/src/lib.rs | 23 +++- .../src/rpc_inspector.rs | 41 +++---- crates/foreign-chain-rpc-factory/Cargo.toml | 1 - .../foreign-chain-rpc-factory/src/clients.rs | 103 ---------------- .../src/inspectors.rs | 114 +++++++++--------- crates/foreign-chain-rpc-factory/src/lib.rs | 4 +- crates/node/src/foreign_chain_probe.rs | 8 +- 10 files changed, 130 insertions(+), 221 deletions(-) delete mode 100644 crates/foreign-chain-rpc-factory/src/clients.rs diff --git a/Cargo.lock b/Cargo.lock index 6b52e72119..31073ca8de 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3990,6 +3990,7 @@ dependencies = [ name = "foreign-chain-inspector" version = "3.14.0" dependencies = [ + "anyhow", "assert_matches", "bs58 0.5.1", "derive_more 2.1.1", @@ -4000,6 +4001,7 @@ dependencies = [ "httpmock", "jsonrpsee", "mockall", + "mpc-node-config", "mpc-primitives", "near-mpc-bounded-collections", "near-mpc-contract-interface", @@ -4021,7 +4023,6 @@ dependencies = [ "foreign-chain-inspector", "foreign-chain-rpc-interfaces", "http", - "jsonrpsee", "mpc-node-config", "near-mpc-bounded-collections", "near-mpc-contract-interface", diff --git a/crates/foreign-chain-health-check/src/probe.rs b/crates/foreign-chain-health-check/src/probe.rs index 68d8d52726..6b81332664 100644 --- a/crates/foreign-chain-health-check/src/probe.rs +++ b/crates/foreign-chain-health-check/src/probe.rs @@ -4,9 +4,8 @@ use std::collections::BTreeMap; use foreign_chain_inspector::{ - FanOut, ForeignChainInspectionError, NetworkFingerprint, ProviderFailure, + BuildInspectors, FanOut, ForeignChainInspectionError, NetworkFingerprint, ProviderFailure, }; -use foreign_chain_rpc_factory::inspectors::BuildInspectors; use mpc_node_config::{ForeignChainConfig, ForeignChainProviderConfig, ForeignChainsConfig}; use near_mpc_bounded_collections::NonEmptyVec; use near_mpc_contract_interface::types::{ForeignChain, ProviderId}; @@ -212,9 +211,7 @@ fn classify( mod tests { use foreign_chain_inspector::mock::{ScriptedInspector, ScriptedReply}; - /// Hands the probe a scripted inspector per provider URL, so nothing builds a client. Do not - /// put the httpmock tests above under paused time: the clock jumps while a real socket is - /// silent. + /// Hands the probe a scripted inspector per provider URL, so nothing builds a client. struct ScriptedInspectors(std::collections::BTreeMap); impl ScriptedInspectors { @@ -250,7 +247,6 @@ mod tests { abstract_chain, adi, aptos, arbitrum, avalanche, base, bitcoin, bnb, ethereum, hyperevm, polygon, starknet, sui, }; - use foreign_chain_rpc_factory::clients::RpcClientFactory; use foreign_chain_rpc_factory::inspectors::InspectorFactory; use foreign_chain_rpc_interfaces::sui::Status; use foreign_chain_rpc_interfaces::sui::proto::ledger_service_server::{ @@ -530,7 +526,7 @@ mod tests { )); // When - let report = probe_all_providers(&config, &InspectorFactory::new(RpcClientFactory)).await; + let report = probe_all_providers(&config, &InspectorFactory).await; // Then assert_eq!( @@ -551,7 +547,7 @@ mod tests { )); // When - let report = probe_all_providers(&config, &InspectorFactory::new(RpcClientFactory)).await; + let report = probe_all_providers(&config, &InspectorFactory).await; // Then assert_eq!( @@ -570,7 +566,7 @@ mod tests { )); // When - let report = probe_all_providers(&config, &InspectorFactory::new(RpcClientFactory)).await; + let report = probe_all_providers(&config, &InspectorFactory).await; // Then assert_eq!( @@ -614,7 +610,7 @@ mod tests { )); // When - let report = probe_all_providers(&config, &InspectorFactory::new(RpcClientFactory)).await; + let report = probe_all_providers(&config, &InspectorFactory).await; // Then assert_eq!( @@ -634,7 +630,7 @@ mod tests { )); // When - let report = probe_all_providers(&config, &InspectorFactory::new(RpcClientFactory)).await; + let report = probe_all_providers(&config, &InspectorFactory).await; // Then assert_eq!( @@ -672,7 +668,7 @@ mod tests { )); // When - let report = probe_all_providers(&config, &InspectorFactory::new(RpcClientFactory)).await; + let report = probe_all_providers(&config, &InspectorFactory).await; // Then assert_eq!( @@ -702,7 +698,7 @@ mod tests { )); // When - let report = probe_all_providers(&config, &InspectorFactory::new(RpcClientFactory)).await; + let report = probe_all_providers(&config, &InspectorFactory).await; // Then assert_eq!( @@ -720,7 +716,7 @@ mod tests { )); // When - let report = probe_all_providers(&config, &InspectorFactory::new(RpcClientFactory)).await; + let report = probe_all_providers(&config, &InspectorFactory).await; // Then assert_eq!( @@ -739,7 +735,7 @@ mod tests { let config = starknet_only(chain_config(Some(MAINNET), providers)); // When - let report = probe_all_providers(&config, &InspectorFactory::new(RpcClientFactory)).await; + let report = probe_all_providers(&config, &InspectorFactory).await; // Then assert_eq!( @@ -769,7 +765,7 @@ mod tests { )); // When - let report = probe_all_providers(&config, &InspectorFactory::new(RpcClientFactory)).await; + let report = probe_all_providers(&config, &InspectorFactory).await; // Then assert_eq!( @@ -796,7 +792,7 @@ mod tests { }; // When - let report = probe_all_providers(&config, &InspectorFactory::new(RpcClientFactory)).await; + let report = probe_all_providers(&config, &InspectorFactory).await; // Then assert_eq!( @@ -831,7 +827,7 @@ mod tests { } // When - let report = probe_all_providers(&config, &InspectorFactory::new(RpcClientFactory)).await; + let report = probe_all_providers(&config, &InspectorFactory).await; // Then for EvmMainnet { chain, .. } in EVM_MAINNETS { @@ -857,7 +853,7 @@ mod tests { ); // When - let report = probe_all_providers(&config, &InspectorFactory::new(RpcClientFactory)).await; + let report = probe_all_providers(&config, &InspectorFactory).await; // Then assert_eq!( @@ -880,7 +876,7 @@ mod tests { )); // When - let report = probe_all_providers(&config, &InspectorFactory::new(RpcClientFactory)).await; + let report = probe_all_providers(&config, &InspectorFactory).await; // Then assert_eq!( @@ -900,7 +896,7 @@ mod tests { )); // When - let report = probe_all_providers(&config, &InspectorFactory::new(RpcClientFactory)).await; + let report = probe_all_providers(&config, &InspectorFactory).await; // Then assert_eq!( @@ -939,7 +935,7 @@ mod tests { }; // When - let report = probe_all_providers(&config, &InspectorFactory::new(RpcClientFactory)).await; + let report = probe_all_providers(&config, &InspectorFactory).await; // Then assert_eq!( @@ -962,7 +958,7 @@ mod tests { }; // When - let report = probe_all_providers(&config, &InspectorFactory::new(RpcClientFactory)).await; + let report = probe_all_providers(&config, &InspectorFactory).await; // Then assert_eq!( @@ -1053,7 +1049,7 @@ mod tests { )); // When - let report = probe_all_providers(&config, &InspectorFactory::new(RpcClientFactory)).await; + let report = probe_all_providers(&config, &InspectorFactory).await; // Then assert_eq!( @@ -1071,7 +1067,7 @@ mod tests { )); // When - let report = probe_all_providers(&config, &InspectorFactory::new(RpcClientFactory)).await; + let report = probe_all_providers(&config, &InspectorFactory).await; // Then assert_eq!( @@ -1125,7 +1121,7 @@ mod tests { )); // When - let report = probe_all_providers(&config, &InspectorFactory::new(RpcClientFactory)).await; + let report = probe_all_providers(&config, &InspectorFactory).await; // Then let ProviderStatus::WrongNetwork { observed, .. } = @@ -1163,7 +1159,7 @@ mod tests { let config = ForeignChainsConfig::default(); // When - let report = probe_all_providers(&config, &InspectorFactory::new(RpcClientFactory)).await; + let report = probe_all_providers(&config, &InspectorFactory).await; // Then assert!(report.rows().is_empty()); @@ -1192,7 +1188,7 @@ mod tests { )); // When - let report = probe_all_providers(&config, &InspectorFactory::new(RpcClientFactory)).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 6cea4507bf..6824a44c2b 100644 --- a/crates/foreign-chain-inspector/Cargo.toml +++ b/crates/foreign-chain-inspector/Cargo.toml @@ -8,6 +8,7 @@ license.workspace = true test-utils = [] [dependencies] +anyhow = { workspace = true } bs58 = { workspace = true } derive_more = { workspace = true } ethereum-types = { workspace = true } @@ -15,6 +16,7 @@ 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 } diff --git a/crates/foreign-chain-inspector/src/lib.rs b/crates/foreign-chain-inspector/src/lib.rs index 8d3a392e88..6a0a39b255 100644 --- a/crates/foreign-chain-inspector/src/lib.rs +++ b/crates/foreign-chain-inspector/src/lib.rs @@ -9,8 +9,9 @@ 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; @@ -236,6 +237,26 @@ pub trait ChainInspector: NetworkFingerprintInspector + Clone + Send + Sync + 's impl ChainInspector for T {} +/// Builds the inspector for one of a chain's providers. `Sync` because a caller that probes +/// several chains at once shares one factory across them. +/// +/// A test implements this and answers for the inspectors directly, building no client at all +/// (see [`mock`]). +pub trait BuildInspectors: Sync { + type Inspector: ChainInspector; + + /// `None` when no inspector exists to probe the chain. + /// + /// `timeout` reaches the transports that can hold one; the JSON-RPC chains take their deadline + /// from the caller instead. + 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); diff --git a/crates/foreign-chain-inspector/src/rpc_inspector.rs b/crates/foreign-chain-inspector/src/rpc_inspector.rs index 43e1a84452..815005c81e 100644 --- a/crates/foreign-chain-inspector/src/rpc_inspector.rs +++ b/crates/foreign-chain-inspector/src/rpc_inspector.rs @@ -10,40 +10,35 @@ 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::AptosRpcClient; -use foreign_chain_rpc_interfaces::sui::SuiRpcClient; -use jsonrpsee::core::client::ClientT; +use foreign_chain_rpc_interfaces::aptos::ReqwestAptosClient; +use foreign_chain_rpc_interfaces::sui::GrpcSuiClient; /// [`NetworkFingerprintInspector`] is not dyn compatible, so a caller that spans chains needs an /// enum rather than a trait object. #[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), +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 -where - JsonRpc: ClientT + Send + Sync, - Aptos: AptosRpcClient + Send + Sync, - Sui: SuiRpcClient + Send + Sync, -{ +impl NetworkFingerprintInspector for RpcInspector { async fn network_fingerprint(&self) -> Result { match self { Self::Abstract(inspector) => inspector.network_fingerprint().await, diff --git a/crates/foreign-chain-rpc-factory/Cargo.toml b/crates/foreign-chain-rpc-factory/Cargo.toml index e5f36bdd36..75afa3a804 100644 --- a/crates/foreign-chain-rpc-factory/Cargo.toml +++ b/crates/foreign-chain-rpc-factory/Cargo.toml @@ -9,7 +9,6 @@ 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 } diff --git a/crates/foreign-chain-rpc-factory/src/clients.rs b/crates/foreign-chain-rpc-factory/src/clients.rs deleted file mode 100644 index 93f055e066..0000000000 --- a/crates/foreign-chain-rpc-factory/src/clients.rs +++ /dev/null @@ -1,103 +0,0 @@ -//! Building the client that talks to one provider, with the provider's credentials applied. - -use std::time::Duration; - -use foreign_chain_inspector::{RpcAuthentication, build_http_client}; -use foreign_chain_rpc_interfaces::aptos::{AptosRpcClient, ReqwestAptosClient}; -use foreign_chain_rpc_interfaces::sui::{GrpcSuiClient, SuiRpcClient}; -use jsonrpsee::core::client::ClientT; -use jsonrpsee::http_client::HttpClient; -use mpc_node_config::ForeignChainProviderConfig; - -use crate::auth_config_to_rpc_auth; - -/// The transports a chain is reached over. [`RpcClientFactory`] reaches the real providers; a -/// caller that needs something else, a test most of all, implements this instead. -pub trait BuildRpcClients { - type JsonRpc: ClientT + Clone + Send + Sync + 'static; - type Aptos: AptosRpcClient + Clone + Send + Sync + 'static; - type Sui: SuiRpcClient + Clone + Send + Sync + 'static; - - fn json_rpc( - &self, - provider: &ForeignChainProviderConfig, - timeout: Duration, - ) -> anyhow::Result; - - fn aptos( - &self, - provider: &ForeignChainProviderConfig, - timeout: Duration, - ) -> anyhow::Result; - - fn sui( - &self, - provider: &ForeignChainProviderConfig, - timeout: Duration, - ) -> anyhow::Result; -} - -/// Reaches the real providers over the network, with each one's credentials applied. -#[derive(Clone, Copy)] -pub struct RpcClientFactory; - -impl RpcClientFactory { - fn authenticate( - provider: &ForeignChainProviderConfig, - ) -> anyhow::Result<(String, RpcAuthentication)> { - let mut url = provider.rpc_url.clone(); - let auth = auth_config_to_rpc_auth(provider.auth.clone(), &mut url)?; - Ok((url, auth)) - } - - fn auth_header(auth: RpcAuthentication) -> Option<(http::HeaderName, http::HeaderValue)> { - match auth { - RpcAuthentication::KeyInUrl => None, - RpcAuthentication::CustomHeader { - header_name, - header_value, - } => Some((header_name, header_value)), - } - } -} - -impl BuildRpcClients for RpcClientFactory { - type Aptos = ReqwestAptosClient; - type JsonRpc = HttpClient; - type Sui = GrpcSuiClient; - - /// jsonrpsee never sees the deadline: these chains stay bounded by the caller's own, as they - /// were before this factory existed. It stays in the signature for an implementation that - /// does want it. - fn json_rpc( - &self, - provider: &ForeignChainProviderConfig, - _timeout: Duration, - ) -> anyhow::Result { - let (url, auth) = Self::authenticate(provider)?; - Ok(build_http_client(url, auth)?) - } - - fn aptos( - &self, - provider: &ForeignChainProviderConfig, - timeout: Duration, - ) -> anyhow::Result { - let (url, auth) = Self::authenticate(provider)?; - Ok(ReqwestAptosClient::new( - url, - Self::auth_header(auth), - timeout, - )) - } - - fn sui( - &self, - provider: &ForeignChainProviderConfig, - timeout: Duration, - ) -> anyhow::Result { - let (url, auth) = Self::authenticate(provider)?; - GrpcSuiClient::new(url, Self::auth_header(auth), timeout) - .map_err(|e| anyhow::anyhow!("failed to build the Sui gRPC client: {e}")) - } -} diff --git a/crates/foreign-chain-rpc-factory/src/inspectors.rs b/crates/foreign-chain-rpc-factory/src/inspectors.rs index f1aa461de5..517d5012bb 100644 --- a/crates/foreign-chain-rpc-factory/src/inspectors.rs +++ b/crates/foreign-chain-rpc-factory/src/inspectors.rs @@ -1,51 +1,27 @@ -//! Building a chain's inspector for one of its providers. -//! -//! A caller that wants to answer for the inspectors themselves, a test most of all, implements -//! [`BuildInspectors`] and builds no client at all. +//! Building a chain's inspector for one of its providers, over the real network transports. use std::time::Duration; -use foreign_chain_inspector::ChainInspector; 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_inspector::{BuildInspectors, RpcAuthentication, build_http_client}; +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::clients::BuildRpcClients; +use crate::auth_config_to_rpc_auth; -/// `Sync` because a caller that probes several chains at once shares one factory across them. -pub trait BuildInspectors: Sync { - type Inspector: ChainInspector; +/// Builds each chain's inspector over the real providers, with each one's credentials applied. +#[derive(Clone, Copy)] +pub struct InspectorFactory; - /// `None` when no inspector exists to probe the chain. - /// - /// `timeout` reaches the transports that can hold one; the JSON-RPC chains take their deadline - /// from the caller instead. - fn build( - &self, - chain: ForeignChain, - provider: &ForeignChainProviderConfig, - timeout: Duration, - ) -> anyhow::Result>; -} - -/// Builds each chain's inspector over the clients it was given. -pub struct InspectorFactory { - clients: Clients, -} - -impl InspectorFactory { - pub fn new(clients: Clients) -> Self { - Self { clients } - } -} - -impl BuildInspectors for InspectorFactory { - type Inspector = RpcInspector; +impl BuildInspectors for InspectorFactory { + type Inspector = RpcInspector; fn build( &self, @@ -53,46 +29,49 @@ impl BuildInspectors for InspectorFactory anyhow::Result> { + let (url, auth) = Self::authenticate(provider)?; + let auth_header = Self::auth_header(auth.clone()); Ok(Some(match chain { ForeignChain::Abstract => { - RpcInspector::Abstract(EvmInspector::new(self.clients.json_rpc(provider, timeout)?)) + RpcInspector::Abstract(EvmInspector::new(build_http_client(url, auth)?)) } ForeignChain::Adi => { - RpcInspector::Adi(EvmInspector::new(self.clients.json_rpc(provider, timeout)?)) - } - ForeignChain::Aptos => { - RpcInspector::Aptos(AptosInspector::new(self.clients.aptos(provider, timeout)?)) + RpcInspector::Adi(EvmInspector::new(build_http_client(url, auth)?)) } + ForeignChain::Aptos => RpcInspector::Aptos(AptosInspector::new( + ReqwestAptosClient::new(url, auth_header, timeout), + )), ForeignChain::Arbitrum => { - RpcInspector::Arbitrum(EvmInspector::new(self.clients.json_rpc(provider, timeout)?)) + RpcInspector::Arbitrum(EvmInspector::new(build_http_client(url, auth)?)) + } + ForeignChain::Avalanche => { + RpcInspector::Avalanche(EvmInspector::new(build_http_client(url, auth)?)) } - ForeignChain::Avalanche => RpcInspector::Avalanche(EvmInspector::new( - self.clients.json_rpc(provider, timeout)?, - )), ForeignChain::Base => { - RpcInspector::Base(EvmInspector::new(self.clients.json_rpc(provider, timeout)?)) + RpcInspector::Base(EvmInspector::new(build_http_client(url, auth)?)) + } + ForeignChain::Bitcoin => { + RpcInspector::Bitcoin(BitcoinInspector::new(build_http_client(url, auth)?)) } - ForeignChain::Bitcoin => RpcInspector::Bitcoin(BitcoinInspector::new( - self.clients.json_rpc(provider, timeout)?, - )), ForeignChain::Bnb => { - RpcInspector::Bnb(EvmInspector::new(self.clients.json_rpc(provider, timeout)?)) + RpcInspector::Bnb(EvmInspector::new(build_http_client(url, auth)?)) } ForeignChain::Ethereum => { - RpcInspector::Ethereum(EvmInspector::new(self.clients.json_rpc(provider, timeout)?)) + RpcInspector::Ethereum(EvmInspector::new(build_http_client(url, auth)?)) } ForeignChain::HyperEvm => { - RpcInspector::HyperEvm(EvmInspector::new(self.clients.json_rpc(provider, timeout)?)) + RpcInspector::HyperEvm(EvmInspector::new(build_http_client(url, auth)?)) } ForeignChain::Polygon => { - RpcInspector::Polygon(EvmInspector::new(self.clients.json_rpc(provider, timeout)?)) + RpcInspector::Polygon(EvmInspector::new(build_http_client(url, auth)?)) } - ForeignChain::Starknet => RpcInspector::Starknet(StarknetInspector::new( - self.clients.json_rpc(provider, timeout)?, - )), - ForeignChain::Sui => { - RpcInspector::Sui(SuiInspector::new(self.clients.sui(provider, timeout)?)) + ForeignChain::Starknet => { + RpcInspector::Starknet(StarknetInspector::new(build_http_client(url, auth)?)) } + ForeignChain::Sui => 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), @@ -100,6 +79,26 @@ impl BuildInspectors for InspectorFactory anyhow::Result<(String, RpcAuthentication)> { + let mut url = provider.rpc_url.clone(); + let auth = auth_config_to_rpc_auth(provider.auth.clone(), &mut url)?; + Ok((url, auth)) + } + + fn auth_header(auth: RpcAuthentication) -> Option<(http::HeaderName, http::HeaderValue)> { + match auth { + RpcAuthentication::KeyInUrl => None, + RpcAuthentication::CustomHeader { + header_name, + header_value, + } => Some((header_name, header_value)), + } + } +} + #[cfg(test)] #[expect(non_snake_case)] mod tests { @@ -111,7 +110,6 @@ mod tests { use near_mpc_bounded_collections::NonEmptyBTreeMap; use super::*; - use crate::clients::RpcClientFactory; /// Set exhaustively, so a chain added to the config has to be answered for here too. fn every_configurable_chain() -> ForeignChainsConfig { @@ -151,7 +149,7 @@ mod tests { async fn build__should_cover_every_configurable_chain_that_has_an_inspector() { // Given let config = every_configurable_chain(); - let factory = InspectorFactory::new(RpcClientFactory); + let factory = InspectorFactory; // When let uncovered: Vec<_> = config diff --git a/crates/foreign-chain-rpc-factory/src/lib.rs b/crates/foreign-chain-rpc-factory/src/lib.rs index 2b47af3d06..b1e278c4a3 100644 --- a/crates/foreign-chain-rpc-factory/src/lib.rs +++ b/crates/foreign-chain-rpc-factory/src/lib.rs @@ -1,8 +1,6 @@ //! Building what talks to a foreign chain, from one provider's configuration: its credentials -//! resolved here, the client that carries them in [`clients`], and the inspector over that client -//! in [`inspectors`]. +//! resolved here, and the inspector that carries them in [`inspectors`]. -pub mod clients; pub mod inspectors; use anyhow::Context; diff --git a/crates/node/src/foreign_chain_probe.rs b/crates/node/src/foreign_chain_probe.rs index cd884845fc..b6c6789e94 100644 --- a/crates/node/src/foreign_chain_probe.rs +++ b/crates/node/src/foreign_chain_probe.rs @@ -7,7 +7,6 @@ use std::future::Future; use foreign_chain_health_check::probe::{ ProbeReport, ProviderHealth, ProviderStatus, probe_all_providers, }; -use foreign_chain_rpc_factory::clients::RpcClientFactory; use foreign_chain_rpc_factory::inspectors::InspectorFactory; use mpc_node_config::ForeignChainsConfig; use near_mpc_contract_interface::types as dtos; @@ -24,8 +23,11 @@ pub async fn run_periodic_probe(foreign_chains: ForeignChainsConfig, ticker: imp return; } - let inspectors = InspectorFactory::new(RpcClientFactory); - probe_periodically(|| probe_all_providers(&foreign_chains, &inspectors), ticker).await; + probe_periodically( + || probe_all_providers(&foreign_chains, &InspectorFactory), + ticker, + ) + .await; } async fn probe_periodically>( From 9a40dfe83668ba04e5ef375e0662a24618d1c30c Mon Sep 17 00:00:00 2001 From: Haiyue Chen Date: Fri, 28 Aug 2026 15:03:51 +0200 Subject: [PATCH 08/14] refactor: move timeout_of onto ForeignChainConfig and generalize mock failures --- crates/foreign-chain-health-check/src/lib.rs | 14 ++-- .../foreign-chain-health-check/src/probe.rs | 70 +++++++--------- crates/foreign-chain-inspector/src/lib.rs | 2 +- crates/foreign-chain-inspector/src/mock.rs | 81 ++++++++++--------- .../src/rpc_inspector.rs | 4 - .../src/inspectors.rs | 73 ----------------- crates/foreign-chain-rpc-factory/src/lib.rs | 3 - crates/node-config/src/foreign_chains.rs | 6 ++ 8 files changed, 85 insertions(+), 168 deletions(-) diff --git a/crates/foreign-chain-health-check/src/lib.rs b/crates/foreign-chain-health-check/src/lib.rs index c517c9bc02..87fe130d5a 100644 --- a/crates/foreign-chain-health-check/src/lib.rs +++ b/crates/foreign-chain-health-check/src/lib.rs @@ -134,10 +134,6 @@ 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() } @@ -182,7 +178,7 @@ 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() { @@ -211,7 +207,7 @@ 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() { @@ -240,7 +236,7 @@ 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() { @@ -269,7 +265,7 @@ 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)) { @@ -312,7 +308,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:#}")), diff --git a/crates/foreign-chain-health-check/src/probe.rs b/crates/foreign-chain-health-check/src/probe.rs index 6b81332664..670e760529 100644 --- a/crates/foreign-chain-health-check/src/probe.rs +++ b/crates/foreign-chain-health-check/src/probe.rs @@ -10,8 +10,6 @@ use mpc_node_config::{ForeignChainConfig, ForeignChainProviderConfig, ForeignCha use near_mpc_bounded_collections::NonEmptyVec; use near_mpc_contract_interface::types::{ForeignChain, ProviderId}; -use crate::timeout_of; - /// One provider's verdict. Anything other than [`ProviderStatus::Healthy`] is unhealthy. #[derive(Debug, Clone, PartialEq, Eq)] pub enum ProviderStatus { @@ -95,7 +93,7 @@ where InspectorFactory: BuildInspectors, { let probe_attempts = config.iter_chains().map(|(chain, chain_config)| { - let timeout = timeout_of(chain_config); + let timeout = chain_config.timeout_duration(); async move { probe_chain(chain, chain_config, |provider| { inspectors.build(chain, provider, timeout) @@ -123,7 +121,7 @@ where for (name, provider) in config.providers.iter() { let provider_id = ProviderId(name.as_str().to_owned()); match build_new_inspector(provider) { - // No inspector for the chain, so none of its providers can be asked. + // 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 { @@ -146,7 +144,7 @@ where 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 { @@ -209,15 +207,15 @@ fn classify( #[cfg(test)] #[expect(non_snake_case)] mod tests { - use foreign_chain_inspector::mock::{ScriptedInspector, ScriptedReply}; + use foreign_chain_inspector::mock::{MockInspector, MockReply}; - /// Hands the probe a scripted inspector per provider URL, so nothing builds a client. - struct ScriptedInspectors(std::collections::BTreeMap); + /// Hands the probe a mock inspector per provider URL. + struct MockInspectors(std::collections::BTreeMap); - impl ScriptedInspectors { - fn new<'a>(scripts: impl IntoIterator) -> Self { + impl MockInspectors { + fn new<'a>(inspectors: impl IntoIterator) -> Self { Self( - scripts + inspectors .into_iter() .map(|(url, inspector)| (url.to_string(), inspector)) .collect(), @@ -225,19 +223,19 @@ mod tests { } } - impl BuildInspectors for ScriptedInspectors { - type Inspector = ScriptedInspector; + impl BuildInspectors for MockInspectors { + type Inspector = MockInspector; fn build( &self, _chain: ForeignChain, provider: &ForeignChainProviderConfig, _timeout: std::time::Duration, - ) -> anyhow::Result> { + ) -> anyhow::Result> { let inspector = self .0 .get(&provider.rpc_url) - .unwrap_or_else(|| panic!("no inspector scripted for `{}`", provider.rpc_url)); + .unwrap_or_else(|| panic!("no mock inspector for `{}`", provider.rpc_url)); Ok(Some(inspector.clone())) } } @@ -457,8 +455,8 @@ mod tests { .await } - fn answering(fingerprint: &str) -> ScriptedReply { - ScriptedReply::Answer { + fn answering(fingerprint: &str) -> MockReply { + MockReply::Answer { delay: std::time::Duration::ZERO, fingerprint: fingerprint.to_string(), } @@ -478,10 +476,10 @@ mod tests { #[tokio::test] async fn probe_all_providers__should_report_a_provider_on_the_expected_network_as_healthy() { // Given - let url = "http://scripted.invalid/only"; + let url = "http://mock.invalid/only"; let config = starknet_only(chain_config(Some(MAINNET), one_provider("publicnode", url))); - let inspector = ScriptedInspector::new([answering(MAINNET)]); - let inspectors = ScriptedInspectors::new([(url, inspector.clone())]); + let inspector = MockInspector::new([answering(MAINNET)]); + let inspectors = MockInspectors::new([(url, inspector.clone())]); // When let report = probe_all_providers(&config, &inspectors).await; @@ -497,10 +495,9 @@ mod tests { #[tokio::test] async fn probe_all_providers__should_report_a_provider_on_another_network_as_wrong_network() { // Given - let url = "http://scripted.invalid/only"; + let url = "http://mock.invalid/only"; let config = starknet_only(chain_config(Some(MAINNET), one_provider("publicnode", url))); - let inspectors = - ScriptedInspectors::new([(url, ScriptedInspector::new([answering(SEPOLIA)]))]); + let inspectors = MockInspectors::new([(url, MockInspector::new([answering(SEPOLIA)]))]); // When let report = probe_all_providers(&config, &inspectors).await; @@ -578,15 +575,13 @@ mod tests { #[tokio::test] async fn probe_all_providers__should_report_a_provider_refusing_the_request_without_retrying() { // Given - let url = "http://scripted.invalid/only"; - let inspector = ScriptedInspector::new([ScriptedReply::Refusal { - delay: std::time::Duration::ZERO, - }]); + 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", url)), 3, )); - let inspectors = ScriptedInspectors::new([(url, inspector.clone())]); + let inspectors = MockInspectors::new([(url, inspector.clone())]); // When let report = probe_all_providers(&config, &inspectors).await; @@ -642,10 +637,9 @@ mod tests { #[tokio::test(start_paused = true)] async fn probe_all_providers__should_report_a_provider_that_does_not_answer_in_time() { // Given - let url = "http://scripted.invalid/slow"; + let url = "http://mock.invalid/slow"; let config = starknet_only(chain_config(Some(MAINNET), one_provider("slow", url))); - let inspectors = - ScriptedInspectors::new([(url, ScriptedInspector::new([ScriptedReply::Hang]))]); + let inspectors = MockInspectors::new([(url, MockInspector::new([MockReply::Hang]))]); // When let report = probe_all_providers(&config, &inspectors).await; @@ -1079,20 +1073,16 @@ mod tests { #[tokio::test(start_paused = true)] async fn probe_all_providers__should_retry_a_provider_that_refused_with_a_rate_limit_code() { // Given - let url = "http://scripted.invalid/keyed"; - let inspector = ScriptedInspector::new([ - ScriptedReply::TransientFailure { - delay: std::time::Duration::from_millis(10), - }, - ScriptedReply::TransientFailure { - delay: std::time::Duration::from_millis(10), - }, + 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", url)), 2, )); - let inspectors = ScriptedInspectors::new([(url, inspector.clone())]); + let inspectors = MockInspectors::new([(url, inspector.clone())]); // When let report = probe_all_providers(&config, &inspectors).await; diff --git a/crates/foreign-chain-inspector/src/lib.rs b/crates/foreign-chain-inspector/src/lib.rs index 6a0a39b255..a2bb4340b1 100644 --- a/crates/foreign-chain-inspector/src/lib.rs +++ b/crates/foreign-chain-inspector/src/lib.rs @@ -241,7 +241,7 @@ impl ChainInspec /// several chains at once shares one factory across them. /// /// A test implements this and answers for the inspectors directly, building no client at all -/// (see [`mock`]). +/// (see the `mock` module). pub trait BuildInspectors: Sync { type Inspector: ChainInspector; diff --git a/crates/foreign-chain-inspector/src/mock.rs b/crates/foreign-chain-inspector/src/mock.rs index da196cd1f9..35a7f9a369 100644 --- a/crates/foreign-chain-inspector/src/mock.rs +++ b/crates/foreign-chain-inspector/src/mock.rs @@ -1,8 +1,7 @@ -//! Scripted test doubles for the network fingerprint probe. +//! Test doubles for the Foreign Tx Inspectors. //! -//! Scripted delays are virtual timers under `#[tokio::test(start_paused = true)]`, so retry, -//! backoff and timeout run in microseconds. Never mix paused time with a real socket (httpmock, -//! tonic): the runtime advances the clock while the socket is silent and fires the timeout first. +//! 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}; @@ -11,37 +10,51 @@ use std::time::Duration; use crate::{ForeignChainInspectionError, NetworkFingerprint, NetworkFingerprintInspector}; -/// One scripted attempt. Outcomes are built per attempt because [`ForeignChainInspectionError`] -/// is not `Clone`. #[derive(Debug)] -pub enum ScriptedReply { - /// Keep `fingerprint` under [`NetworkFingerprint`]'s length cap, or it is truncated on the - /// way out. +pub enum MockReply { Answer { delay: Duration, fingerprint: String, }, - /// [`FanOut`](crate::FanOut) retries a transient failure. - TransientFailure { delay: Duration }, - /// [`FanOut`](crate::FanOut) does not retry a refusal. - Refusal { delay: Duration }, - /// Never resolves; only the caller's timeout ends the attempt. + Fail { + delay: Duration, + error: ForeignChainInspectionError, + }, + /// Never resolves Hang, } -/// Answers from a queue of [`ScriptedReply`]s and panics past the end of the script, so an -/// unexpected extra attempt fails loudly. Clones share the queue and the counter, so give each -/// provider its own and keep a clone for [`ScriptedInspector::calls`]. +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 ScriptedInspector { - script: Arc>>, +pub struct MockInspector { + replies: Arc>>, calls: Arc, } -impl ScriptedInspector { - pub fn new(replies: impl IntoIterator) -> Self { +impl MockInspector { + pub fn new(replies: impl IntoIterator) -> Self { Self { - script: Arc::new(Mutex::new(replies.into_iter().collect())), + replies: Arc::new(Mutex::new(replies.into_iter().collect())), calls: Arc::new(AtomicUsize::new(0)), } } @@ -51,33 +64,25 @@ impl ScriptedInspector { } } -impl NetworkFingerprintInspector for ScriptedInspector { +impl NetworkFingerprintInspector for MockInspector { async fn network_fingerprint(&self) -> Result { self.calls.fetch_add(1, Ordering::SeqCst); let reply = self - .script + .replies .lock() - .expect("script mutex poisoned") + .expect("replies mutex poisoned") .pop_front() - .expect("call beyond the script"); + .expect("call beyond the queued replies"); match reply { - ScriptedReply::Answer { delay, fingerprint } => { + MockReply::Answer { delay, fingerprint } => { tokio::time::sleep(delay).await; Ok(NetworkFingerprint::new(fingerprint)) } - ScriptedReply::TransientFailure { delay } => { - tokio::time::sleep(delay).await; - Err(ForeignChainInspectionError::RpcRequestFailed( - "scripted transient failure".to_string(), - )) - } - ScriptedReply::Refusal { delay } => { + MockReply::Fail { delay, error } => { tokio::time::sleep(delay).await; - Err(ForeignChainInspectionError::RpcRequestRejected( - "scripted refusal".to_string(), - )) + Err(error) } - ScriptedReply::Hang => std::future::pending().await, + MockReply::Hang => std::future::pending().await, } } diff --git a/crates/foreign-chain-inspector/src/rpc_inspector.rs b/crates/foreign-chain-inspector/src/rpc_inspector.rs index 815005c81e..0d0a9c1931 100644 --- a/crates/foreign-chain-inspector/src/rpc_inspector.rs +++ b/crates/foreign-chain-inspector/src/rpc_inspector.rs @@ -1,5 +1,3 @@ -//! One type holding any chain's inspector. - use crate::abstract_chain::inspector::Abstract; use crate::adi::inspector::Adi; use crate::aptos::inspector::AptosInspector; @@ -19,8 +17,6 @@ use crate::{ForeignChainInspectionError, NetworkFingerprint, NetworkFingerprintI use foreign_chain_rpc_interfaces::aptos::ReqwestAptosClient; use foreign_chain_rpc_interfaces::sui::GrpcSuiClient; -/// [`NetworkFingerprintInspector`] is not dyn compatible, so a caller that spans chains needs an -/// enum rather than a trait object. #[derive(Clone)] pub enum RpcInspector { Abstract(EvmInspector), diff --git a/crates/foreign-chain-rpc-factory/src/inspectors.rs b/crates/foreign-chain-rpc-factory/src/inspectors.rs index 517d5012bb..651aaa7ee8 100644 --- a/crates/foreign-chain-rpc-factory/src/inspectors.rs +++ b/crates/foreign-chain-rpc-factory/src/inspectors.rs @@ -1,5 +1,3 @@ -//! Building a chain's inspector for one of its providers, over the real network transports. - use std::time::Duration; use foreign_chain_inspector::aptos::inspector::AptosInspector; @@ -16,7 +14,6 @@ use near_mpc_contract_interface::types::ForeignChain; use crate::auth_config_to_rpc_auth; -/// Builds each chain's inspector over the real providers, with each one's credentials applied. #[derive(Clone, Copy)] pub struct InspectorFactory; @@ -98,73 +95,3 @@ impl InspectorFactory { } } } - -#[cfg(test)] -#[expect(non_snake_case)] -mod tests { - use std::num::NonZeroU64; - - use mpc_node_config::{ - AuthConfig, ForeignChainConfig, ForeignChainProviderConfig, ForeignChainsConfig, - }; - use near_mpc_bounded_collections::NonEmptyBTreeMap; - - use super::*; - - /// Set exhaustively, so a chain added to the config has to be answered for here too. - fn every_configurable_chain() -> ForeignChainsConfig { - let section = || { - Some(ForeignChainConfig { - timeout_sec: NonZeroU64::new(1).unwrap(), - max_retries: NonZeroU64::new(1).unwrap(), - expected_network_fingerprint: None, - providers: NonEmptyBTreeMap::new( - "only".to_string().into(), - ForeignChainProviderConfig { - rpc_url: "http://127.0.0.1:9".to_string(), - auth: AuthConfig::None, - }, - ), - }) - }; - ForeignChainsConfig { - solana: section(), - bitcoin: section(), - ethereum: section(), - abstract_chain: section(), - starknet: section(), - bnb: section(), - base: section(), - arbitrum: section(), - hyper_evm: section(), - polygon: section(), - aptos: section(), - sui: section(), - avalanche: section(), - adi: section(), - } - } - - #[tokio::test] - async fn build__should_cover_every_configurable_chain_that_has_an_inspector() { - // Given - let config = every_configurable_chain(); - let factory = InspectorFactory; - - // When - let uncovered: Vec<_> = config - .iter_chains() - .filter(|(chain, chain_config)| { - let provider = chain_config.providers.iter().next().expect("a provider").1; - factory - .build(*chain, provider, Duration::from_secs(1)) - .expect("the provider is well formed") - .is_none() - }) - .map(|(chain, _)| chain) - .collect(); - - // Then - assert_eq!(uncovered, vec![ForeignChain::Solana]); - } -} diff --git a/crates/foreign-chain-rpc-factory/src/lib.rs b/crates/foreign-chain-rpc-factory/src/lib.rs index b1e278c4a3..f7a5c43f5e 100644 --- a/crates/foreign-chain-rpc-factory/src/lib.rs +++ b/crates/foreign-chain-rpc-factory/src/lib.rs @@ -1,6 +1,3 @@ -//! Building what talks to a foreign chain, from one provider's configuration: its credentials -//! resolved here, and the inspector that carries them in [`inspectors`]. - pub mod inspectors; use anyhow::Context; 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, From 15a20c3857ae9dd2f500dc0db536930974c32b22 Mon Sep 17 00:00:00 2001 From: Haiyue Chen Date: Fri, 28 Aug 2026 15:24:32 +0200 Subject: [PATCH 09/14] chore: trim the inspector trait docs to the load-bearing points --- crates/foreign-chain-inspector/src/lib.rs | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/crates/foreign-chain-inspector/src/lib.rs b/crates/foreign-chain-inspector/src/lib.rs index a2bb4340b1..7744204ebf 100644 --- a/crates/foreign-chain-inspector/src/lib.rs +++ b/crates/foreign-chain-inspector/src/lib.rs @@ -232,23 +232,18 @@ where } } -/// What a caller needs of an inspector to hold it, clone it into tasks and keep it alive. +/// An inspector a caller can hold, clone and share across chains. pub trait ChainInspector: NetworkFingerprintInspector + Clone + Send + Sync + 'static {} impl ChainInspector for T {} -/// Builds the inspector for one of a chain's providers. `Sync` because a caller that probes -/// several chains at once shares one factory across them. -/// -/// A test implements this and answers for the inspectors directly, building no client at all -/// (see the `mock` module). +/// Builds the inspector for one of a chain's providers. `Sync` so one factory serves all the +/// chains a caller probes at once; tests implement it to answer without a network (see `mock`). pub trait BuildInspectors: Sync { type Inspector: ChainInspector; - /// `None` when no inspector exists to probe the chain. - /// - /// `timeout` reaches the transports that can hold one; the JSON-RPC chains take their deadline - /// from the caller instead. + /// `None` when the chain has no inspector to probe. `timeout` reaches the transports that can + /// hold one; the JSON-RPC chains take their deadline from the caller instead. fn build( &self, chain: ForeignChain, From 3d9dc87b6d9f016489cf4fbef8bd56fadc4c8a4a Mon Sep 17 00:00:00 2001 From: Haiyue Chen Date: Fri, 28 Aug 2026 15:29:48 +0200 Subject: [PATCH 10/14] chore: trim the inspector seam docs to the load-bearing points --- crates/foreign-chain-inspector/src/lib.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/crates/foreign-chain-inspector/src/lib.rs b/crates/foreign-chain-inspector/src/lib.rs index 7744204ebf..493abf65ff 100644 --- a/crates/foreign-chain-inspector/src/lib.rs +++ b/crates/foreign-chain-inspector/src/lib.rs @@ -237,13 +237,9 @@ pub trait ChainInspector: NetworkFingerprintInspector + Clone + Send + Sync + 's impl ChainInspector for T {} -/// Builds the inspector for one of a chain's providers. `Sync` so one factory serves all the -/// chains a caller probes at once; tests implement it to answer without a network (see `mock`). pub trait BuildInspectors: Sync { type Inspector: ChainInspector; - /// `None` when the chain has no inspector to probe. `timeout` reaches the transports that can - /// hold one; the JSON-RPC chains take their deadline from the caller instead. fn build( &self, chain: ForeignChain, From 75fb5e59bf6aa5ebf59f11f3f01c2eb9a71ed72c Mon Sep 17 00:00:00 2001 From: Haiyue Chen Date: Fri, 28 Aug 2026 15:41:08 +0200 Subject: [PATCH 11/14] chore: drop the dev dependencies orphaned by the deleted rpc_inspector tests --- crates/foreign-chain-rpc-factory/Cargo.toml | 2 -- 1 file changed, 2 deletions(-) diff --git a/crates/foreign-chain-rpc-factory/Cargo.toml b/crates/foreign-chain-rpc-factory/Cargo.toml index 75afa3a804..97c3165290 100644 --- a/crates/foreign-chain-rpc-factory/Cargo.toml +++ b/crates/foreign-chain-rpc-factory/Cargo.toml @@ -15,8 +15,6 @@ url = { workspace = true } [dev-dependencies] assert_matches = { workspace = true } -near-mpc-bounded-collections = { workspace = true } -tokio = { workspace = true } [lints] workspace = true From 875ad4f4a997dcc1967c0fcca91c3cfd4fa5cef0 Mon Sep 17 00:00:00 2001 From: Haiyue Chen Date: Fri, 28 Aug 2026 15:45:55 +0200 Subject: [PATCH 12/14] chore: sync Cargo.lock with the pruned rpc-factory deps --- Cargo.lock | 2 -- 1 file changed, 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 31073ca8de..df8ef22f51 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4024,9 +4024,7 @@ dependencies = [ "foreign-chain-rpc-interfaces", "http", "mpc-node-config", - "near-mpc-bounded-collections", "near-mpc-contract-interface", - "tokio", "url", ] From 397768f1db9e1a0f6f6fc4a4e0d73d774a79afe8 Mon Sep 17 00:00:00 2001 From: Haiyue Chen Date: Mon, 31 Aug 2026 10:45:31 +0200 Subject: [PATCH 13/14] refactor: move build_http_client and resolve_provider_auth into the rpc factory --- Cargo.lock | 2 + crates/foreign-chain-health-check/src/lib.rs | 44 ++--------- crates/foreign-chain-inspector/Cargo.toml | 3 +- crates/foreign-chain-inspector/src/lib.rs | 41 ---------- .../tests/abstract_rpc_manual.rs | 19 ++--- .../tests/adi_rpc_manual.rs | 19 ++--- .../tests/arbitrum_rpc_manual.rs | 19 ++--- .../tests/avalanche_rpc_manual.rs | 19 ++--- .../tests/base_rpc_manual.rs | 19 ++--- .../tests/bitcoin_inspector.rs | 17 +++- .../tests/bitcoin_rpc_manual.rs | 19 ++--- .../tests/bnb_rpc_manual.rs | 19 ++--- .../tests/ethereum_rpc_manual.rs | 19 ++--- .../tests/evm_inspector.rs | 18 +++-- .../tests/hyperevm_rpc_manual.rs | 19 ++--- .../tests/polygon_rpc_manual.rs | 19 ++--- .../tests/starknet_inspector.rs | 15 +++- .../tests/starknet_rpc_manual.rs | 19 ++--- crates/foreign-chain-rpc-factory/Cargo.toml | 1 + .../src/inspectors.rs | 72 +++++++---------- crates/foreign-chain-rpc-factory/src/lib.rs | 78 +++++++++++++++---- .../foreign-chain-rpc-interfaces/src/aptos.rs | 2 +- .../node/src/providers/verify_foreign_tx.rs | 44 ++++------- 23 files changed, 262 insertions(+), 284 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index df8ef22f51..df7d2f4484 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3995,6 +3995,7 @@ dependencies = [ "bs58 0.5.1", "derive_more 2.1.1", "ethereum-types", + "foreign-chain-rpc-factory", "foreign-chain-rpc-interfaces", "hex", "http", @@ -4023,6 +4024,7 @@ dependencies = [ "foreign-chain-inspector", "foreign-chain-rpc-interfaces", "http", + "jsonrpsee", "mpc-node-config", "near-mpc-contract-interface", "url", diff --git a/crates/foreign-chain-health-check/src/lib.rs b/crates/foreign-chain-health-check/src/lib.rs index 87fe130d5a..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_factory::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}; @@ -138,27 +135,6 @@ 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,7 +158,7 @@ async fn run_evm( 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,7 +187,7 @@ async fn run_bitcoin( 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,7 +216,7 @@ async fn run_starknet( 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)) => { @@ -268,7 +244,7 @@ async fn run_aptos( 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))) => { @@ -326,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-inspector/Cargo.toml b/crates/foreign-chain-inspector/Cargo.toml index 6824a44c2b..6ada695690 100644 --- a/crates/foreign-chain-inspector/Cargo.toml +++ b/crates/foreign-chain-inspector/Cargo.toml @@ -14,7 +14,6 @@ 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 } @@ -27,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/lib.rs b/crates/foreign-chain-inspector/src/lib.rs index 493abf65ff..fd86ad5a98 100644 --- a/crates/foreign-chain-inspector/src/lib.rs +++ b/crates/foreign-chain-inspector/src/lib.rs @@ -4,11 +4,9 @@ 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::{ForeignChain, ProviderId}; @@ -292,19 +290,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); @@ -524,32 +509,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 { 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-factory/Cargo.toml b/crates/foreign-chain-rpc-factory/Cargo.toml index 97c3165290..72fb10b1c4 100644 --- a/crates/foreign-chain-rpc-factory/Cargo.toml +++ b/crates/foreign-chain-rpc-factory/Cargo.toml @@ -9,6 +9,7 @@ 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 } diff --git a/crates/foreign-chain-rpc-factory/src/inspectors.rs b/crates/foreign-chain-rpc-factory/src/inspectors.rs index 651aaa7ee8..3c757e1d89 100644 --- a/crates/foreign-chain-rpc-factory/src/inspectors.rs +++ b/crates/foreign-chain-rpc-factory/src/inspectors.rs @@ -1,18 +1,18 @@ 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_inspector::{BuildInspectors, RpcAuthentication, build_http_client}; 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::auth_config_to_rpc_auth; +use crate::{build_http_client, resolve_provider_auth}; #[derive(Clone, Copy)] pub struct InspectorFactory; @@ -26,72 +26,54 @@ impl BuildInspectors for InspectorFactory { provider: &ForeignChainProviderConfig, timeout: Duration, ) -> anyhow::Result> { - let (url, auth) = Self::authenticate(provider)?; - let auth_header = Self::auth_header(auth.clone()); Ok(Some(match chain { ForeignChain::Abstract => { - RpcInspector::Abstract(EvmInspector::new(build_http_client(url, auth)?)) + RpcInspector::Abstract(EvmInspector::new(build_http_client(provider)?)) } - ForeignChain::Adi => { - RpcInspector::Adi(EvmInspector::new(build_http_client(url, auth)?)) + 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::Aptos => RpcInspector::Aptos(AptosInspector::new( - ReqwestAptosClient::new(url, auth_header, timeout), - )), ForeignChain::Arbitrum => { - RpcInspector::Arbitrum(EvmInspector::new(build_http_client(url, auth)?)) + RpcInspector::Arbitrum(EvmInspector::new(build_http_client(provider)?)) } ForeignChain::Avalanche => { - RpcInspector::Avalanche(EvmInspector::new(build_http_client(url, auth)?)) + RpcInspector::Avalanche(EvmInspector::new(build_http_client(provider)?)) } ForeignChain::Base => { - RpcInspector::Base(EvmInspector::new(build_http_client(url, auth)?)) + RpcInspector::Base(EvmInspector::new(build_http_client(provider)?)) } ForeignChain::Bitcoin => { - RpcInspector::Bitcoin(BitcoinInspector::new(build_http_client(url, auth)?)) - } - ForeignChain::Bnb => { - RpcInspector::Bnb(EvmInspector::new(build_http_client(url, auth)?)) + 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(url, auth)?)) + RpcInspector::Ethereum(EvmInspector::new(build_http_client(provider)?)) } ForeignChain::HyperEvm => { - RpcInspector::HyperEvm(EvmInspector::new(build_http_client(url, auth)?)) + RpcInspector::HyperEvm(EvmInspector::new(build_http_client(provider)?)) } ForeignChain::Polygon => { - RpcInspector::Polygon(EvmInspector::new(build_http_client(url, auth)?)) + RpcInspector::Polygon(EvmInspector::new(build_http_client(provider)?)) } ForeignChain::Starknet => { - RpcInspector::Starknet(StarknetInspector::new(build_http_client(url, auth)?)) + 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::Sui => 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), })) } } - -impl InspectorFactory { - fn authenticate( - provider: &ForeignChainProviderConfig, - ) -> anyhow::Result<(String, RpcAuthentication)> { - let mut url = provider.rpc_url.clone(); - let auth = auth_config_to_rpc_auth(provider.auth.clone(), &mut url)?; - Ok((url, auth)) - } - - fn auth_header(auth: RpcAuthentication) -> Option<(http::HeaderName, http::HeaderValue)> { - match auth { - RpcAuthentication::KeyInUrl => None, - RpcAuthentication::CustomHeader { - header_name, - header_value, - } => Some((header_name, header_value)), - } - } -} diff --git a/crates/foreign-chain-rpc-factory/src/lib.rs b/crates/foreign-chain-rpc-factory/src/lib.rs index f7a5c43f5e..04ef9382b1 100644 --- a/crates/foreign-chain-rpc-factory/src/lib.rs +++ b/crates/foreign-chain-rpc-factory/src/lib.rs @@ -1,17 +1,26 @@ +use anyhow::Context; +use http::{HeaderMap, HeaderName, HeaderValue}; +use jsonrpsee::http_client::{HttpClient, HttpClientBuilder}; +use mpc_node_config::{AuthConfig, ForeignChainProviderConfig}; + pub mod inspectors; -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( +#[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 { @@ -27,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); @@ -54,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 { @@ -98,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(), @@ -118,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(), @@ -140,7 +184,7 @@ mod tests { fn auth_config_to_rpc_auth__header_auth_without_scheme_uses_raw_token() { // 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(), @@ -162,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(), 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/src/providers/verify_foreign_tx.rs b/crates/node/src/providers/verify_foreign_tx.rs index 6e8bc42ab5..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_factory::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, From 1886e3153d1d8068867c85178346f13bc5f3b070 Mon Sep 17 00:00:00 2001 From: Haiyue Chen Date: Mon, 31 Aug 2026 10:56:14 +0200 Subject: [PATCH 14/14] chore: drop the ChainInspector doc that restates its bounds --- crates/foreign-chain-inspector/src/lib.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/foreign-chain-inspector/src/lib.rs b/crates/foreign-chain-inspector/src/lib.rs index fd86ad5a98..da4efff810 100644 --- a/crates/foreign-chain-inspector/src/lib.rs +++ b/crates/foreign-chain-inspector/src/lib.rs @@ -230,7 +230,6 @@ where } } -/// An inspector a caller can hold, clone and share across chains. pub trait ChainInspector: NetworkFingerprintInspector + Clone + Send + Sync + 'static {} impl ChainInspector for T {}