Skip to main content

espresso_node/api/
endpoints.rs

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