diff --git a/packages/ciphernode/Cargo.lock b/packages/ciphernode/Cargo.lock index c8f87742..83bf4899 100644 --- a/packages/ciphernode/Cargo.lock +++ b/packages/ciphernode/Cargo.lock @@ -2311,7 +2311,9 @@ dependencies = [ "alloy-primitives 0.6.4", "anyhow", "async-trait", + "base64", "cipher 0.1.0", + "config", "data", "enclave-core", "enclave_node", @@ -2320,6 +2322,7 @@ dependencies = [ "sortition", "tokio", "tracing", + "url", "zeroize", ] @@ -3081,6 +3084,124 @@ dependencies = [ "tracing", ] +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.72", +] + [[package]] name = "idna" version = "0.4.0" @@ -3093,12 +3214,23 @@ dependencies = [ [[package]] name = "idna" -version = "0.5.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" dependencies = [ - "unicode-bidi", - "unicode-normalization", + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", ] [[package]] @@ -3774,6 +3906,12 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +[[package]] +name = "litemap" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" + [[package]] name = "lock_api" version = "0.4.12" @@ -5651,6 +5789,12 @@ dependencies = [ "der", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "static_assertions" version = "1.1.0" @@ -5931,6 +6075,16 @@ dependencies = [ "crunchy", ] +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + [[package]] name = "tinyvec" version = "1.8.0" @@ -6296,12 +6450,12 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.2" +version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", - "idna 0.5.0", + "idna 1.0.3", "percent-encoding", ] @@ -6311,6 +6465,18 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "utf8parse" version = "0.2.2" @@ -6730,6 +6896,18 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + [[package]] name = "ws_stream_wasm" version = "0.7.4" @@ -6799,6 +6977,30 @@ dependencies = [ "time", ] +[[package]] +name = "yoke" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.72", + "synstructure", +] + [[package]] name = "zerocopy" version = "0.7.35" @@ -6820,6 +7022,27 @@ dependencies = [ "syn 2.0.72", ] +[[package]] +name = "zerofrom" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.72", + "synstructure", +] + [[package]] name = "zeroize" version = "1.8.1" @@ -6839,3 +7062,25 @@ dependencies = [ "quote", "syn 2.0.72", ] + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.72", +] diff --git a/packages/ciphernode/Cargo.toml b/packages/ciphernode/Cargo.toml index a47e6edf..fa1f8ffe 100644 --- a/packages/ciphernode/Cargo.toml +++ b/packages/ciphernode/Cargo.toml @@ -54,6 +54,7 @@ sha2 = "0.10.8" tokio = { version = "1.38", features = ["full"] } tracing = "0.1.37" tracing-subscriber = { version = "0.3", features = ["env-filter"] } +url = "2.5.4" libp2p = { version = "0.54.1", features = [ "async-std", "identify", diff --git a/packages/ciphernode/config/src/app_config.rs b/packages/ciphernode/config/src/app_config.rs index ec340ccb..903af725 100644 --- a/packages/ciphernode/config/src/app_config.rs +++ b/packages/ciphernode/config/src/app_config.rs @@ -45,11 +45,27 @@ pub struct ContractAddresses { pub filter_registry: Contract, } +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] +#[serde(tag = "type", content = "credentials")] +pub enum RpcAuth { + None, + Basic { username: String, password: String }, + Bearer(String), +} + +impl Default for RpcAuth { + fn default() -> Self { + RpcAuth::None + } +} + #[derive(Debug, Deserialize, Serialize)] pub struct ChainConfig { pub enabled: Option, pub name: String, pub rpc_url: String, // We may need multiple per chain for redundancy at a later point + #[serde(default)] + pub rpc_auth: RpcAuth, pub contracts: ContractAddresses, } @@ -326,11 +342,16 @@ mod tests { let filedir = format!("{}/.config/enclave", home); jail.create_dir(filedir)?; jail.create_file( - filename, + filename.clone(), r#" chains: - name: "hardhat" rpc_url: "ws://localhost:8545" + rpc_auth: + type: "Basic" + credentials: + username: "testUser" + password: "testPassword" contracts: enclave: "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0" ciphernode_registry: @@ -340,8 +361,8 @@ chains: "#, )?; - let config: AppConfig = load_config(None).map_err(|err| err.to_string())?; - let chain = config.chains().first().unwrap(); + let mut config: AppConfig = load_config(None).map_err(|err| err.to_string())?; + let mut chain = config.chains().first().unwrap(); assert_eq!(chain.name, "hardhat"); assert_eq!(chain.rpc_url, "ws://localhost:8545"); @@ -353,11 +374,60 @@ chains: chain.contracts.ciphernode_registry.address(), "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9" ); + assert_eq!( + chain.rpc_auth, + RpcAuth::Basic { + username: "testUser".to_string(), + password: "testPassword".to_string(), + } + ); assert_eq!(chain.contracts.enclave.deploy_block(), None); assert_eq!( chain.contracts.ciphernode_registry.deploy_block(), Some(1764352873645) ); + + jail.create_file( + filename.clone(), + r#" +chains: + - name: "hardhat" + rpc_url: "ws://localhost:8545" + contracts: + enclave: "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0" + ciphernode_registry: + address: "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9" + deploy_block: 1764352873645 + filter_registry: "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9" +"#, + )?; + config = load_config(None).map_err(|err| err.to_string())?; + chain = config.chains().first().unwrap(); + + assert_eq!(chain.rpc_auth, RpcAuth::None); + + jail.create_file( + filename, + r#" +chains: + - name: "hardhat" + rpc_url: "ws://localhost:8545" + rpc_auth: + type: "Bearer" + credentials: "testToken" + contracts: + enclave: "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0" + ciphernode_registry: + address: "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9" + deploy_block: 1764352873645 + filter_registry: "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9" +"#, + )?; + + config = load_config(None).map_err(|err| err.to_string())?; + chain = config.chains().first().unwrap(); + assert_eq!(chain.rpc_auth, RpcAuth::Bearer("testToken".to_string())); + Ok(()) }); } diff --git a/packages/ciphernode/enclave_node/src/aggregator.rs b/packages/ciphernode/enclave_node/src/aggregator.rs index bdb4b8c5..a63bf636 100644 --- a/packages/ciphernode/enclave_node/src/aggregator.rs +++ b/packages/ciphernode/enclave_node/src/aggregator.rs @@ -4,10 +4,7 @@ use cipher::Cipher; use config::AppConfig; use enclave_core::EventBus; use evm::{ - helpers::{ - create_provider_with_signer, create_readonly_provider, ensure_http_rpc, ensure_ws_rpc, - get_signer_from_repository, - }, + helpers::{get_signer_from_repository, ProviderConfig, RPC}, CiphernodeRegistrySol, EnclaveSol, RegistryFilterSol, }; use logger::SimpleLogger; @@ -45,10 +42,12 @@ pub async fn setup_aggregator( .iter() .filter(|chain| chain.enabled.unwrap_or(true)) { - let rpc_url = &chain.rpc_url; - let read_provider = create_readonly_provider(&ensure_ws_rpc(rpc_url)).await?; - let write_provider = - create_provider_with_signer(&ensure_http_rpc(rpc_url), &signer).await?; + let rpc_url = RPC::from_url(&chain.rpc_url).map_err(|e| { + anyhow::anyhow!("Failed to parse RPC URL for chain {}: {}", chain.name, e) + })?; + let provider_config = ProviderConfig::new(rpc_url, chain.rpc_auth.clone()); + let read_provider = provider_config.create_readonly_provider().await?; + let write_provider = provider_config.create_ws_signer_provider(&signer).await?; EnclaveSol::attach( &bus, diff --git a/packages/ciphernode/enclave_node/src/ciphernode.rs b/packages/ciphernode/enclave_node/src/ciphernode.rs index c8dedf14..30386ab2 100644 --- a/packages/ciphernode/enclave_node/src/ciphernode.rs +++ b/packages/ciphernode/enclave_node/src/ciphernode.rs @@ -5,7 +5,7 @@ use cipher::Cipher; use config::AppConfig; use enclave_core::{get_tag, EventBus}; use evm::{ - helpers::{create_readonly_provider, ensure_ws_rpc}, + helpers::{ProviderConfig, RPC}, CiphernodeRegistrySol, EnclaveSolReader, }; use logger::SimpleLogger; @@ -44,9 +44,11 @@ pub async fn setup_ciphernode( .iter() .filter(|chain| chain.enabled.unwrap_or(true)) { - let rpc_url = &chain.rpc_url; - - let read_provider = create_readonly_provider(&ensure_ws_rpc(rpc_url)).await?; + let rpc_url = RPC::from_url(&chain.rpc_url).map_err(|e| { + anyhow::anyhow!("Failed to parse RPC URL for chain {}: {}", chain.name, e) + })?; + let provider_config = ProviderConfig::new(rpc_url, chain.rpc_auth.clone()); + let read_provider = provider_config.create_readonly_provider().await?; EnclaveSolReader::attach( &bus, &read_provider, diff --git a/packages/ciphernode/evm/Cargo.toml b/packages/ciphernode/evm/Cargo.toml index fba4aea4..0a50c22a 100644 --- a/packages/ciphernode/evm/Cargo.toml +++ b/packages/ciphernode/evm/Cargo.toml @@ -9,14 +9,17 @@ alloy = { workspace = true } alloy-primitives = { workspace = true } anyhow = { workspace = true } async-trait = { workspace = true } -enclave-core = { path = "../core" } +base64 = { workspace = true } +cipher = { path = "../cipher" } +config = { path = "../config" } data = { path = "../data" } +enclave-core = { path = "../core" } futures-util = { workspace = true } sortition = { path = "../sortition" } -cipher = { path = "../cipher" } +serde = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } -serde = { workspace = true } +url = { workspace = true } zeroize = { workspace = true } [dev-dependencies] diff --git a/packages/ciphernode/evm/src/ciphernode_registry_sol.rs b/packages/ciphernode/evm/src/ciphernode_registry_sol.rs index d88e6552..a616411b 100644 --- a/packages/ciphernode/evm/src/ciphernode_registry_sol.rs +++ b/packages/ciphernode/evm/src/ciphernode_registry_sol.rs @@ -8,6 +8,7 @@ use alloy::{ primitives::{LogData, B256}, sol, sol_types::SolEvent, + transports::BoxTransport, }; use anyhow::Result; use data::Repository; @@ -102,7 +103,7 @@ pub struct CiphernodeRegistrySolReader; impl CiphernodeRegistrySolReader { pub async fn attach( bus: &Addr, - provider: &WithChainId, + provider: &WithChainId, contract_address: &str, repository: &Repository, start_block: Option, @@ -128,7 +129,7 @@ pub struct CiphernodeRegistrySol; impl CiphernodeRegistrySol { pub async fn attach( bus: &Addr, - provider: &WithChainId, + provider: &WithChainId, contract_address: &str, repository: &Repository, start_block: Option, diff --git a/packages/ciphernode/evm/src/enclave_sol.rs b/packages/ciphernode/evm/src/enclave_sol.rs index ed1f76bc..bcdcab98 100644 --- a/packages/ciphernode/evm/src/enclave_sol.rs +++ b/packages/ciphernode/evm/src/enclave_sol.rs @@ -2,9 +2,10 @@ use crate::{ enclave_sol_reader::EnclaveSolReader, enclave_sol_writer::EnclaveSolWriter, event_reader::EvmEventReaderState, - helpers::{ReadonlyProvider, SignerProvider, WithChainId}, + helpers::{ReadonlyProvider, RpcWSClient, SignerProvider, WithChainId}, }; use actix::Addr; +use alloy::transports::BoxTransport; use anyhow::Result; use data::Repository; use enclave_core::EventBus; @@ -13,8 +14,8 @@ pub struct EnclaveSol; impl EnclaveSol { pub async fn attach( bus: &Addr, - read_provider: &WithChainId, - write_provider: &WithChainId, + read_provider: &WithChainId, + write_provider: &WithChainId, RpcWSClient>, contract_address: &str, repository: &Repository, start_block: Option, diff --git a/packages/ciphernode/evm/src/enclave_sol_writer.rs b/packages/ciphernode/evm/src/enclave_sol_writer.rs index 77d53ab7..36b519b0 100644 --- a/packages/ciphernode/evm/src/enclave_sol_writer.rs +++ b/packages/ciphernode/evm/src/enclave_sol_writer.rs @@ -1,5 +1,4 @@ -use crate::helpers::SignerProvider; -use crate::helpers::WithChainId; +use crate::helpers::{RpcWSClient, SignerProvider, WithChainId}; use actix::prelude::*; use actix::Addr; use alloy::{primitives::Address, sol}; @@ -21,7 +20,7 @@ sol!( /// Consumes events from the event bus and calls EVM methods on the Enclave.sol contract pub struct EnclaveSolWriter { - provider: WithChainId, + provider: WithChainId, RpcWSClient>, contract_address: Address, bus: Addr, } @@ -29,7 +28,7 @@ pub struct EnclaveSolWriter { impl EnclaveSolWriter { pub fn new( bus: &Addr, - provider: &WithChainId, + provider: &WithChainId, RpcWSClient>, contract_address: Address, ) -> Result { Ok(Self { @@ -41,7 +40,7 @@ impl EnclaveSolWriter { pub async fn attach( bus: &Addr, - provider: &WithChainId, + provider: &WithChainId, RpcWSClient>, contract_address: &str, ) -> Result> { let addr = EnclaveSolWriter::new(bus, provider, contract_address.parse()?)?.start(); @@ -108,7 +107,7 @@ impl Handler for EnclaveSolWriter { } async fn publish_plaintext_output( - provider: WithChainId, + provider: WithChainId, RpcWSClient>, contract_address: Address, e3_id: E3id, decrypted_output: Vec, diff --git a/packages/ciphernode/evm/src/helpers.rs b/packages/ciphernode/evm/src/helpers.rs index 116f3c2c..9f6d7373 100644 --- a/packages/ciphernode/evm/src/helpers.rs +++ b/packages/ciphernode/evm/src/helpers.rs @@ -7,21 +7,120 @@ use alloy::{ }, Identity, Provider, ProviderBuilder, RootProvider, }, + pubsub::PubSubFrontend, + rpc::client::RpcClient, signers::local::PrivateKeySigner, - transports::{BoxTransport, Transport}, + transports::{ + http::{ + reqwest::{ + header::{HeaderMap, HeaderValue, AUTHORIZATION}, + Client, + }, + Http, + }, + ws::WsConnect, + Authorization, BoxTransport, Transport, + }, }; use anyhow::{bail, Context, Result}; +use base64::{engine::general_purpose::STANDARD, Engine}; use cipher::Cipher; +use config::RpcAuth; use data::Repository; use std::{env, marker::PhantomData, sync::Arc}; +use url::Url; use zeroize::Zeroizing; +#[derive(Clone)] +pub enum RPC { + Http(String), + Https(String), + Ws(String), + Wss(String), +} + +impl RPC { + pub fn from_url(url: &str) -> Result { + let parsed = Url::parse(url).context("Invalid URL format")?; + match parsed.scheme() { + "http" => Ok(RPC::Http(url.to_string())), + "https" => Ok(RPC::Https(url.to_string())), + "ws" => Ok(RPC::Ws(url.to_string())), + "wss" => Ok(RPC::Wss(url.to_string())), + _ => bail!("Invalid protocol. Expected: http://, https://, ws://, wss://"), + } + } + + pub fn as_http_url(&self) -> String { + match self { + RPC::Http(url) | RPC::Https(url) => url.clone(), + RPC::Ws(url) | RPC::Wss(url) => { + let mut parsed = Url::parse(url).expect(&format!("Failed to parse URL: {}", url)); + parsed + .set_scheme(if self.is_secure() { "https" } else { "http" }) + .expect("http(s) are valid schemes"); + parsed.to_string() + } + } + } + + pub fn as_ws_url(&self) -> String { + match self { + RPC::Ws(url) | RPC::Wss(url) => url.clone(), + RPC::Http(url) | RPC::Https(url) => { + let mut parsed = Url::parse(url).expect(&format!("Failed to parse URL: {}", url)); + parsed + .set_scheme(if self.is_secure() { "wss" } else { "ws" }) + .expect("ws(s) are valid schemes"); + parsed.to_string() + } + } + } + + pub fn is_websocket(&self) -> bool { + matches!(self, RPC::Ws(_) | RPC::Wss(_)) + } + + pub fn is_secure(&self) -> bool { + matches!(self, RPC::Https(_) | RPC::Wss(_)) + } +} + +pub trait AuthConversions { + fn to_header_value(&self) -> Option; + fn to_ws_auth(&self) -> Option; +} + +impl AuthConversions for RpcAuth { + fn to_header_value(&self) -> Option { + match self { + RpcAuth::None => None, + RpcAuth::Basic { username, password } => { + let auth = format!( + "Basic {}", + STANDARD.encode(Zeroizing::new(format!("{}:{}", username, password))) + ); + HeaderValue::from_str(&auth).ok() + } + RpcAuth::Bearer(token) => HeaderValue::from_str(&format!("Bearer {}", token)).ok(), + } + } + + fn to_ws_auth(&self) -> Option { + match self { + RpcAuth::None => None, + RpcAuth::Basic { username, password } => Some(Authorization::basic(username, password)), + RpcAuth::Bearer(token) => Some(Authorization::bearer(token)), + } + } +} + /// We need to cache the chainId so we can easily use it in a non-async situation /// This wrapper just stores the chain_id with the Provider #[derive(Clone)] // We have to be generic over T as the transport provider in order to handle different transport // mechanisms such as the HttpClient etc. -pub struct WithChainId +pub struct WithChainId where P: Provider, T: Transport + Clone, @@ -54,20 +153,9 @@ where } } -pub type ReadonlyProvider = RootProvider; - -pub async fn create_readonly_provider( - rpc_url: &str, -) -> Result> { - let provider = ProviderBuilder::new() - .on_builtin(rpc_url) - .await - .context("Could not create ReadOnlyProvider")? - .into(); - Ok(WithChainId::new(provider).await?) -} - -pub type SignerProvider = FillProvider< +pub type RpcWSClient = PubSubFrontend; +pub type RpcHttpClient = Http; +pub type SignerProvider = FillProvider< JoinFill< JoinFill< Identity, @@ -75,23 +163,95 @@ pub type SignerProvider = FillProvider< >, WalletFiller, >, - RootProvider, - BoxTransport, + RootProvider, + T, Ethereum, >; -pub async fn create_provider_with_signer( - rpc_url: &str, - signer: &Arc, -) -> Result> { - let wallet = EthereumWallet::from(signer.clone()); - let provider = ProviderBuilder::new() - .with_recommended_fillers() - .wallet(wallet) - .on_builtin(rpc_url) - .await?; - - Ok(WithChainId::new(provider).await?) +pub type ReadonlyProvider = RootProvider; + +#[derive(Clone)] +pub struct ProviderConfig { + rpc: RPC, + auth: RpcAuth, +} + +impl ProviderConfig { + pub fn new(rpc: RPC, auth: RpcAuth) -> Self { + Self { rpc, auth } + } + + async fn create_ws_provider(&self) -> Result> { + Ok(ProviderBuilder::new() + .on_ws(self.create_ws_connect()) + .await? + .boxed()) + } + + async fn create_http_provider(&self) -> Result> { + Ok(ProviderBuilder::new() + .on_client(self.create_http_client()?) + .boxed()) + } + + pub async fn create_readonly_provider( + &self, + ) -> Result> { + let provider = if self.rpc.is_websocket() { + self.create_ws_provider().await? + } else { + self.create_http_provider().await? + }; + WithChainId::new(provider).await + } + + pub async fn create_ws_signer_provider( + &self, + signer: &Arc, + ) -> Result, RpcWSClient>> { + let wallet = EthereumWallet::from(signer.clone()); + let provider = ProviderBuilder::new() + .with_recommended_fillers() + .wallet(wallet) + .on_ws(self.create_ws_connect()) + .await + .context("Failed to create WS signer provider")?; + + WithChainId::new(provider).await + } + + pub async fn create_http_signer_provider( + &self, + signer: &Arc, + ) -> Result, RpcHttpClient>> { + let wallet = EthereumWallet::from(signer.clone()); + let provider = ProviderBuilder::new() + .with_recommended_fillers() + .wallet(wallet) + .on_client(self.create_http_client()?); + WithChainId::new(provider).await + } + + fn create_ws_connect(&self) -> WsConnect { + if let Some(ws_auth) = self.auth.to_ws_auth() { + WsConnect::new(self.rpc.as_ws_url()).with_auth(ws_auth) + } else { + WsConnect::new(self.rpc.as_ws_url()) + } + } + + fn create_http_client(&self) -> Result>> { + let mut headers = HeaderMap::new(); + if let Some(auth_header) = self.auth.to_header_value() { + headers.insert(AUTHORIZATION, auth_header); + } + let client = Client::builder() + .default_headers(headers) + .build() + .context("Failed to create HTTP client")?; + let http = Http::with_client(client, self.rpc.as_http_url().parse()?); + Ok(RpcClient::new(http, false)) + } } pub async fn pull_eth_signer_from_env(var: &str) -> Result> { @@ -117,40 +277,49 @@ pub async fn get_signer_from_repository( Ok(Arc::new(signer)) } -pub fn ensure_http_rpc(rpc_url: &str) -> String { - if rpc_url.starts_with("ws://") { - return rpc_url.replacen("ws://", "http://", 1); - } else if rpc_url.starts_with("wss://") { - return rpc_url.replacen("wss://", "https://", 1); - } - rpc_url.to_string() -} - -pub fn ensure_ws_rpc(rpc_url: &str) -> String { - if rpc_url.starts_with("http://") { - return rpc_url.replacen("http://", "ws://", 1); - } else if rpc_url.starts_with("https://") { - return rpc_url.replacen("https://", "wss://", 1); - } - rpc_url.to_string() -} - #[cfg(test)] mod test { use super::*; #[test] - fn test_ensure_http_rpc() { - assert_eq!(ensure_http_rpc("http://foo.com"), "http://foo.com"); - assert_eq!(ensure_http_rpc("https://foo.com"), "https://foo.com"); - assert_eq!(ensure_http_rpc("ws://foo.com"), "http://foo.com"); - assert_eq!(ensure_http_rpc("wss://foo.com"), "https://foo.com"); + fn test_rpc_type_conversion() { + // Test HTTP URLs + let http = RPC::from_url("http://localhost:8545/").unwrap(); + assert!(matches!(http, RPC::Http(_))); + assert_eq!(http.as_http_url(), "http://localhost:8545/"); + assert_eq!(http.as_ws_url(), "ws://localhost:8545/"); + + // Test HTTPS URLs + let https = RPC::from_url("https://example.com/").unwrap(); + assert!(matches!(https, RPC::Https(_))); + assert_eq!(https.as_http_url(), "https://example.com/"); + assert_eq!(https.as_ws_url(), "wss://example.com/"); + + // Test WS URLs + let ws = RPC::from_url("ws://localhost:8545/").unwrap(); + assert!(matches!(ws, RPC::Ws(_))); + assert_eq!(ws.as_http_url(), "http://localhost:8545/"); + assert_eq!(ws.as_ws_url(), "ws://localhost:8545/"); + + // Test WSS URLs + let wss = RPC::from_url("wss://example.com/").unwrap(); + assert!(matches!(wss, RPC::Wss(_))); + assert_eq!(wss.as_http_url(), "https://example.com/"); + assert_eq!(wss.as_ws_url(), "wss://example.com/"); } + #[test] - fn test_ensure_ws_rpc() { - assert_eq!(ensure_ws_rpc("http://foo.com"), "ws://foo.com"); - assert_eq!(ensure_ws_rpc("https://foo.com"), "wss://foo.com"); - assert_eq!(ensure_ws_rpc("wss://foo.com"), "wss://foo.com"); - assert_eq!(ensure_ws_rpc("ws://foo.com"), "ws://foo.com"); + fn test_rpc_type_properties() { + assert!(!RPC::from_url("http://example.com/").unwrap().is_secure()); + assert!(RPC::from_url("https://example.com/").unwrap().is_secure()); + assert!(!RPC::from_url("ws://example.com/").unwrap().is_secure()); + assert!(RPC::from_url("wss://example.com/").unwrap().is_secure()); + + assert!(!RPC::from_url("http://example.com/").unwrap().is_websocket()); + assert!(!RPC::from_url("https://example.com/") + .unwrap() + .is_websocket()); + assert!(RPC::from_url("ws://example.com/").unwrap().is_websocket()); + assert!(RPC::from_url("wss://example.com/").unwrap().is_websocket()); } } diff --git a/packages/ciphernode/evm/src/registry_filter_sol.rs b/packages/ciphernode/evm/src/registry_filter_sol.rs index 2c8dedf9..b4b8f770 100644 --- a/packages/ciphernode/evm/src/registry_filter_sol.rs +++ b/packages/ciphernode/evm/src/registry_filter_sol.rs @@ -1,10 +1,9 @@ -use crate::helpers::{SignerProvider, WithChainId}; +use crate::helpers::{RpcWSClient, SignerProvider, WithChainId}; use actix::prelude::*; use alloy::{ primitives::{Address, Bytes, U256}, rpc::types::TransactionReceipt, sol, - transports::BoxTransport, }; use anyhow::Result; use enclave_core::{ @@ -20,7 +19,7 @@ sol!( ); pub struct RegistryFilterSolWriter { - provider: WithChainId, + provider: WithChainId, RpcWSClient>, contract_address: Address, bus: Addr, } @@ -28,7 +27,7 @@ pub struct RegistryFilterSolWriter { impl RegistryFilterSolWriter { pub async fn new( bus: &Addr, - provider: &WithChainId, + provider: &WithChainId, RpcWSClient>, contract_address: Address, ) -> Result { Ok(Self { @@ -40,7 +39,7 @@ impl RegistryFilterSolWriter { pub async fn attach( bus: &Addr, - provider: &WithChainId, + provider: &WithChainId, RpcWSClient>, contract_address: &str, ) -> Result> { let addr = RegistryFilterSolWriter::new(bus, provider, contract_address.parse()?) @@ -107,7 +106,7 @@ impl Handler for RegistryFilterSolWriter { } pub async fn publish_committee( - provider: WithChainId, + provider: WithChainId, RpcWSClient>, contract_address: Address, e3_id: E3id, nodes: OrderedSet, @@ -129,7 +128,7 @@ pub struct RegistryFilterSol; impl RegistryFilterSol { pub async fn attach( bus: &Addr, - provider: &WithChainId, + provider: &WithChainId, RpcWSClient>, contract_address: &str, ) -> Result<()> { RegistryFilterSolWriter::attach(bus, provider, contract_address).await?;