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}
232
233impl Default for ReconnectionConfig {
234 fn default() -> Self {
235 Self {
236 initial_delay: Duration::from_millis(100),
237 max_delay: Duration::from_secs(60),
238 max_attempts: None, backoff_multiplier: 2.0,
240 http2_keep_alive_interval: Some(Duration::from_secs(30)),
241 }
242 }
243}
244
245impl ReconnectionConfig {
246 pub fn new() -> Self {
247 Self::default()
248 }
249
250 pub fn with_initial_delay(mut self, delay: Duration) -> Self {
251 self.initial_delay = delay;
252 self
253 }
254
255 pub fn with_max_delay(mut self, delay: Duration) -> Self {
256 self.max_delay = delay;
257 self
258 }
259
260 pub fn with_max_attempts(mut self, attempts: u32) -> Self {
261 self.max_attempts = Some(attempts);
262 self
263 }
264
265 pub fn with_backoff_multiplier(mut self, multiplier: f64) -> Self {
266 self.backoff_multiplier = multiplier;
267 self
268 }
269
270 pub fn with_http2_keep_alive_interval(mut self, interval: Duration) -> Self {
271 self.http2_keep_alive_interval = Some(interval);
272 self
273 }
274
275 pub fn next_backoff(&self, current: Duration) -> Duration {
277 let next_secs = current.as_secs_f64() * self.backoff_multiplier;
278 let capped_secs = next_secs.min(self.max_delay.as_secs_f64());
279 Duration::from_secs_f64(capped_secs)
280 }
281}
282
283#[derive(Clone, Debug)]
285pub struct WebSocketConfig {
286 pub bind_address: SocketAddr,
287}
288
289pub const WEBSOCKET_PROTOCOL_VERSION: u8 = crate::websocket::subscription::PROTOCOL_VERSION;
291
292impl Default for WebSocketConfig {
293 fn default() -> Self {
294 Self {
295 bind_address: "[::]:8877".parse().expect("valid socket address"),
296 }
297 }
298}
299
300impl WebSocketConfig {
301 pub fn new(bind_address: impl Into<SocketAddr>) -> Self {
302 Self {
303 bind_address: bind_address.into(),
304 }
305 }
306}
307
308#[derive(Clone, Debug)]
310pub struct YellowstoneConfig {
311 pub endpoint: String,
312 pub x_token: Option<String>,
313}
314
315impl YellowstoneConfig {
316 pub fn new(endpoint: impl Into<String>) -> Self {
317 Self {
318 endpoint: endpoint.into(),
319 x_token: None,
320 }
321 }
322
323 pub fn with_token(mut self, token: impl Into<String>) -> Self {
324 self.x_token = Some(token.into());
325 self
326 }
327}
328
329#[derive(Clone, Debug, Default)]
331pub struct ServerConfig {
332 pub runtime_plan: RuntimePlan,
333 pub websocket: Option<WebSocketConfig>,
334 pub yellowstone: Option<YellowstoneConfig>,
335 pub health: Option<HealthConfig>,
336 pub http_health: Option<HttpHealthConfig>,
337 pub reconnection: Option<ReconnectionConfig>,
338 pub transactions: Option<TransactionConfig>,
339 pub solana_gateway_target_id: Option<String>,
340 pub program_read_binding_target_id: Option<String>,
341 pub snapshots: Option<crate::snapshot::SnapshotConfig>,
343}
344
345impl ServerConfig {
346 pub fn new() -> Self {
347 Self::default()
348 }
349
350 pub fn with_websocket(mut self, config: WebSocketConfig) -> Self {
351 self.websocket = Some(config);
352 self.runtime_plan.websocket = true;
353 self.runtime_plan.live_runtime = true;
354 self
355 }
356
357 pub fn with_yellowstone(mut self, config: YellowstoneConfig) -> Self {
358 self.yellowstone = Some(config);
359 self.runtime_plan.live_runtime = true;
360 self
361 }
362
363 pub fn with_health(mut self, config: HealthConfig) -> Self {
364 self.health = Some(config);
365 self.runtime_plan.health = true;
366 self
367 }
368
369 pub fn with_http_health(mut self, config: HttpHealthConfig) -> Self {
370 self.http_health = Some(config);
371 self.runtime_plan.health = true;
372 self.runtime_plan.chain_reads = true;
373 self.runtime_plan.program_reads = true;
374 self.runtime_plan.stack_queries = true;
375 self
376 }
377
378 pub fn with_reconnection(mut self, config: ReconnectionConfig) -> Self {
379 self.reconnection = Some(config);
380 self
381 }
382
383 pub fn with_transactions(mut self, config: TransactionConfig) -> Self {
384 self.runtime_plan.transactions = config.enabled;
385 self.transactions = Some(config);
386 self
387 }
388
389 pub fn with_runtime_plan(mut self, runtime_plan: RuntimePlan) -> Self {
390 self.runtime_plan = runtime_plan;
391 self
392 }
393
394 pub fn with_snapshots(mut self, config: crate::snapshot::SnapshotConfig) -> Self {
395 self.snapshots = Some(config);
396 self
397 }
398}