1use std::{collections::BTreeSet, env, path::PathBuf, sync::Arc};
4
5use ::light_client::{state::LightClientOptions, storage::LightClientSqliteOptions};
6use anyhow::{Context, bail};
7use clap::Parser;
8use espresso_telemetry as telemetry;
9use espresso_types::v0::traits::NullEventConsumer;
10use futures::{channel::oneshot, future::BoxFuture};
11use hotshot_query_service::{
12 data_source::{ExtensibleDataSource, MetricsDataSource},
13 status::{HasMetrics, UpdateStatusData},
14};
15use hotshot_types::traits::metrics::{Metrics, NoMetrics};
16use process_metrics::ProcessMetrics;
17use url::Url;
18
19use super::{
20 ApiState, StorageState,
21 context::ApiContext,
22 data_source::{DataSourceOptions, PruningDataSource, SequencerDataSource, provider},
23 fs, sql,
24 state::NodeApiStateImpl,
25 update::{ApiEventConsumer, ApiSink},
26};
27use crate::{
28 catchup::CatchupStorage, context::TaskList, options::PublicNodeConfig, persistence,
29 request_response::data_source::Storage as RequestResponseStorage,
30};
31
32#[derive(Clone, Debug)]
33pub struct Options {
34 pub http: Http,
35 pub query: Option<Query>,
36 pub submit: Option<Submit>,
37 pub status: Option<Status>,
38 pub catchup: Option<Catchup>,
39 pub config: Option<Config>,
40 pub explorer: Option<Explorer>,
41 pub storage_fs: Option<persistence::fs::Options>,
42 pub storage_sql: Option<persistence::sql::Options>,
43 pub public_node_config: Option<Box<PublicNodeConfig>>,
44}
45
46impl From<Http> for Options {
47 fn from(http: Http) -> Self {
48 Self {
49 http,
50 query: None,
51 submit: None,
52 status: None,
53 catchup: None,
54 config: None,
55 explorer: None,
56 storage_fs: None,
57 storage_sql: None,
58 public_node_config: None,
59 }
60 }
61}
62
63impl Options {
64 pub fn with_port(port: u16) -> Self {
66 Http::with_port(port).into()
67 }
68
69 pub fn query_sql(mut self, query: Query, storage: persistence::sql::Options) -> Self {
71 self.query = Some(query);
72 self.storage_sql = Some(storage);
73 self
74 }
75
76 pub fn query_fs(mut self, query: Query, storage: persistence::fs::Options) -> Self {
78 self.query = Some(query);
79 self.storage_fs = Some(storage);
80 self
81 }
82
83 pub fn submit(mut self, opt: Submit) -> Self {
85 self.submit = Some(opt);
86 self
87 }
88
89 pub fn status(mut self, opt: Status) -> Self {
91 self.status = Some(opt);
92 self
93 }
94
95 pub fn catchup(mut self, opt: Catchup) -> Self {
97 self.catchup = Some(opt);
98 self
99 }
100
101 pub fn config(mut self, opt: Config) -> Self {
103 self.config = Some(opt);
104 self
105 }
106
107 pub fn public_node_config(mut self, c: PublicNodeConfig) -> Self {
111 self.public_node_config = Some(Box::new(c));
112 self
113 }
114
115 pub fn explorer(mut self, opt: Explorer) -> Self {
117 self.explorer = Some(opt);
118 self
119 }
120
121 pub fn has_query_module(&self) -> bool {
123 self.query.is_some() && (self.storage_fs.is_some() || self.storage_sql.is_some())
124 }
125
126 pub async fn serve<C, F>(mut self, init_context: F) -> anyhow::Result<C>
132 where
133 C: ApiContext + Clone,
134 F: FnOnce(
135 Box<dyn Metrics>,
136 Box<dyn ApiSink>,
137 Option<RequestResponseStorage>,
138 ) -> BoxFuture<'static, anyhow::Result<C>>,
139 {
140 let (send_ctx, recv_ctx) = oneshot::channel();
144 let state = ApiState::new(async move {
145 recv_ctx
146 .await
147 .expect("context initialized and sent over channel")
148 });
149 let mut tasks = TaskList::default();
150
151 #[allow(clippy::type_complexity)]
154 let (metrics, consumer, storage): (
155 Box<dyn Metrics>,
156 Box<dyn ApiSink>,
157 Option<RequestResponseStorage>,
158 ) = if let Some(query_opt) = self.query.take() {
159 if let Some(opt) = self.storage_sql.take() {
160 match opt.into_backend() {
161 persistence::sql::SqlBackendOptions::Sqlite(opt) => {
162 sql::sqlite::init_query_module(&self, query_opt, opt, state, &mut tasks)
163 .await?
164 },
165 persistence::sql::SqlBackendOptions::Postgres(opt) => {
166 sql::postgres::init_query_module(&self, query_opt, opt, state, &mut tasks)
167 .await?
168 },
169 }
170 } else if let Some(opt) = self.storage_fs.take() {
171 self.init_with_query_module_fs(query_opt, opt, state, &mut tasks)
172 .await?
173 } else {
174 bail!("query module requested but not storage provided");
175 }
176 } else if self.status.is_some() {
177 let ds = MetricsDataSource::default();
181 let metrics = ds.populate_metrics();
182 telemetry::set_registry(Arc::new(ds.metrics().registry().clone()));
183 tasks.spawn("process_metrics", ProcessMetrics::new(ds.metrics()).run());
184 let axum_ds = Arc::new(ExtensibleDataSource::new(ds, state.clone()));
185
186 let port = self.http.port;
187 let env_vars = get_public_env_vars().unwrap_or_default();
188 let node_cfg = self.public_node_config.as_deref().cloned();
189 let modules = espresso_api::OptionalModules {
190 submit: self.submit.is_some(),
191 catchup: self.catchup.is_some(),
192 config: self.config.is_some(),
193 ..Default::default()
194 };
195 let max_connections = self.http.max_connections;
196 tasks.spawn("API server", async move {
197 let state = NodeApiStateImpl::new(axum_ds)
198 .with_env_vars(env_vars)
199 .with_public_node_config(node_cfg);
200 if let Err(e) =
201 espresso_api::serve_axum_status(port, state, modules, max_connections).await
202 {
203 tracing::error!("Axum server error: {}", e);
204 }
205 anyhow::Ok(())
206 });
207
208 if self.http.tonic_port.is_some() {
209 tracing::warn!("gRPC API not available in status-only mode");
210 }
211
212 (metrics, Box::new(NullEventConsumer), None)
213 } else {
214 let port = self.http.port;
221 let env_vars = get_public_env_vars().unwrap_or_default();
222 let node_cfg = self.public_node_config.as_deref().cloned();
223 let modules = espresso_api::OptionalModules {
224 submit: self.submit.is_some(),
225 catchup: self.catchup.is_some(),
226 config: self.config.is_some(),
227 ..Default::default()
228 };
229 let axum_ds = Arc::new(state.clone());
230 let max_connections = self.http.max_connections;
231 tasks.spawn("API server", async move {
232 let state = NodeApiStateImpl::new(axum_ds)
233 .with_env_vars(env_vars)
234 .with_public_node_config(node_cfg);
235 if let Err(e) =
236 espresso_api::serve_axum_bare(port, state, modules, max_connections).await
237 {
238 tracing::error!("Axum server error: {}", e);
239 }
240 anyhow::Ok(())
241 });
242
243 (Box::new(NoMetrics), Box::new(NullEventConsumer), None)
244 };
245
246 let ctx = init_context(metrics, consumer, storage.clone()).await?;
247 send_ctx
248 .send(Box::new(ctx.clone()))
249 .ok()
250 .context("API server exited without receiving context")?;
251 Ok(ctx.with_task_list(tasks))
252 }
253
254 async fn init_with_query_module_fs(
255 &self,
256 query_opt: Query,
257 mod_opt: persistence::fs::Options,
258 state: ApiState,
259 tasks: &mut TaskList,
260 ) -> anyhow::Result<(
261 Box<dyn Metrics>,
262 Box<dyn ApiSink>,
263 Option<RequestResponseStorage>,
264 )> {
265 let light_client_db = mod_opt.light_client_db(&query_opt)?;
266 let ds = <fs::DataSource as SequencerDataSource>::create(
267 mod_opt,
268 provider(
269 query_opt.peers,
270 &state,
271 query_opt.light_client,
272 light_client_db,
273 )
274 .await?,
275 false,
276 )
277 .await?;
278
279 let inner_storage = ds.inner();
281
282 tasks.spawn("process_metrics", ProcessMetrics::new(ds.metrics()).run());
283
284 let (metrics, ds) = init_query_data_source(ds, state.clone());
285
286 let port = self.http.port;
287 let ds_for_axum = ds.clone();
288 let env_vars = get_public_env_vars().unwrap_or_default();
289 let node_cfg = self.public_node_config.as_deref().cloned();
290 let modules = espresso_api::OptionalModules {
291 submit: self.submit.is_some(),
292 config: self.config.is_some(),
293 ..Default::default()
294 };
295 let max_connections = self.http.max_connections;
296 tasks.spawn("API server", async move {
297 let state = NodeApiStateImpl::new(ds_for_axum)
298 .with_env_vars(env_vars)
299 .with_public_node_config(node_cfg);
300 if let Err(e) = espresso_api::serve_axum_fs(port, state, modules, max_connections).await
301 {
302 tracing::error!("Axum server error: {}", e);
303 }
304 anyhow::Ok(())
305 });
306
307 if self.http.tonic_port.is_some() {
308 tracing::warn!("gRPC API not available with filesystem storage");
309 }
310
311 Ok((
312 metrics,
313 Box::new(ApiEventConsumer::from(ds)),
314 Some(RequestResponseStorage::Fs(inner_storage)),
315 ))
316 }
317}
318
319#[derive(Parser, Clone, Copy, Debug)]
324pub struct Http {
325 #[clap(long, env = "ESPRESSO_NODE_API_PORT", default_value = "8080")]
327 pub port: u16,
328
329 #[clap(long, env = "ESPRESSO_NODE_API_MAX_CONNECTIONS")]
335 pub max_connections: Option<usize>,
336
337 #[clap(long, env = "ESPRESSO_NODE_TONIC_PORT")]
339 pub tonic_port: Option<u16>,
340}
341
342impl Http {
343 pub fn with_port(port: u16) -> Self {
345 Self {
346 port,
347 max_connections: None,
348 tonic_port: None,
349 }
350 }
351}
352
353#[derive(Parser, Clone, Copy, Debug, Default)]
355pub struct Submit;
356
357#[derive(Parser, Clone, Copy, Debug, Default)]
359pub struct Status;
360
361#[derive(Parser, Clone, Copy, Debug, Default)]
363pub struct Catchup;
364
365#[derive(Parser, Clone, Copy, Debug, Default)]
367pub struct Config;
368
369#[derive(Parser, Clone, Debug, Default)]
371pub struct Query {
372 #[clap(long, env = "ESPRESSO_NODE_API_PEERS", value_delimiter = ',')]
374 pub peers: Vec<Url>,
375
376 #[clap(flatten)]
378 pub light_client: LightClientOptions,
379
380 #[clap(flatten)]
382 pub light_client_db: LightClientSqliteOptions,
383
384 #[clap(long, env = "ESPRESSO_NODE_STORAGE_PATH")]
387 pub storage_path: Option<PathBuf>,
388}
389
390impl Query {
391 pub fn sqlite_light_client_db(&self) -> LightClientSqliteOptions {
392 let lc_path = self.light_client_db.lc_path.clone().or_else(|| {
393 self.storage_path
394 .as_ref()
395 .map(|dir| dir.join("light-client").join("lc.db"))
396 });
397 LightClientSqliteOptions {
398 lc_path,
399 ..self.light_client_db.clone()
400 }
401 }
402}
403
404#[cfg(test)]
405impl Query {
406 pub fn test() -> Self {
407 Self::default()
408 }
409}
410
411#[derive(Parser, Clone, Copy, Debug, Default)]
413pub struct State;
414
415#[derive(Parser, Clone, Copy, Debug, Default)]
418pub struct HotshotEvents;
419
420#[derive(Parser, Clone, Copy, Debug, Default)]
422pub struct Explorer;
423
424#[derive(Parser, Clone, Copy, Debug, Default)]
427pub struct LightClient;
428
429#[allow(clippy::type_complexity)]
435pub(super) fn init_query_data_source<D>(
436 ds: D,
437 state: ApiState,
438) -> (Box<dyn Metrics>, Arc<StorageState<D>>)
439where
440 D: SequencerDataSource + CatchupStorage + PruningDataSource + Send + Sync + 'static,
441{
442 let metrics = ds.populate_metrics();
443 telemetry::set_registry(Arc::new(ds.metrics().registry().clone()));
444 let ds = Arc::new(ExtensibleDataSource::new(ds, state));
445 (metrics, ds)
446}
447
448pub(super) fn get_public_env_vars() -> anyhow::Result<Vec<String>> {
450 #[derive(serde::Deserialize)]
451 struct PublicEnvVars {
452 variables: BTreeSet<String>,
453 }
454
455 let PublicEnvVars { variables } =
456 toml::from_str(include_str!("../../api/public-env-vars.toml"))?;
457 Ok(variables
458 .into_iter()
459 .map(|key| {
460 let value = env::var(&key).unwrap_or_default();
461 format!("{key}={value}")
462 })
463 .collect())
464}