Skip to main content

arete_server/
config.rs

1use std::net::SocketAddr;
2use std::time::Duration;
3
4use anyhow::{bail, Context, Result};
5use ipnet::IpNet;
6
7pub use crate::health::HealthConfig;
8pub use crate::http_health::HttpHealthConfig;
9pub use crate::http_server::HttpServerConfig;
10
11/// Runtime capabilities selected for one server process.
12#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
13pub struct RuntimePlan {
14    pub health: bool,
15    pub chain_reads: bool,
16    pub program_reads: bool,
17    pub stack_queries: bool,
18    pub transactions: bool,
19    pub websocket: bool,
20    pub live_runtime: bool,
21}
22
23impl RuntimePlan {
24    /// Health, chain reads, and fixed transaction routes only.
25    pub const fn solana_gateway() -> Self {
26        Self {
27            health: true,
28            chain_reads: true,
29            program_reads: false,
30            stack_queries: false,
31            transactions: true,
32            websocket: false,
33            live_runtime: false,
34        }
35    }
36
37    pub fn http() -> Self {
38        Self {
39            health: true,
40            chain_reads: true,
41            program_reads: true,
42            stack_queries: true,
43            ..Self::default()
44        }
45    }
46
47    pub fn program_reads() -> Self {
48        Self {
49            health: true,
50            program_reads: true,
51            ..Self::default()
52        }
53    }
54
55    pub fn live_runtime_enabled(self) -> bool {
56        self.live_runtime || self.websocket
57    }
58}
59
60/// Explicit configuration for the fixed transaction relay routes.
61#[derive(Clone, Debug)]
62pub struct TransactionConfig {
63    pub enabled: bool,
64    pub rpc_url: Option<String>,
65    /// Development-only: allow relay requests when no auth plugin is configured.
66    pub allow_unauthenticated: bool,
67    pub max_body_bytes: usize,
68    pub max_transaction_bytes: usize,
69    pub inspect_timeout: Duration,
70    pub send_timeout: Duration,
71    pub status_timeout: Duration,
72    pub inspect_concurrency: usize,
73    pub send_concurrency: usize,
74    pub inspect_requests_per_minute: u32,
75    pub send_requests_per_minute: u32,
76    pub status_requests_per_minute: u32,
77    pub trusted_proxy_cidrs: Vec<IpNet>,
78    pub usage_enabled: bool,
79    pub usage_endpoint: Option<String>,
80    pub usage_token: Option<String>,
81    pub usage_spool_capacity: usize,
82}
83
84impl Default for TransactionConfig {
85    fn default() -> Self {
86        Self {
87            enabled: false,
88            rpc_url: None,
89            allow_unauthenticated: false,
90            max_body_bytes: 4 * 1024,
91            max_transaction_bytes: 1232,
92            inspect_timeout: Duration::from_secs(10),
93            send_timeout: Duration::from_secs(15),
94            status_timeout: Duration::from_secs(10),
95            inspect_concurrency: 64,
96            send_concurrency: 16,
97            inspect_requests_per_minute: 600,
98            send_requests_per_minute: 60,
99            status_requests_per_minute: 600,
100            trusted_proxy_cidrs: Vec::new(),
101            usage_enabled: false,
102            usage_endpoint: None,
103            usage_token: None,
104            usage_spool_capacity: 1_000,
105        }
106    }
107}
108
109impl TransactionConfig {
110    /// Load transaction settings. Routes remain disabled unless explicitly enabled.
111    pub fn from_env() -> Result<Self> {
112        let mut config = Self::default();
113        config.enabled = env_bool("ARETE_TRANSACTIONS_ENABLED")?.unwrap_or(false);
114        config.rpc_url =
115            first_nonempty(&["ARETE_TRANSACTION_RPC_URL", "SOLANA_RPC_URL", "RPC_URL"]);
116        config.allow_unauthenticated =
117            env_bool("ARETE_TRANSACTIONS_ALLOW_UNAUTHENTICATED")?.unwrap_or(false);
118        config.max_body_bytes =
119            env_parse("ARETE_TRANSACTION_MAX_BODY_BYTES")?.unwrap_or(config.max_body_bytes);
120        config.max_transaction_bytes =
121            env_parse("ARETE_TRANSACTION_MAX_BYTES")?.unwrap_or(config.max_transaction_bytes);
122        config.inspect_timeout = Duration::from_millis(
123            env_parse("ARETE_TRANSACTION_INSPECT_TIMEOUT_MS")?
124                .unwrap_or(config.inspect_timeout.as_millis() as u64),
125        );
126        config.send_timeout = Duration::from_millis(
127            env_parse("ARETE_TRANSACTION_SEND_TIMEOUT_MS")?
128                .unwrap_or(config.send_timeout.as_millis() as u64),
129        );
130        config.status_timeout = Duration::from_millis(
131            env_parse("ARETE_TRANSACTION_STATUS_TIMEOUT_MS")?
132                .unwrap_or(config.status_timeout.as_millis() as u64),
133        );
134        config.inspect_concurrency = env_parse("ARETE_TRANSACTION_INSPECT_CONCURRENCY")?
135            .unwrap_or(config.inspect_concurrency);
136        config.send_concurrency =
137            env_parse("ARETE_TRANSACTION_SEND_CONCURRENCY")?.unwrap_or(config.send_concurrency);
138        config.inspect_requests_per_minute =
139            env_parse("ARETE_TRANSACTION_INSPECT_REQUESTS_PER_MINUTE")?
140                .unwrap_or(config.inspect_requests_per_minute);
141        config.send_requests_per_minute = env_parse("ARETE_TRANSACTION_SEND_REQUESTS_PER_MINUTE")?
142            .unwrap_or(config.send_requests_per_minute);
143        config.status_requests_per_minute =
144            env_parse("ARETE_TRANSACTION_STATUS_REQUESTS_PER_MINUTE")?
145                .unwrap_or(config.status_requests_per_minute);
146        config.trusted_proxy_cidrs = std::env::var("ARETE_TRUSTED_PROXY_CIDRS")
147            .ok()
148            .filter(|value| !value.trim().is_empty())
149            .map(|value| {
150                value
151                    .split(',')
152                    .map(|cidr| cidr.trim().parse().context("invalid trusted proxy CIDR"))
153                    .collect::<Result<Vec<_>>>()
154            })
155            .transpose()?
156            .unwrap_or_default();
157        config.usage_enabled = env_bool("ARETE_TRANSACTION_USAGE_ENABLED")?.unwrap_or(false);
158        config.usage_endpoint = first_nonempty(&["ARETE_TRANSACTION_USAGE_ENDPOINT"]);
159        config.usage_token = first_nonempty(&["ARETE_TRANSACTION_USAGE_TOKEN"]);
160        config.usage_spool_capacity = env_parse("ARETE_TRANSACTION_USAGE_SPOOL_CAPACITY")?
161            .unwrap_or(config.usage_spool_capacity);
162        config.validate()?;
163        Ok(config)
164    }
165
166    pub fn validate(&self) -> Result<()> {
167        if self.enabled && self.rpc_url.as_deref().unwrap_or_default().is_empty() {
168            bail!("transactions are enabled but no transaction RPC URL is configured");
169        }
170        if self.max_body_bytes == 0
171            || self.max_transaction_bytes == 0
172            || self.inspect_concurrency == 0
173            || self.send_concurrency == 0
174        {
175            bail!("transaction size and concurrency limits must be greater than zero");
176        }
177        if self.usage_enabled && (self.usage_endpoint.is_none() || self.usage_token.is_none()) {
178            bail!("transaction usage requires an endpoint and token");
179        }
180        Ok(())
181    }
182}
183
184fn first_nonempty(keys: &[&str]) -> Option<String> {
185    keys.iter()
186        .find_map(|key| std::env::var(key).ok())
187        .filter(|value| !value.trim().is_empty())
188}
189
190fn env_parse<T>(key: &str) -> Result<Option<T>>
191where
192    T: std::str::FromStr,
193    T::Err: std::error::Error + Send + Sync + 'static,
194{
195    std::env::var(key)
196        .ok()
197        .map(|value| value.parse().with_context(|| format!("invalid {key}")))
198        .transpose()
199}
200
201fn env_bool(key: &str) -> Result<Option<bool>> {
202    let Some(value) = std::env::var(key).ok() else {
203        return Ok(None);
204    };
205    match value.to_ascii_lowercase().as_str() {
206        "true" | "1" | "yes" => Ok(Some(true)),
207        "false" | "0" | "no" => Ok(Some(false)),
208        _ => bail!("invalid {key}; expected true or false"),
209    }
210}
211
212/// Configuration for gRPC stream reconnection with exponential backoff
213#[derive(Clone, Debug)]
214pub struct ReconnectionConfig {
215    /// Initial delay before first reconnection attempt
216    pub initial_delay: Duration,
217    /// Maximum delay between reconnection attempts
218    pub max_delay: Duration,
219    /// Maximum number of reconnection attempts (None = infinite)
220    pub max_attempts: Option<u32>,
221    /// Multiplier for exponential backoff (typically 2.0)
222    pub backoff_multiplier: f64,
223    /// HTTP/2 keep-alive interval to prevent silent disconnects
224    pub http2_keep_alive_interval: Option<Duration>,
225}
226
227impl Default for ReconnectionConfig {
228    fn default() -> Self {
229        Self {
230            initial_delay: Duration::from_millis(100),
231            max_delay: Duration::from_secs(60),
232            max_attempts: None, // Infinite retries by default
233            backoff_multiplier: 2.0,
234            http2_keep_alive_interval: Some(Duration::from_secs(30)),
235        }
236    }
237}
238
239impl ReconnectionConfig {
240    pub fn new() -> Self {
241        Self::default()
242    }
243
244    pub fn with_initial_delay(mut self, delay: Duration) -> Self {
245        self.initial_delay = delay;
246        self
247    }
248
249    pub fn with_max_delay(mut self, delay: Duration) -> Self {
250        self.max_delay = delay;
251        self
252    }
253
254    pub fn with_max_attempts(mut self, attempts: u32) -> Self {
255        self.max_attempts = Some(attempts);
256        self
257    }
258
259    pub fn with_backoff_multiplier(mut self, multiplier: f64) -> Self {
260        self.backoff_multiplier = multiplier;
261        self
262    }
263
264    pub fn with_http2_keep_alive_interval(mut self, interval: Duration) -> Self {
265        self.http2_keep_alive_interval = Some(interval);
266        self
267    }
268
269    /// Calculate the next backoff duration given the current one
270    pub fn next_backoff(&self, current: Duration) -> Duration {
271        let next_secs = current.as_secs_f64() * self.backoff_multiplier;
272        let capped_secs = next_secs.min(self.max_delay.as_secs_f64());
273        Duration::from_secs_f64(capped_secs)
274    }
275}
276
277/// WebSocket server configuration
278#[derive(Clone, Debug)]
279pub struct WebSocketConfig {
280    pub bind_address: SocketAddr,
281}
282
283/// The only wire protocol accepted by the WebSocket server.
284pub const WEBSOCKET_PROTOCOL_VERSION: u8 = crate::websocket::subscription::PROTOCOL_VERSION;
285
286impl Default for WebSocketConfig {
287    fn default() -> Self {
288        Self {
289            bind_address: "[::]:8877".parse().expect("valid socket address"),
290        }
291    }
292}
293
294impl WebSocketConfig {
295    pub fn new(bind_address: impl Into<SocketAddr>) -> Self {
296        Self {
297            bind_address: bind_address.into(),
298        }
299    }
300}
301
302/// Yellowstone gRPC configuration
303#[derive(Clone, Debug)]
304pub struct YellowstoneConfig {
305    pub endpoint: String,
306    pub x_token: Option<String>,
307}
308
309impl YellowstoneConfig {
310    pub fn new(endpoint: impl Into<String>) -> Self {
311        Self {
312            endpoint: endpoint.into(),
313            x_token: None,
314        }
315    }
316
317    pub fn with_token(mut self, token: impl Into<String>) -> Self {
318        self.x_token = Some(token.into());
319        self
320    }
321}
322
323/// Main server configuration
324#[derive(Clone, Debug, Default)]
325pub struct ServerConfig {
326    pub runtime_plan: RuntimePlan,
327    pub websocket: Option<WebSocketConfig>,
328    pub yellowstone: Option<YellowstoneConfig>,
329    pub health: Option<HealthConfig>,
330    pub http_health: Option<HttpHealthConfig>,
331    pub reconnection: Option<ReconnectionConfig>,
332    pub transactions: Option<TransactionConfig>,
333    pub solana_gateway_target_id: Option<String>,
334    pub program_read_binding_target_id: Option<String>,
335}
336
337impl ServerConfig {
338    pub fn new() -> Self {
339        Self::default()
340    }
341
342    pub fn with_websocket(mut self, config: WebSocketConfig) -> Self {
343        self.websocket = Some(config);
344        self.runtime_plan.websocket = true;
345        self.runtime_plan.live_runtime = true;
346        self
347    }
348
349    pub fn with_yellowstone(mut self, config: YellowstoneConfig) -> Self {
350        self.yellowstone = Some(config);
351        self.runtime_plan.live_runtime = true;
352        self
353    }
354
355    pub fn with_health(mut self, config: HealthConfig) -> Self {
356        self.health = Some(config);
357        self.runtime_plan.health = true;
358        self
359    }
360
361    pub fn with_http_health(mut self, config: HttpHealthConfig) -> Self {
362        self.http_health = Some(config);
363        self.runtime_plan.health = true;
364        self.runtime_plan.chain_reads = true;
365        self.runtime_plan.program_reads = true;
366        self.runtime_plan.stack_queries = true;
367        self
368    }
369
370    pub fn with_reconnection(mut self, config: ReconnectionConfig) -> Self {
371        self.reconnection = Some(config);
372        self
373    }
374
375    pub fn with_transactions(mut self, config: TransactionConfig) -> Self {
376        self.runtime_plan.transactions = config.enabled;
377        self.transactions = Some(config);
378        self
379    }
380
381    pub fn with_runtime_plan(mut self, runtime_plan: RuntimePlan) -> Self {
382        self.runtime_plan = runtime_plan;
383        self
384    }
385}