pyth_lazer_agent/
config.rs1use config::{Environment, File};
2use derivative::Derivative;
3use serde::Deserialize;
4use std::cmp::min;
5use std::fmt::{Debug, Display, Formatter};
6use std::net::SocketAddr;
7use std::path::PathBuf;
8use std::time::Duration;
9use url::Url;
10
11#[derive(Deserialize, Derivative, Clone, PartialEq)]
12#[derivative(Debug)]
13pub struct Config {
14 pub listen_address: SocketAddr,
15 pub relayer_urls: Vec<Url>,
16 #[serde(default)]
17 pub relayer_connections: Option<usize>,
18 pub authorization_token: Option<AuthorizationToken>,
19 #[derivative(Debug = "ignore")]
20 pub publish_keypair_path: PathBuf,
21 #[serde(with = "humantime_serde", default = "default_publish_interval")]
22 pub publish_interval_duration: Duration,
23 #[serde(default = "default_history_service_url")]
24 pub history_service_url: Url,
25 #[serde(default)]
26 pub enable_update_deduplication: bool,
27 #[serde(with = "humantime_serde", default = "default_update_deduplication_ttl")]
28 pub update_deduplication_ttl: Duration,
29 pub proxy_url: Option<RedactedUrl>,
30 #[serde(with = "humantime_serde", default = "default_legacy_sched_interval")]
31 pub legacy_sched_interval_duration: Duration,
32}
33
34#[derive(Deserialize, Clone, PartialEq)]
40#[serde(transparent)]
41pub struct RedactedUrl(Url);
42
43impl RedactedUrl {
44 pub fn expose(&self) -> &Url {
47 &self.0
48 }
49}
50
51fn redact_url(url: &Url) -> String {
53 let scheme = url.scheme();
54 match (url.host_str(), url.port()) {
55 (Some(host), Some(port)) => format!("{scheme}://{host}:{port}"),
56 (Some(host), None) => format!("{scheme}://{host}"),
57 (None, _) => scheme.to_string(),
58 }
59}
60
61impl Display for RedactedUrl {
62 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
63 f.write_str(&redact_url(&self.0))
64 }
65}
66
67impl Debug for RedactedUrl {
68 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
69 write!(f, "{:?}", redact_url(&self.0))
70 }
71}
72
73#[derive(Deserialize, Derivative, Clone, PartialEq)]
74pub struct AuthorizationToken(pub String);
75
76impl Debug for AuthorizationToken {
77 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
78 let token_string = self.0.to_ascii_lowercase();
79 #[allow(clippy::string_slice, reason = "false positive")]
80 let last_chars = &token_string[token_string.len() - min(4, token_string.len())..];
81 write!(f, "\"...{last_chars}\"")
82 }
83}
84
85fn default_publish_interval() -> Duration {
86 Duration::from_millis(25)
87}
88
89fn default_update_deduplication_ttl() -> Duration {
90 Duration::from_millis(500)
91}
92
93fn default_legacy_sched_interval() -> Duration {
94 Duration::from_millis(500)
95}
96
97fn default_history_service_url() -> Url {
98 #[allow(clippy::expect_used, reason = "hardcoded URL is always valid")]
99 "https://pyth.dourolabs.app/v1/symbols"
100 .parse()
101 .expect("hardcoded URL is valid")
102}
103
104pub fn load_config(config_path: String) -> anyhow::Result<Config> {
105 let config = config::Config::builder()
106 .add_source(File::with_name(&config_path))
107 .add_source(Environment::with_prefix("LAZER_AGENT").separator("__"))
108 .build()?
109 .try_deserialize()?;
110 Ok(config)
111}
112
113pub const CHANNEL_CAPACITY: usize = 1000;
115
116#[cfg(test)]
117mod tests {
118 use super::RedactedUrl;
119 use url::Url;
120
121 fn redacted(s: &str) -> RedactedUrl {
122 RedactedUrl(Url::parse(s).unwrap())
123 }
124
125 #[test]
126 fn redacted_url_hides_credentials() {
127 let url = redacted("http://alice:s3cr3t@proxy.example.com:8080/path?q=1");
128
129 let display = format!("{url}");
130 let debug = format!("{url:?}");
131 assert_eq!(display, "http://proxy.example.com:8080");
132 assert_eq!(debug, "\"http://proxy.example.com:8080\"");
133 for rendered in [&display, &debug] {
134 assert!(!rendered.contains("alice"));
135 assert!(!rendered.contains("s3cr3t"));
136 }
137
138 assert_eq!(url.expose().password(), Some("s3cr3t"));
140
141 let no_port = redacted("https://bob:hunter2@proxy.example.com");
143 assert_eq!(format!("{no_port}"), "https://proxy.example.com");
144 }
145}