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
762 Ok(api)
763}
764
765pub(super) fn submit<N, P, S, ApiVer: StaticVersionType + 'static>(
766 api_ver: semver::Version,
767) -> Result<Api<S, Error, ApiVer>>
768where
769 N: ConnectedNetwork<PubKey>,
770 S: 'static + Send + Sync + ReadState,
771 P: SequencerPersistence,
772 S::State: Send + Sync + SubmitDataSource<N, P>,
773{
774 let toml = toml::from_str::<toml::Value>(include_str!("../../api/submit.toml"))?;
775 let mut api = Api::<S, Error, ApiVer>::new(toml)?;
776
777 api.with_version(api_ver).at("submit", |req, state| {
778 async move {
779 let tx = req
780 .body_auto::<Transaction, ApiVer>(ApiVer::instance())
781 .map_err(Error::from_request_error)?;
782
783 let hash = tx.commit();
784 state
785 .read(|state| state.submit(tx).boxed())
786 .await
787 .map_err(|err| Error::internal(err.to_string()))?;
788 Ok(hash)
789 }
790 .boxed()
791 })?;
792
793 Ok(api)
794}
795
796pub(super) fn state_signature<N, S, ApiVer: StaticVersionType + 'static>(
797 _: ApiVer,
798 api_ver: semver::Version,
799) -> Result<Api<S, Error, ApiVer>>
800where
801 N: ConnectedNetwork<PubKey>,
802 S: 'static + Send + Sync + ReadState,
803 S::State: Send + Sync + StateSignatureDataSource<N>,
804{
805 let toml = toml::from_str::<toml::Value>(include_str!("../../api/state_signature.toml"))?;
806 let mut api = Api::<S, Error, ApiVer>::new(toml)?;
807 api.with_version(api_ver);
808
809 api.get("get_state_signature", |req, state| {
810 async move {
811 let height = req
812 .integer_param("height")
813 .map_err(Error::from_request_error)?;
814 state
815 .get_state_signature(height)
816 .await
817 .ok_or(tide_disco::Error::catch_all(
818 StatusCode::NOT_FOUND,
819 "Signature not found.".to_owned(),
820 ))
821 }
822 .boxed()
823 })?;
824
825 Ok(api)
826}
827
828pub(super) fn catchup<S, ApiVer: StaticVersionType + 'static>(
829 _: ApiVer,
830 api_ver: semver::Version,
831) -> Result<Api<S, Error, ApiVer>>
832where
833 S: 'static + Send + Sync + ReadState,
834 S::State: Send + Sync + NodeStateDataSource + CatchupDataSource,
835{
836 let toml = toml::from_str::<toml::Value>(include_str!("../../api/catchup.toml"))?;
837 let mut api = Api::<S, Error, ApiVer>::new(toml)?;
838 api.with_version(api_ver);
839
840 let parse_height_view = |req: &RequestParams| -> Result<(u64, ViewNumber), Error> {
841 let height = req
842 .integer_param("height")
843 .map_err(Error::from_request_error)?;
844 let view = req
845 .integer_param("view")
846 .map_err(Error::from_request_error)?;
847 Ok((height, ViewNumber::new(view)))
848 };
849
850 let parse_fee_account = |req: &RequestParams| -> Result<FeeAccount, Error> {
851 let raw = req
852 .string_param("address")
853 .map_err(Error::from_request_error)?;
854 raw.parse().map_err(|err| {
855 Error::catch_all(
856 StatusCode::BAD_REQUEST,
857 format!("malformed fee account {raw}: {err}"),
858 )
859 })
860 };
861
862 let parse_reward_account = |req: &RequestParams| -> Result<RewardAccountV2, Error> {
863 let raw = req
864 .string_param("address")
865 .map_err(Error::from_request_error)?;
866 raw.parse().map_err(|err| {
867 Error::catch_all(
868 StatusCode::BAD_REQUEST,
869 format!("malformed reward account {raw}: {err}"),
870 )
871 })
872 };
873
874 api.get("account", move |req, state| {
875 async move {
876 let (height, view) = parse_height_view(&req)?;
877 let account = parse_fee_account(&req)?;
878 state
879 .get_account(&state.node_state().await, height, view, account)
880 .await
881 .map_err(|err| Error::catch_all(StatusCode::NOT_FOUND, format!("{err:#}")))
882 }
883 .boxed()
884 })?
885 .at("accounts", move |req, state| {
886 async move {
887 let (height, view) = parse_height_view(&req)?;
888 let accounts = req
889 .body_auto::<Vec<FeeAccount>, ApiVer>(ApiVer::instance())
890 .map_err(Error::from_request_error)?;
891
892 state
893 .read(|state| {
894 async move {
895 state
896 .get_accounts(&state.node_state().await, height, view, &accounts)
897 .await
898 .map_err(|err| {
899 Error::catch_all(StatusCode::NOT_FOUND, format!("{err:#}"))
900 })
901 }
902 .boxed()
903 })
904 .await
905 }
906 .boxed()
907 })?
908 .get("reward_account", move |req, state| {
909 async move {
910 let (height, view) = parse_height_view(&req)?;
911 let account = parse_reward_account(&req)?;
912 state
913 .get_reward_account_v1(&state.node_state().await, height, view, account.into())
914 .await
915 .map_err(|err| Error::catch_all(StatusCode::NOT_FOUND, format!("{err:#}")))
916 }
917 .boxed()
918 })?
919 .at("reward_accounts", move |req, state| {
920 async move {
921 let (height, view) = parse_height_view(&req)?;
922 let accounts = req
923 .body_auto::<Vec<RewardAccountV1>, ApiVer>(ApiVer::instance())
924 .map_err(Error::from_request_error)?;
925
926 state
927 .read(|state| {
928 async move {
929 state
930 .get_reward_accounts_v1(
931 &state.node_state().await,
932 height,
933 view,
934 &accounts,
935 )
936 .await
937 .map_err(|err| {
938 Error::catch_all(StatusCode::NOT_FOUND, format!("{err:#}"))
939 })
940 }
941 .boxed()
942 })
943 .await
944 }
945 .boxed()
946 })?
947 .get("reward_account_v2", move |req, state| {
948 async move {
949 let (height, view) = parse_height_view(&req)?;
950 let account = parse_reward_account(&req)?;
951
952 state
953 .get_reward_account_v2(&state.node_state().await, height, view, account)
954 .await
955 .map_err(|err| Error::catch_all(StatusCode::NOT_FOUND, format!("{err:#}")))
956 }
957 .boxed()
958 })?
959 .get("reward_amounts", move |_req, _state| {
960 async move {
961 Err::<u64, _>(Error::catch_all(
962 StatusCode::NOT_FOUND,
963 "catchup/reward-amounts is deprecated".to_string(),
964 ))
965 }
966 .boxed()
967 })?
968 .at("reward_accounts_v2", move |_req, _state| {
969 async move {
970 Err::<u64, _>(Error::catch_all(
971 StatusCode::NOT_FOUND,
972 "catchup/reward-accounts-v2 is deprecated".to_string(),
973 ))
974 }
975 .boxed()
976 })?
977 .get("blocks", |req, state| {
978 async move {
979 let height = req
980 .integer_param("height")
981 .map_err(Error::from_request_error)?;
982 let view = req
983 .integer_param("view")
984 .map_err(Error::from_request_error)?;
985
986 state
987 .get_frontier(&state.node_state().await, height, ViewNumber::new(view))
988 .await
989 .map_err(|err| Error::catch_all(StatusCode::NOT_FOUND, format!("{err:#}")))
990 }
991 .boxed()
992 })?
993 .get("chainconfig", |req, state| {
994 async move {
995 let commitment = req
996 .blob_param("commitment")
997 .map_err(Error::from_request_error)?;
998
999 state
1000 .get_chain_config(commitment)
1001 .await
1002 .map_err(|err| Error::catch_all(StatusCode::NOT_FOUND, format!("{err:#}")))
1003 }
1004 .boxed()
1005 })?
1006 .get("leafchain", |req, state| {
1007 async move {
1008 let height = req
1009 .integer_param("height")
1010 .map_err(Error::from_request_error)?;
1011 state
1012 .get_leaf_chain(height)
1013 .await
1014 .map_err(|err| Error::catch_all(StatusCode::NOT_FOUND, format!("{err:#}")))
1015 }
1016 .boxed()
1017 })?
1018 .get("cert2", |req, state| {
1019 async move {
1020 let height = req
1021 .integer_param("height")
1022 .map_err(Error::from_request_error)?;
1023 let response = state
1024 .get_cert2(height)
1025 .await
1026 .map_err(|err| Error::catch_all(StatusCode::NOT_FOUND, format!("{err:#}")))?;
1027 response.ok_or_else(|| {
1028 Error::catch_all(
1029 StatusCode::NOT_FOUND,
1030 format!("no cert2 available for height {height}"),
1031 )
1032 })
1033 }
1034 .boxed()
1035 })?
1036 .get("reward_merkle_tree_v2", move |req, state| {
1037 async move {
1038 let (height, view) = parse_height_view(&req)?;
1039 state
1040 .get_reward_merkle_tree_v2(height, view)
1041 .await
1042 .map_err(|err| Error::catch_all(StatusCode::NOT_FOUND, format!("{err:#}")))
1043 }
1044 .boxed()
1045 })?
1046 .get("state_cert", |req, state| {
1047 async move {
1048 let epoch = req
1049 .integer_param("epoch")
1050 .map_err(Error::from_request_error)?;
1051 state
1052 .get_state_cert(epoch)
1053 .await
1054 .map_err(|err| Error::catch_all(StatusCode::NOT_FOUND, format!("{err:#}")))
1055 }
1056 .boxed()
1057 })?;
1058
1059 Ok(api)
1060}
1061
1062type MerklizedStateApi<N, P, D, ApiVer> = Api<AvailState<N, P, D>, merklized_state::Error, ApiVer>;
1063pub(super) fn merklized_state<N, P, D, S, const ARITY: usize>(
1064 api_ver: semver::Version,
1065) -> Result<MerklizedStateApi<N, P, D, SequencerApiVersion>>
1066where
1067 N: ConnectedNetwork<PubKey>,
1068 D: MerklizedStateDataSource<SeqTypes, S, ARITY>
1069 + Send
1070 + Sync
1071 + MerklizedStateHeightPersistence
1072 + 'static,
1073 S: MerklizedState<SeqTypes, ARITY>,
1074 P: SequencerPersistence,
1075 for<'a> <S::Commit as TryFrom<&'a TaggedBase64>>::Error: std::fmt::Display,
1076{
1077 let api = merklized_state::define_api::<
1078 AvailState<N, P, D>,
1079 SeqTypes,
1080 S,
1081 SequencerApiVersion,
1082 ARITY,
1083 >(&Default::default(), api_ver)?;
1084 Ok(api)
1085}
1086
1087pub(super) fn config<S, ApiVer: StaticVersionType + 'static>(
1088 _: ApiVer,
1089 api_ver: semver::Version,
1090 public_node_config: Option<PublicNodeConfig>,
1091) -> Result<Api<S, Error, ApiVer>>
1092where
1093 S: 'static + Send + Sync + ReadState,
1094 S::State: Send + Sync + HotShotConfigDataSource,
1095{
1096 let toml = toml::from_str::<toml::Value>(include_str!("../../api/config.toml"))?;
1097 let mut api = Api::<S, Error, ApiVer>::new(toml)?;
1098 api.with_version(api_ver);
1099
1100 let env_variables = get_public_env_vars()
1101 .map_err(|err| Error::catch_all(StatusCode::INTERNAL_SERVER_ERROR, format!("{err:#}")))?;
1102
1103 api.get("hotshot", |_, state| {
1104 async move { Ok(state.get_config().await) }.boxed()
1105 })?
1106 .get("env", move |_, _| {
1107 {
1108 let env_variables = env_variables.clone();
1109 async move { Ok(env_variables) }
1110 }
1111 .boxed()
1112 })?
1113 .get("runtime", move |_, _| {
1114 let public_node_config = public_node_config.clone();
1115 async move {
1116 public_node_config.ok_or_else(|| {
1117 Error::catch_all(
1118 StatusCode::NOT_FOUND,
1119 "runtime config not available".to_string(),
1120 )
1121 })
1122 }
1123 .boxed()
1124 })?;
1125
1126 Ok(api)
1127}
1128
1129fn get_public_env_vars() -> Result<Vec<String>> {
1130 let toml: toml::Value = toml::from_str(include_str!("../../api/public-env-vars.toml"))?;
1131
1132 let keys = toml
1133 .get("variables")
1134 .ok_or_else(|| toml::de::Error::custom("variables not found"))?
1135 .as_array()
1136 .ok_or_else(|| toml::de::Error::custom("variables is not an array"))?
1137 .clone()
1138 .into_iter()
1139 .map(|v| v.try_into())
1140 .collect::<Result<BTreeSet<String>, toml::de::Error>>()?;
1141
1142 let hashmap: HashMap<String, String> = env::vars().collect();
1143 let mut public_env_vars: Vec<String> = Vec::new();
1144 for key in keys {
1145 let value = hashmap.get(&key).cloned().unwrap_or_default();
1146 public_env_vars.push(format!("{key}={value}"));
1147 }
1148
1149 Ok(public_env_vars)
1150}