Skip to main content

foundry_config/
endpoints.rs

1//! Support for multiple RPC-endpoints
2
3use crate::resolve::{RE_PLACEHOLDER, UnresolvedEnvVarError, interpolate};
4use serde::{Deserialize, Deserializer, Serialize, Serializer, ser::SerializeMap};
5use std::{
6    collections::BTreeMap,
7    fmt,
8    ops::{Deref, DerefMut},
9};
10
11/// Container type for API endpoints, like various RPC endpoints
12#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(transparent)]
14pub struct RpcEndpoints {
15    endpoints: BTreeMap<String, RpcEndpoint>,
16}
17
18impl RpcEndpoints {
19    /// Creates a new list of endpoints
20    pub fn new(
21        endpoints: impl IntoIterator<Item = (impl Into<String>, impl Into<RpcEndpointType>)>,
22    ) -> Self {
23        Self {
24            endpoints: endpoints
25                .into_iter()
26                .map(|(name, e)| match e.into() {
27                    RpcEndpointType::String(url) => (name.into(), RpcEndpoint::new(url)),
28                    RpcEndpointType::Config(config) => (name.into(), config),
29                })
30                .collect(),
31        }
32    }
33
34    /// Returns `true` if this type doesn't contain any endpoints
35    pub fn is_empty(&self) -> bool {
36        self.endpoints.is_empty()
37    }
38
39    /// Returns all (alias -> rpc_endpoint) pairs
40    pub fn resolved(self) -> ResolvedRpcEndpoints {
41        ResolvedRpcEndpoints {
42            endpoints: self.endpoints.into_iter().map(|(name, e)| (name, e.resolve())).collect(),
43        }
44    }
45}
46
47impl Deref for RpcEndpoints {
48    type Target = BTreeMap<String, RpcEndpoint>;
49
50    fn deref(&self) -> &Self::Target {
51        &self.endpoints
52    }
53}
54
55/// RPC endpoint wrapper type
56#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
57#[serde(untagged)]
58pub enum RpcEndpointType {
59    /// Raw Endpoint url string
60    String(RpcEndpointUrl),
61    /// Config object
62    Config(RpcEndpoint),
63}
64
65impl RpcEndpointType {
66    /// Returns the url or config this type holds
67    ///
68    /// # Error
69    ///
70    /// Returns an error if the type holds a reference to an env var and the env var is not set
71    pub fn resolve(self) -> Result<String, UnresolvedEnvVarError> {
72        match self {
73            Self::String(url) => url.resolve(),
74            Self::Config(config) => config.endpoint.resolve(),
75        }
76    }
77}
78
79impl fmt::Display for RpcEndpointType {
80    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
81        match self {
82            Self::String(url) => url.fmt(f),
83            Self::Config(config) => config.fmt(f),
84        }
85    }
86}
87
88impl TryFrom<RpcEndpointType> for String {
89    type Error = UnresolvedEnvVarError;
90
91    fn try_from(value: RpcEndpointType) -> Result<Self, Self::Error> {
92        match value {
93            RpcEndpointType::String(url) => url.resolve(),
94            RpcEndpointType::Config(config) => config.endpoint.resolve(),
95        }
96    }
97}
98
99/// Represents a single endpoint
100///
101/// This type preserves the value as it's stored in the config. If the value is a reference to an
102/// env var, then the `Endpoint::Env` var will hold the reference (`${MAIN_NET}`) and _not_ the
103/// value of the env var itself.
104/// In other words, this type does not resolve env vars when it's being deserialized
105#[derive(Clone, Debug, PartialEq, Eq)]
106pub enum RpcEndpointUrl {
107    /// A raw Url (ws, http)
108    Url(String),
109    /// An endpoint that contains at least one `${ENV_VAR}` placeholder
110    ///
111    /// **Note:** this contains the endpoint as is, like `https://eth-mainnet.alchemyapi.io/v2/${API_KEY}` or `${EPC_ENV_VAR}`
112    Env(String),
113}
114
115impl RpcEndpointUrl {
116    /// Returns the url variant
117    pub fn as_url(&self) -> Option<&str> {
118        match self {
119            Self::Url(url) => Some(url),
120            Self::Env(_) => None,
121        }
122    }
123
124    /// Returns the url this type holds
125    ///
126    /// # Error
127    ///
128    /// Returns an error if the type holds a reference to an env var and the env var is not set
129    pub fn resolve(self) -> Result<String, UnresolvedEnvVarError> {
130        match self {
131            Self::Url(url) => Ok(url),
132            Self::Env(val) => interpolate(&val),
133        }
134    }
135}
136
137impl fmt::Display for RpcEndpointUrl {
138    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
139        match self {
140            Self::Url(url) => url.fmt(f),
141            Self::Env(var) => var.fmt(f),
142        }
143    }
144}
145
146impl TryFrom<RpcEndpointUrl> for String {
147    type Error = UnresolvedEnvVarError;
148
149    fn try_from(value: RpcEndpointUrl) -> Result<Self, Self::Error> {
150        value.resolve()
151    }
152}
153
154impl Serialize for RpcEndpointUrl {
155    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
156    where
157        S: Serializer,
158    {
159        serializer.serialize_str(&self.to_string())
160    }
161}
162
163impl<'de> Deserialize<'de> for RpcEndpointUrl {
164    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
165    where
166        D: Deserializer<'de>,
167    {
168        let val = String::deserialize(deserializer)?;
169        let endpoint = if RE_PLACEHOLDER.is_match(&val) { Self::Env(val) } else { Self::Url(val) };
170
171        Ok(endpoint)
172    }
173}
174
175impl From<RpcEndpointUrl> for RpcEndpointType {
176    fn from(endpoint: RpcEndpointUrl) -> Self {
177        Self::String(endpoint)
178    }
179}
180
181impl From<RpcEndpointUrl> for RpcEndpoint {
182    fn from(endpoint: RpcEndpointUrl) -> Self {
183        Self { endpoint, ..Default::default() }
184    }
185}
186
187/// The auth token to be used for RPC endpoints
188/// It works in the same way as the `RpcEndpoint` type, where it can be a raw string or a reference
189#[derive(Clone, Debug, PartialEq, Eq)]
190pub enum RpcAuth {
191    Raw(String),
192    Env(String),
193}
194
195impl RpcAuth {
196    /// Returns the auth token this type holds
197    ///
198    /// # Error
199    ///
200    /// Returns an error if the type holds a reference to an env var and the env var is not set
201    pub fn resolve(self) -> Result<String, UnresolvedEnvVarError> {
202        match self {
203            Self::Raw(raw_auth) => Ok(raw_auth),
204            Self::Env(var) => interpolate(&var),
205        }
206    }
207}
208
209impl fmt::Display for RpcAuth {
210    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
211        match self {
212            Self::Raw(url) => url.fmt(f),
213            Self::Env(var) => var.fmt(f),
214        }
215    }
216}
217
218impl Serialize for RpcAuth {
219    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
220    where
221        S: Serializer,
222    {
223        serializer.serialize_str(&self.to_string())
224    }
225}
226
227impl<'de> Deserialize<'de> for RpcAuth {
228    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
229    where
230        D: Deserializer<'de>,
231    {
232        let val = String::deserialize(deserializer)?;
233        let auth = if RE_PLACEHOLDER.is_match(&val) { Self::Env(val) } else { Self::Raw(val) };
234
235        Ok(auth)
236    }
237}
238
239// Rpc endpoint configuration
240#[derive(Debug, Clone, Default, PartialEq, Eq)]
241pub struct RpcEndpointConfig {
242    /// The number of retries.
243    pub retries: Option<u32>,
244
245    /// Initial retry backoff.
246    pub retry_backoff: Option<u64>,
247
248    /// The available compute units per second.
249    ///
250    /// See also <https://docs.alchemy.com/reference/compute-units#what-are-cups-compute-units-per-second>
251    pub compute_units_per_second: Option<u64>,
252}
253
254impl fmt::Display for RpcEndpointConfig {
255    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
256        let Self { retries, retry_backoff, compute_units_per_second } = self;
257
258        if let Some(retries) = retries {
259            write!(f, ", retries={retries}")?;
260        }
261
262        if let Some(retry_backoff) = retry_backoff {
263            write!(f, ", retry_backoff={retry_backoff}")?;
264        }
265
266        if let Some(compute_units_per_second) = compute_units_per_second {
267            write!(f, ", compute_units_per_second={compute_units_per_second}")?;
268        }
269
270        Ok(())
271    }
272}
273
274/// Rpc endpoint configuration variant
275#[derive(Debug, Clone, PartialEq, Eq)]
276pub struct RpcEndpoint {
277    /// endpoint url or env
278    pub endpoint: RpcEndpointUrl,
279
280    /// Token to be used as authentication
281    pub auth: Option<RpcAuth>,
282
283    /// additional configuration
284    pub config: RpcEndpointConfig,
285}
286
287impl RpcEndpoint {
288    pub fn new(endpoint: RpcEndpointUrl) -> Self {
289        Self { endpoint, ..Default::default() }
290    }
291
292    /// Resolves environment variables in fields into their raw values
293    pub fn resolve(self) -> ResolvedRpcEndpoint {
294        ResolvedRpcEndpoint {
295            endpoint: self.endpoint.resolve(),
296            auth: self.auth.map(|auth| auth.resolve()),
297            config: self.config,
298        }
299    }
300}
301
302impl fmt::Display for RpcEndpoint {
303    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
304        let Self { endpoint, auth, config } = self;
305        write!(f, "{endpoint}")?;
306        write!(f, "{config}")?;
307        if let Some(auth) = auth {
308            write!(f, ", auth={auth}")?;
309        }
310        Ok(())
311    }
312}
313
314impl Serialize for RpcEndpoint {
315    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
316    where
317        S: Serializer,
318    {
319        if self.config.retries.is_none()
320            && self.config.retry_backoff.is_none()
321            && self.config.compute_units_per_second.is_none()
322            && self.auth.is_none()
323        {
324            // serialize as endpoint if there's no additional config
325            self.endpoint.serialize(serializer)
326        } else {
327            let mut map = serializer.serialize_map(Some(5))?;
328            map.serialize_entry("endpoint", &self.endpoint)?;
329            map.serialize_entry("retries", &self.config.retries)?;
330            map.serialize_entry("retry_backoff", &self.config.retry_backoff)?;
331            map.serialize_entry("compute_units_per_second", &self.config.compute_units_per_second)?;
332            map.serialize_entry("auth", &self.auth)?;
333            map.end()
334        }
335    }
336}
337
338impl<'de> Deserialize<'de> for RpcEndpoint {
339    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
340    where
341        D: Deserializer<'de>,
342    {
343        let value = serde_json::Value::deserialize(deserializer)?;
344        if value.is_string() {
345            return Ok(Self {
346                endpoint: serde_json::from_value(value).map_err(serde::de::Error::custom)?,
347                ..Default::default()
348            });
349        }
350
351        #[derive(Deserialize)]
352        struct RpcEndpointConfigInner {
353            #[serde(alias = "url")]
354            endpoint: RpcEndpointUrl,
355            retries: Option<u32>,
356            retry_backoff: Option<u64>,
357            compute_units_per_second: Option<u64>,
358            auth: Option<RpcAuth>,
359        }
360
361        let RpcEndpointConfigInner {
362            endpoint,
363            retries,
364            retry_backoff,
365            compute_units_per_second,
366            auth,
367        } = serde_json::from_value(value).map_err(serde::de::Error::custom)?;
368
369        Ok(Self {
370            endpoint,
371            auth,
372            config: RpcEndpointConfig { retries, retry_backoff, compute_units_per_second },
373        })
374    }
375}
376
377impl From<RpcEndpoint> for RpcEndpointType {
378    fn from(config: RpcEndpoint) -> Self {
379        Self::Config(config)
380    }
381}
382
383impl Default for RpcEndpoint {
384    fn default() -> Self {
385        Self {
386            endpoint: RpcEndpointUrl::Url("http://localhost:8545".to_string()),
387            config: RpcEndpointConfig::default(),
388            auth: None,
389        }
390    }
391}
392
393/// Rpc endpoint with environment variables resolved to values, see [`RpcEndpoint::resolve`].
394#[derive(Clone, Debug, PartialEq, Eq)]
395pub struct ResolvedRpcEndpoint {
396    pub endpoint: Result<String, UnresolvedEnvVarError>,
397    pub auth: Option<Result<String, UnresolvedEnvVarError>>,
398    pub config: RpcEndpointConfig,
399}
400
401impl ResolvedRpcEndpoint {
402    /// Returns the url this type holds, see [`RpcEndpoint::resolve`]
403    pub fn url(&self) -> Result<String, UnresolvedEnvVarError> {
404        self.endpoint.clone()
405    }
406
407    // Returns true if all environment variables are resolved successfully
408    pub fn is_unresolved(&self) -> bool {
409        let endpoint_err = self.endpoint.is_err();
410        let auth_err = self.auth.as_ref().map(|auth| auth.is_err()).unwrap_or(false);
411        endpoint_err || auth_err
412    }
413
414    // Attempts to resolve unresolved environment variables into a new instance
415    pub fn try_resolve(mut self) -> Self {
416        if !self.is_unresolved() {
417            return self;
418        }
419        if let Err(err) = self.endpoint {
420            self.endpoint = err.try_resolve()
421        }
422        if let Some(Err(err)) = self.auth {
423            self.auth = Some(err.try_resolve())
424        }
425        self
426    }
427}
428
429/// Container type for _resolved_ endpoints.
430#[derive(Clone, Debug, Default, PartialEq, Eq)]
431pub struct ResolvedRpcEndpoints {
432    endpoints: BTreeMap<String, ResolvedRpcEndpoint>,
433}
434
435impl ResolvedRpcEndpoints {
436    /// Returns true if there's an endpoint that couldn't be resolved
437    pub fn has_unresolved(&self) -> bool {
438        self.endpoints.values().any(|e| e.is_unresolved())
439    }
440}
441
442impl Deref for ResolvedRpcEndpoints {
443    type Target = BTreeMap<String, ResolvedRpcEndpoint>;
444
445    fn deref(&self) -> &Self::Target {
446        &self.endpoints
447    }
448}
449
450impl DerefMut for ResolvedRpcEndpoints {
451    fn deref_mut(&mut self) -> &mut Self::Target {
452        &mut self.endpoints
453    }
454}
455
456#[cfg(test)]
457mod tests {
458    use super::*;
459
460    #[test]
461    fn serde_rpc_config() {
462        let s = r#"{
463            "endpoint": "http://localhost:8545",
464            "retries": 5,
465            "retry_backoff": 250,
466            "compute_units_per_second": 100,
467            "auth": "Bearer 123"
468        }"#;
469        let config: RpcEndpoint = serde_json::from_str(s).unwrap();
470        assert_eq!(
471            config,
472            RpcEndpoint {
473                endpoint: RpcEndpointUrl::Url("http://localhost:8545".to_string()),
474                config: RpcEndpointConfig {
475                    retries: Some(5),
476                    retry_backoff: Some(250),
477                    compute_units_per_second: Some(100),
478                },
479                auth: Some(RpcAuth::Raw("Bearer 123".to_string())),
480            }
481        );
482
483        let s = "\"http://localhost:8545\"";
484        let config: RpcEndpoint = serde_json::from_str(s).unwrap();
485        assert_eq!(
486            config,
487            RpcEndpoint {
488                endpoint: RpcEndpointUrl::Url("http://localhost:8545".to_string()),
489                config: RpcEndpointConfig {
490                    retries: None,
491                    retry_backoff: None,
492                    compute_units_per_second: None,
493                },
494                auth: None,
495            }
496        );
497    }
498}