1use 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#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(transparent)]
14pub struct RpcEndpoints {
15 endpoints: BTreeMap<String, RpcEndpoint>,
16}
17
18impl RpcEndpoints {
19 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 pub fn is_empty(&self) -> bool {
36 self.endpoints.is_empty()
37 }
38
39 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#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
57#[serde(untagged)]
58pub enum RpcEndpointType {
59 String(RpcEndpointUrl),
61 Config(RpcEndpoint),
63}
64
65impl RpcEndpointType {
66 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#[derive(Clone, Debug, PartialEq, Eq)]
106pub enum RpcEndpointUrl {
107 Url(String),
109 Env(String),
113}
114
115impl RpcEndpointUrl {
116 pub fn as_url(&self) -> Option<&str> {
118 match self {
119 Self::Url(url) => Some(url),
120 Self::Env(_) => None,
121 }
122 }
123
124 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#[derive(Clone, Debug, PartialEq, Eq)]
190pub enum RpcAuth {
191 Raw(String),
192 Env(String),
193}
194
195impl RpcAuth {
196 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#[derive(Debug, Clone, Default, PartialEq, Eq)]
241pub struct RpcEndpointConfig {
242 pub retries: Option<u32>,
244
245 pub retry_backoff: Option<u64>,
247
248 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#[derive(Debug, Clone, PartialEq, Eq)]
276pub struct RpcEndpoint {
277 pub endpoint: RpcEndpointUrl,
279
280 pub extra_endpoints: Vec<RpcEndpointUrl>,
284
285 pub auth: Option<RpcAuth>,
287
288 pub config: RpcEndpointConfig,
290}
291
292impl RpcEndpoint {
293 pub fn new(endpoint: RpcEndpointUrl) -> Self {
294 Self { endpoint, ..Default::default() }
295 }
296
297 pub fn resolve(self) -> ResolvedRpcEndpoint {
299 ResolvedRpcEndpoint {
300 endpoint: self.endpoint.resolve(),
301 extra_endpoints: self.extra_endpoints.into_iter().map(|e| e.resolve()).collect(),
302 auth: self.auth.map(|auth| auth.resolve()),
303 config: self.config,
304 }
305 }
306}
307
308impl fmt::Display for RpcEndpoint {
309 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
310 let Self { endpoint, auth, config, .. } = self;
311 write!(f, "{endpoint}")?;
312 write!(f, "{config}")?;
313 if let Some(auth) = auth {
314 write!(f, ", auth={auth}")?;
315 }
316 Ok(())
317 }
318}
319
320impl Serialize for RpcEndpoint {
321 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
322 where
323 S: Serializer,
324 {
325 let has_config = self.config.retries.is_some()
326 || self.config.retry_backoff.is_some()
327 || self.config.compute_units_per_second.is_some()
328 || self.auth.is_some();
329
330 if !has_config && self.extra_endpoints.is_empty() {
331 self.endpoint.serialize(serializer)
333 } else {
334 let mut map = serializer.serialize_map(None)?;
335 if self.extra_endpoints.is_empty() {
336 map.serialize_entry("endpoint", &self.endpoint)?;
337 } else {
338 let all: Vec<&RpcEndpointUrl> =
340 std::iter::once(&self.endpoint).chain(&self.extra_endpoints).collect();
341 map.serialize_entry("endpoints", &all)?;
342 }
343 map.serialize_entry("retries", &self.config.retries)?;
344 map.serialize_entry("retry_backoff", &self.config.retry_backoff)?;
345 map.serialize_entry("compute_units_per_second", &self.config.compute_units_per_second)?;
346 map.serialize_entry("auth", &self.auth)?;
347 map.end()
348 }
349 }
350}
351
352impl<'de> Deserialize<'de> for RpcEndpoint {
353 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
354 where
355 D: Deserializer<'de>,
356 {
357 let value = serde_json::Value::deserialize(deserializer)?;
358 if value.is_string() {
359 return Ok(Self {
360 endpoint: serde_json::from_value(value).map_err(serde::de::Error::custom)?,
361 ..Default::default()
362 });
363 }
364
365 #[derive(Deserialize)]
367 struct RpcEndpointConfigInner {
368 #[serde(alias = "url")]
369 endpoint: Option<RpcEndpointUrl>,
370 endpoints: Option<Vec<RpcEndpointUrl>>,
372 retries: Option<u32>,
373 retry_backoff: Option<u64>,
374 compute_units_per_second: Option<u64>,
375 auth: Option<RpcAuth>,
376 }
377
378 let RpcEndpointConfigInner {
379 endpoint,
380 endpoints,
381 retries,
382 retry_backoff,
383 compute_units_per_second,
384 auth,
385 } = serde_json::from_value(value).map_err(serde::de::Error::custom)?;
386
387 let (primary, extra) = match (endpoint, endpoints) {
388 (Some(ep), None) => (ep, vec![]),
390 (None, Some(mut eps)) => {
392 if eps.is_empty() {
393 return Err(serde::de::Error::custom(
394 "endpoints array must contain at least one URL",
395 ));
396 }
397 let primary = eps.remove(0);
398 (primary, eps)
399 }
400 (Some(_), Some(_)) => {
402 return Err(serde::de::Error::custom(
403 "cannot specify both `endpoint` and `endpoints`",
404 ));
405 }
406 (None, None) => {
408 return Err(serde::de::Error::custom(
409 "must specify either `endpoint` or `endpoints`",
410 ));
411 }
412 };
413
414 Ok(Self {
415 endpoint: primary,
416 extra_endpoints: extra,
417 auth,
418 config: RpcEndpointConfig { retries, retry_backoff, compute_units_per_second },
419 })
420 }
421}
422
423impl From<RpcEndpoint> for RpcEndpointType {
424 fn from(config: RpcEndpoint) -> Self {
425 Self::Config(config)
426 }
427}
428
429impl Default for RpcEndpoint {
430 fn default() -> Self {
431 Self {
432 endpoint: RpcEndpointUrl::Url("http://localhost:8545".to_string()),
433 extra_endpoints: vec![],
434 config: RpcEndpointConfig::default(),
435 auth: None,
436 }
437 }
438}
439
440#[derive(Clone, Debug, PartialEq, Eq)]
442pub struct ResolvedRpcEndpoint {
443 pub endpoint: Result<String, UnresolvedEnvVarError>,
444 pub extra_endpoints: Vec<Result<String, UnresolvedEnvVarError>>,
446 pub auth: Option<Result<String, UnresolvedEnvVarError>>,
447 pub config: RpcEndpointConfig,
448}
449
450impl ResolvedRpcEndpoint {
451 pub fn url(&self) -> Result<String, UnresolvedEnvVarError> {
453 self.endpoint.clone()
454 }
455
456 pub fn all_urls(&self) -> Result<Vec<String>, UnresolvedEnvVarError> {
459 let primary = self.endpoint.clone()?;
460 if self.extra_endpoints.is_empty() {
461 return Ok(vec![primary]);
462 }
463 let mut urls = vec![primary];
464 for ep in &self.extra_endpoints {
465 urls.push(ep.clone()?);
466 }
467 Ok(urls)
468 }
469
470 pub fn is_unresolved(&self) -> bool {
472 let endpoint_err = self.endpoint.is_err();
473 let extra_err = self.extra_endpoints.iter().any(|e| e.is_err());
474 let auth_err = self.auth.as_ref().map(|auth| auth.is_err()).unwrap_or(false);
475 endpoint_err || extra_err || auth_err
476 }
477
478 pub fn try_resolve(mut self) -> Self {
480 if !self.is_unresolved() {
481 return self;
482 }
483 if let Err(err) = self.endpoint {
484 self.endpoint = err.try_resolve()
485 }
486 for ep in &mut self.extra_endpoints {
487 if let Err(err) = std::mem::replace(ep, Ok(String::new())) {
488 *ep = err.try_resolve();
489 }
490 }
491 if let Some(Err(err)) = self.auth {
492 self.auth = Some(err.try_resolve())
493 }
494 self
495 }
496}
497
498#[derive(Clone, Debug, Default, PartialEq, Eq)]
500pub struct ResolvedRpcEndpoints {
501 endpoints: BTreeMap<String, ResolvedRpcEndpoint>,
502}
503
504impl ResolvedRpcEndpoints {
505 pub fn has_unresolved(&self) -> bool {
507 self.endpoints.values().any(|e| e.is_unresolved())
508 }
509}
510
511impl Deref for ResolvedRpcEndpoints {
512 type Target = BTreeMap<String, ResolvedRpcEndpoint>;
513
514 fn deref(&self) -> &Self::Target {
515 &self.endpoints
516 }
517}
518
519impl DerefMut for ResolvedRpcEndpoints {
520 fn deref_mut(&mut self) -> &mut Self::Target {
521 &mut self.endpoints
522 }
523}
524
525pub fn builtin_rpc_url(alias: &str) -> Option<&'static str> {
530 match alias {
531 "tempo" => Some("https://rpc.mpp.tempo.xyz"),
532 "moderato" => Some("https://rpc.mpp.moderato.tempo.xyz"),
533 _ => None,
534 }
535}
536
537#[cfg(test)]
538mod tests {
539 use super::*;
540
541 #[test]
542 fn serde_rpc_config() {
543 let s = r#"{
544 "endpoint": "http://localhost:8545",
545 "retries": 5,
546 "retry_backoff": 250,
547 "compute_units_per_second": 100,
548 "auth": "Bearer 123"
549 }"#;
550 let config: RpcEndpoint = serde_json::from_str(s).unwrap();
551 assert_eq!(
552 config,
553 RpcEndpoint {
554 endpoint: RpcEndpointUrl::Url("http://localhost:8545".to_string()),
555 extra_endpoints: vec![],
556 config: RpcEndpointConfig {
557 retries: Some(5),
558 retry_backoff: Some(250),
559 compute_units_per_second: Some(100),
560 },
561 auth: Some(RpcAuth::Raw("Bearer 123".to_string())),
562 }
563 );
564
565 let s = "\"http://localhost:8545\"";
566 let config: RpcEndpoint = serde_json::from_str(s).unwrap();
567 assert_eq!(
568 config,
569 RpcEndpoint {
570 endpoint: RpcEndpointUrl::Url("http://localhost:8545".to_string()),
571 extra_endpoints: vec![],
572 config: RpcEndpointConfig {
573 retries: None,
574 retry_backoff: None,
575 compute_units_per_second: None,
576 },
577 auth: None,
578 }
579 );
580 }
581
582 #[test]
583 fn serde_rpc_config_multi_endpoints() {
584 let s = r#"{
586 "endpoints": ["https://rpc1.example.com", "https://rpc2.example.com", "https://rpc3.example.com"],
587 "retries": 5,
588 "retry_backoff": 1000
589 }"#;
590 let config: RpcEndpoint = serde_json::from_str(s).unwrap();
591 assert_eq!(
592 config,
593 RpcEndpoint {
594 endpoint: RpcEndpointUrl::Url("https://rpc1.example.com".to_string()),
595 extra_endpoints: vec![
596 RpcEndpointUrl::Url("https://rpc2.example.com".to_string()),
597 RpcEndpointUrl::Url("https://rpc3.example.com".to_string()),
598 ],
599 config: RpcEndpointConfig {
600 retries: Some(5),
601 retry_backoff: Some(1000),
602 compute_units_per_second: None,
603 },
604 auth: None,
605 }
606 );
607
608 let resolved = config.resolve();
610 let all_urls = resolved.all_urls().unwrap();
611 assert_eq!(
612 all_urls,
613 vec![
614 "https://rpc1.example.com".to_string(),
615 "https://rpc2.example.com".to_string(),
616 "https://rpc3.example.com".to_string(),
617 ]
618 );
619 }
620
621 #[test]
622 fn serde_rpc_config_rejects_both_endpoint_and_endpoints() {
623 let s = r#"{
624 "endpoint": "https://rpc1.example.com",
625 "endpoints": ["https://rpc2.example.com"]
626 }"#;
627 let result: Result<RpcEndpoint, _> = serde_json::from_str(s);
628 assert!(result.is_err());
629 assert!(result.unwrap_err().to_string().contains("cannot specify both"));
630 }
631
632 #[test]
633 fn serde_rpc_config_rejects_empty_endpoints() {
634 let s = r#"{ "endpoints": [] }"#;
635 let result: Result<RpcEndpoint, _> = serde_json::from_str(s);
636 assert!(result.is_err());
637 assert!(result.unwrap_err().to_string().contains("at least one URL"));
638 }
639}