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 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 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 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 let mut options = node::Options::default();
411 let extension = toml::from_str(include_str!("../../api/token.toml"))?;
412 options.extensions.push(extension);
413
414 let mut api =
416 node::define_api::<S, SeqTypes, _>(&options, SequencerApiVersion::instance(), api_ver)?;
417
418 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 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
470async 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 let mut options = node::Options::default();
521 let extension = toml::from_str(include_str!("../../api/node.toml"))?;
522 options.extensions.push(extension);
523
524 let mut api =
526 node::define_api::<S, SeqTypes, _>(&options, SequencerApiVersion::instance(), api_ver)?;
527
528 api.at("stake_table", |req, state| {
530 async move {
531 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 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}