Skip to content

Commit

Permalink
feat: add auth field to RPCEndpointConfig (#8570)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
Co-authored-by: Matthias Seitz <[email protected]>
  • Loading branch information
3 people authored Aug 8, 2024
1 parent 14e50ed commit 56cd9a9
Show file tree
Hide file tree
Showing 2 changed files with 162 additions and 6 deletions.
96 changes: 90 additions & 6 deletions crates/config/src/endpoints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
}
}
}
Expand Down Expand Up @@ -210,6 +223,58 @@ impl From<RpcEndpoint> 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<String, UnresolvedEnvVarError> {
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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.to_string())
}
}

impl<'de> Deserialize<'de> for RpcAuth {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
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 {
Expand All @@ -226,6 +291,9 @@ pub struct RpcEndpointConfig {
///
/// See also <https://docs.alchemy.com/reference/compute-units#what-are-cups-compute-units-per-second>
pub compute_units_per_second: Option<u64>,

/// Token to be used as authentication
pub auth: Option<RpcAuth>,
}

impl RpcEndpointConfig {
Expand All @@ -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}")?;

Expand All @@ -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(())
}
}
Expand All @@ -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()
}
}
Expand All @@ -299,12 +372,18 @@ impl<'de> Deserialize<'de> for RpcEndpointConfig {
retries: Option<u32>,
retry_backoff: Option<u64>,
compute_units_per_second: Option<u64>,
auth: Option<RpcAuth>,
}

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 })
}
}

Expand All @@ -321,6 +400,7 @@ impl Default for RpcEndpointConfig {
retries: None,
retry_backoff: None,
compute_units_per_second: None,
auth: None,
}
}
}
Expand All @@ -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<String, Result<String, UnresolvedEnvVarError>>,
auths: BTreeMap<String, Result<Option<String>, UnresolvedEnvVarError>>,
}

impl ResolvedRpcEndpoints {
Expand Down Expand Up @@ -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!(
Expand All @@ -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())),
}
);

Expand All @@ -386,6 +469,7 @@ mod tests {
retries: None,
retry_backoff: None,
compute_units_per_second: None,
auth: None,
}
);
}
Expand Down
72 changes: 72 additions & 0 deletions crates/config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -3449,6 +3450,7 @@ mod tests {
retries: Some(3),
retry_backoff: Some(1000),
compute_units_per_second: Some(1000),
auth: None,
})
),
]),
Expand All @@ -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| {
Expand Down

0 comments on commit 56cd9a9

Please sign in to comment.