sequencer/api/
endpoints.rs

1//! Sequencer-specific API endpoint handlers.
2
3use std::{
4    collections::{BTreeSet, HashMap},
5    env,
6    time::Duration,
7};
8
9use anyhow::Result;
10use committable::Committable;
11use espresso_types::{
12    v0_1::ADVZNsProof, v0_3::RewardAccountV1, v0_4::RewardAccountV2, FeeAccount, FeeMerkleTree,
13    NamespaceId, NsProof, PubKey, Transaction,
14};
15// re-exported here to avoid breaking changes in consumers
16// "deprecated" does not work with "pub use": https://github.com/rust-lang/rust/issues/30827
17#[deprecated(note = "use espresso_types::ADVZNamespaceProofQueryData")]
18pub type ADVZNamespaceProofQueryData = espresso_types::ADVZNamespaceProofQueryData;
19#[deprecated(note = "use espresso_types::NamespaceProofQueryData")]
20pub type NamespaceProofQueryData = espresso_types::NamespaceProofQueryData;
21
22use futures::{try_join, FutureExt};
23use hotshot_query_service::{
24    availability::{self, AvailabilityDataSource, CustomSnafu, FetchBlockSnafu},
25    explorer::{self, ExplorerDataSource},
26    merklized_state::{
27        self, MerklizedState, MerklizedStateDataSource, MerklizedStateHeightPersistence, Snapshot,
28    },
29    node::{self, NodeDataSource},
30    ApiState, Error, VidCommon,
31};
32use hotshot_types::{
33    data::{EpochNumber, VidCommitment, VidShare, ViewNumber},
34    traits::{
35        network::ConnectedNetwork,
36        node_implementation::{ConsensusTime, Versions},
37    },
38    vid::avidm::AvidMShare,
39};
40use jf_merkle_tree::MerkleTreeScheme;
41use serde::de::Error as _;
42use snafu::OptionExt;
43use tagged_base64::TaggedBase64;
44use tide_disco::{method::ReadState, Api, Error as _, RequestParams, StatusCode};
45use tracing::warn;
46use vbs::version::{StaticVersion, StaticVersionType};
47use vid::avid_m::namespaced::NsAvidMScheme;
48
49use super::{
50    data_source::{
51        CatchupDataSource, HotShotConfigDataSource, NodeStateDataSource, RequestResponseDataSource,
52        SequencerDataSource, StakeTableDataSource, StateSignatureDataSource, SubmitDataSource,
53    },
54    StorageState,
55};
56use crate::{SeqTypes, SequencerApiVersion, SequencerPersistence};
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(super) fn reward<State, Ver, MT, const ARITY: usize>(
96    api_ver: semver::Version,
97) -> Result<Api<State, merklized_state::Error, Ver>>
98where
99    State: 'static + Send + Sync + ReadState,
100    Ver: 'static + StaticVersionType,
101    MT: MerklizedState<SeqTypes, ARITY>,
102    for<'a> <MT::Commit as TryFrom<&'a TaggedBase64>>::Error: std::fmt::Display,
103    <MT as MerklizedState<SeqTypes, ARITY>>::Entry: std::marker::Copy,
104    <State as ReadState>::State: Send
105        + Sync
106        + MerklizedStateDataSource<SeqTypes, MT, ARITY>
107        + MerklizedStateHeightPersistence,
108{
109    let mut options = merklized_state::Options::default();
110    let extension = toml::from_str(include_str!("../../api/reward.toml"))?;
111    options.extensions.push(extension);
112
113    let mut api =
114        merklized_state::define_api::<State, SeqTypes, MT, Ver, ARITY>(&options, api_ver)?;
115
116    api.get("get_latest_reward_balance", move |req, state| {
117        async move {
118            let address = req.string_param("address")?;
119            let height = state.get_last_state_height().await?;
120            let snapshot = Snapshot::Index(height as u64);
121            let key = address
122                .parse()
123                .map_err(|_| merklized_state::Error::Custom {
124                    message: "failed to parse reward address".to_string(),
125                    status: StatusCode::BAD_REQUEST,
126                })?;
127            let path = state.get_path(snapshot, key).await?;
128            Ok(path.elem().copied())
129        }
130        .boxed()
131    })?
132    .get("get_reward_balance", move |req, state| {
133        async move {
134            let address = req.string_param("address")?;
135            let height: usize = req.integer_param("height")?;
136            let snapshot = Snapshot::Index(height as u64);
137            let key = address
138                .parse()
139                .map_err(|_| merklized_state::Error::Custom {
140                    message: "failed to parse reward address".to_string(),
141                    status: StatusCode::BAD_REQUEST,
142                })?;
143            let path = state.get_path(snapshot, key).await?;
144            Ok(path.elem().copied())
145        }
146        .boxed()
147    })?;
148    Ok(api)
149}
150
151pub(super) type AvailState<N, P, D, ApiVer> = ApiState<StorageState<N, P, D, ApiVer>>;
152
153type AvailabilityApi<N, P, D, V, ApiVer> = Api<AvailState<N, P, D, V>, availability::Error, ApiVer>;
154
155// TODO (abdul): replace snafu with `this_error` in  hotshot query service
156// Snafu has been replaced by `this_error` everywhere.
157// However, the query service still uses snafu
158pub(super) fn availability<N, P, D, V: Versions>(
159    api_ver: semver::Version,
160) -> Result<AvailabilityApi<N, P, D, V, SequencerApiVersion>>
161where
162    N: ConnectedNetwork<PubKey>,
163    D: SequencerDataSource + Send + Sync + 'static,
164    P: SequencerPersistence,
165{
166    let mut options = availability::Options::default();
167    let extension = toml::from_str(include_str!("../../api/availability.toml"))?;
168    options.extensions.push(extension);
169    let timeout = options.fetch_timeout;
170
171    let mut api = availability::define_api::<AvailState<N, P, D, _>, SeqTypes, _>(
172        &options,
173        SequencerApiVersion::instance(),
174        api_ver.clone(),
175    )?;
176
177    if api_ver.major == 1 {
178        if api_ver.minor >= 1 {
179            // >= V1.1 api returns both correct and incorrect encoding proofs
180            api.get("getnamespaceproof", move |req, state| {
181                async move {
182                    let height: usize = req.integer_param("height")?;
183                    let ns_id = NamespaceId::from(req.integer_param::<_, u32>("namespace")?);
184                    let (block, common) = try_join!(
185                        async move {
186                            state
187                                .get_block(height)
188                                .await
189                                .with_timeout(timeout)
190                                .await
191                                .context(FetchBlockSnafu {
192                                    resource: height.to_string(),
193                                })
194                        },
195                        async move {
196                            state
197                                .get_vid_common(height)
198                                .await
199                                .with_timeout(timeout)
200                                .await
201                                .context(FetchBlockSnafu {
202                                    resource: height.to_string(),
203                                })
204                        }
205                    )?;
206
207                    let ns_table = block.payload().ns_table();
208                    if let Some(ns_index) = ns_table.find_ns_id(&ns_id) {
209                        match NsProof::v1_1_new_with_correct_encoding(
210                            block.payload(),
211                            &ns_index,
212                            common.common(),
213                        ) {
214                            Some(proof) => Ok(espresso_types::NamespaceProofQueryData {
215                                transactions: proof.export_all_txs(&ns_id),
216                                proof: Some(proof),
217                            }),
218                            None => {
219                                // if we fail to generate the correct encoding proof, we try to generate the incorrect encoding proof
220                                tracing::debug!(
221                                    "Failed to generate namespace proof for block {height} and \
222                                     namespace {ns_id}, trying to generate incorrect encoding \
223                                     proof"
224                                );
225                                let mut vid_shares = state
226                                    .request_vid_shares(
227                                        height as u64,
228                                        common.clone(),
229                                        Duration::from_secs(40),
230                                    )
231                                    .await
232                                    .map_err(|err| {
233                                        warn!("Failed to request VID shares from network: {err:#}");
234                                        hotshot_query_service::availability::Error::Custom {
235                                            message: "Failed to request VID shares from network"
236                                                .to_string(),
237                                            status: StatusCode::NOT_FOUND,
238                                        }
239                                    })?;
240                                let vid_share = state.vid_share(height).await;
241                                if let Ok(vid_share) = vid_share {
242                                    vid_shares.push(vid_share);
243                                };
244
245                                // Collect the shares as V1 shares
246                                let vid_shares: Vec<AvidMShare> = vid_shares
247                                    .into_iter()
248                                    .filter_map(|share| {
249                                        if let VidShare::V1(share) = share {
250                                            Some(share)
251                                        } else {
252                                            None
253                                        }
254                                    })
255                                    .collect();
256
257                                match NsProof::v1_1_new_with_incorrect_encoding(
258                                    &vid_shares,
259                                    ns_table,
260                                    &ns_index,
261                                    &common.payload_hash(),
262                                    common.common(),
263                                ) {
264                                    Some(proof) => Ok(espresso_types::NamespaceProofQueryData {
265                                        transactions: vec![],
266                                        proof: Some(proof),
267                                    }),
268                                    None => {
269                                        warn!("Failed to generate proof of incorrect encoding");
270                                        Err(availability::Error::Custom {
271                                            message: "Failed to generate proof of incorrect \
272                                                      encoding"
273                                                .to_string(),
274                                            status: StatusCode::INTERNAL_SERVER_ERROR,
275                                        })
276                                    },
277                                }
278                            },
279                        }
280                    } else {
281                        // ns_id not found in ns_table
282                        Err(availability::Error::Custom {
283                            message: "Namespace not found".to_string(),
284                            status: StatusCode::NOT_FOUND,
285                        })
286                    }
287                }
288                .boxed()
289            })?
290            .at("incorrect_encoding_proof", |req, state| {
291                async move {
292                    // Get the block number from the request
293                    let block_number =
294                        req.integer_param::<_, u64>("block_number").map_err(|_| {
295                            hotshot_query_service::availability::Error::Custom {
296                                message: "Block number is required".to_string(),
297                                status: StatusCode::BAD_REQUEST,
298                            }
299                        })?;
300
301                    // Get or fetch the VID common data for the given block number
302                    // TODO: Time this out
303                    let vid_common = state
304                        .read(|state| state.get_vid_common(block_number as usize).boxed())
305                        .await
306                        .await;
307
308                    // Request the VID shares from other nodes. Use the VID common and common metadata to
309                    // verify that they are correct
310                    let vid_common_clone = vid_common.clone();
311                    let mut vid_shares = state
312                        .read(|state| {
313                            state.request_vid_shares(
314                                block_number,
315                                vid_common_clone,
316                                Duration::from_secs(40),
317                            )
318                        })
319                        .await
320                        .map_err(|err| {
321                            warn!("Failed to request VID shares from network: {err:#}");
322                            hotshot_query_service::availability::Error::Custom {
323                                message: "Failed to request VID shares from network".to_string(),
324                                status: StatusCode::NOT_FOUND,
325                            }
326                        })?;
327
328                    // Get our own share and add it. We don't need to verify here
329                    let vid_share = state
330                        .read(|state| state.vid_share(block_number as usize).boxed())
331                        .await;
332                    if let Ok(vid_share) = vid_share {
333                        vid_shares.push(vid_share);
334                    };
335
336                    // Get the total VID weight based on the VID common data
337                    let avidm_param = match vid_common.common() {
338                        VidCommon::V0(_) => {
339                            // TODO: This needs to be done via the stake table
340                            return Err(hotshot_query_service::availability::Error::Custom {
341                                message: "V0 shares not supported yet".to_string(),
342                                status: StatusCode::NOT_FOUND,
343                            });
344                        },
345                        VidCommon::V1(v1) => v1,
346                    };
347
348                    // Get the payload hash
349                    let VidCommitment::V1(local_payload_hash) = vid_common.payload_hash() else {
350                        return Err(hotshot_query_service::availability::Error::Custom {
351                            message: "V0 shares not supported yet".to_string(),
352                            status: StatusCode::NOT_FOUND,
353                        });
354                    };
355
356                    // Collect the shares as V1 shares
357                    let avidm_shares: Vec<AvidMShare> = vid_shares
358                        .into_iter()
359                        .filter_map(|share| {
360                            if let VidShare::V1(share) = share {
361                                Some(share)
362                            } else {
363                                None
364                            }
365                        })
366                        .collect();
367
368                    match NsAvidMScheme::proof_of_incorrect_encoding(
369                        avidm_param,
370                        &local_payload_hash,
371                        &avidm_shares,
372                    ) {
373                        Ok(proof) => Ok(proof),
374                        Err(err) => {
375                            warn!("Failed to generate proof of incorrect encoding: {err:#}");
376                            Err(hotshot_query_service::availability::Error::Custom {
377                                message: "Failed to generate proof of incorrect encoding"
378                                    .to_string(),
379                                status: StatusCode::INTERNAL_SERVER_ERROR,
380                            })
381                        },
382                    }
383                }
384                .boxed()
385            })?;
386        } else {
387            // V1.0 api only returns the correct encoding proof
388            api.get("getnamespaceproof", move |req, state| {
389                async move {
390                    let height: usize = req.integer_param("height")?;
391                    let ns_id = NamespaceId::from(req.integer_param::<_, u32>("namespace")?);
392                    let (block, common) = try_join!(
393                        async move {
394                            state
395                                .get_block(height)
396                                .await
397                                .with_timeout(timeout)
398                                .await
399                                .context(FetchBlockSnafu {
400                                    resource: height.to_string(),
401                                })
402                        },
403                        async move {
404                            state
405                                .get_vid_common(height)
406                                .await
407                                .with_timeout(timeout)
408                                .await
409                                .context(FetchBlockSnafu {
410                                    resource: height.to_string(),
411                                })
412                        }
413                    )?;
414
415                    if let Some(ns_index) = block.payload().ns_table().find_ns_id(&ns_id) {
416                        let proof = NsProof::new(block.payload(), &ns_index, common.common())
417                            .context(CustomSnafu {
418                                message: format!("failed to make proof for namespace {ns_id}"),
419                                status: StatusCode::NOT_FOUND,
420                            })?;
421
422                        Ok(espresso_types::NamespaceProofQueryData {
423                            transactions: proof.export_all_txs(&ns_id),
424                            proof: Some(proof),
425                        })
426                    } else {
427                        // ns_id not found in ns_table
428                        Ok(espresso_types::NamespaceProofQueryData {
429                            proof: None,
430                            transactions: Vec::new(),
431                        })
432                    }
433                }
434                .boxed()
435            })?;
436        }
437    } else {
438        api.get("getnamespaceproof", move |req, state| {
439            async move {
440                let height: usize = req.integer_param("height")?;
441                let ns_id = NamespaceId::from(req.integer_param::<_, u32>("namespace")?);
442                let (block, common) = try_join!(
443                    async move {
444                        state
445                            .get_block(height)
446                            .await
447                            .with_timeout(timeout)
448                            .await
449                            .context(FetchBlockSnafu {
450                                resource: height.to_string(),
451                            })
452                    },
453                    async move {
454                        state
455                            .get_vid_common(height)
456                            .await
457                            .with_timeout(timeout)
458                            .await
459                            .context(FetchBlockSnafu {
460                                resource: height.to_string(),
461                            })
462                    }
463                )?;
464
465                if let Some(ns_index) = block.payload().ns_table().find_ns_id(&ns_id) {
466                    let VidCommon::V0(common) = &common.common().clone() else {
467                        return Err(availability::Error::Custom {
468                            message: "Unsupported VID version, use new API version instead."
469                                .to_string(),
470                            status: StatusCode::NOT_FOUND,
471                        });
472                    };
473                    let proof = ADVZNsProof::new(block.payload(), &ns_index, common).context(
474                        CustomSnafu {
475                            message: format!("failed to make proof for namespace {ns_id}"),
476                            status: StatusCode::NOT_FOUND,
477                        },
478                    )?;
479
480                    Ok(espresso_types::ADVZNamespaceProofQueryData {
481                        transactions: proof.export_all_txs(&ns_id),
482                        proof: Some(proof),
483                    })
484                } else {
485                    // ns_id not found in ns_table
486                    Ok(espresso_types::ADVZNamespaceProofQueryData {
487                        proof: None,
488                        transactions: Vec::new(),
489                    })
490                }
491            }
492            .boxed()
493        })?;
494    }
495
496    Ok(api)
497}
498
499type ExplorerApi<N, P, D, V, ApiVer> = Api<AvailState<N, P, D, V>, explorer::Error, ApiVer>;
500
501pub(super) fn explorer<N, P, D, V: Versions>(
502    api_ver: semver::Version,
503) -> Result<ExplorerApi<N, P, D, V, SequencerApiVersion>>
504where
505    N: ConnectedNetwork<PubKey>,
506    D: ExplorerDataSource<SeqTypes> + Send + Sync + 'static,
507    P: SequencerPersistence,
508{
509    let api = explorer::define_api::<AvailState<N, P, D, V>, SeqTypes, _>(
510        SequencerApiVersion::instance(),
511        api_ver,
512    )?;
513    Ok(api)
514}
515
516pub(super) fn node<S>(api_ver: semver::Version) -> Result<Api<S, node::Error, StaticVersion<0, 1>>>
517where
518    S: 'static + Send + Sync + ReadState,
519    <S as ReadState>::State: Send
520        + Sync
521        + StakeTableDataSource<SeqTypes>
522        + NodeDataSource<SeqTypes>
523        + AvailabilityDataSource<SeqTypes>,
524{
525    // Extend the base API
526    let mut options = node::Options::default();
527    let extension = toml::from_str(include_str!("../../api/node.toml"))?;
528    options.extensions.push(extension);
529
530    // Create the base API with our extensions
531    let mut api =
532        node::define_api::<S, SeqTypes, _>(&options, SequencerApiVersion::instance(), api_ver)?;
533
534    // Tack on the application logic
535    api.at("stake_table", |req, state| {
536        async move {
537            // Try to get the epoch from the request. If this fails, error
538            // as it was probably a mistake
539            let epoch = req
540                .opt_integer_param("epoch_number")
541                .map_err(|_| hotshot_query_service::node::Error::Custom {
542                    message: "Epoch number is required".to_string(),
543                    status: StatusCode::BAD_REQUEST,
544                })?
545                .map(EpochNumber::new);
546
547            state
548                .read(|state| state.get_stake_table(epoch).boxed())
549                .await
550                .map_err(|err| node::Error::Custom {
551                    message: format!("failed to get stake table for epoch={epoch:?}. err={err:#}"),
552                    status: StatusCode::NOT_FOUND,
553                })
554        }
555        .boxed()
556    })?
557    .at("stake_table_current", |_, state| {
558        async move {
559            state
560                .read(|state| state.get_stake_table_current().boxed())
561                .await
562                .map_err(|err| node::Error::Custom {
563                    message: format!("failed to get current stake table. err={err:#}"),
564                    status: StatusCode::NOT_FOUND,
565                })
566        }
567        .boxed()
568    })?
569    .at("get_validators", |req, state| {
570        async move {
571            let epoch = req.integer_param::<_, u64>("epoch_number").map_err(|_| {
572                hotshot_query_service::node::Error::Custom {
573                    message: "Epoch number is required".to_string(),
574                    status: StatusCode::BAD_REQUEST,
575                }
576            })?;
577
578            state
579                .read(|state| state.get_validators(EpochNumber::new(epoch)).boxed())
580                .await
581                .map_err(|err| hotshot_query_service::node::Error::Custom {
582                    message: format!("failed to get validators mapping: err: {err}"),
583                    status: StatusCode::NOT_FOUND,
584                })
585        }
586        .boxed()
587    })?
588    .at("current_proposal_participation", |_, state| {
589        async move {
590            Ok(state
591                .read(|state| state.current_proposal_participation().boxed())
592                .await)
593        }
594        .boxed()
595    })?
596    .at("previous_proposal_participation", |_, state| {
597        async move {
598            Ok(state
599                .read(|state| state.previous_proposal_participation().boxed())
600                .await)
601        }
602        .boxed()
603    })?
604    .at("get_block_reward", |req, state| {
605        async move {
606            let epoch = req
607                .opt_integer_param::<_, u64>("epoch_number")?
608                .map(EpochNumber::new);
609
610            state
611                .read(|state| state.get_block_reward(epoch).boxed())
612                .await
613                .map_err(|err| node::Error::Custom {
614                    message: format!("failed to get block reward. err={err:#}"),
615                    status: StatusCode::NOT_FOUND,
616                })
617        }
618        .boxed()
619    })?;
620
621    Ok(api)
622}
623pub(super) fn submit<N, P, S, ApiVer: StaticVersionType + 'static>(
624    api_ver: semver::Version,
625) -> Result<Api<S, Error, ApiVer>>
626where
627    N: ConnectedNetwork<PubKey>,
628    S: 'static + Send + Sync + ReadState,
629    P: SequencerPersistence,
630    S::State: Send + Sync + SubmitDataSource<N, P>,
631{
632    let toml = toml::from_str::<toml::Value>(include_str!("../../api/submit.toml"))?;
633    let mut api = Api::<S, Error, ApiVer>::new(toml)?;
634
635    api.with_version(api_ver).at("submit", |req, state| {
636        async move {
637            let tx = req
638                .body_auto::<Transaction, ApiVer>(ApiVer::instance())
639                .map_err(Error::from_request_error)?;
640
641            let hash = tx.commit();
642            state
643                .read(|state| state.submit(tx).boxed())
644                .await
645                .map_err(|err| Error::internal(err.to_string()))?;
646            Ok(hash)
647        }
648        .boxed()
649    })?;
650
651    Ok(api)
652}
653
654pub(super) fn state_signature<N, S, ApiVer: StaticVersionType + 'static>(
655    _: ApiVer,
656    api_ver: semver::Version,
657) -> Result<Api<S, Error, ApiVer>>
658where
659    N: ConnectedNetwork<PubKey>,
660    S: 'static + Send + Sync + ReadState,
661    S::State: Send + Sync + StateSignatureDataSource<N>,
662{
663    let toml = toml::from_str::<toml::Value>(include_str!("../../api/state_signature.toml"))?;
664    let mut api = Api::<S, Error, ApiVer>::new(toml)?;
665    api.with_version(api_ver);
666
667    api.get("get_state_signature", |req, state| {
668        async move {
669            let height = req
670                .integer_param("height")
671                .map_err(Error::from_request_error)?;
672            state
673                .get_state_signature(height)
674                .await
675                .ok_or(tide_disco::Error::catch_all(
676                    StatusCode::NOT_FOUND,
677                    "Signature not found.".to_owned(),
678                ))
679        }
680        .boxed()
681    })?;
682
683    Ok(api)
684}
685
686pub(super) fn catchup<S, ApiVer: StaticVersionType + 'static>(
687    _: ApiVer,
688    api_ver: semver::Version,
689) -> Result<Api<S, Error, ApiVer>>
690where
691    S: 'static + Send + Sync + ReadState,
692    S::State: Send + Sync + NodeStateDataSource + CatchupDataSource,
693{
694    let toml = toml::from_str::<toml::Value>(include_str!("../../api/catchup.toml"))?;
695    let mut api = Api::<S, Error, ApiVer>::new(toml)?;
696    api.with_version(api_ver);
697
698    let parse_height_view = |req: &RequestParams| -> Result<(u64, ViewNumber), Error> {
699        let height = req
700            .integer_param("height")
701            .map_err(Error::from_request_error)?;
702        let view = req
703            .integer_param("view")
704            .map_err(Error::from_request_error)?;
705        Ok((height, ViewNumber::new(view)))
706    };
707
708    let parse_fee_account = |req: &RequestParams| -> Result<FeeAccount, Error> {
709        let raw = req
710            .string_param("address")
711            .map_err(Error::from_request_error)?;
712        raw.parse().map_err(|err| {
713            Error::catch_all(
714                StatusCode::BAD_REQUEST,
715                format!("malformed fee account {raw}: {err}"),
716            )
717        })
718    };
719
720    let parse_reward_account = |req: &RequestParams| -> Result<RewardAccountV2, Error> {
721        let raw = req
722            .string_param("address")
723            .map_err(Error::from_request_error)?;
724        raw.parse().map_err(|err| {
725            Error::catch_all(
726                StatusCode::BAD_REQUEST,
727                format!("malformed reward account {raw}: {err}"),
728            )
729        })
730    };
731
732    api.get("account", move |req, state| {
733        async move {
734            let (height, view) = parse_height_view(&req)?;
735            let account = parse_fee_account(&req)?;
736            state
737                .get_account(&state.node_state().await, height, view, account)
738                .await
739                .map_err(|err| Error::catch_all(StatusCode::NOT_FOUND, format!("{err:#}")))
740        }
741        .boxed()
742    })?
743    .at("accounts", move |req, state| {
744        async move {
745            let (height, view) = parse_height_view(&req)?;
746            let accounts = req
747                .body_auto::<Vec<FeeAccount>, ApiVer>(ApiVer::instance())
748                .map_err(Error::from_request_error)?;
749
750            state
751                .read(|state| {
752                    async move {
753                        state
754                            .get_accounts(&state.node_state().await, height, view, &accounts)
755                            .await
756                            .map_err(|err| {
757                                Error::catch_all(StatusCode::NOT_FOUND, format!("{err:#}"))
758                            })
759                    }
760                    .boxed()
761                })
762                .await
763        }
764        .boxed()
765    })?
766    .get("reward_account", move |req, state| {
767        async move {
768            let (height, view) = parse_height_view(&req)?;
769            let account = parse_reward_account(&req)?;
770            state
771                .get_reward_account_v1(&state.node_state().await, height, view, account.into())
772                .await
773                .map_err(|err| Error::catch_all(StatusCode::NOT_FOUND, format!("{err:#}")))
774        }
775        .boxed()
776    })?
777    .at("reward_accounts", move |req, state| {
778        async move {
779            let (height, view) = parse_height_view(&req)?;
780            let accounts = req
781                .body_auto::<Vec<RewardAccountV1>, ApiVer>(ApiVer::instance())
782                .map_err(Error::from_request_error)?;
783
784            state
785                .read(|state| {
786                    async move {
787                        state
788                            .get_reward_accounts_v1(
789                                &state.node_state().await,
790                                height,
791                                view,
792                                &accounts,
793                            )
794                            .await
795                            .map_err(|err| {
796                                Error::catch_all(StatusCode::NOT_FOUND, format!("{err:#}"))
797                            })
798                    }
799                    .boxed()
800                })
801                .await
802        }
803        .boxed()
804    })?
805    .get("reward_account_v2", move |req, state| {
806        async move {
807            let (height, view) = parse_height_view(&req)?;
808            let account = parse_reward_account(&req)?;
809
810            state
811                .get_reward_account_v2(&state.node_state().await, height, view, account)
812                .await
813                .map_err(|err| Error::catch_all(StatusCode::NOT_FOUND, format!("{err:#}")))
814        }
815        .boxed()
816    })?
817    .at("reward_accounts_v2", move |req, state| {
818        async move {
819            let (height, view) = parse_height_view(&req)?;
820            let accounts = req
821                .body_auto::<Vec<RewardAccountV2>, ApiVer>(ApiVer::instance())
822                .map_err(Error::from_request_error)?;
823
824            state
825                .read(|state| {
826                    async move {
827                        state
828                            .get_reward_accounts_v2(
829                                &state.node_state().await,
830                                height,
831                                view,
832                                &accounts,
833                            )
834                            .await
835                            .map_err(|err| {
836                                Error::catch_all(StatusCode::NOT_FOUND, format!("{err:#}"))
837                            })
838                    }
839                    .boxed()
840                })
841                .await
842        }
843        .boxed()
844    })?
845    .get("blocks", |req, state| {
846        async move {
847            let height = req
848                .integer_param("height")
849                .map_err(Error::from_request_error)?;
850            let view = req
851                .integer_param("view")
852                .map_err(Error::from_request_error)?;
853
854            state
855                .get_frontier(&state.node_state().await, height, ViewNumber::new(view))
856                .await
857                .map_err(|err| Error::catch_all(StatusCode::NOT_FOUND, format!("{err:#}")))
858        }
859        .boxed()
860    })?
861    .get("chainconfig", |req, state| {
862        async move {
863            let commitment = req
864                .blob_param("commitment")
865                .map_err(Error::from_request_error)?;
866
867            state
868                .get_chain_config(commitment)
869                .await
870                .map_err(|err| Error::catch_all(StatusCode::NOT_FOUND, format!("{err:#}")))
871        }
872        .boxed()
873    })?
874    .get("leafchain", |req, state| {
875        async move {
876            let height = req
877                .integer_param("height")
878                .map_err(Error::from_request_error)?;
879            state
880                .get_leaf_chain(height)
881                .await
882                .map_err(|err| Error::catch_all(StatusCode::NOT_FOUND, format!("{err:#}")))
883        }
884        .boxed()
885    })?;
886
887    Ok(api)
888}
889
890type MerklizedStateApi<N, P, D, V, ApiVer> =
891    Api<AvailState<N, P, D, V>, merklized_state::Error, ApiVer>;
892pub(super) fn merklized_state<N, P, D, S, V: Versions, const ARITY: usize>(
893    api_ver: semver::Version,
894) -> Result<MerklizedStateApi<N, P, D, V, SequencerApiVersion>>
895where
896    N: ConnectedNetwork<PubKey>,
897    D: MerklizedStateDataSource<SeqTypes, S, ARITY>
898        + Send
899        + Sync
900        + MerklizedStateHeightPersistence
901        + 'static,
902    S: MerklizedState<SeqTypes, ARITY>,
903    P: SequencerPersistence,
904    for<'a> <S::Commit as TryFrom<&'a TaggedBase64>>::Error: std::fmt::Display,
905{
906    let api = merklized_state::define_api::<
907        AvailState<N, P, D, V>,
908        SeqTypes,
909        S,
910        SequencerApiVersion,
911        ARITY,
912    >(&Default::default(), api_ver)?;
913    Ok(api)
914}
915
916pub(super) fn config<S, ApiVer: StaticVersionType + 'static>(
917    _: ApiVer,
918    api_ver: semver::Version,
919) -> Result<Api<S, Error, ApiVer>>
920where
921    S: 'static + Send + Sync + ReadState,
922    S::State: Send + Sync + HotShotConfigDataSource,
923{
924    let toml = toml::from_str::<toml::Value>(include_str!("../../api/config.toml"))?;
925    let mut api = Api::<S, Error, ApiVer>::new(toml)?;
926    api.with_version(api_ver);
927
928    let env_variables = get_public_env_vars()
929        .map_err(|err| Error::catch_all(StatusCode::INTERNAL_SERVER_ERROR, format!("{err:#}")))?;
930
931    api.get("hotshot", |_, state| {
932        async move { Ok(state.get_config().await) }.boxed()
933    })?
934    .get("env", move |_, _| {
935        {
936            let env_variables = env_variables.clone();
937            async move { Ok(env_variables) }
938        }
939        .boxed()
940    })?;
941
942    Ok(api)
943}
944
945fn get_public_env_vars() -> Result<Vec<String>> {
946    let toml: toml::Value = toml::from_str(include_str!("../../api/public-env-vars.toml"))?;
947
948    let keys = toml
949        .get("variables")
950        .ok_or_else(|| toml::de::Error::custom("variables not found"))?
951        .as_array()
952        .ok_or_else(|| toml::de::Error::custom("variables is not an array"))?
953        .clone()
954        .into_iter()
955        .map(|v| v.try_into())
956        .collect::<Result<BTreeSet<String>, toml::de::Error>>()?;
957
958    let hashmap: HashMap<String, String> = env::vars().collect();
959    let mut public_env_vars: Vec<String> = Vec::new();
960    for key in keys {
961        let value = hashmap.get(&key).cloned().unwrap_or_default();
962        public_env_vars.push(format!("{key}={value}"));
963    }
964
965    Ok(public_env_vars)
966}