Skip to main content

espresso_node/api/
endpoints.rs

1//! Sequencer-specific API endpoint handlers.
2
3use std::{
4    collections::{BTreeSet, HashMap},
5    env,
6};
7
8use alloy::primitives::utils::format_ether;
9use anyhow::{Context, Result};
10use committable::Committable;
11use espresso_types::{
12    FeeAccount, FeeMerkleTree, PubKey, Transaction,
13    v0_3::RewardAccountV1,
14    v0_4::{RewardAccountV2, RewardClaimError},
15};
16
17use crate::api::{
18    RewardAmount, RewardMerkleTreeV2Data, data_source::TokenDataSource, unlock_schedule,
19};
20// re-exported here to avoid breaking changes in consumers
21// "deprecated" does not work with "pub use": https://github.com/rust-lang/rust/issues/30827
22#[deprecated(note = "use espresso_types::ADVZNamespaceProofQueryData")]
23pub type ADVZNamespaceProofQueryData = espresso_types::ADVZNamespaceProofQueryData;
24#[deprecated(note = "use espresso_types::NamespaceProofQueryData")]
25pub type NamespaceProofQueryData = espresso_types::NamespaceProofQueryData;
26
27use futures::FutureExt;
28use hotshot_query_service::{
29    Error,
30    availability::AvailabilityDataSource,
31    explorer::{self, ExplorerDataSource},
32    merklized_state::{
33        self, MerklizedState, MerklizedStateDataSource, MerklizedStateHeightPersistence, Snapshot,
34    },
35    node::{self, NodeDataSource},
36};
37use hotshot_types::{
38    data::{EpochNumber, ViewNumber},
39    traits::network::ConnectedNetwork,
40};
41use jf_merkle_tree_compat::MerkleTreeScheme;
42use serde::de::Error as _;
43use tagged_base64::TaggedBase64;
44use tide_disco::{Api, Error as _, RequestParams, StatusCode, method::ReadState};
45use vbs::version::{StaticVersion, StaticVersionType};
46
47use super::data_source::{
48    CatchupDataSource, DatabaseMetadataSource, HotShotConfigDataSource, NodeStateDataSource,
49    PruningDataSource, StakeTableDataSource, StateSignatureDataSource, SubmitDataSource,
50};
51use crate::{
52    SeqTypes, SequencerApiVersion, SequencerPersistence, api::RewardMerkleTreeDataSource,
53    options::PublicNodeConfig,
54};
55
56pub(crate) mod availability;
57pub(super) use availability::*;
58
59pub(super) fn fee<State, Ver>(
60    api_ver: semver::Version,
61) -> Result<Api<State, merklized_state::Error, Ver>>
62where
63    State: 'static + Send + Sync + ReadState,
64    Ver: 'static + StaticVersionType,
65    <State as ReadState>::State: Send
66        + Sync
67        + MerklizedStateDataSource<SeqTypes, FeeMerkleTree, { FeeMerkleTree::ARITY }>
68        + MerklizedStateHeightPersistence,
69{
70    let mut options = merklized_state::Options::default();
71    let extension = toml::from_str(include_str!("../../api/fee.toml"))?;
72    options.extensions.push(extension);
73
74    let mut api =
75        merklized_state::define_api::<State, SeqTypes, FeeMerkleTree, Ver, 256>(&options, api_ver)?;
76
77    api.get("getfeebalance", move |req, state| {
78        async move {
79            let address = req.string_param("address")?;
80            let height = state.get_last_state_height().await?;
81            let snapshot = Snapshot::Index(height as u64);
82            let key = address
83                .parse()
84                .map_err(|_| merklized_state::Error::Custom {
85                    message: "failed to parse address".to_string(),
86                    status: StatusCode::BAD_REQUEST,
87                })?;
88            let path = state.get_path(snapshot, key).await?;
89            Ok(path.elem().copied())
90        }
91        .boxed()
92    })?;
93    Ok(api)
94}
95
96pub enum RewardMerkleTreeVersion {
97    V1,
98    V2,
99}
100
101pub(super) fn reward<State, Ver, MT, const ARITY: usize>(
102    api_ver: semver::Version,
103    merkle_tree_version: RewardMerkleTreeVersion,
104) -> Result<Api<State, merklized_state::Error, Ver>>
105where
106    State: 'static + Send + Sync + ReadState,
107    Ver: 'static + StaticVersionType,
108    MT: MerklizedState<SeqTypes, ARITY>,
109    for<'a> <MT::Commit as TryFrom<&'a TaggedBase64>>::Error: std::fmt::Display,
110    <MT as MerklizedState<SeqTypes, ARITY>>::Entry: std::marker::Copy,
111    <State as ReadState>::State: Send
112        + Sync
113        + RewardMerkleTreeDataSource
114        + MerklizedStateDataSource<SeqTypes, MT, ARITY>
115        + MerklizedStateHeightPersistence,
116{
117    let mut options = merklized_state::Options::default();
118    let extension = toml::from_str(include_str!("../../api/reward.toml"))?;
119    options.extensions.push(extension);
120
121    let mut api =
122        merklized_state::define_api::<State, SeqTypes, MT, Ver, ARITY>(&options, api_ver)?;
123
124    api.get("get_latest_reward_balance", move |req, state| {
125        async move {
126            let address = req.string_param("address")?;
127            let account = address
128                .parse()
129                .map_err(|_| merklized_state::Error::Custom {
130                    message: format!("invalid reward address: {address}"),
131                    status: StatusCode::BAD_REQUEST,
132                })?;
133
134            state
135                .load_latest_reward_account_proof_v2(account)
136                .await
137                .map_err(|err| merklized_state::Error::Custom {
138                    message: format!(
139                        "failed to load latest reward account proof from storage for account \
140                         {account}: {err}"
141                    ),
142                    status: StatusCode::NOT_FOUND,
143                })
144                .map(|proof| RewardAmount(proof.balance))
145        }
146        .boxed()
147    })?
148    .get("get_latest_reward_account_proof", move |req, state| {
149        async move {
150            let address = req.string_param("address")?;
151            let account = address
152                .parse()
153                .map_err(|_| merklized_state::Error::Custom {
154                    message: format!("invalid reward address: {address}"),
155                    status: StatusCode::BAD_REQUEST,
156                })?;
157
158            state
159                .load_latest_reward_account_proof_v2(account)
160                .await
161                .map_err(|err| merklized_state::Error::Custom {
162                    message: format!(
163                        "failed to load latest reward account proof from storage for account \
164                         {account}: {err}"
165                    ),
166                    status: StatusCode::NOT_FOUND,
167                })
168        }
169        .boxed()
170    })?
171    .get("get_reward_balance", move |req, state| {
172        async move {
173            let address = req.string_param("address")?;
174            let height = req.integer_param("height")?;
175            let account = address
176                .parse()
177                .map_err(|_| merklized_state::Error::Custom {
178                    message: format!("invalid reward address: {address}"),
179                    status: StatusCode::BAD_REQUEST,
180                })?;
181
182            state
183                .load_reward_account_proof_v2(height, account)
184                .await
185                .map_err(|err| merklized_state::Error::Custom {
186                    message: format!(
187                        "failed to load v2 reward account {address} at height {height}: {err}"
188                    ),
189                    status: StatusCode::NOT_FOUND,
190                })
191                .map(|proof| Some(RewardAmount(proof.balance)))
192        }
193        .boxed()
194    })?
195    .get("get_reward_amounts", move |req, state| {
196        async move {
197            let height = req.integer_param("height")?;
198            let limit: usize = req.integer_param("limit")?;
199            let offset = req.integer_param("offset")?;
200
201            if limit > 10_000 {
202                return Err(merklized_state::Error::Custom {
203                    message: format!("limit {limit} exceeds maximum allowed 10000"),
204                    status: StatusCode::BAD_REQUEST,
205                });
206            }
207
208            let tree_bytes =
209                state
210                    .load_tree(height)
211                    .await
212                    .map_err(|err| merklized_state::Error::Custom {
213                        message: format!(
214                            "failed to load RewardMerkleTreeV2Data from storage at height \
215                             {height}: {err}"
216                        ),
217                        status: StatusCode::NOT_FOUND,
218                    })?;
219
220            let tree_data = bincode::deserialize::<RewardMerkleTreeV2Data>(&tree_bytes)
221                .context(
222                    "Failed to deserialize RewardMerkleTreeV2 for height {height} from storage; \
223                     this should never happen.",
224                )
225                .map_err(|err| merklized_state::Error::Custom {
226                    message: format!(
227                        "failed to load RewardMerkleTreeV2Data from storage at height {height}: \
228                         {err}"
229                    ),
230                    status: StatusCode::NOT_FOUND,
231                })?;
232
233            let end = std::cmp::min(offset + limit, tree_data.balances.len());
234
235            let result = tree_data
236                .balances
237                .get(offset..end)
238                .ok_or(merklized_state::Error::Custom {
239                    message: "Range out of bounds for balances".to_string(),
240                    status: StatusCode::NOT_FOUND,
241                })?
242                .iter()
243                .rev()
244                .cloned()
245                .collect::<Vec<_>>();
246
247            Ok(result)
248        }
249        .boxed()
250    })?
251    .get("get_reward_merkle_tree_v2", move |req, state| {
252        async move {
253            let height = req.integer_param("height")?;
254
255            state
256                .load_tree(height)
257                .await
258                .map_err(|err| merklized_state::Error::Custom {
259                    message: format!(
260                        "failed to load RewardMerkleTreeV2Data from storage at height {height}: \
261                         {err}"
262                    ),
263                    status: StatusCode::NOT_FOUND,
264                })
265        }
266        .boxed()
267    })?;
268
269    match merkle_tree_version {
270        RewardMerkleTreeVersion::V1 => {
271            api.get("get_reward_account_proof", move |req, state| {
272                async move {
273                    let address = req.string_param("address")?;
274                    let height = req.integer_param("height")?;
275                    let account = address
276                        .parse()
277                        .map_err(|_| merklized_state::Error::Custom {
278                            message: format!("invalid reward address: {address}"),
279                            status: StatusCode::BAD_REQUEST,
280                        })?;
281
282                    state
283                        .load_v1_reward_account_proof(height, account)
284                        .await
285                        .map_err(|err| merklized_state::Error::Custom {
286                            message: format!(
287                                "failed to load v1 reward account {address} at height {height}: \
288                                 {err}"
289                            ),
290                            status: StatusCode::NOT_FOUND,
291                        })
292                }
293                .boxed()
294            })?;
295        },
296        RewardMerkleTreeVersion::V2 => {
297            api.get("get_reward_account_proof", move |req, state| {
298                async move {
299                    let address = req.string_param("address")?;
300                    let height = req.integer_param("height")?;
301                    let account = address
302                        .parse()
303                        .map_err(|_| merklized_state::Error::Custom {
304                            message: format!("invalid reward address: {address}"),
305                            status: StatusCode::BAD_REQUEST,
306                        })?;
307
308                    state
309                        .load_reward_account_proof_v2(height, account)
310                        .await
311                        .map_err(|err| merklized_state::Error::Custom {
312                            message: format!(
313                                "failed to load v2 reward account {address} at height {height}: \
314                                 {err}"
315                            ),
316                            status: StatusCode::NOT_FOUND,
317                        })
318                }
319                .boxed()
320            })?;
321
322            api.get("get_reward_claim_input", move |req, state| {
323                async move {
324                    let address = req.string_param("address")?;
325                    let height = req.integer_param("height")?;
326                    let account = address
327                        .parse()
328                        .map_err(|_| merklized_state::Error::Custom {
329                            message: format!("invalid reward address: {address}"),
330                            status: StatusCode::BAD_REQUEST,
331                        })?;
332
333                    let proof = state
334                        .load_reward_account_proof_v2(height, account)
335                        .await
336                        .map_err(|err| merklized_state::Error::Custom {
337                            message: format!(
338                                "failed to load v2 reward account {address} at height {height}: \
339                                 {err}"
340                            ),
341                            status: StatusCode::NOT_FOUND,
342                        })?;
343
344                    // Auth root inputs (other than the reward merkle tree root) are currently
345                    // all zero placeholder values. This may be extended in the future.
346                    let claim_input = match proof.to_reward_claim_input() {
347                        Ok(input) => input,
348                        Err(RewardClaimError::ZeroRewardError) => {
349                            return Err(merklized_state::Error::Custom {
350                                message: format!(
351                                    "zero reward balance for {address} at height {height}"
352                                ),
353                                status: StatusCode::NOT_FOUND,
354                            });
355                        },
356                        Err(RewardClaimError::ProofConversionError(err)) => {
357                            let message = format!(
358                                "failed to create solidity proof for {address} at height \
359                                 {height}: {err}",
360                            );
361                            tracing::warn!("{message}");
362                            // Normally we would not want to return the internal error via the
363                            // API response but this is an error that should never occur. No
364                            // secret data involved so it seems fine to return it.
365                            return Err(merklized_state::Error::Custom {
366                                message,
367                                status: StatusCode::INTERNAL_SERVER_ERROR,
368                            });
369                        },
370                    };
371
372                    Ok(claim_input)
373                }
374                .boxed()
375            })?;
376        },
377    }
378
379    Ok(api)
380}
381
382type ExplorerApi<N, P, D, ApiVer> = Api<AvailState<N, P, D>, explorer::Error, ApiVer>;
383
384pub(super) fn explorer<N, P, D>(
385    api_ver: semver::Version,
386) -> Result<ExplorerApi<N, P, D, SequencerApiVersion>>
387where
388    N: ConnectedNetwork<PubKey>,
389    D: ExplorerDataSource<SeqTypes> + Send + Sync + 'static,
390    P: SequencerPersistence,
391{
392    let api = explorer::define_api::<AvailState<N, P, D>, SeqTypes, _>(
393        SequencerApiVersion::instance(),
394        api_ver,
395    )?;
396    Ok(api)
397}
398
399pub(super) fn token<S>(api_ver: semver::Version) -> Result<Api<S, node::Error, StaticVersion<0, 1>>>
400where
401    S: 'static + Send + Sync + ReadState,
402    <S as ReadState>::State: Send
403        + Sync
404        + TokenDataSource<SeqTypes>
405        + NodeDataSource<SeqTypes>
406        + NodeStateDataSource
407        + AvailabilityDataSource<SeqTypes>,
408{
409    // Extend the base API
410    let mut options = node::Options::default();
411    let extension = toml::from_str(include_str!("../../api/token.toml"))?;
412    options.extensions.push(extension);
413
414    // Create the base API with our extensions
415    let mut api =
416        node::define_api::<S, SeqTypes, _>(&options, SequencerApiVersion::instance(), api_ver)?;
417
418    // Tack on the application logic
419    api.at("get_total_minted_supply", |_, state| {
420        async move {
421            let value = state
422                .read(|state| state.get_total_supply_l1().boxed())
423                .await
424                .map_err(|err| node::Error::Custom {
425                    message: format!("failed to get total supply. err={err:#}"),
426                    status: StatusCode::NOT_FOUND,
427                })?;
428
429            Ok(format_ether(value))
430        }
431        .boxed()
432    })?;
433
434    api.at("get_circulating_supply", |_, state| {
435        async move {
436            let calc = fetch_supply_inputs(state).await?;
437            Ok(format_ether(calc.circulating_supply()))
438        }
439        .boxed()
440    })?;
441
442    api.at("get_circulating_supply_ethereum", |_, state| {
443        async move {
444            let calc = fetch_supply_inputs(state).await?;
445            Ok(format_ether(calc.circulating_supply_ethereum()))
446        }
447        .boxed()
448    })?;
449
450    api.at("get_total_issued_supply", |_, state| {
451        async move {
452            let calc = fetch_supply_inputs(state).await?;
453            Ok(format_ether(calc.total_issued_supply()))
454        }
455        .boxed()
456    })?;
457
458    // Reuses fetch_supply_inputs for uniformity; the extra Ethereum fetches are cached.
459    api.at("get_total_reward_distributed", |_, state| {
460        async move {
461            let calc = fetch_supply_inputs(state).await?;
462            Ok(format_ether(calc.total_reward_distributed()))
463        }
464        .boxed()
465    })?;
466
467    Ok(api)
468}
469
470/// Fetch state data and build a [`unlock_schedule::SupplyCalculator`].
471async fn fetch_supply_inputs<S: ReadState>(
472    state: &S,
473) -> Result<unlock_schedule::SupplyCalculator, node::Error>
474where
475    S::State: Send + Sync + TokenDataSource<SeqTypes> + NodeStateDataSource,
476{
477    let node_state = state.read(|s| s.node_state().boxed()).await;
478    let chain_id = node_state.chain_config.chain_id;
479
480    let header = state.read(|s| s.get_decided_header().boxed()).await;
481    let now_secs = header.timestamp_internal();
482    let total_reward_distributed = header.total_reward_distributed();
483
484    let initial_supply = state
485        .read(|s| s.get_initial_supply_l1().boxed())
486        .await
487        .map_err(|err| node::Error::Custom {
488            message: format!("failed to get initial supply: {err:#}"),
489            status: StatusCode::INTERNAL_SERVER_ERROR,
490        })?;
491
492    let total_supply_l1 = state
493        .read(|s| s.get_total_supply_l1().boxed())
494        .await
495        .map_err(|err| node::Error::Custom {
496            message: format!("failed to get total supply: {err:#}"),
497            status: StatusCode::INTERNAL_SERVER_ERROR,
498        })?;
499
500    Ok(unlock_schedule::SupplyCalculator::new(
501        chain_id,
502        now_secs,
503        initial_supply,
504        total_supply_l1,
505        total_reward_distributed,
506    ))
507}
508
509pub(super) fn node<S>(api_ver: semver::Version) -> Result<Api<S, node::Error, StaticVersion<0, 1>>>
510where
511    S: 'static + Send + Sync + ReadState,
512    <S as ReadState>::State: Send
513        + Sync
514        + StakeTableDataSource<SeqTypes>
515        + NodeDataSource<SeqTypes>
516        + AvailabilityDataSource<SeqTypes>
517        + PruningDataSource,
518{
519    // Extend the base API
520    let mut options = node::Options::default();
521    let extension = toml::from_str(include_str!("../../api/node.toml"))?;
522    options.extensions.push(extension);
523
524    // Create the base API with our extensions
525    let mut api =
526        node::define_api::<S, SeqTypes, _>(&options, SequencerApiVersion::instance(), api_ver)?;
527
528    // Tack on the application logic
529    api.at("stake_table", |req, state| {
530        async move {
531            // Try to get the epoch from the request. If this fails, error
532            // as it was probably a mistake
533            let epoch = req
534                .opt_integer_param("epoch_number")
535                .map_err(|_| hotshot_query_service::node::Error::Custom {
536                    message: "Epoch number is required".to_string(),
537                    status: StatusCode::BAD_REQUEST,
538                })?
539                .map(EpochNumber::new);
540
541            state
542                .read(|state| state.get_stake_table(epoch).boxed())
543                .await
544                .map_err(|err| node::Error::Custom {
545                    message: format!("failed to get stake table for epoch={epoch:?}. err={err:#}"),
546                    status: StatusCode::NOT_FOUND,
547                })
548        }
549        .boxed()
550    })?
551    .at("stake_table_current", |_, state| {
552        async move {
553            state
554                .read(|state| state.get_stake_table_current().boxed())
555                .await
556                .map_err(|err| node::Error::Custom {
557                    message: format!("failed to get current stake table. err={err:#}"),
558                    status: StatusCode::NOT_FOUND,
559                })
560        }
561        .boxed()
562    })?
563    .at("da_stake_table", |req, state| {
564        async move {
565            // Try to get the epoch from the request. If this fails, error
566            // as it was probably a mistake
567            let epoch = req
568                .opt_integer_param("epoch_number")
569                .map_err(|_| hotshot_query_service::node::Error::Custom {
570                    message: "Epoch number is required".to_string(),
571                    status: StatusCode::BAD_REQUEST,
572                })?
573                .map(EpochNumber::new);
574
575            state
576                .read(|state| state.get_da_stake_table(epoch).boxed())
577                .await
578                .map_err(|err| node::Error::Custom {
579                    message: format!(
580                        "failed to get DA stake table for epoch={epoch:?}. err={err:#}"
581                    ),
582                    status: StatusCode::NOT_FOUND,
583                })
584        }
585        .boxed()
586    })?
587    .at("da_stake_table_current", |_, state| {
588        async move {
589            state
590                .read(|state| state.get_da_stake_table_current().boxed())
591                .await
592                .map_err(|err| node::Error::Custom {
593                    message: format!("failed to get current DA stake table. err={err:#}"),
594                    status: StatusCode::NOT_FOUND,
595                })
596        }
597        .boxed()
598    })?
599    .at("get_validators", |req, state| {
600        async move {
601            let epoch = req.integer_param::<_, u64>("epoch_number").map_err(|_| {
602                hotshot_query_service::node::Error::Custom {
603                    message: "Epoch number is required".to_string(),
604                    status: StatusCode::BAD_REQUEST,
605                }
606            })?;
607
608            state
609                .read(|state| state.get_validators(EpochNumber::new(epoch)).boxed())
610                .await
611                .map_err(|err| hotshot_query_service::node::Error::Custom {
612                    message: format!("failed to get validators mapping: err: {err}"),
613                    status: StatusCode::NOT_FOUND,
614                })
615        }
616        .boxed()
617    })?
618    .at("get_all_validators", |req, state| {
619        async move {
620            let epoch = req.integer_param::<_, u64>("epoch_number").map_err(|_| {
621                hotshot_query_service::node::Error::Custom {
622                    message: "Epoch number is required".to_string(),
623                    status: StatusCode::BAD_REQUEST,
624                }
625            })?;
626
627            let offset = req.integer_param::<_, u64>("offset")?;
628
629            let limit = req.integer_param::<_, u64>("limit")?;
630            if limit > 1000 {
631                return Err(hotshot_query_service::node::Error::Custom {
632                    message: "Limit cannot be greater than 1000".to_string(),
633                    status: StatusCode::BAD_REQUEST,
634                });
635            }
636
637            state
638                .read(|state| {
639                    state
640                        .get_all_validators(EpochNumber::new(epoch), offset, limit)
641                        .boxed()
642                })
643                .await
644                .map_err(|err| hotshot_query_service::node::Error::Custom {
645                    message: format!("failed to get all validators : err: {err}"),
646                    status: StatusCode::INTERNAL_SERVER_ERROR,
647                })
648        }
649        .boxed()
650    })?
651    .at("current_proposal_participation", |_, state| {
652        async move {
653            Ok(state
654                .read(|state| state.current_proposal_participation().boxed())
655                .await)
656        }
657        .boxed()
658    })?
659    .at("proposal_participation", |req, state| {
660        async move {
661            let epoch = req.integer_param::<_, u64>("epoch").map_err(|_| {
662                hotshot_query_service::node::Error::Custom {
663                    message: "Epoch number is required".to_string(),
664                    status: StatusCode::BAD_REQUEST,
665                }
666            })?;
667
668            Ok(state
669                .read(|state| state.proposal_participation(epoch.into()).boxed())
670                .await)
671        }
672        .boxed()
673    })?
674    .at("current_vote_participation", |_, state| {
675        async move {
676            Ok(state
677                .read(|state| state.current_vote_participation().boxed())
678                .await)
679        }
680        .boxed()
681    })?
682    .at("vote_participation", |req, state| {
683        async move {
684            let epoch = req.integer_param::<_, u64>("epoch").map_err(|_| {
685                hotshot_query_service::node::Error::Custom {
686                    message: "Epoch number is required".to_string(),
687                    status: StatusCode::BAD_REQUEST,
688                }
689            })?;
690
691            Ok(state
692                .read(|state| state.vote_participation(epoch.into()).boxed())
693                .await)
694        }
695        .boxed()
696    })?
697    .at("get_block_reward", |req, state| {
698        async move {
699            let epoch = req
700                .opt_integer_param::<_, u64>("epoch_number")?
701                .map(EpochNumber::new);
702
703            state
704                .read(|state| state.get_block_reward(epoch).boxed())
705                .await
706                .map_err(|err| node::Error::Custom {
707                    message: format!("failed to get block reward. err={err:#}"),
708                    status: StatusCode::NOT_FOUND,
709                })
710        }
711        .boxed()
712    })?
713    .at("get_oldest_block", |_req, state| {
714        async move {
715            state
716                .read(|state| state.get_oldest_block().boxed())
717                .await
718                .map_err(|err| node::Error::Custom {
719                    message: format!("failed to get oldest block: {err:#}"),
720                    status: StatusCode::INTERNAL_SERVER_ERROR,
721                })
722        }
723        .boxed()
724    })?
725    .at("get_oldest_leaf", |_req, state| {
726        async move {
727            state
728                .read(|state| state.get_oldest_leaf().boxed())
729                .await
730                .map_err(|err| node::Error::Custom {
731                    message: format!("failed to get oldest leaf: {err:#}"),
732                    status: StatusCode::INTERNAL_SERVER_ERROR,
733                })
734        }
735        .boxed()
736    })?;
737
738    Ok(api)
739}
740
741pub(super) fn database<S, ApiVer: StaticVersionType + 'static>(
742    api_ver: semver::Version,
743) -> Result<Api<S, Error, ApiVer>>
744where
745    S: 'static + Send + Sync + ReadState,
746    <S as ReadState>::State: Send + Sync + DatabaseMetadataSource,
747{
748    let toml = toml::from_str::<toml::Value>(include_str!("../../api/database.toml"))?;
749    let mut api = Api::<S, Error, ApiVer>::new(toml)?;
750
751    api.with_version(api_ver)
752        .at("get_table_sizes", |_req, state| {
753            async move {
754                state
755                    .read(|state| state.get_table_sizes().boxed())
756                    .await
757                    .map_err(|err| Error::internal(format!("failed to get table sizes: {err:#}")))
758            }
759            .boxed()
760        })?;
761
762    Ok(api)
763}
764
765pub(super) fn submit<N, P, S, ApiVer: StaticVersionType + 'static>(
766    api_ver: semver::Version,
767) -> Result<Api<S, Error, ApiVer>>
768where
769    N: ConnectedNetwork<PubKey>,
770    S: 'static + Send + Sync + ReadState,
771    P: SequencerPersistence,
772    S::State: Send + Sync + SubmitDataSource<N, P>,
773{
774    let toml = toml::from_str::<toml::Value>(include_str!("../../api/submit.toml"))?;
775    let mut api = Api::<S, Error, ApiVer>::new(toml)?;
776
777    api.with_version(api_ver).at("submit", |req, state| {
778        async move {
779            let tx = req
780                .body_auto::<Transaction, ApiVer>(ApiVer::instance())
781                .map_err(Error::from_request_error)?;
782
783            let hash = tx.commit();
784            state
785                .read(|state| state.submit(tx).boxed())
786                .await
787                .map_err(|err| Error::internal(err.to_string()))?;
788            Ok(hash)
789        }
790        .boxed()
791    })?;
792
793    Ok(api)
794}
795
796pub(super) fn state_signature<N, S, ApiVer: StaticVersionType + 'static>(
797    _: ApiVer,
798    api_ver: semver::Version,
799) -> Result<Api<S, Error, ApiVer>>
800where
801    N: ConnectedNetwork<PubKey>,
802    S: 'static + Send + Sync + ReadState,
803    S::State: Send + Sync + StateSignatureDataSource<N>,
804{
805    let toml = toml::from_str::<toml::Value>(include_str!("../../api/state_signature.toml"))?;
806    let mut api = Api::<S, Error, ApiVer>::new(toml)?;
807    api.with_version(api_ver);
808
809    api.get("get_state_signature", |req, state| {
810        async move {
811            let height = req
812                .integer_param("height")
813                .map_err(Error::from_request_error)?;
814            state
815                .get_state_signature(height)
816                .await
817                .ok_or(tide_disco::Error::catch_all(
818                    StatusCode::NOT_FOUND,
819                    "Signature not found.".to_owned(),
820                ))
821        }
822        .boxed()
823    })?;
824
825    Ok(api)
826}
827
828pub(super) fn catchup<S, ApiVer: StaticVersionType + 'static>(
829    _: ApiVer,
830    api_ver: semver::Version,
831) -> Result<Api<S, Error, ApiVer>>
832where
833    S: 'static + Send + Sync + ReadState,
834    S::State: Send + Sync + NodeStateDataSource + CatchupDataSource,
835{
836    let toml = toml::from_str::<toml::Value>(include_str!("../../api/catchup.toml"))?;
837    let mut api = Api::<S, Error, ApiVer>::new(toml)?;
838    api.with_version(api_ver);
839
840    let parse_height_view = |req: &RequestParams| -> Result<(u64, ViewNumber), Error> {
841        let height = req
842            .integer_param("height")
843            .map_err(Error::from_request_error)?;
844        let view = req
845            .integer_param("view")
846            .map_err(Error::from_request_error)?;
847        Ok((height, ViewNumber::new(view)))
848    };
849
850    let parse_fee_account = |req: &RequestParams| -> Result<FeeAccount, Error> {
851        let raw = req
852            .string_param("address")
853            .map_err(Error::from_request_error)?;
854        raw.parse().map_err(|err| {
855            Error::catch_all(
856                StatusCode::BAD_REQUEST,
857                format!("malformed fee account {raw}: {err}"),
858            )
859        })
860    };
861
862    let parse_reward_account = |req: &RequestParams| -> Result<RewardAccountV2, Error> {
863        let raw = req
864            .string_param("address")
865            .map_err(Error::from_request_error)?;
866        raw.parse().map_err(|err| {
867            Error::catch_all(
868                StatusCode::BAD_REQUEST,
869                format!("malformed reward account {raw}: {err}"),
870            )
871        })
872    };
873
874    api.get("account", move |req, state| {
875        async move {
876            let (height, view) = parse_height_view(&req)?;
877            let account = parse_fee_account(&req)?;
878            state
879                .get_account(&state.node_state().await, height, view, account)
880                .await
881                .map_err(|err| Error::catch_all(StatusCode::NOT_FOUND, format!("{err:#}")))
882        }
883        .boxed()
884    })?
885    .at("accounts", move |req, state| {
886        async move {
887            let (height, view) = parse_height_view(&req)?;
888            let accounts = req
889                .body_auto::<Vec<FeeAccount>, ApiVer>(ApiVer::instance())
890                .map_err(Error::from_request_error)?;
891
892            state
893                .read(|state| {
894                    async move {
895                        state
896                            .get_accounts(&state.node_state().await, height, view, &accounts)
897                            .await
898                            .map_err(|err| {
899                                Error::catch_all(StatusCode::NOT_FOUND, format!("{err:#}"))
900                            })
901                    }
902                    .boxed()
903                })
904                .await
905        }
906        .boxed()
907    })?
908    .get("reward_account", move |req, state| {
909        async move {
910            let (height, view) = parse_height_view(&req)?;
911            let account = parse_reward_account(&req)?;
912            state
913                .get_reward_account_v1(&state.node_state().await, height, view, account.into())
914                .await
915                .map_err(|err| Error::catch_all(StatusCode::NOT_FOUND, format!("{err:#}")))
916        }
917        .boxed()
918    })?
919    .at("reward_accounts", move |req, state| {
920        async move {
921            let (height, view) = parse_height_view(&req)?;
922            let accounts = req
923                .body_auto::<Vec<RewardAccountV1>, ApiVer>(ApiVer::instance())
924                .map_err(Error::from_request_error)?;
925
926            state
927                .read(|state| {
928                    async move {
929                        state
930                            .get_reward_accounts_v1(
931                                &state.node_state().await,
932                                height,
933                                view,
934                                &accounts,
935                            )
936                            .await
937                            .map_err(|err| {
938                                Error::catch_all(StatusCode::NOT_FOUND, format!("{err:#}"))
939                            })
940                    }
941                    .boxed()
942                })
943                .await
944        }
945        .boxed()
946    })?
947    .get("reward_account_v2", move |req, state| {
948        async move {
949            let (height, view) = parse_height_view(&req)?;
950            let account = parse_reward_account(&req)?;
951
952            state
953                .get_reward_account_v2(&state.node_state().await, height, view, account)
954                .await
955                .map_err(|err| Error::catch_all(StatusCode::NOT_FOUND, format!("{err:#}")))
956        }
957        .boxed()
958    })?
959    .get("reward_amounts", move |_req, _state| {
960        async move {
961            Err::<u64, _>(Error::catch_all(
962                StatusCode::NOT_FOUND,
963                "catchup/reward-amounts is deprecated".to_string(),
964            ))
965        }
966        .boxed()
967    })?
968    .at("reward_accounts_v2", move |_req, _state| {
969        async move {
970            Err::<u64, _>(Error::catch_all(
971                StatusCode::NOT_FOUND,
972                "catchup/reward-accounts-v2 is deprecated".to_string(),
973            ))
974        }
975        .boxed()
976    })?
977    .get("blocks", |req, state| {
978        async move {
979            let height = req
980                .integer_param("height")
981                .map_err(Error::from_request_error)?;
982            let view = req
983                .integer_param("view")
984                .map_err(Error::from_request_error)?;
985
986            state
987                .get_frontier(&state.node_state().await, height, ViewNumber::new(view))
988                .await
989                .map_err(|err| Error::catch_all(StatusCode::NOT_FOUND, format!("{err:#}")))
990        }
991        .boxed()
992    })?
993    .get("chainconfig", |req, state| {
994        async move {
995            let commitment = req
996                .blob_param("commitment")
997                .map_err(Error::from_request_error)?;
998
999            state
1000                .get_chain_config(commitment)
1001                .await
1002                .map_err(|err| Error::catch_all(StatusCode::NOT_FOUND, format!("{err:#}")))
1003        }
1004        .boxed()
1005    })?
1006    .get("leafchain", |req, state| {
1007        async move {
1008            let height = req
1009                .integer_param("height")
1010                .map_err(Error::from_request_error)?;
1011            state
1012                .get_leaf_chain(height)
1013                .await
1014                .map_err(|err| Error::catch_all(StatusCode::NOT_FOUND, format!("{err:#}")))
1015        }
1016        .boxed()
1017    })?
1018    .get("cert2", |req, state| {
1019        async move {
1020            let height = req
1021                .integer_param("height")
1022                .map_err(Error::from_request_error)?;
1023            let response = state
1024                .get_cert2(height)
1025                .await
1026                .map_err(|err| Error::catch_all(StatusCode::NOT_FOUND, format!("{err:#}")))?;
1027            response.ok_or_else(|| {
1028                Error::catch_all(
1029                    StatusCode::NOT_FOUND,
1030                    format!("no cert2 available for height {height}"),
1031                )
1032            })
1033        }
1034        .boxed()
1035    })?
1036    .get("reward_merkle_tree_v2", move |req, state| {
1037        async move {
1038            let (height, view) = parse_height_view(&req)?;
1039            state
1040                .get_reward_merkle_tree_v2(height, view)
1041                .await
1042                .map_err(|err| Error::catch_all(StatusCode::NOT_FOUND, format!("{err:#}")))
1043        }
1044        .boxed()
1045    })?
1046    .get("state_cert", |req, state| {
1047        async move {
1048            let epoch = req
1049                .integer_param("epoch")
1050                .map_err(Error::from_request_error)?;
1051            state
1052                .get_state_cert(epoch)
1053                .await
1054                .map_err(|err| Error::catch_all(StatusCode::NOT_FOUND, format!("{err:#}")))
1055        }
1056        .boxed()
1057    })?;
1058
1059    Ok(api)
1060}
1061
1062type MerklizedStateApi<N, P, D, ApiVer> = Api<AvailState<N, P, D>, merklized_state::Error, ApiVer>;
1063pub(super) fn merklized_state<N, P, D, S, const ARITY: usize>(
1064    api_ver: semver::Version,
1065) -> Result<MerklizedStateApi<N, P, D, SequencerApiVersion>>
1066where
1067    N: ConnectedNetwork<PubKey>,
1068    D: MerklizedStateDataSource<SeqTypes, S, ARITY>
1069        + Send
1070        + Sync
1071        + MerklizedStateHeightPersistence
1072        + 'static,
1073    S: MerklizedState<SeqTypes, ARITY>,
1074    P: SequencerPersistence,
1075    for<'a> <S::Commit as TryFrom<&'a TaggedBase64>>::Error: std::fmt::Display,
1076{
1077    let api = merklized_state::define_api::<
1078        AvailState<N, P, D>,
1079        SeqTypes,
1080        S,
1081        SequencerApiVersion,
1082        ARITY,
1083    >(&Default::default(), api_ver)?;
1084    Ok(api)
1085}
1086
1087pub(super) fn config<S, ApiVer: StaticVersionType + 'static>(
1088    _: ApiVer,
1089    api_ver: semver::Version,
1090    public_node_config: Option<PublicNodeConfig>,
1091) -> Result<Api<S, Error, ApiVer>>
1092where
1093    S: 'static + Send + Sync + ReadState,
1094    S::State: Send + Sync + HotShotConfigDataSource,
1095{
1096    let toml = toml::from_str::<toml::Value>(include_str!("../../api/config.toml"))?;
1097    let mut api = Api::<S, Error, ApiVer>::new(toml)?;
1098    api.with_version(api_ver);
1099
1100    let env_variables = get_public_env_vars()
1101        .map_err(|err| Error::catch_all(StatusCode::INTERNAL_SERVER_ERROR, format!("{err:#}")))?;
1102
1103    api.get("hotshot", |_, state| {
1104        async move { Ok(state.get_config().await) }.boxed()
1105    })?
1106    .get("env", move |_, _| {
1107        {
1108            let env_variables = env_variables.clone();
1109            async move { Ok(env_variables) }
1110        }
1111        .boxed()
1112    })?
1113    .get("runtime", move |_, _| {
1114        let public_node_config = public_node_config.clone();
1115        async move {
1116            public_node_config.ok_or_else(|| {
1117                Error::catch_all(
1118                    StatusCode::NOT_FOUND,
1119                    "runtime config not available".to_string(),
1120                )
1121            })
1122        }
1123        .boxed()
1124    })?;
1125
1126    Ok(api)
1127}
1128
1129fn get_public_env_vars() -> Result<Vec<String>> {
1130    let toml: toml::Value = toml::from_str(include_str!("../../api/public-env-vars.toml"))?;
1131
1132    let keys = toml
1133        .get("variables")
1134        .ok_or_else(|| toml::de::Error::custom("variables not found"))?
1135        .as_array()
1136        .ok_or_else(|| toml::de::Error::custom("variables is not an array"))?
1137        .clone()
1138        .into_iter()
1139        .map(|v| v.try_into())
1140        .collect::<Result<BTreeSet<String>, toml::de::Error>>()?;
1141
1142    let hashmap: HashMap<String, String> = env::vars().collect();
1143    let mut public_env_vars: Vec<String> = Vec::new();
1144    for key in keys {
1145        let value = hashmap.get(&key).cloned().unwrap_or_default();
1146        public_env_vars.push(format!("{key}={value}"));
1147    }
1148
1149    Ok(public_env_vars)
1150}