From 56cd9a94434a5de63ef0fd655b0d6d856f1e45c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ca=C3=ADque=20Porfirio?= <56317416+caiquejjx@users.noreply.github.com> Date: Thu, 8 Aug 2024 11:09:52 -0300 Subject: [PATCH] feat: add `auth` field to `RPCEndpointConfig` (#8570) * add auth parsing in RPC config * add comment explaining auth param * add missing field in test * fix formatting * fix formatting * fix failing test * fix failing test * undo wrong formatting * remove reminiscent ; * auth option as enum to be able to resolve env vars * add test for auth resolving and new field to resolved endpoint --------- Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com> Co-authored-by: Matthias Seitz --- crates/config/src/endpoints.rs | 96 +++++++++++++++++++++++++++++++--- crates/config/src/lib.rs | 72 +++++++++++++++++++++++++ 2 files changed, 162 insertions(+), 6 deletions(-) diff --git a/crates/config/src/endpoints.rs b/crates/config/src/endpoints.rs index eabc5acb196e..78cda4f7343b 100644 --- a/crates/config/src/endpoints.rs +++ b/crates/config/src/endpoints.rs @@ -41,7 +41,20 @@ impl RpcEndpoints { /// Returns all (alias -> url) pairs pub fn resolved(self) -> ResolvedRpcEndpoints { ResolvedRpcEndpoints { - endpoints: self.endpoints.into_iter().map(|(name, e)| (name, e.resolve())).collect(), + endpoints: self + .endpoints + .clone() + .into_iter() + .map(|(name, e)| (name, e.resolve())) + .collect(), + auths: self + .endpoints + .into_iter() + .map(|(name, e)| match e.auth { + Some(auth) => (name, auth.resolve().map(Some)), + None => (name, Ok(None)), + }) + .collect(), } } } @@ -210,6 +223,58 @@ impl From for RpcEndpointConfig { } } +/// The auth token to be used for RPC endpoints +/// It works in the same way as the `RpcEndpoint` type, where it can be a raw string or a reference +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum RpcAuth { + Raw(String), + Env(String), +} + +impl RpcAuth { + /// Returns the auth token this type holds + /// + /// # Error + /// + /// Returns an error if the type holds a reference to an env var and the env var is not set + pub fn resolve(self) -> Result { + match self { + Self::Raw(raw_auth) => Ok(raw_auth), + Self::Env(var) => interpolate(&var), + } + } +} + +impl fmt::Display for RpcAuth { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Raw(url) => url.fmt(f), + Self::Env(var) => var.fmt(f), + } + } +} + +impl Serialize for RpcAuth { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +impl<'de> Deserialize<'de> for RpcAuth { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let val = String::deserialize(deserializer)?; + let auth = if RE_PLACEHOLDER.is_match(&val) { Self::Env(val) } else { Self::Raw(val) }; + + Ok(auth) + } +} + /// Rpc endpoint configuration variant #[derive(Debug, Clone, PartialEq, Eq)] pub struct RpcEndpointConfig { @@ -226,6 +291,9 @@ pub struct RpcEndpointConfig { /// /// See also pub compute_units_per_second: Option, + + /// Token to be used as authentication + pub auth: Option, } impl RpcEndpointConfig { @@ -237,7 +305,7 @@ impl RpcEndpointConfig { impl fmt::Display for RpcEndpointConfig { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let Self { endpoint, retries, retry_backoff, compute_units_per_second } = self; + let Self { endpoint, retries, retry_backoff, compute_units_per_second, auth } = self; write!(f, "{endpoint}")?; @@ -253,6 +321,10 @@ impl fmt::Display for RpcEndpointConfig { write!(f, ", compute_units_per_second={compute_units_per_second}")?; } + if let Some(auth) = auth { + write!(f, ", auth={auth}")?; + } + Ok(()) } } @@ -274,6 +346,7 @@ impl Serialize for RpcEndpointConfig { map.serialize_entry("retries", &self.retries)?; map.serialize_entry("retry_backoff", &self.retry_backoff)?; map.serialize_entry("compute_units_per_second", &self.compute_units_per_second)?; + map.serialize_entry("auth", &self.auth)?; map.end() } } @@ -299,12 +372,18 @@ impl<'de> Deserialize<'de> for RpcEndpointConfig { retries: Option, retry_backoff: Option, compute_units_per_second: Option, + auth: Option, } - let RpcEndpointConfigInner { endpoint, retries, retry_backoff, compute_units_per_second } = - serde_json::from_value(value).map_err(serde::de::Error::custom)?; + let RpcEndpointConfigInner { + endpoint, + retries, + retry_backoff, + compute_units_per_second, + auth, + } = serde_json::from_value(value).map_err(serde::de::Error::custom)?; - Ok(Self { endpoint, retries, retry_backoff, compute_units_per_second }) + Ok(Self { endpoint, retries, retry_backoff, compute_units_per_second, auth }) } } @@ -321,6 +400,7 @@ impl Default for RpcEndpointConfig { retries: None, retry_backoff: None, compute_units_per_second: None, + auth: None, } } } @@ -331,6 +411,7 @@ pub struct ResolvedRpcEndpoints { /// contains all named endpoints and their URL or an error if we failed to resolve the env var /// alias endpoints: BTreeMap>, + auths: BTreeMap, UnresolvedEnvVarError>>, } impl ResolvedRpcEndpoints { @@ -364,7 +445,8 @@ mod tests { "endpoint": "http://localhost:8545", "retries": 5, "retry_backoff": 250, - "compute_units_per_second": 100 + "compute_units_per_second": 100, + "auth": "Bearer 123" }"#; let config: RpcEndpointConfig = serde_json::from_str(s).unwrap(); assert_eq!( @@ -374,6 +456,7 @@ mod tests { retries: Some(5), retry_backoff: Some(250), compute_units_per_second: Some(100), + auth: Some(RpcAuth::Raw("Bearer 123".to_string())), } ); @@ -386,6 +469,7 @@ mod tests { retries: None, retry_backoff: None, compute_units_per_second: None, + auth: None, } ); } diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs index d9c762581027..7033cc31a0dc 100644 --- a/crates/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -2799,6 +2799,7 @@ mod tests { endpoints::{RpcEndpointConfig, RpcEndpointType}, etherscan::ResolvedEtherscanConfigs, }; + use endpoints::RpcAuth; use figment::error::Kind::InvalidType; use foundry_compilers::artifacts::{ vyper::VyperOptimizationMode, ModelCheckerEngine, YulDetails, @@ -3449,6 +3450,7 @@ mod tests { retries: Some(3), retry_backoff: Some(1000), compute_units_per_second: Some(1000), + auth: None, }) ), ]), @@ -3471,6 +3473,76 @@ mod tests { }) } + #[test] + fn test_resolve_auth() { + figment::Jail::expect_with(|jail| { + jail.create_file( + "foundry.toml", + r#" + [profile.default] + eth_rpc_url = "optimism" + [rpc_endpoints] + optimism = "https://example.com/" + mainnet = { endpoint = "${_CONFIG_MAINNET}", retries = 3, retry_backoff = 1000, compute_units_per_second = 1000, auth = "Bearer ${_CONFIG_AUTH}" } + "#, + )?; + + let config = Config::load(); + + jail.set_env("_CONFIG_AUTH", "123456"); + jail.set_env("_CONFIG_MAINNET", "https://eth-mainnet.alchemyapi.io/v2/123455"); + + assert_eq!( + RpcEndpoints::new([ + ( + "optimism", + RpcEndpointType::String(RpcEndpoint::Url( + "https://example.com/".to_string() + )) + ), + ( + "mainnet", + RpcEndpointType::Config(RpcEndpointConfig { + endpoint: RpcEndpoint::Env("${_CONFIG_MAINNET}".to_string()), + retries: Some(3), + retry_backoff: Some(1000), + compute_units_per_second: Some(1000), + auth: Some(RpcAuth::Env("Bearer ${_CONFIG_AUTH}".to_string())), + }) + ), + ]), + config.rpc_endpoints + ); + let resolved = config.rpc_endpoints.resolved(); + assert_eq!( + RpcEndpoints::new([ + ( + "optimism", + RpcEndpointType::String(RpcEndpoint::Url( + "https://example.com/".to_string() + )) + ), + ( + "mainnet", + RpcEndpointType::Config(RpcEndpointConfig { + endpoint: RpcEndpoint::Url( + "https://eth-mainnet.alchemyapi.io/v2/123455".to_string() + ), + retries: Some(3), + retry_backoff: Some(1000), + compute_units_per_second: Some(1000), + auth: Some(RpcAuth::Raw("Bearer 123456".to_string())), + }) + ), + ]) + .resolved(), + resolved + ); + + Ok(()) + }); + } + #[test] fn test_resolve_endpoints() { figment::Jail::expect_with(|jail| {