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#[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 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#[derive(Clone, Debug)]
62pub struct TransactionConfig {
63 pub enabled: bool,
64 pub rpc_url: Option<String>,
65 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: 32 * 1024,
95 max_transaction_bytes: 4096,
98 inspect_timeout: Duration::from_secs(10),
99 send_timeout: Duration::from_secs(15),
100 status_timeout: Duration::from_secs(10),
101 inspect_concurrency: 64,
102 send_concurrency: 16,
103 inspect_requests_per_minute: 600,
104 send_requests_per_minute: 60,
105 status_requests_per_minute: 600,
106 trusted_proxy_cidrs: Vec::new(),
107 usage_enabled: false,
108 usage_endpoint: None,
109 usage_token: None,
110 usage_spool_capacity: 1_000,
111 }
112 }
113}
114
115impl TransactionConfig {
116 pub fn from_env() -> Result<Self> {
118 let mut config = Self::default();
119 config.enabled = env_bool("ARETE_TRANSACTIONS_ENABLED")?.unwrap_or(false);
120 config.rpc_url =
121 first_nonempty(&["ARETE_TRANSACTION_RPC_URL", "SOLANA_RPC_URL", "RPC_URL"]);
122 config.allow_unauthenticated =
123 env_bool("ARETE_TRANSACTIONS_ALLOW_UNAUTHENTICATED")?.unwrap_or(false);
124 config.max_body_bytes =
125 env_parse("ARETE_TRANSACTION_MAX_BODY_BYTES")?.unwrap_or(config.max_body_bytes);
126 config.max_transaction_bytes =
127 env_parse("ARETE_TRANSACTION_MAX_BYTES")?.unwrap_or(config.max_transaction_bytes);
128 config.inspect_timeout = Duration::from_millis(
129 env_parse("ARETE_TRANSACTION_INSPECT_TIMEOUT_MS")?
130 .unwrap_or(config.inspect_timeout.as_millis() as u64),
131 );
132 config.send_timeout = Duration::from_millis(
133 env_parse("ARETE_TRANSACTION_SEND_TIMEOUT_MS")?
134 .unwrap_or(config.send_timeout.as_millis() as u64),
135 );
136 config.status_timeout = Duration::from_millis(
137 env_parse("ARETE_TRANSACTION_STATUS_TIMEOUT_MS")?
138 .unwrap_or(config.status_timeout.as_millis() as u64),
139 );
140 config.inspect_concurrency = env_parse("ARETE_TRANSACTION_INSPECT_CONCURRENCY")?
141 .unwrap_or(config.inspect_concurrency);
142 config.send_concurrency =
143 env_parse("ARETE_TRANSACTION_SEND_CONCURRENCY")?.unwrap_or(config.send_concurrency);
144 config.inspect_requests_per_minute =
145 env_parse("ARETE_TRANSACTION_INSPECT_REQUESTS_PER_MINUTE")?
146 .unwrap_or(config.inspect_requests_per_minute);
147 config.send_requests_per_minute = env_parse("ARETE_TRANSACTION_SEND_REQUESTS_PER_MINUTE")?
148 .unwrap_or(config.send_requests_per_minute);
149 config.status_requests_per_minute =
150 env_parse("ARETE_TRANSACTION_STATUS_REQUESTS_PER_MINUTE")?
151 .unwrap_or(config.status_requests_per_minute);
152 config.trusted_proxy_cidrs = std::env::var("ARETE_TRUSTED_PROXY_CIDRS")
153 .ok()
154 .filter(|value| !value.trim().is_empty())
155 .map(|value| {
156 value
157 .split(',')
158 .map(|cidr| cidr.trim().parse().context("invalid trusted proxy CIDR"))
159 .collect::<Result<Vec<_>>>()
160 })
161 .transpose()?
162 .unwrap_or_default();
163 config.usage_enabled = env_bool("ARETE_TRANSACTION_USAGE_ENABLED")?.unwrap_or(false);
164 config.usage_endpoint = first_nonempty(&["ARETE_TRANSACTION_USAGE_ENDPOINT"]);
165 config.usage_token = first_nonempty(&["ARETE_TRANSACTION_USAGE_TOKEN"]);
166 config.usage_spool_capacity = env_parse("ARETE_TRANSACTION_USAGE_SPOOL_CAPACITY")?
167 .unwrap_or(config.usage_spool_capacity);
168 config.validate()?;
169 Ok(config)
170 }
171
172 pub fn validate(&self) -> Result<()> {
173 if self.enabled && self.rpc_url.as_deref().unwrap_or_default().is_empty() {
174 bail!("transactions are enabled but no transaction RPC URL is configured");
175 }
176 if self.max_body_bytes == 0
177 || self.max_transaction_bytes == 0
178 || self.inspect_concurrency == 0
179 || self.send_concurrency == 0
180 {
181 bail!("transaction size and concurrency limits must be greater than zero");
182 }
183 if self.usage_enabled && (self.usage_endpoint.is_none() || self.usage_token.is_none()) {
184 bail!("transaction usage requires an endpoint and token");
185 }
186 Ok(())
187 }
188}
189
190fn first_nonempty(keys: &[&str]) -> Option<String> {
191 keys.iter()
192 .find_map(|key| std::env::var(key).ok())
193 .filter(|value| !value.trim().is_empty())
194}
195
196pub(crate) fn env_parse<T>(key: &str) -> Result<Option<T>>
197where
198 T: std::str::FromStr,
199 T::Err: std::error::Error + Send + Sync + 'static,
200{
201 std::env::var(key)
202 .ok()
203 .map(|value| value.parse().with_context(|| format!("invalid {key}")))
204 .transpose()
205}
206
207pub(crate) fn env_bool(key: &str) -> Result<Option<bool>> {
208 let Some(value) = std::env::var(key).ok() else {
209 return Ok(None);
210 };
211 match value.to_ascii_lowercase().as_str() {
212 "true" | "1" | "yes" => Ok(Some(true)),
213 "false" | "0" | "no" => Ok(Some(false)),
214 _ => bail!("invalid {key}; expected true or false"),
215 }
216}
217
218#[derive(Clone, Debug)]
220pub struct ReconnectionConfig {
221 pub initial_delay: Duration,
223 pub max_delay: Duration,
225 pub max_attempts: Option<u32>,
227 pub backoff_multiplier: f64,
229 pub http2_keep_alive_interval: Option<Duration>,
231 pub live_fallback_attempts: Option<u32>,
238}
239
240impl Default for ReconnectionConfig {
241 fn default() -> Self {
242 Self {
243 initial_delay: Duration::from_millis(100),
244 max_delay: Duration::from_secs(60),
245 max_attempts: None, backoff_multiplier: 2.0,
247 http2_keep_alive_interval: Some(Duration::from_secs(30)),
248 live_fallback_attempts: Some(3),
249 }
250 }
251}
252
253impl ReconnectionConfig {
254 pub fn new() -> Self {
255 Self::default()
256 }
257
258 pub fn with_initial_delay(mut self, delay: Duration) -> Self {
259 self.initial_delay = delay;
260 self
261 }
262
263 pub fn with_max_delay(mut self, delay: Duration) -> Self {
264 self.max_delay = delay;
265 self
266 }
267
268 pub fn with_max_attempts(mut self, attempts: u32) -> Self {
269 self.max_attempts = Some(attempts);
270 self
271 }
272
273 pub fn with_backoff_multiplier(mut self, multiplier: f64) -> Self {
274 self.backoff_multiplier = multiplier;
275 self
276 }
277
278 pub fn with_http2_keep_alive_interval(mut self, interval: Duration) -> Self {
279 self.http2_keep_alive_interval = Some(interval);
280 self
281 }
282
283 pub fn fail_closed(mut self) -> Self {
286 self.live_fallback_attempts = None;
287 self
288 }
289
290 pub fn next_backoff(&self, current: Duration) -> Duration {
292 let next_secs = current.as_secs_f64() * self.backoff_multiplier;
293 let capped_secs = next_secs.min(self.max_delay.as_secs_f64());
294 Duration::from_secs_f64(capped_secs)
295 }
296}
297
298#[derive(Clone, Debug)]
300pub struct WebSocketConfig {
301 pub bind_address: SocketAddr,
302}
303
304pub const WEBSOCKET_PROTOCOL_VERSION: u8 = crate::websocket::subscription::PROTOCOL_VERSION;
306
307impl Default for WebSocketConfig {
308 fn default() -> Self {
309 Self {
310 bind_address: "[::]:8877".parse().expect("valid socket address"),
311 }
312 }
313}
314
315impl WebSocketConfig {
316 pub fn new(bind_address: impl Into<SocketAddr>) -> Self {
317 Self {
318 bind_address: bind_address.into(),
319 }
320 }
321}
322
323#[derive(Clone, Debug)]
325pub struct YellowstoneConfig {
326 pub endpoint: String,
327 pub x_token: Option<String>,
328}
329
330impl YellowstoneConfig {
331 pub fn new(endpoint: impl Into<String>) -> Self {
332 Self {
333 endpoint: endpoint.into(),
334 x_token: None,
335 }
336 }
337
338 pub fn with_token(mut self, token: impl Into<String>) -> Self {
339 self.x_token = Some(token.into());
340 self
341 }
342}
343
344#[derive(Clone, Debug, Default)]
346pub struct ServerConfig {
347 pub runtime_plan: RuntimePlan,
348 pub websocket: Option<WebSocketConfig>,
349 pub yellowstone: Option<YellowstoneConfig>,
350 pub health: Option<HealthConfig>,
351 pub http_health: Option<HttpHealthConfig>,
352 pub reconnection: Option<ReconnectionConfig>,
353 pub transactions: Option<TransactionConfig>,
354 pub solana_gateway_target_id: Option<String>,
355 pub program_read_binding_target_id: Option<String>,
356 pub snapshots: Option<crate::snapshot::SnapshotConfig>,
358 pub journal: Option<crate::journal::JournalConfig>,
364}
365
366impl ServerConfig {
367 pub fn new() -> Self {
368 Self::default()
369 }
370
371 pub fn with_websocket(mut self, config: WebSocketConfig) -> Self {
372 self.websocket = Some(config);
373 self.runtime_plan.websocket = true;
374 self.runtime_plan.live_runtime = true;
375 self
376 }
377
378 pub fn with_yellowstone(mut self, config: YellowstoneConfig) -> Self {
379 self.yellowstone = Some(config);
380 self.runtime_plan.live_runtime = true;
381 self
382 }
383
384 pub fn with_health(mut self, config: HealthConfig) -> Self {
385 self.health = Some(config);
386 self.runtime_plan.health = true;
387 self
388 }
389
390 pub fn with_http_health(mut self, config: HttpHealthConfig) -> Self {
391 self.http_health = Some(config);
392 self.runtime_plan.health = true;
393 self.runtime_plan.chain_reads = true;
394 self.runtime_plan.program_reads = true;
395 self.runtime_plan.stack_queries = true;
396 self
397 }
398
399 pub fn with_reconnection(mut self, config: ReconnectionConfig) -> Self {
400 self.reconnection = Some(config);
401 self
402 }
403
404 pub fn with_transactions(mut self, config: TransactionConfig) -> Self {
405 self.runtime_plan.transactions = config.enabled;
406 self.transactions = Some(config);
407 self
408 }
409
410 pub fn with_runtime_plan(mut self, runtime_plan: RuntimePlan) -> Self {
411 self.runtime_plan = runtime_plan;
412 self
413 }
414
415 pub fn with_snapshots(mut self, config: crate::snapshot::SnapshotConfig) -> Self {
416 self.snapshots = Some(config);
417 self
418 }
419}