sequencer/api/
endpoints.rs

1//! Sequencer-specific API endpoint handlers.
2
3use std::{
4    collections::{BTreeSet, HashMap},
5    env,
6};
7
8use anyhow::Result;
9use committable::Committable;
10use espresso_types::{
11    v0_3::RewardAccountV1,
12    v0_4::{RewardAccountV2, RewardClaimError},
13    FeeAccount, FeeMerkleTree, PubKey, Transaction,
14};
15
16use crate::{api::data_source::TokenDataSource, U256};
17// re-exported here to avoid breaking changes in consumers
18// "deprecated" does not work with "pub use": https://github.com/rust-lang/rust/issues/30827
19#[deprecated(note = "use espresso_types::ADVZNamespaceProofQueryData")]
20pub type ADVZNamespaceProofQueryData = espresso_types::ADVZNamespaceProofQueryData;
21#[deprecated(note = "use espresso_types::NamespaceProofQueryData")]
22pub type NamespaceProofQueryData = espresso_types::NamespaceProofQueryData;
23
24use futures::FutureExt;
25use hotshot_query_service::{
26    availability::AvailabilityDataSource,
27    explorer::{self, ExplorerDataSource},
28    merklized_state::{
29        self, MerklizedState, MerklizedStateDataSource, MerklizedStateHeightPersistence, Snapshot,
30    },
31    node::{self, NodeDataSource},
32    Error,
33};
34use hotshot_types::{
35    data::{EpochNumber, ViewNumber},
36    traits::{
37        network::ConnectedNetwork,
38        node_implementation::{ConsensusTime, Versions},
39    },
40};
41use jf_merkle_tree_compat::MerkleTreeScheme;
42use serde::de::Error as _;
43use tagged_base64::TaggedBase64;
44use tide_disco::{method::ReadState, Api, Error as _, RequestParams, StatusCode};
45use vbs::version::{StaticVersion, StaticVersionType};
46
47use super::data_source::{
48    CatchupDataSource, HotShotConfigDataSource, NodeStateDataSource, StakeTableDataSource,
49    StateSignatureDataSource, SubmitDataSource,
50};
51use crate::{
52    api::RewardAccountProofDataSource, SeqTypes, SequencerApiVersion, SequencerPersistence,
53};
54
55mod availability;
56pub(super) use availability::*;
57
58pub(super) fn fee<State, Ver>(
59    api_ver: semver::Version,
60) -> Result<Api<State, merklized_state::Error, Ver>>
61where
62    State: 'static + Send + Sync + ReadState,
63    Ver: 'static + StaticVersionType,
64    <State as ReadState>::State: Send
65        + Sync
66        + MerklizedStateDataSource<SeqTypes, FeeMerkleTree, { FeeMerkleTree::ARITY }>
67        + MerklizedStateHeightPersistence,
68{
69    let mut options = merklized_state::Options::default();
70    let extension = toml::from_str(include_str!("../../api/fee.toml"))?;
71    options.extensions.push(extension);
72
73    let mut api =
74        merklized_state::define_api::<State, SeqTypes, FeeMerkleTree, Ver, 256>(&options, api_ver)?;
75
76    api.get("getfeebalance", move |req, state| {
77        async move {
78            let address = req.string_param("address")?;
79            let height = state.get_last_state_height().await?;
80            let snapshot = Snapshot::Index(height as u64);
81            let key = address
82                .parse()
83                .map_err(|_| merklized_state::Error::Custom {
84                    message: "failed to parse address".to_string(),
85                    status: StatusCode::BAD_REQUEST,
86                })?;
87            let path = state.get_path(snapshot, key).await?;
88            Ok(path.elem().copied())
89        }
90        .boxed()
91    })?;
92    Ok(api)
93}
94
95pub enum RewardMerkleTreeVersion {
96    V1,
97    V2,
98}
99
100pub(super) fn reward<State, Ver, MT, const ARITY: usize>(
101    api_ver: semver::Version,
102    merkle_tree_version: RewardMerkleTreeVersion,
103) -> Result<Api<State, merklized_state::Error, Ver>>
104where
105    State: 'static + Send + Sync + ReadState,
106    Ver: 'static + StaticVersionType,
107    MT: MerklizedState<SeqTypes, ARITY>,
108    for<'a> <MT::Commit as TryFrom<&'a TaggedBase64>>::Error: std::fmt::Display,
109    <MT as MerklizedState<SeqTypes, ARITY>>::Entry: std::marker::Copy,
110    <State as ReadState>::State: Send
111        + Sync
112        + RewardAccountProofDataSource
113        + MerklizedStateDataSource<SeqTypes, MT, ARITY>
114        + MerklizedStateHeightPersistence,
115{
116    let mut options = merklized_state::Options::default();
117    let extension = toml::from_str(include_str!("../../api/reward.toml"))?;
118    options.extensions.push(extension);
119
120    let mut api =
121        merklized_state::define_api::<State, SeqTypes, MT, Ver, ARITY>(&options, api_ver)?;
122
123    api.get("get_latest_reward_balance", move |req, state| {
124        async move {
125            let address = req.string_param("address")?;
126            let height = state.get_last_state_height().await?;
127            let snapshot = Snapshot::Index(height as u64);
128            let key = address
129                .parse()
130                .map_err(|_| merklized_state::Error::Custom {
131                    message: "failed to parse reward address".to_string(),
132                    status: StatusCode::BAD_REQUEST,
133                })?;
134            let path = state.get_path(snapshot, key).await?;
135            Ok(path.elem().copied())
136        }
137        .boxed()
138    })?
139    .get("get_reward_balance", move |req, state| {
140        async move {
141            let address = req.string_param("address")?;
142            let height: usize = req.integer_param("height")?;
143            let snapshot = Snapshot::Index(height as u64);
144            let key = address
145                .parse()
146                .map_err(|_| merklized_state::Error::Custom {
147                    message: "failed to parse reward address".to_string(),
148                    status: StatusCode::BAD_REQUEST,
149                })?;
150            let path = state.get_path(snapshot, key).await?;
151
152            let last_height = state.get_last_state_height().await?;
153
154            if height > last_height {
155                return Err(merklized_state::Error::Custom {
156                    message: format!(
157                        "requested height {height} is greater than last known height {last_height}"
158                    ),
159                    status: StatusCode::BAD_REQUEST,
160                });
161            }
162
163            Ok(path.elem().copied())
164        }
165        .boxed()
166    })?;
167
168    match merkle_tree_version {
169        RewardMerkleTreeVersion::V1 => {
170            api.get("get_reward_account_proof", move |req, state| {
171                async move {
172                    let address = req.string_param("address")?;
173                    let height = req.integer_param("height")?;
174                    let account = address
175                        .parse()
176                        .map_err(|_| merklized_state::Error::Custom {
177                            message: format!("invalid reward address: {address}"),
178                            status: StatusCode::BAD_REQUEST,
179                        })?;
180
181                    state
182                        .load_v1_reward_account_proof(height, account)
183                        .await
184                        .map_err(|err| merklized_state::Error::Custom {
185                            message: format!(
186                                "failed to load v1 reward account {address} at height {height}: \
187                                 {err}"
188                            ),
189                            status: StatusCode::NOT_FOUND,
190                        })
191                }
192                .boxed()
193            })?;
194        },
195        RewardMerkleTreeVersion::V2 => {
196            api.get("get_reward_account_proof", move |req, state| {
197                async move {
198                    let address = req.string_param("address")?;
199                    let height = req.integer_param("height")?;
200                    let account = address
201                        .parse()
202                        .map_err(|_| merklized_state::Error::Custom {
203                            message: format!("invalid reward address: {address}"),
204                            status: StatusCode::BAD_REQUEST,
205                        })?;
206
207                    state
208                        .load_v2_reward_account_proof(height, account)
209                        .await
210                        .map_err(|err| merklized_state::Error::Custom {
211                            message: format!(
212                                "failed to load v2 reward account {address} at height {height}: \
213                                 {err}"
214                            ),
215                            status: StatusCode::NOT_FOUND,
216                        })
217                }
218                .boxed()
219            })?;
220
221            api.get("get_reward_claim_input", move |req, state| {
222                async move {
223                    let address = req.string_param("address")?;
224                    let height = req.integer_param("height")?;
225                    let account = address
226                        .parse()
227                        .map_err(|_| merklized_state::Error::Custom {
228                            message: format!("invalid reward address: {address}"),
229                            status: StatusCode::BAD_REQUEST,
230                        })?;
231
232                    let proof = state
233                        .load_v2_reward_account_proof(height, account)
234                        .await
235                        .map_err(|err| merklized_state::Error::Custom {
236                            message: format!(
237                                "failed to load v2 reward account {address} at height {height}: \
238                                 {err}"
239                            ),
240                            status: StatusCode::NOT_FOUND,
241                        })?;
242
243                    // Auth root inputs (other than the reward merkle tree root) are currently
244                    // all zero placeholder values. This may be extended in the future.
245                    let claim_input = match proof.to_reward_claim_input() {
246                        Ok(input) => input,
247                        Err(RewardClaimError::ZeroRewardError) => {
248                            return Err(merklized_state::Error::Custom {
249                                message: format!(
250                                    "zero reward balance for {address} at height {height}"
251                                ),
252                                status: StatusCode::NOT_FOUND,
253                            })
254                        },
255                        Err(RewardClaimError::ProofConversionError(err)) => {
256                            let message = format!(
257                                "failed to create solidity proof for {address} at height \
258                                 {height}: {err}",
259                            );
260                            tracing::warn!("{message}");
261                            // Normally we would not want to return the internal error via the
262                            // API response but this is an error that should never occur. No
263                            // secret data involved so it seems fine to return it.
264                            return Err(merklized_state::Error::Custom {
265                                message,
266                                status: StatusCode::INTERNAL_SERVER_ERROR,
267                            });
268                        },
269                    };
270
271                    Ok(claim_input)
272                }
273                .boxed()
274            })?;
275        },
276    }
277
278    Ok(api)
279}
280
281type ExplorerApi<N, P, D, V, ApiVer> = Api<AvailState<N, P, D, V>, explorer::Error, ApiVer>;
282
283pub(super) fn explorer<N, P, D, V: Versions>(
284    api_ver: semver::Version,
285) -> Result<ExplorerApi<N, P, D, V, SequencerApiVersion>>
286where
287    N: ConnectedNetwork<PubKey>,
288    D: ExplorerDataSource<SeqTypes> + Send + Sync + 'static,
289    P: SequencerPersistence,
290{
291    let api = explorer::define_api::<AvailState<N, P, D, V>, SeqTypes, _>(
292        SequencerApiVersion::instance(),
293        api_ver,
294    )?;
295    Ok(api)
296}
297
298pub(super) fn token<S>(api_ver: semver::Version) -> Result<Api<S, node::Error, StaticVersion<0, 1>>>
299where
300    S: 'static + Send + Sync + ReadState,
301    <S as ReadState>::State: Send
302        + Sync
303        + TokenDataSource<SeqTypes>
304        + NodeDataSource<SeqTypes>
305        + AvailabilityDataSource<SeqTypes>,
306{
307    // Extend the base API
308    let mut options = node::Options::default();
309    let extension = toml::from_str(include_str!("../../api/token.toml"))?;
310    options.extensions.push(extension);
311
312    // Create the base API with our extensions
313    let mut api =
314        node::define_api::<S, SeqTypes, _>(&options, SequencerApiVersion::instance(), api_ver)?;
315
316    // Tack on the application logic
317    api.at("get_total_minted_supply", |_, state| {
318        async move {
319            let value = state
320                .read(|state| state.get_total_supply_l1().boxed())
321                .await
322                .map_err(|err| node::Error::Custom {
323                    message: format!("failed to get total supply. err={err:#}"),
324                    status: StatusCode::NOT_FOUND,
325                })?;
326
327            let scale = U256::from(10u64.pow(18));
328            let quotient = value / scale;
329            let remainder = value % scale;
330
331            Ok(format!("{quotient}.{remainder}"))
332        }
333        .boxed()
334    })?;
335
336    Ok(api)
337}
338
339pub(super) fn node<S>(api_ver: semver::Version) -> Result<Api<S, node::Error, StaticVersion<0, 1>>>
340where
341    S: 'static + Send + Sync + ReadState,
342    <S as ReadState>::State: Send
343        + Sync
344        + StakeTableDataSource<SeqTypes>
345        + NodeDataSource<SeqTypes>
346        + AvailabilityDataSource<SeqTypes>,
347{
348    // Extend the base API
349    let mut options = node::Options::default();
350    let extension = toml::from_str(include_str!("../../api/node.toml"))?;
351    options.extensions.push(extension);
352
353    // Create the base API with our extensions
354    let mut api =
355        node::define_api::<S, SeqTypes, _>(&options, SequencerApiVersion::instance(), api_ver)?;
356
357    // Tack on the application logic
358    api.at("stake_table", |req, state| {
359        async move {
360            // Try to get the epoch from the request. If this fails, error
361            // as it was probably a mistake
362            let epoch = req
363                .opt_integer_param("epoch_number")
364                .map_err(|_| hotshot_query_service::node::Error::Custom {
365                    message: "Epoch number is required".to_string(),
366                    status: StatusCode::BAD_REQUEST,
367                })?
368                .map(EpochNumber::new);
369
370            state
371                .read(|state| state.get_stake_table(epoch).boxed())
372                .await
373                .map_err(|err| node::Error::Custom {
374                    message: format!("failed to get stake table for epoch={epoch:?}. err={err:#}"),
375                    status: StatusCode::NOT_FOUND,
376                })
377        }
378        .boxed()
379    })?
380    .at("stake_table_current", |_, state| {
381        async move {
382            state
383                .read(|state| state.get_stake_table_current().boxed())
384                .await
385                .map_err(|err| node::Error::Custom {
386                    message: format!("failed to get current stake table. err={err:#}"),
387                    status: StatusCode::NOT_FOUND,
388                })
389        }
390        .boxed()
391    })?
392    .at("da_stake_table", |req, state| {
393        async move {
394            // Try to get the epoch from the request. If this fails, error
395            // as it was probably a mistake
396            let epoch = req
397                .opt_integer_param("epoch_number")
398                .map_err(|_| hotshot_query_service::node::Error::Custom {
399                    message: "Epoch number is required".to_string(),
400                    status: StatusCode::BAD_REQUEST,
401                })?
402                .map(EpochNumber::new);
403
404            state
405                .read(|state| state.get_da_stake_table(epoch).boxed())
406                .await
407                .map_err(|err| node::Error::Custom {
408                    message: format!(
409                        "failed to get DA stake table for epoch={epoch:?}. err={err:#}"
410                    ),
411                    status: StatusCode::NOT_FOUND,
412                })
413        }
414        .boxed()
415    })?
416    .at("da_stake_table_current", |_, state| {
417        async move {
418            state
419                .read(|state| state.get_da_stake_table_current().boxed())
420                .await
421                .map_err(|err| node::Error::Custom {
422                    message: format!("failed to get current DA stake table. err={err:#}"),
423                    status: StatusCode::NOT_FOUND,
424                })
425        }
426        .boxed()
427    })?
428    .at("get_validators", |req, state| {
429        async move {
430            let epoch = req.integer_param::<_, u64>("epoch_number").map_err(|_| {
431                hotshot_query_service::node::Error::Custom {
432                    message: "Epoch number is required".to_string(),
433                    status: StatusCode::BAD_REQUEST,
434                }
435            })?;
436
437            state
438                .read(|state| state.get_validators(EpochNumber::new(epoch)).boxed())
439                .await
440                .map_err(|err| hotshot_query_service::node::Error::Custom {
441                    message: format!("failed to get validators mapping: err: {err}"),
442                    status: StatusCode::NOT_FOUND,
443                })
444        }
445        .boxed()
446    })?
447    .at("get_all_validators", |req, state| {
448        async move {
449            let epoch = req.integer_param::<_, u64>("epoch_number").map_err(|_| {
450                hotshot_query_service::node::Error::Custom {
451                    message: "Epoch number is required".to_string(),
452                    status: StatusCode::BAD_REQUEST,
453                }
454            })?;
455
456            let offset = req.integer_param::<_, u64>("offset")?;
457
458            let limit = req.integer_param::<_, u64>("limit")?;
459            if limit > 1000 {
460                return Err(hotshot_query_service::node::Error::Custom {
461                    message: "Limit cannot be greater than 1000".to_string(),
462                    status: StatusCode::BAD_REQUEST,
463                });
464            }
465
466            state
467                .read(|state| {
468                    state
469                        .get_all_validators(EpochNumber::new(epoch), offset, limit)
470                        .boxed()
471                })
472                .await
473                .map_err(|err| hotshot_query_service::node::Error::Custom {
474                    message: format!("failed to get all validators : err: {err}"),
475                    status: StatusCode::INTERNAL_SERVER_ERROR,
476                })
477        }
478        .boxed()
479    })?
480    .at("current_proposal_participation", |_, state| {
481        async move {
482            Ok(state
483                .read(|state| state.current_proposal_participation().boxed())
484                .await)
485        }
486        .boxed()
487    })?
488    .at("previous_proposal_participation", |_, state| {
489        async move {
490            Ok(state
491                .read(|state| state.previous_proposal_participation().boxed())
492                .await)
493        }
494        .boxed()
495    })?
496    .at("get_block_reward", |req, state| {
497        async move {
498            let epoch = req
499                .opt_integer_param::<_, u64>("epoch_number")?
500                .map(EpochNumber::new);
501
502            state
503                .read(|state| state.get_block_reward(epoch).boxed())
504                .await
505                .map_err(|err| node::Error::Custom {
506                    message: format!("failed to get block reward. err={err:#}"),
507                    status: StatusCode::NOT_FOUND,
508                })
509        }
510        .boxed()
511    })?;
512
513    Ok(api)
514}
515pub(super) fn submit<N, P, S, ApiVer: StaticVersionType + 'static>(
516    api_ver: semver::Version,
517) -> Result<Api<S, Error, ApiVer>>
518where
519    N: ConnectedNetwork<PubKey>,
520    S: 'static + Send + Sync + ReadState,
521    P: SequencerPersistence,
522    S::State: Send + Sync + SubmitDataSource<N, P>,
523{
524    let toml = toml::from_str::<toml::Value>(include_str!("../../api/submit.toml"))?;
525    let mut api = Api::<S, Error, ApiVer>::new(toml)?;
526
527    api.with_version(api_ver).at("submit", |req, state| {
528        async move {
529            let tx = req
530                .body_auto::<Transaction, ApiVer>(ApiVer::instance())
531                .map_err(Error::from_request_error)?;
532
533            let hash = tx.commit();
534            state
535                .read(|state| state.submit(tx).boxed())
536                .await
537                .map_err(|err| Error::internal(err.to_string()))?;
538            Ok(hash)
539        }
540        .boxed()
541    })?;
542
543    Ok(api)
544}
545
546pub(super) fn state_signature<N, S, ApiVer: StaticVersionType + 'static>(
547    _: ApiVer,
548    api_ver: semver::Version,
549) -> Result<Api<S, Error, ApiVer>>
550where
551    N: ConnectedNetwork<PubKey>,
552    S: 'static + Send + Sync + ReadState,
553    S::State: Send + Sync + StateSignatureDataSource<N>,
554{
555    let toml = toml::from_str::<toml::Value>(include_str!("../../api/state_signature.toml"))?;
556    let mut api = Api::<S, Error, ApiVer>::new(toml)?;
557    api.with_version(api_ver);
558
559    api.get("get_state_signature", |req, state| {
560        async move {
561            let height = req
562                .integer_param("height")
563                .map_err(Error::from_request_error)?;
564            state
565                .get_state_signature(height)
566                .await
567                .ok_or(tide_disco::Error::catch_all(
568                    StatusCode::NOT_FOUND,
569                    "Signature not found.".to_owned(),
570                ))
571        }
572        .boxed()
573    })?;
574
575    Ok(api)
576}
577
578pub(super) fn catchup<S, ApiVer: StaticVersionType + 'static>(
579    _: ApiVer,
580    api_ver: semver::Version,
581) -> Result<Api<S, Error, ApiVer>>
582where
583    S: 'static + Send + Sync + ReadState,
584    S::State: Send + Sync + NodeStateDataSource + CatchupDataSource,
585{
586    let toml = toml::from_str::<toml::Value>(include_str!("../../api/catchup.toml"))?;
587    let mut api = Api::<S, Error, ApiVer>::new(toml)?;
588    api.with_version(api_ver);
589
590    let parse_height_view = |req: &RequestParams| -> Result<(u64, ViewNumber), Error> {
591        let height = req
592            .integer_param("height")
593            .map_err(Error::from_request_error)?;
594        let view = req
595            .integer_param("view")
596            .map_err(Error::from_request_error)?;
597        Ok((height, ViewNumber::new(view)))
598    };
599
600    let parse_fee_account = |req: &RequestParams| -> Result<FeeAccount, Error> {
601        let raw = req
602            .string_param("address")
603            .map_err(Error::from_request_error)?;
604        raw.parse().map_err(|err| {
605            Error::catch_all(
606                StatusCode::BAD_REQUEST,
607                format!("malformed fee account {raw}: {err}"),
608            )
609        })
610    };
611
612    let parse_reward_account = |req: &RequestParams| -> Result<RewardAccountV2, Error> {
613        let raw = req
614            .string_param("address")
615            .map_err(Error::from_request_error)?;
616        raw.parse().map_err(|err| {
617            Error::catch_all(
618                StatusCode::BAD_REQUEST,
619                format!("malformed reward account {raw}: {err}"),
620            )
621        })
622    };
623
624    api.get("account", move |req, state| {
625        async move {
626            let (height, view) = parse_height_view(&req)?;
627            let account = parse_fee_account(&req)?;
628            state
629                .get_account(&state.node_state().await, height, view, account)
630                .await
631                .map_err(|err| Error::catch_all(StatusCode::NOT_FOUND, format!("{err:#}")))
632        }
633        .boxed()
634    })?
635    .at("accounts", move |req, state| {
636        async move {
637            let (height, view) = parse_height_view(&req)?;
638            let accounts = req
639                .body_auto::<Vec<FeeAccount>, ApiVer>(ApiVer::instance())
640                .map_err(Error::from_request_error)?;
641
642            state
643                .read(|state| {
644                    async move {
645                        state
646                            .get_accounts(&state.node_state().await, height, view, &accounts)
647                            .await
648                            .map_err(|err| {
649                                Error::catch_all(StatusCode::NOT_FOUND, format!("{err:#}"))
650                            })
651                    }
652                    .boxed()
653                })
654                .await
655        }
656        .boxed()
657    })?
658    .get("reward_account", move |req, state| {
659        async move {
660            let (height, view) = parse_height_view(&req)?;
661            let account = parse_reward_account(&req)?;
662            state
663                .get_reward_account_v1(&state.node_state().await, height, view, account.into())
664                .await
665                .map_err(|err| Error::catch_all(StatusCode::NOT_FOUND, format!("{err:#}")))
666        }
667        .boxed()
668    })?
669    .at("reward_accounts", move |req, state| {
670        async move {
671            let (height, view) = parse_height_view(&req)?;
672            let accounts = req
673                .body_auto::<Vec<RewardAccountV1>, ApiVer>(ApiVer::instance())
674                .map_err(Error::from_request_error)?;
675
676            state
677                .read(|state| {
678                    async move {
679                        state
680                            .get_reward_accounts_v1(
681                                &state.node_state().await,
682                                height,
683                                view,
684                                &accounts,
685                            )
686                            .await
687                            .map_err(|err| {
688                                Error::catch_all(StatusCode::NOT_FOUND, format!("{err:#}"))
689                            })
690                    }
691                    .boxed()
692                })
693                .await
694        }
695        .boxed()
696    })?
697    .get("reward_account_v2", move |req, state| {
698        async move {
699            let (height, view) = parse_height_view(&req)?;
700            let account = parse_reward_account(&req)?;
701
702            state
703                .get_reward_account_v2(&state.node_state().await, height, view, account)
704                .await
705                .map_err(|err| Error::catch_all(StatusCode::NOT_FOUND, format!("{err:#}")))
706        }
707        .boxed()
708    })?
709    .get("reward_amounts", move |req, state| {
710        async move {
711            let height = req
712                .integer_param::<_, u64>("height")
713                .map_err(Error::from_request_error)?;
714            let offset = req
715                .integer_param::<_, u64>("offset")
716                .map_err(Error::from_request_error)?;
717            let limit = req
718                .integer_param::<_, u64>("limit")
719                .map_err(Error::from_request_error)?;
720
721            if limit > 10_000 {
722                return Err(Error::catch_all(
723                    StatusCode::BAD_REQUEST,
724                    format!("limit {limit} exceeds maximum allowed 10000"),
725                ));
726            }
727
728            state
729                .get_all_reward_accounts(height, offset, limit)
730                .await
731                .map_err(|err| Error::catch_all(StatusCode::NOT_FOUND, format!("{err:#}")))
732        }
733        .boxed()
734    })?
735    .at("reward_accounts_v2", move |req, state| {
736        async move {
737            let (height, view) = parse_height_view(&req)?;
738            let accounts = req
739                .body_auto::<Vec<RewardAccountV2>, ApiVer>(ApiVer::instance())
740                .map_err(Error::from_request_error)?;
741
742            state
743                .read(|state| {
744                    async move {
745                        state
746                            .get_reward_accounts_v2(
747                                &state.node_state().await,
748                                height,
749                                view,
750                                &accounts,
751                            )
752                            .await
753                            .map_err(|err| {
754                                Error::catch_all(StatusCode::NOT_FOUND, format!("{err:#}"))
755                            })
756                    }
757                    .boxed()
758                })
759                .await
760        }
761        .boxed()
762    })?
763    .get("blocks", |req, state| {
764        async move {
765            let height = req
766                .integer_param("height")
767                .map_err(Error::from_request_error)?;
768            let view = req
769                .integer_param("view")
770                .map_err(Error::from_request_error)?;
771
772            state
773                .get_frontier(&state.node_state().await, height, ViewNumber::new(view))
774                .await
775                .map_err(|err| Error::catch_all(StatusCode::NOT_FOUND, format!("{err:#}")))
776        }
777        .boxed()
778    })?
779    .get("chainconfig", |req, state| {
780        async move {
781            let commitment = req
782                .blob_param("commitment")
783                .map_err(Error::from_request_error)?;
784
785            state
786                .get_chain_config(commitment)
787                .await
788                .map_err(|err| Error::catch_all(StatusCode::NOT_FOUND, format!("{err:#}")))
789        }
790        .boxed()
791    })?
792    .get("leafchain", |req, state| {
793        async move {
794            let height = req
795                .integer_param("height")
796                .map_err(Error::from_request_error)?;
797            state
798                .get_leaf_chain(height)
799                .await
800                .map_err(|err| Error::catch_all(StatusCode::NOT_FOUND, format!("{err:#}")))
801        }
802        .boxed()
803    })?
804    .get("state_cert", |req, state| {
805        async move {
806            let epoch = req
807                .integer_param("epoch")
808                .map_err(Error::from_request_error)?;
809            state
810                .get_state_cert(epoch)
811                .await
812                .map_err(|err| Error::catch_all(StatusCode::NOT_FOUND, format!("{err:#}")))
813        }
814        .boxed()
815    })?;
816
817    Ok(api)
818}
819
820type MerklizedStateApi<N, P, D, V, ApiVer> =
821    Api<AvailState<N, P, D, V>, merklized_state::Error, ApiVer>;
822pub(super) fn merklized_state<N, P, D, S, V: Versions, const ARITY: usize>(
823    api_ver: semver::Version,
824) -> Result<MerklizedStateApi<N, P, D, V, SequencerApiVersion>>
825where
826    N: ConnectedNetwork<PubKey>,
827    D: MerklizedStateDataSource<SeqTypes, S, ARITY>
828        + Send
829        + Sync
830        + MerklizedStateHeightPersistence
831        + 'static,
832    S: MerklizedState<SeqTypes, ARITY>,
833    P: SequencerPersistence,
834    for<'a> <S::Commit as TryFrom<&'a TaggedBase64>>::Error: std::fmt::Display,
835{
836    let api = merklized_state::define_api::<
837        AvailState<N, P, D, V>,
838        SeqTypes,
839        S,
840        SequencerApiVersion,
841        ARITY,
842    >(&Default::default(), api_ver)?;
843    Ok(api)
844}
845
846pub(super) fn config<S, ApiVer: StaticVersionType + 'static>(
847    _: ApiVer,
848    api_ver: semver::Version,
849) -> Result<Api<S, Error, ApiVer>>
850where
851    S: 'static + Send + Sync + ReadState,
852    S::State: Send + Sync + HotShotConfigDataSource,
853{
854    let toml = toml::from_str::<toml::Value>(include_str!("../../api/config.toml"))?;
855    let mut api = Api::<S, Error, ApiVer>::new(toml)?;
856    api.with_version(api_ver);
857
858    let env_variables = get_public_env_vars()
859        .map_err(|err| Error::catch_all(StatusCode::INTERNAL_SERVER_ERROR, format!("{err:#}")))?;
860
861    api.get("hotshot", |_, state| {
862        async move { Ok(state.get_config().await) }.boxed()
863    })?
864    .get("env", move |_, _| {
865        {
866            let env_variables = env_variables.clone();
867            async move { Ok(env_variables) }
868        }
869        .boxed()
870    })?;
871
872    Ok(api)
873}
874
875fn get_public_env_vars() -> Result<Vec<String>> {
876    let toml: toml::Value = toml::from_str(include_str!("../../api/public-env-vars.toml"))?;
877
878    let keys = toml
879        .get("variables")
880        .ok_or_else(|| toml::de::Error::custom("variables not found"))?
881        .as_array()
882        .ok_or_else(|| toml::de::Error::custom("variables is not an array"))?
883        .clone()
884        .into_iter()
885        .map(|v| v.try_into())
886        .collect::<Result<BTreeSet<String>, toml::de::Error>>()?;
887
888    let hashmap: HashMap<String, String> = env::vars().collect();
889    let mut public_env_vars: Vec<String> = Vec::new();
890    for key in keys {
891        let value = hashmap.get(&key).cloned().unwrap_or_default();
892        public_env_vars.push(format!("{key}={value}"));
893    }
894
895    Ok(public_env_vars)
896}