foundry_config/
endpoints.rs

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