1use 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#[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 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 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 let mut options = node::Options::default();
408 let extension = toml::from_str(include_str!("../../api/token.toml"))?;
409 options.extensions.push(extension);
410
411 let mut api =
413 node::define_api::<S, SeqTypes, _>(&options, SequencerApiVersion::instance(), api_ver)?;
414
415 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
450async 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 let mut options = node::Options::default();
500 let extension = toml::from_str(include_str!("../../api/node.toml"))?;
501 options.extensions.push(extension);
502
503 let mut api =
505 node::define_api::<S, SeqTypes, _>(&options, SequencerApiVersion::instance(), api_ver)?;
506
507 api.at("stake_table", |req, state| {
509 async move {
510 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 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}