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: 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 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#[derive(Clone, Debug)]
214pub struct ReconnectionConfig {
215 pub initial_delay: Duration,
217 pub max_delay: Duration,
219 pub max_attempts: Option<u32>,
221 pub backoff_multiplier: f64,
223 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, 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 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#[derive(Clone, Debug)]
279pub struct WebSocketConfig {
280 pub bind_address: SocketAddr,
281}
282
283pub 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#[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#[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}