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