1pub mod cache;
2pub mod config;
3pub mod dashboard;
4pub mod health;
5pub mod metrics;
6pub mod proxy;
7pub mod types;
8
9use config::{
10 ApiKeyConfig, CacheConfig, CacheMethodConfig, ChainConfig, Config, EndpointAuth,
11 EndpointConfig, HealthConfig, HedgeConfig, RateLimitConfig, RotationStrategy, ServerConfig,
12};
13use proxy::build_router;
14use std::path::Path;
15
16pub struct Turbine {
17 config: Config,
18}
19
20pub struct TurbineBuilder {
21 chains: Vec<ChainConfig>,
22 dashboard_secret: Option<String>,
23 api_keys: Vec<ApiKeyConfig>,
24}
25
26pub struct ChainBuilder {
27 name: String,
28 route: String,
29 endpoints: Vec<EndpointConfig>,
30 max_consecutive_failures: u32,
31 cooldown_seconds: u64,
32 health_method: Option<String>,
33 health_check_interval_seconds: u64,
34 max_block_lag: u64,
35 rotation: RotationStrategy,
36 cache_enabled: bool,
37 cache_preset: Option<String>,
38 cache_max_capacity: Option<u64>,
39 cache_methods: Vec<CacheMethodConfig>,
40 chain_id: Option<u64>,
41 max_retries: u32,
42 retry_delay_ms: u64,
43 rate_limit_max_requests: Option<u32>,
44 rate_limit_window_seconds: Option<u64>,
45 hedge_delay_ms: Option<u64>,
46 hedge_max_count: Option<u32>,
47 parent: TurbineBuilder,
48}
49
50impl Turbine {
51 pub fn from_config(path: &Path) -> Result<Self, Box<dyn std::error::Error>> {
53 let config = Config::load(path)?;
54 Ok(Self { config })
55 }
56
57 pub fn from_raw_config(config: Config) -> Self {
59 Self { config }
60 }
61
62 pub fn builder() -> TurbineBuilder {
64 TurbineBuilder {
65 chains: Vec::new(),
66 dashboard_secret: None,
67 api_keys: Vec::new(),
68 }
69 }
70
71 pub fn host(&self) -> &str {
73 &self.config.server.host
74 }
75
76 pub fn port(&self) -> u16 {
78 self.config.server.port
79 }
80
81 pub fn into_router(self) -> axum::Router {
83 build_router(&self.config)
84 }
85
86 pub async fn serve(self, addr: &str) -> Result<(), Box<dyn std::error::Error>> {
88 let router = build_router(&self.config);
89 let listener = tokio::net::TcpListener::bind(addr).await?;
90 tracing::info!("Turbine starting on {}", addr);
91 axum::serve(listener, router).await?;
92 Ok(())
93 }
94}
95
96impl TurbineBuilder {
97 pub fn add_chain(self, name: &str) -> ChainBuilder {
99 ChainBuilder {
100 name: name.to_string(),
101 route: format!("/{}", name),
102 endpoints: Vec::new(),
103 max_consecutive_failures: 3,
104 cooldown_seconds: 30,
105 health_method: None,
106 health_check_interval_seconds: 30,
107 max_block_lag: 10,
108 rotation: RotationStrategy::RoundRobin,
109 cache_enabled: false,
110 cache_preset: None,
111 cache_max_capacity: None,
112 cache_methods: Vec::new(),
113 chain_id: None,
114 max_retries: 1,
115 retry_delay_ms: 0,
116 rate_limit_max_requests: None,
117 rate_limit_window_seconds: None,
118 hedge_delay_ms: None,
119 hedge_max_count: None,
120 parent: self,
121 }
122 }
123
124 pub fn dashboard_secret(mut self, secret: &str) -> Self {
129 self.dashboard_secret = Some(secret.to_string());
130 self
131 }
132
133 pub fn api_key(mut self, name: &str, key: &str, rate_limit: Option<(u32, u64)>) -> Self {
157 self.api_keys.push(ApiKeyConfig {
158 name: name.to_string(),
159 key: key.to_string(),
160 rate_limit: rate_limit.map(|(max_requests, window_seconds)| RateLimitConfig {
161 max_requests,
162 window_seconds,
163 }),
164 });
165 self
166 }
167
168 pub fn build(self) -> Result<Turbine, Box<dyn std::error::Error>> {
170 if self.chains.is_empty() {
171 return Err("At least one chain must be configured".into());
172 }
173 let config = Config {
174 server: ServerConfig {
175 host: "127.0.0.1".to_string(),
176 port: 8080,
177 dashboard_secret: self.dashboard_secret,
178 api_keys: self.api_keys,
179 },
180 chains: self.chains,
181 };
182 Ok(Turbine { config })
183 }
184}
185
186impl ChainBuilder {
187 pub fn endpoint(mut self, url: &str) -> Self {
189 self.endpoints.push(EndpointConfig {
190 url: url.to_string(),
191 weight: 1,
192 auth: None,
193 methods: None,
194 ws_url: None,
195 });
196 self
197 }
198
199 pub fn weighted_endpoint(mut self, url: &str, weight: u32) -> Self {
201 self.endpoints.push(EndpointConfig {
202 url: url.to_string(),
203 weight,
204 auth: None,
205 methods: None,
206 ws_url: None,
207 });
208 self
209 }
210
211 pub fn endpoint_with_basic_auth(mut self, url: &str, username: &str, password: &str) -> Self {
213 self.endpoints.push(EndpointConfig {
214 url: url.to_string(),
215 weight: 1,
216 auth: Some(EndpointAuth::Basic {
217 username: username.to_string(),
218 password: password.to_string(),
219 }),
220 methods: None,
221 ws_url: None,
222 });
223 self
224 }
225
226 pub fn endpoint_with_bearer(mut self, url: &str, token: &str) -> Self {
228 self.endpoints.push(EndpointConfig {
229 url: url.to_string(),
230 weight: 1,
231 auth: Some(EndpointAuth::Bearer(token.to_string())),
232 methods: None,
233 ws_url: None,
234 });
235 self
236 }
237
238 pub fn endpoint_with_header(
240 mut self,
241 url: &str,
242 header_name: &str,
243 header_value: &str,
244 ) -> Self {
245 self.endpoints.push(EndpointConfig {
246 url: url.to_string(),
247 weight: 1,
248 auth: Some(EndpointAuth::Header {
249 name: header_name.to_string(),
250 value: header_value.to_string(),
251 }),
252 methods: None,
253 ws_url: None,
254 });
255 self
256 }
257
258 pub fn endpoint_with_ws(mut self, url: &str, ws_url: &str) -> Self {
261 self.endpoints.push(EndpointConfig {
262 url: url.to_string(),
263 weight: 1,
264 auth: None,
265 methods: None,
266 ws_url: Some(ws_url.to_string()),
267 });
268 self
269 }
270
271 pub fn restricted_endpoint(mut self, url: &str, methods: &[&str]) -> Self {
274 self.endpoints.push(EndpointConfig {
275 url: url.to_string(),
276 weight: 1,
277 auth: None,
278 methods: Some(methods.iter().map(|s| s.to_string()).collect()),
279 ws_url: None,
280 });
281 self
282 }
283
284 pub fn route(mut self, route: &str) -> Self {
286 self.route = route.to_string();
287 self
288 }
289
290 pub fn max_failures(mut self, n: u32) -> Self {
292 self.max_consecutive_failures = n;
293 self
294 }
295
296 pub fn cooldown_secs(mut self, secs: u64) -> Self {
298 self.cooldown_seconds = secs;
299 self
300 }
301
302 pub fn health_method(mut self, method: &str) -> Self {
304 self.health_method = Some(method.to_string());
305 self
306 }
307
308 pub fn health_check_interval(mut self, secs: u64) -> Self {
310 self.health_check_interval_seconds = secs;
311 self
312 }
313
314 pub fn max_block_lag(mut self, lag: u64) -> Self {
316 self.max_block_lag = lag;
317 self
318 }
319
320 pub fn weighted(mut self) -> Self {
322 self.rotation = RotationStrategy::Weighted;
323 self
324 }
325
326 pub fn latency_based(mut self) -> Self {
329 self.rotation = RotationStrategy::Latency;
330 self
331 }
332
333 pub fn cache(mut self, enabled: bool) -> Self {
335 self.cache_enabled = enabled;
336 self
337 }
338
339 pub fn cache_preset(mut self, preset: &str) -> Self {
341 self.cache_preset = Some(preset.to_string());
342 self
343 }
344
345 pub fn cache_max_capacity(mut self, capacity: u64) -> Self {
347 self.cache_max_capacity = Some(capacity);
348 self
349 }
350
351 pub fn cache_method(mut self, method: &str, ttl_seconds: u64) -> Self {
353 self.cache_methods.push(CacheMethodConfig {
354 name: method.to_string(),
355 ttl_seconds,
356 });
357 self
358 }
359
360 pub fn chain_id(mut self, id: u64) -> Self {
361 self.chain_id = Some(id);
362 self
363 }
364
365 pub fn max_retries(mut self, n: u32) -> Self {
366 self.max_retries = n;
367 self
368 }
369
370 pub fn retry_delay_ms(mut self, ms: u64) -> Self {
371 self.retry_delay_ms = ms;
372 self
373 }
374
375 pub fn rate_limit(mut self, max_requests: u32, window_seconds: u64) -> Self {
376 self.rate_limit_max_requests = Some(max_requests);
377 self.rate_limit_window_seconds = Some(window_seconds);
378 self
379 }
380
381 pub fn hedge(mut self, delay_ms: u64, max_count: u32) -> Self {
384 self.hedge_delay_ms = Some(delay_ms);
385 self.hedge_max_count = Some(max_count);
386 self
387 }
388
389 pub fn done(self) -> TurbineBuilder {
391 let cache = if self.cache_enabled {
392 Some(CacheConfig {
393 enabled: true,
394 preset: self.cache_preset,
395 max_capacity: self.cache_max_capacity,
396 methods: self.cache_methods,
397 })
398 } else {
399 None
400 };
401
402 let rate_limit = match (self.rate_limit_max_requests, self.rate_limit_window_seconds) {
403 (Some(max_requests), Some(window_seconds)) => Some(RateLimitConfig {
404 max_requests,
405 window_seconds,
406 }),
407 _ => None,
408 };
409
410 let hedge = self.hedge_delay_ms.map(|delay_ms| HedgeConfig {
411 delay_ms,
412 max_count: self.hedge_max_count.unwrap_or(1),
413 });
414
415 let chain = ChainConfig {
416 name: self.name,
417 route: self.route,
418 endpoints: self.endpoints,
419 health: HealthConfig {
420 max_consecutive_failures: self.max_consecutive_failures,
421 cooldown_seconds: self.cooldown_seconds,
422 health_method: self.health_method,
423 health_check_interval_seconds: self.health_check_interval_seconds,
424 max_block_lag: self.max_block_lag,
425 max_retries: self.max_retries,
426 retry_delay_ms: self.retry_delay_ms,
427 },
428 rotation: self.rotation,
429 cache,
430 chain_id: self.chain_id,
431 rate_limit,
432 hedge,
433 };
434 let mut parent = self.parent;
435 parent.chains.push(chain);
436 parent
437 }
438}