Skip to main content

espresso_node/api/
state.rs

1//! Implementations of the v1 API traits and the v2 tonic service traits, both reading the one
2//! data source this type wraps.
3
4use std::{
5    ops::{Bound, Deref},
6    time::Duration,
7};
8
9use alloy::primitives::utils::format_ether;
10use async_trait::async_trait;
11use committable::Committable as _;
12use disco_types::{error::Error as _, status::StatusCode};
13use espresso_api::{
14    error::{AvailabilityError, to_status},
15    proto,
16    v1::{self, HotShotAvailabilityApi},
17};
18use espresso_types::{
19    NamespaceId, NamespaceProofQueryData, NsProof, SeqTypes,
20    v0::sparse_mt::KeccakNode,
21    v0_3::{RewardAccountV1, RewardAmount as InternalRewardAmount, RewardMerkleTreeV1},
22    v0_4::{
23        RewardAccountQueryDataV2 as InternalRewardAccountQueryData, RewardAccountV2,
24        RewardMerkleTreeV2,
25    },
26    v0_6::RewardClaimError,
27};
28use futures::{StreamExt as _, join, stream::BoxStream};
29use hotshot_contract_adapter::reward::RewardClaimInput as InternalRewardClaimInput;
30use hotshot_events_service::events_source::EventsSource as _;
31use hotshot_new_protocol::message::Certificate2;
32use hotshot_query_service::{
33    Header as HsHeader, QueryError,
34    availability::{
35        AvailabilityDataSource, BlockId as HsBlockId, BlockQueryData, BlockSummaryQueryData,
36        LeafId as HsLeafId, LeafQueryData, Limits as HsLimits, PayloadQueryData,
37        QueryablePayload as _, TransactionQueryData, TransactionWithProofQueryData,
38        VidCommonQueryData,
39    },
40    explorer::{
41        BlockIdentifier, BlockRange, ExplorerDataSource as _, GetBlockSummariesRequest,
42        GetTransactionSummariesRequest, TransactionIdentifier, TransactionRange,
43        TransactionSummaryFilter,
44    },
45    merklized_state::{
46        MerklizedStateDataSource, MerklizedStateHeightPersistence, Snapshot as HsSnapshot,
47    },
48    node::{NodeDataSource as _, WindowStart},
49    status::HasMetrics as _,
50    types::HeightIndexed as _,
51};
52use hotshot_types::{
53    data::VidShare,
54    utils::{epoch_from_block_number, root_block_in_epoch},
55    vid::avidm::AvidMShare,
56};
57use jf_merkle_tree_compat::prelude::{
58    MerkleProof as InternalMerkleProof, MerkleProof as JfMerkleProof,
59};
60use prometheus::Encoder as _;
61use serde_json;
62use tagged_base64::TaggedBase64;
63
64use super::{
65    RewardMerkleTreeDataSource, RewardMerkleTreeV2Data as InternalRewardTreeData,
66    data_source::{
67        CatchupDataSource, DatabaseMetadataSource, HotShotConfigDataSource, MigrationStatus,
68        NodeKeysDataSource, NodePublicKeys, NodeStateDataSource, PruningDataSource,
69        RequestResponseDataSource, StakeTableDataSource, StakeTableWithEpochNumber,
70        StateCertDataSource, StateCertFetchingDataSource, StateSignatureDataSource,
71        SubmitDataSource, TableSize, TokenDataSource,
72    },
73};
74
75/// Timeout for failing requests due to missing data.
76///
77/// If data needed to respond to a request is missing, it can (in some cases) be fetched from an
78/// external provider. This parameter controls how long the request handler will wait for
79/// missing data to be fetched before giving up and failing the request.
80///
81/// Matches the `hotshot_query_service` availability API default.
82const FETCH_TIMEOUT: Duration = Duration::from_millis(500);
83
84/// Node API state implementation
85///
86/// This struct implements the v1 API traits (internal types) and the v2 tonic service traits
87/// (proto types).
88#[derive(Clone)]
89pub struct NodeApiStateImpl<D> {
90    data_source: D,
91    env_vars: std::sync::Arc<Vec<String>>,
92    public_node_config: Option<std::sync::Arc<crate::options::PublicNodeConfig>>,
93}
94
95impl<D> NodeApiStateImpl<D> {
96    pub fn new(data_source: D) -> Self {
97        Self {
98            data_source,
99            env_vars: std::sync::Arc::new(Vec::new()),
100            public_node_config: None,
101        }
102    }
103
104    pub fn with_env_vars(mut self, env_vars: Vec<String>) -> Self {
105        self.env_vars = std::sync::Arc::new(env_vars);
106        self
107    }
108
109    pub fn with_public_node_config(
110        mut self,
111        config: Option<crate::options::PublicNodeConfig>,
112    ) -> Self {
113        self.public_node_config = config.map(std::sync::Arc::new);
114        self
115    }
116}
117
118#[async_trait]
119impl<D> v1::RewardApi for NodeApiStateImpl<D>
120where
121    D: RewardMerkleTreeDataSource + Deref,
122    D::Target: hotshot_query_service::merklized_state::MerklizedStateHeightPersistence
123        + hotshot_query_service::merklized_state::MerklizedStateDataSource<
124            SeqTypes,
125            espresso_types::v0_3::RewardMerkleTreeV1,
126            {
127                <espresso_types::v0_3::RewardMerkleTreeV1 as jf_merkle_tree_compat::MerkleTreeScheme>::ARITY
128            },
129        > + hotshot_query_service::merklized_state::MerklizedStateDataSource<
130            SeqTypes,
131            espresso_types::v0_4::RewardMerkleTreeV2,
132            {
133                <espresso_types::v0_4::RewardMerkleTreeV2 as jf_merkle_tree_compat::MerkleTreeScheme>::ARITY
134            },
135        > + Send
136        + Sync,
137{
138    type RewardClaimInput = InternalRewardClaimInput;
139    type RewardBalance = InternalRewardAmount;
140    type RewardAccountQueryData = InternalRewardAccountQueryData;
141    type RewardAmounts = Vec<(alloy::primitives::Address, InternalRewardAmount)>;
142    type RewardMerkleTreeData = Vec<u8>;
143    type RewardAccountQueryDataV1 = espresso_types::v0_3::RewardAccountQueryDataV1;
144    type RewardStatePathV1 = InternalMerkleProof<
145        InternalRewardAmount,
146        espresso_types::v0_3::RewardAccountV1,
147        jf_merkle_tree_compat::prelude::Sha3Node,
148        {
149            <espresso_types::v0_3::RewardMerkleTreeV1 as jf_merkle_tree_compat::MerkleTreeScheme>::ARITY
150        },
151    >;
152    type RewardStatePathV2 = InternalMerkleProof<
153        InternalRewardAmount,
154        RewardAccountV2,
155        KeccakNode,
156        {
157            <espresso_types::v0_4::RewardMerkleTreeV2 as jf_merkle_tree_compat::MerkleTreeScheme>::ARITY
158        },
159    >;
160
161    async fn get_reward_state_height(&self) -> anyhow::Result<u64> {
162        let ds = &*self.data_source;
163        ds.get_last_state_height()
164            .await
165            .map(|h| h as u64)
166            .map_err(classify_query_error)
167    }
168
169    async fn get_reward_state_v2_height(&self) -> anyhow::Result<u64> {
170        // `last_merklized_state_height` is the same row for every merklized-state module in
171        // this file (reward V1/V2, block-state, fee-state), not just these two.
172        self.get_reward_state_height().await
173    }
174
175    async fn get_reward_account_proof_v1(
176        &self,
177        height: u64,
178        address: String,
179    ) -> anyhow::Result<Self::RewardAccountQueryDataV1> {
180        let account: RewardAccountV1 = address
181            .parse()
182            .map_err(|_| bad_request(format!("invalid ethereum address: {}", address)))?;
183
184        self.data_source
185            .load_v1_reward_account_proof(height, account)
186            .await
187            .map_err(|err| {
188                not_found(format!(
189                    "failed to load v1 reward account {} at height {}: {}",
190                    address, height, err
191                ))
192            })
193    }
194
195    async fn get_reward_claim_input(
196        &self,
197        block_height: u64,
198        address: String,
199    ) -> anyhow::Result<Self::RewardClaimInput> {
200        // Parse the Ethereum address
201        let addr: alloy::primitives::Address = address
202            .parse()
203            .map_err(|_| bad_request(format!("invalid ethereum address: {}", address)))?;
204
205        // Load the reward account proof from the data source
206        let proof = self
207            .data_source
208            .load_reward_account_proof_v2(block_height, addr.into())
209            .await
210            .map_err(|err| {
211                not_found(format!(
212                    "failed to load reward account {} at height {}: {}",
213                    address, block_height, err
214                ))
215            })?;
216
217        // Convert the proof to reward claim input (internal type)
218        let claim_input = proof.to_reward_claim_input().map_err(|err| match err {
219            RewardClaimError::ZeroRewardError => not_found(format!(
220                "zero reward balance for {} at height {}",
221                address, block_height
222            )),
223            RewardClaimError::ProofConversionError(e) => {
224                anyhow::anyhow!(
225                    "failed to create solidity proof for {} at height {}: {}",
226                    address,
227                    block_height,
228                    e
229                )
230            },
231        })?;
232
233        Ok(claim_input)
234    }
235
236    async fn get_reward_balance(
237        &self,
238        height: u64,
239        address: String,
240    ) -> anyhow::Result<Self::RewardBalance> {
241        // Parse the Ethereum address
242        let addr: alloy::primitives::Address = address
243            .parse()
244            .map_err(|_| bad_request(format!("invalid ethereum address: {}", address)))?;
245
246        // Load the reward account proof from the data source
247        let proof = self
248            .data_source
249            .load_reward_account_proof_v2(height, addr.into())
250            .await
251            .map_err(|err| {
252                not_found(format!(
253                    "failed to load reward account {} at height {}: {}",
254                    address, height, err
255                ))
256            })?;
257
258        Ok(InternalRewardAmount(proof.balance))
259    }
260
261    async fn get_latest_reward_balance(
262        &self,
263        address: String,
264    ) -> anyhow::Result<Self::RewardBalance> {
265        let addr: alloy::primitives::Address = address
266            .parse()
267            .map_err(|_| bad_request(format!("invalid ethereum address: {}", address)))?;
268
269        let proof = self
270            .data_source
271            .load_latest_reward_account_proof_v2(addr.into())
272            .await
273            .map_err(|err| {
274                not_found(format!(
275                    "failed to load latest reward account {}: {}",
276                    address, err
277                ))
278            })?;
279
280        Ok(InternalRewardAmount(proof.balance))
281    }
282
283    async fn get_reward_account_proof(
284        &self,
285        height: u64,
286        address: String,
287    ) -> anyhow::Result<Self::RewardAccountQueryData> {
288        // Parse the Ethereum address
289        let addr: alloy::primitives::Address = address
290            .parse()
291            .map_err(|_| bad_request(format!("invalid ethereum address: {}", address)))?;
292
293        // Load and return the reward account proof directly (internal type)
294        let proof = self
295            .data_source
296            .load_reward_account_proof_v2(height, addr.into())
297            .await
298            .map_err(|err| {
299                not_found(format!(
300                    "failed to load reward account {} at height {}: {}",
301                    address, height, err
302                ))
303            })?;
304
305        Ok(proof)
306    }
307
308    async fn get_latest_reward_account_proof(
309        &self,
310        address: String,
311    ) -> anyhow::Result<Self::RewardAccountQueryData> {
312        // Parse the Ethereum address
313        let addr: alloy::primitives::Address = address
314            .parse()
315            .map_err(|_| bad_request(format!("invalid ethereum address: {}", address)))?;
316
317        let proof = self
318            .data_source
319            .load_latest_reward_account_proof_v2(addr.into())
320            .await
321            .map_err(|err| {
322                not_found(format!(
323                    "failed to load latest reward account {}: {}",
324                    address, err
325                ))
326            })?;
327
328        Ok(proof)
329    }
330
331    async fn get_reward_amounts(
332        &self,
333        height: u64,
334        offset: u64,
335        limit: u64,
336    ) -> anyhow::Result<Self::RewardAmounts> {
337        if limit > 10000 {
338            return Err(bad_request(format!(
339                "limit {} exceeds maximum allowed value of 10000",
340                limit
341            )));
342        }
343
344        let tree_bytes = self.data_source.load_tree(height).await.map_err(|err| {
345            not_found(format!(
346                "failed to load reward tree at height {}: {}",
347                height, err
348            ))
349        })?;
350
351        let tree_data: InternalRewardTreeData =
352            bincode::deserialize(&tree_bytes).map_err(|err| {
353                not_found(format!(
354                    "failed to deserialize RewardMerkleTreeV2Data at height {}: {}",
355                    height, err
356                ))
357            })?;
358
359        let offset_usize = offset as usize;
360        let limit_usize = limit as usize;
361
362        if offset_usize > tree_data.balances.len() {
363            return Err(not_found(format!("offset {} out of bounds", offset)));
364        }
365
366        let end = std::cmp::min(offset_usize + limit_usize, tree_data.balances.len());
367        let slice = &tree_data.balances[offset_usize..end];
368
369        let result: Vec<(alloy::primitives::Address, InternalRewardAmount)> = slice
370            .iter()
371            .rev()
372            .map(|(account, amount)| (account.0, *amount))
373            .collect();
374
375        Ok(result)
376    }
377
378    async fn get_reward_merkle_tree_v2(
379        &self,
380        height: u64,
381    ) -> anyhow::Result<Self::RewardMerkleTreeData> {
382        self.data_source.load_tree(height).await.map_err(|err| {
383            not_found(format!(
384                "failed to load reward tree at height {}: {}",
385                height, err
386            ))
387        })
388    }
389
390    async fn get_reward_state_path_v1(
391        &self,
392        snapshot: v1::Snapshot,
393        key: String,
394    ) -> anyhow::Result<Self::RewardStatePathV1> {
395        let hs_snapshot = match snapshot {
396            v1::Snapshot::Height(h) => HsSnapshot::Index(h),
397            v1::Snapshot::Commit(c) => {
398                let tb64: TaggedBase64 = c
399                    .parse()
400                    .map_err(|_| bad_request("failed to parse commit param"))?;
401                let commit = (&tb64)
402                    .try_into()
403                    .map_err(|_| bad_request("failed to parse commit param"))?;
404                HsSnapshot::Commit(commit)
405            },
406        };
407        let key: RewardAccountV1 = key
408            .parse()
409            .map_err(|_| bad_request("failed to parse Key param"))?;
410        let ds = &*self.data_source;
411        MerklizedStateDataSource::<SeqTypes, RewardMerkleTreeV1, _>::get_path(ds, hs_snapshot, key)
412            .await
413            .map_err(classify_query_error)
414    }
415
416    async fn get_reward_state_path_v2(
417        &self,
418        snapshot: v1::Snapshot,
419        key: String,
420    ) -> anyhow::Result<Self::RewardStatePathV2> {
421        let hs_snapshot = match snapshot {
422            v1::Snapshot::Height(h) => HsSnapshot::Index(h),
423            v1::Snapshot::Commit(c) => {
424                let tb64: TaggedBase64 = c
425                    .parse()
426                    .map_err(|_| bad_request("failed to parse commit param"))?;
427                let commit = (&tb64)
428                    .try_into()
429                    .map_err(|_| bad_request("failed to parse commit param"))?;
430                HsSnapshot::Commit(commit)
431            },
432        };
433        let key: RewardAccountV2 = key
434            .parse()
435            .map_err(|_| bad_request("failed to parse Key param"))?;
436        let ds = &*self.data_source;
437        MerklizedStateDataSource::<SeqTypes, RewardMerkleTreeV2, _>::get_path(ds, hs_snapshot, key)
438            .await
439            .map_err(classify_query_error)
440    }
441}
442
443#[async_trait]
444impl<D> v1::AvailabilityApi for NodeApiStateImpl<D>
445where
446    D: Deref + Clone + Send + Sync + 'static,
447    // No `RewardMerkleTreeDataSource` bound here: unlike `v1::RewardApi`, none of these methods
448    // touch the reward merkle tree, so filesystem storage (which doesn't implement it) can serve
449    // this module too.
450    D::Target: hotshot_query_service::availability::AvailabilityDataSource<SeqTypes>
451        + hotshot_query_service::node::NodeDataSource<SeqTypes>
452        + RequestResponseDataSource<SeqTypes>
453        + StateCertDataSource
454        + StateCertFetchingDataSource<SeqTypes>
455        + Send
456        + Sync,
457{
458    type NamespaceProofQueryData = espresso_types::NamespaceProofQueryData;
459    type IncorrectEncodingProof = espresso_types::v0_3::AvidMIncorrectEncodingNsProof;
460    type StateCertQueryDataV1 = espresso_types::StateCertQueryDataV1<SeqTypes>;
461    type StateCertQueryDataV2 = espresso_types::StateCertQueryDataV2<SeqTypes>;
462
463    async fn get_namespace_proof(
464        &self,
465        block_id: v1::availability::BlockId,
466        namespace: u32,
467    ) -> anyhow::Result<Self::NamespaceProofQueryData> {
468        let ns_id = NamespaceId::from(namespace);
469
470        // Convert v1 BlockId to hotshot BlockId
471        let hs_block_id = match block_id {
472            v1::availability::BlockId::Height(h) => HsBlockId::Number(h as usize),
473            v1::availability::BlockId::Hash(h) => {
474                let hash = h
475                    .parse()
476                    .map_err(|_| bad_request(format!("invalid block hash: {}", h)))?;
477                HsBlockId::Hash(hash)
478            },
479            v1::availability::BlockId::PayloadHash(h) => {
480                let payload_hash = h
481                    .parse()
482                    .map_err(|_| bad_request(format!("invalid payload hash: {}", h)))?;
483                HsBlockId::PayloadHash(payload_hash)
484            },
485        };
486
487        // Fetch block and VID common data
488        let ds = &*self.data_source;
489        let timeout = FETCH_TIMEOUT;
490        let (block_fetch, vid_fetch) =
491            join!(ds.get_block(hs_block_id), ds.get_vid_common(hs_block_id));
492        let (block, vid_common) = join!(
493            block_fetch.with_timeout(timeout),
494            vid_fetch.with_timeout(timeout)
495        );
496
497        let block =
498            block.ok_or_else(|| not_found(format!("block {} not available", hs_block_id)))?;
499        let vid_common = vid_common.ok_or_else(|| {
500            not_found(format!(
501                "VID common for block {} not available",
502                hs_block_id
503            ))
504        })?;
505
506        // Namespace absent from the block: an empty result, not an error.
507        let ns_table = block.payload().ns_table();
508        let Some(ns_index) = ns_table.find_ns_id(&ns_id) else {
509            return Ok(espresso_types::NamespaceProofQueryData {
510                transactions: vec![],
511                proof: None,
512            });
513        };
514
515        // Generate namespace proof
516        let Some(proof) = NsProof::new(block.payload(), &ns_index, vid_common.common()) else {
517            // Failed to generate proof - namespace exists but proof generation failed
518            return Ok(espresso_types::NamespaceProofQueryData {
519                transactions: vec![],
520                proof: None,
521            });
522        };
523
524        let transactions = proof.export_all_txs(&ns_id);
525
526        Ok(espresso_types::NamespaceProofQueryData {
527            transactions,
528            proof: Some(proof),
529        })
530    }
531
532    async fn get_namespace_proof_range(
533        &self,
534        from: u64,
535        until: u64,
536        namespace: u32,
537    ) -> anyhow::Result<Vec<Self::NamespaceProofQueryData>> {
538        let ns_id = NamespaceId::from(namespace);
539
540        // Validate range
541        if until <= from {
542            return Err(bad_request(format!(
543                "invalid range: until ({}) must be greater than from ({})",
544                until, from
545            )));
546        }
547
548        let range_size = until - from;
549        const MAX_RANGE: u64 = 100;
550        if range_size > MAX_RANGE {
551            return Err(range_exceeded(format!(
552                "range too large: {} blocks (max {})",
553                range_size, MAX_RANGE
554            )));
555        }
556
557        // Fetch blocks and VID common data for the range
558        let (blocks_stream, vids_stream) = join!(
559            self.data_source
560                .get_block_range(from as usize..until as usize),
561            self.data_source
562                .get_vid_common_range(from as usize..until as usize)
563        );
564
565        let blocks: Vec<_> = blocks_stream
566            .then(|block| async move { block.resolve().await })
567            .collect()
568            .await;
569        let vids: Vec<_> = vids_stream
570            .then(|vid| async move { vid.resolve().await })
571            .collect()
572            .await;
573
574        if blocks.len() != vids.len() {
575            return Err(anyhow::anyhow!(
576                "mismatch between blocks and VID common data"
577            ));
578        }
579
580        // Generate proofs for each block
581        let mut proofs = Vec::new();
582
583        for (block, vid) in blocks.into_iter().zip(vids) {
584            let ns_table = block.payload().ns_table();
585
586            // Check if namespace exists in this block
587            if let Some(ns_index) = ns_table.find_ns_id(&ns_id) {
588                if let Some(proof) = NsProof::new(block.payload(), &ns_index, vid.common()) {
589                    let transactions = proof.export_all_txs(&ns_id);
590                    proofs.push(espresso_types::NamespaceProofQueryData {
591                        transactions,
592                        proof: Some(proof),
593                    });
594                } else {
595                    // Failed to generate proof - return empty result for this block
596                    proofs.push(espresso_types::NamespaceProofQueryData {
597                        transactions: vec![],
598                        proof: None,
599                    });
600                }
601            } else {
602                // Namespace not present in this block
603                proofs.push(espresso_types::NamespaceProofQueryData {
604                    transactions: vec![],
605                    proof: None,
606                });
607            }
608        }
609
610        Ok(proofs)
611    }
612
613    async fn stream_namespace_proofs(
614        &self,
615        from: usize,
616        namespace: u32,
617    ) -> anyhow::Result<BoxStream<'static, Self::NamespaceProofQueryData>> {
618        let ns_id = NamespaceId::from(namespace);
619        let ds = self.data_source.clone();
620        let blocks = (*ds).subscribe_blocks(from).await;
621        let vids = (*ds).subscribe_vid_common(from).await;
622
623        let stream = blocks
624            .zip(vids)
625            .map(move |(block, vid)| {
626                let ns_table = block.payload().ns_table();
627                if let Some(ns_index) = ns_table.find_ns_id(&ns_id) {
628                    if let Some(proof) = NsProof::new(block.payload(), &ns_index, vid.common()) {
629                        let transactions = proof.export_all_txs(&ns_id);
630                        NamespaceProofQueryData {
631                            transactions,
632                            proof: Some(proof),
633                        }
634                    } else {
635                        NamespaceProofQueryData {
636                            transactions: vec![],
637                            proof: None,
638                        }
639                    }
640                } else {
641                    NamespaceProofQueryData {
642                        transactions: vec![],
643                        proof: None,
644                    }
645                }
646            })
647            .boxed();
648
649        Ok(stream)
650    }
651
652    async fn get_incorrect_encoding_proof(
653        &self,
654        block_id: v1::availability::BlockId,
655        namespace: u32,
656    ) -> anyhow::Result<Self::IncorrectEncodingProof> {
657        let ns_id = NamespaceId::from(namespace);
658
659        let hs_block_id = match block_id {
660            v1::availability::BlockId::Height(h) => HsBlockId::Number(h as usize),
661            v1::availability::BlockId::Hash(h) => {
662                let hash = h
663                    .parse()
664                    .map_err(|_| anyhow::anyhow!("invalid block hash: {}", h))?;
665                HsBlockId::Hash(hash)
666            },
667            v1::availability::BlockId::PayloadHash(h) => {
668                let payload_hash = h
669                    .parse()
670                    .map_err(|_| anyhow::anyhow!("invalid payload hash: {}", h))?;
671                HsBlockId::PayloadHash(payload_hash)
672            },
673        };
674
675        let ds = &*self.data_source;
676        let timeout = FETCH_TIMEOUT;
677        let (block_fetch, vid_fetch) =
678            join!(ds.get_block(hs_block_id), ds.get_vid_common(hs_block_id));
679        let (block, vid_common) = join!(
680            block_fetch.with_timeout(timeout),
681            vid_fetch.with_timeout(timeout)
682        );
683
684        let block = block.ok_or_else(|| anyhow::anyhow!("block not found"))?;
685        let vid_common = vid_common.ok_or_else(|| anyhow::anyhow!("VID common data not found"))?;
686
687        let ns_table = block.payload().ns_table();
688        let ns_index = ns_table
689            .find_ns_id(&ns_id)
690            .ok_or_else(|| anyhow::anyhow!("namespace {} not present in block", namespace))?;
691
692        if NsProof::new(block.payload(), &ns_index, vid_common.common()).is_some() {
693            return Err(anyhow::anyhow!("block was correctly encoded"));
694        }
695
696        // Block has incorrect encoding: fetch VID shares to construct the proof.
697        let vid_shares_future = ds
698            .request_vid_shares(block.height(), vid_common.clone(), Duration::from_secs(40))
699            .await;
700        let mut vid_shares = vid_shares_future
701            .await
702            .map_err(|e| anyhow::anyhow!("failed to fetch VID shares: {e:#}"))?;
703
704        if let Ok(local_share) = ds.vid_share(block.height() as usize).await {
705            vid_shares.push(local_share);
706        }
707
708        let avidm_shares: Vec<AvidMShare> = vid_shares
709            .into_iter()
710            .filter_map(|s| {
711                if let VidShare::V1(s) = s {
712                    Some(s)
713                } else {
714                    None
715                }
716            })
717            .collect();
718
719        match NsProof::v1_1_new_with_incorrect_encoding(
720            &avidm_shares,
721            ns_table,
722            &ns_index,
723            &vid_common.payload_hash(),
724            vid_common.common(),
725        ) {
726            Some(NsProof::V1IncorrectEncoding(proof)) => Ok(proof),
727            _ => Err(anyhow::anyhow!(
728                "failed to generate incorrect encoding proof"
729            )),
730        }
731    }
732
733    async fn get_state_cert(&self, epoch: u64) -> anyhow::Result<Self::StateCertQueryDataV1> {
734        // Try to get from local storage first
735        let state_cert = self.data_source.get_state_cert_by_epoch(epoch).await?;
736
737        let cert = match state_cert {
738            Some(cert) => cert,
739            None => {
740                // Not found locally, try to fetch from peers
741                const TIMEOUT: Duration = Duration::from_secs(40);
742                let cert = self
743                    .data_source
744                    .request_state_cert(epoch, TIMEOUT)
745                    .await
746                    .map_err(|e| {
747                        anyhow::anyhow!("failed to fetch state cert for epoch {}: {}", epoch, e)
748                    })?;
749
750                // Store the fetched certificate
751                self.data_source
752                    .insert_state_cert(epoch, cert.clone())
753                    .await?;
754
755                cert
756            },
757        };
758
759        Ok(espresso_types::StateCertQueryDataV1::from(
760            espresso_types::StateCertQueryDataV2(cert),
761        ))
762    }
763
764    async fn get_state_cert_v2(&self, epoch: u64) -> anyhow::Result<Self::StateCertQueryDataV2> {
765        // Try to get from local storage first
766        let state_cert = self.data_source.get_state_cert_by_epoch(epoch).await?;
767
768        let cert = match state_cert {
769            Some(cert) => cert,
770            None => {
771                // Not found locally, try to fetch from peers
772                const TIMEOUT: Duration = Duration::from_secs(40);
773                let cert = self
774                    .data_source
775                    .request_state_cert(epoch, TIMEOUT)
776                    .await
777                    .map_err(|e| {
778                        anyhow::anyhow!("failed to fetch state cert for epoch {}: {}", epoch, e)
779                    })?;
780
781                // Store the fetched certificate
782                self.data_source
783                    .insert_state_cert(epoch, cert.clone())
784                    .await?;
785
786                cert
787            },
788        };
789
790        Ok(espresso_types::StateCertQueryDataV2(cert))
791    }
792}
793
794fn not_found(msg: impl Into<String>) -> anyhow::Error {
795    AvailabilityError::NotFound(msg.into()).into()
796}
797
798fn bad_request(msg: impl Into<String>) -> anyhow::Error {
799    AvailabilityError::BadRequest(msg.into()).into()
800}
801
802fn range_exceeded(msg: impl Into<String>) -> anyhow::Error {
803    AvailabilityError::RangeExceeded(msg.into()).into()
804}
805
806fn enforce_range(from: usize, until: usize, limit: usize) -> anyhow::Result<()> {
807    if until.saturating_sub(from) > limit {
808        return Err(range_exceeded(format!(
809            "range {from}..{until} exceeds limit {limit}"
810        )));
811    }
812    Ok(())
813}
814
815// Range limits for list endpoints, read from `hotshot_query_service`'s `Options` (their only
816// remaining declaration) so a dependency bump that changes the defaults changes enforcement too.
817fn small_object_range_limit() -> usize {
818    hotshot_query_service::availability::Options::default().small_object_range_limit
819}
820
821fn large_object_range_limit() -> usize {
822    hotshot_query_service::availability::Options::default().large_object_range_limit
823}
824
825#[async_trait]
826impl<D> HotShotAvailabilityApi for NodeApiStateImpl<D>
827where
828    D: Deref + Clone + Send + Sync + 'static,
829    D::Target: AvailabilityDataSource<SeqTypes> + Send + Sync,
830{
831    type Leaf = LeafQueryData<SeqTypes>;
832    type Block = BlockQueryData<SeqTypes>;
833    type Header = HsHeader<SeqTypes>;
834    type Payload = PayloadQueryData<SeqTypes>;
835    type VidCommon = VidCommonQueryData<SeqTypes>;
836    type Transaction = TransactionQueryData<SeqTypes>;
837    type TransactionWithProof = TransactionWithProofQueryData<SeqTypes>;
838    type BlockSummary = BlockSummaryQueryData<SeqTypes>;
839    type Limits = HsLimits;
840    type Cert2 = Certificate2<SeqTypes>;
841
842    async fn get_leaf(&self, id: v1::availability::LeafId) -> anyhow::Result<Self::Leaf> {
843        let hs_id = match id {
844            v1::availability::LeafId::Height(h) => HsLeafId::Number(h as usize),
845            v1::availability::LeafId::Hash(h) => {
846                HsLeafId::Hash(h.parse().map_err(|_| bad_request("invalid leaf hash"))?)
847            },
848        };
849        let ds = &*self.data_source;
850        ds.get_leaf(hs_id)
851            .await
852            .with_timeout(FETCH_TIMEOUT)
853            .await
854            .ok_or_else(|| not_found("leaf not found"))
855    }
856
857    async fn get_leaf_range(&self, from: usize, until: usize) -> anyhow::Result<Vec<Self::Leaf>> {
858        enforce_range(from, until, small_object_range_limit())?;
859        let timeout = FETCH_TIMEOUT;
860        let ds = &*self.data_source;
861        let stream = ds.get_leaf_range(from..until).await;
862        let mut results = Vec::new();
863        futures::pin_mut!(stream);
864        let mut i = from;
865        while let Some(fetch) = stream.next().await {
866            let item = fetch
867                .with_timeout(timeout)
868                .await
869                .ok_or_else(|| not_found(format!("leaf {} not found", i)))?;
870            results.push(item);
871            i += 1;
872        }
873        Ok(results)
874    }
875
876    async fn get_header(&self, id: v1::availability::BlockId) -> anyhow::Result<Self::Header> {
877        let hs_id = block_id_to_hs(id)?;
878        let ds = &*self.data_source;
879        ds.get_header(hs_id)
880            .await
881            .with_timeout(FETCH_TIMEOUT)
882            .await
883            .ok_or_else(|| not_found(format!("header not found for {}", hs_id)))
884    }
885
886    async fn get_header_range(
887        &self,
888        from: usize,
889        until: usize,
890    ) -> anyhow::Result<Vec<Self::Header>> {
891        enforce_range(from, until, large_object_range_limit())?;
892        let timeout = FETCH_TIMEOUT;
893        let ds = &*self.data_source;
894        let stream = ds.get_header_range(from..until).await;
895        let mut results = Vec::new();
896        futures::pin_mut!(stream);
897        let mut i = from;
898        while let Some(fetch) = stream.next().await {
899            let item = fetch
900                .with_timeout(timeout)
901                .await
902                .ok_or_else(|| not_found(format!("header {} not found", i)))?;
903            results.push(item);
904            i += 1;
905        }
906        Ok(results)
907    }
908
909    async fn get_block(&self, id: v1::availability::BlockId) -> anyhow::Result<Self::Block> {
910        let hs_id = block_id_to_hs(id)?;
911        let ds = &*self.data_source;
912        ds.get_block(hs_id)
913            .await
914            .with_timeout(FETCH_TIMEOUT)
915            .await
916            .ok_or_else(|| not_found(format!("block not found for {}", hs_id)))
917    }
918
919    async fn get_block_range(&self, from: usize, until: usize) -> anyhow::Result<Vec<Self::Block>> {
920        enforce_range(from, until, large_object_range_limit())?;
921        let timeout = FETCH_TIMEOUT;
922        let ds = &*self.data_source;
923        let stream = ds.get_block_range(from..until).await;
924        let mut results = Vec::new();
925        futures::pin_mut!(stream);
926        let mut i = from;
927        while let Some(fetch) = stream.next().await {
928            let item = fetch
929                .with_timeout(timeout)
930                .await
931                .ok_or_else(|| not_found(format!("block {} not found", i)))?;
932            results.push(item);
933            i += 1;
934        }
935        Ok(results)
936    }
937
938    async fn get_payload(&self, id: v1::availability::PayloadId) -> anyhow::Result<Self::Payload> {
939        let hs_id = payload_id_to_hs(id)?;
940        let ds = &*self.data_source;
941        ds.get_payload(hs_id)
942            .await
943            .with_timeout(FETCH_TIMEOUT)
944            .await
945            .ok_or_else(|| not_found(format!("payload not found for {}", hs_id)))
946    }
947
948    async fn get_payload_range(
949        &self,
950        from: usize,
951        until: usize,
952    ) -> anyhow::Result<Vec<Self::Payload>> {
953        enforce_range(from, until, large_object_range_limit())?;
954        let timeout = FETCH_TIMEOUT;
955        let ds = &*self.data_source;
956        let stream = ds.get_payload_range(from..until).await;
957        let mut results = Vec::new();
958        futures::pin_mut!(stream);
959        let mut i = from;
960        while let Some(fetch) = stream.next().await {
961            let item = fetch
962                .with_timeout(timeout)
963                .await
964                .ok_or_else(|| not_found(format!("payload {} not found", i)))?;
965            results.push(item);
966            i += 1;
967        }
968        Ok(results)
969    }
970
971    async fn get_vid_common(
972        &self,
973        id: v1::availability::BlockId,
974    ) -> anyhow::Result<Self::VidCommon> {
975        let hs_id = block_id_to_hs(id)?;
976        let ds = &*self.data_source;
977        ds.get_vid_common(hs_id)
978            .await
979            .with_timeout(FETCH_TIMEOUT)
980            .await
981            .ok_or_else(|| not_found(format!("VID common not found for {}", hs_id)))
982    }
983
984    async fn get_vid_common_range(
985        &self,
986        from: usize,
987        until: usize,
988    ) -> anyhow::Result<Vec<Self::VidCommon>> {
989        enforce_range(from, until, small_object_range_limit())?;
990        let timeout = FETCH_TIMEOUT;
991        let ds = &*self.data_source;
992        let stream = ds.get_vid_common_range(from..until).await;
993        let mut results = Vec::new();
994        futures::pin_mut!(stream);
995        let mut i = from;
996        while let Some(fetch) = stream.next().await {
997            let item = fetch
998                .with_timeout(timeout)
999                .await
1000                .ok_or_else(|| not_found(format!("VID common {} not found", i)))?;
1001            results.push(item);
1002            i += 1;
1003        }
1004        Ok(results)
1005    }
1006
1007    async fn get_transaction_by_position(
1008        &self,
1009        height: u64,
1010        index: u64,
1011    ) -> anyhow::Result<Self::Transaction> {
1012        let ds = &*self.data_source;
1013        let block = ds
1014            .get_block(HsBlockId::Number(height as usize))
1015            .await
1016            .with_timeout(FETCH_TIMEOUT)
1017            .await
1018            .ok_or_else(|| not_found(format!("block {} not found", height)))?;
1019
1020        let idx = block
1021            .payload()
1022            .nth(block.metadata(), index as usize)
1023            .ok_or_else(|| {
1024                not_found(format!(
1025                    "transaction index {} out of bounds in block {}",
1026                    index, height
1027                ))
1028            })?;
1029        let tx = block
1030            .transaction(&idx)
1031            .ok_or_else(|| not_found(format!("transaction not found at index {}", index)))?;
1032        TransactionQueryData::new(tx, &block, &idx, index)
1033            .ok_or_else(|| anyhow::anyhow!("failed to build transaction query data"))
1034    }
1035
1036    async fn get_transaction_by_hash(&self, hash: String) -> anyhow::Result<Self::Transaction> {
1037        let ds = &*self.data_source;
1038        let tx_hash: hotshot_query_service::availability::TransactionHash<SeqTypes> = hash
1039            .parse()
1040            .map_err(|_| bad_request(format!("invalid transaction hash: {}", hash)))?;
1041        let bwt = ds
1042            .get_block_containing_transaction(tx_hash)
1043            .await
1044            .with_timeout(FETCH_TIMEOUT)
1045            .await
1046            .ok_or_else(|| not_found("transaction not found"))?;
1047        Ok(bwt.transaction)
1048    }
1049
1050    async fn get_transaction_proof_by_position(
1051        &self,
1052        height: u64,
1053        index: u64,
1054    ) -> anyhow::Result<Self::TransactionWithProof> {
1055        let ds = &*self.data_source;
1056        let timeout = FETCH_TIMEOUT;
1057
1058        let (block_fetch, vid_fetch) = futures::join!(
1059            ds.get_block(HsBlockId::Number(height as usize)),
1060            ds.get_vid_common(HsBlockId::Number(height as usize))
1061        );
1062        let (block, vid) = futures::join!(
1063            block_fetch.with_timeout(timeout),
1064            vid_fetch.with_timeout(timeout)
1065        );
1066
1067        let block = block.ok_or_else(|| not_found(format!("block {} not found", height)))?;
1068        let vid =
1069            vid.ok_or_else(|| not_found(format!("VID common not found for block {}", height)))?;
1070
1071        let idx = block
1072            .payload()
1073            .nth(block.metadata(), index as usize)
1074            .ok_or_else(|| {
1075                not_found(format!(
1076                    "transaction index {} out of bounds in block {}",
1077                    index, height
1078                ))
1079            })?;
1080        let tx = block
1081            .transaction(&idx)
1082            .ok_or_else(|| not_found(format!("transaction not found at index {}", index)))?;
1083        let tx_data = TransactionQueryData::new(tx, &block, &idx, index)
1084            .ok_or_else(|| anyhow::anyhow!("failed to build transaction query data"))?;
1085        let proof = block
1086            .transaction_proof(&vid, &idx)
1087            .ok_or_else(|| anyhow::anyhow!("failed to build transaction proof"))?;
1088        Ok(TransactionWithProofQueryData::new(tx_data, proof))
1089    }
1090
1091    async fn get_transaction_proof_by_hash(
1092        &self,
1093        hash: String,
1094    ) -> anyhow::Result<Self::TransactionWithProof> {
1095        let ds = &*self.data_source;
1096        let timeout = FETCH_TIMEOUT;
1097
1098        let tx_hash: hotshot_query_service::availability::TransactionHash<SeqTypes> = hash
1099            .parse()
1100            .map_err(|_| bad_request(format!("invalid transaction hash: {}", hash)))?;
1101        let bwt = ds
1102            .get_block_containing_transaction(tx_hash)
1103            .await
1104            .with_timeout(timeout)
1105            .await
1106            .ok_or_else(|| not_found("transaction not found"))?;
1107
1108        let vid = ds
1109            .get_vid_common(HsBlockId::Number(bwt.block.height() as usize))
1110            .await
1111            .with_timeout(timeout)
1112            .await
1113            .ok_or_else(|| {
1114                not_found(format!(
1115                    "VID common not found for block {}",
1116                    bwt.block.height()
1117                ))
1118            })?;
1119
1120        let proof = bwt
1121            .block
1122            .transaction_proof(&vid, &bwt.index)
1123            .ok_or_else(|| anyhow::anyhow!("failed to build transaction proof"))?;
1124        Ok(TransactionWithProofQueryData::new(bwt.transaction, proof))
1125    }
1126
1127    async fn get_block_summary(&self, height: usize) -> anyhow::Result<Self::BlockSummary> {
1128        let ds = &*self.data_source;
1129        let block = ds
1130            .get_block(HsBlockId::Number(height))
1131            .await
1132            .with_timeout(FETCH_TIMEOUT)
1133            .await
1134            .ok_or_else(|| not_found(format!("block {} not found", height)))?;
1135        Ok(BlockSummaryQueryData::from(block))
1136    }
1137
1138    async fn get_block_summary_range(
1139        &self,
1140        from: usize,
1141        until: usize,
1142    ) -> anyhow::Result<Vec<Self::BlockSummary>> {
1143        enforce_range(from, until, large_object_range_limit())?;
1144        let timeout = FETCH_TIMEOUT;
1145        let ds = &*self.data_source;
1146        let stream = ds.get_block_range(from..until).await;
1147        let mut results = Vec::new();
1148        futures::pin_mut!(stream);
1149        let mut i = from;
1150        while let Some(fetch) = stream.next().await {
1151            let block = fetch
1152                .with_timeout(timeout)
1153                .await
1154                .ok_or_else(|| not_found(format!("block {} not found", i)))?;
1155            results.push(BlockSummaryQueryData::from(block));
1156            i += 1;
1157        }
1158        Ok(results)
1159    }
1160
1161    async fn get_limits(&self) -> anyhow::Result<Self::Limits> {
1162        Ok(HsLimits {
1163            small_object_range_limit: small_object_range_limit(),
1164            large_object_range_limit: large_object_range_limit(),
1165        })
1166    }
1167
1168    async fn get_cert2(&self, height: u64) -> anyhow::Result<Option<Self::Cert2>> {
1169        Ok(self
1170            .data_source
1171            .get_cert2(height)
1172            .await
1173            .with_timeout(FETCH_TIMEOUT)
1174            .await)
1175    }
1176
1177    async fn stream_leaves(&self, from: usize) -> anyhow::Result<BoxStream<'static, Self::Leaf>> {
1178        let ds = self.data_source.clone();
1179        Ok((*ds).subscribe_leaves(from).await.boxed())
1180    }
1181
1182    async fn stream_headers(
1183        &self,
1184        from: usize,
1185    ) -> anyhow::Result<BoxStream<'static, Self::Header>> {
1186        let ds = self.data_source.clone();
1187        Ok((*ds).subscribe_headers(from).await.boxed())
1188    }
1189
1190    async fn stream_blocks(&self, from: usize) -> anyhow::Result<BoxStream<'static, Self::Block>> {
1191        let ds = self.data_source.clone();
1192        Ok((*ds).subscribe_blocks(from).await.boxed())
1193    }
1194
1195    async fn stream_payloads(
1196        &self,
1197        from: usize,
1198    ) -> anyhow::Result<BoxStream<'static, Self::Payload>> {
1199        let ds = self.data_source.clone();
1200        Ok((*ds).subscribe_payloads(from).await.boxed())
1201    }
1202
1203    async fn stream_vid_common(
1204        &self,
1205        from: usize,
1206    ) -> anyhow::Result<BoxStream<'static, Self::VidCommon>> {
1207        let ds = self.data_source.clone();
1208        Ok((*ds).subscribe_vid_common(from).await.boxed())
1209    }
1210
1211    async fn stream_transactions(
1212        &self,
1213        from: usize,
1214        namespace: Option<u32>,
1215    ) -> anyhow::Result<BoxStream<'static, Self::Transaction>> {
1216        let ds = self.data_source.clone();
1217        let stream = (*ds)
1218            .subscribe_blocks(from)
1219            .await
1220            .flat_map(move |block| {
1221                let ns_filter = namespace.map(NamespaceId::from);
1222                let txs: Vec<Self::Transaction> = block
1223                    .enumerate()
1224                    .enumerate()
1225                    .filter_map(|(position_in_block, (tx_index, _tx))| {
1226                        let tx = block.transaction(&tx_index)?;
1227                        if let Some(ns) = ns_filter
1228                            && tx.namespace() != ns
1229                        {
1230                            return None;
1231                        }
1232                        TransactionQueryData::new(tx, &block, &tx_index, position_in_block as u64)
1233                    })
1234                    .collect();
1235                futures::stream::iter(txs)
1236            })
1237            .boxed();
1238        Ok(stream)
1239    }
1240}
1241
1242fn block_id_to_hs(id: v1::availability::BlockId) -> anyhow::Result<HsBlockId<SeqTypes>> {
1243    match id {
1244        v1::availability::BlockId::Height(h) => Ok(HsBlockId::Number(h as usize)),
1245        v1::availability::BlockId::Hash(h) => {
1246            let hash = h
1247                .parse()
1248                .map_err(|_| bad_request(format!("invalid block hash: {}", h)))?;
1249            Ok(HsBlockId::Hash(hash))
1250        },
1251        v1::availability::BlockId::PayloadHash(h) => {
1252            let payload_hash = h
1253                .parse()
1254                .map_err(|_| bad_request(format!("invalid payload hash: {}", h)))?;
1255            Ok(HsBlockId::PayloadHash(payload_hash))
1256        },
1257    }
1258}
1259
1260fn payload_id_to_hs(id: v1::availability::PayloadId) -> anyhow::Result<HsBlockId<SeqTypes>> {
1261    match id {
1262        v1::availability::PayloadId::Height(h) => Ok(HsBlockId::Number(h as usize)),
1263        v1::availability::PayloadId::Hash(h) => {
1264            let payload_hash = h
1265                .parse()
1266                .map_err(|_| bad_request(format!("invalid payload hash: {}", h)))?;
1267            Ok(HsBlockId::PayloadHash(payload_hash))
1268        },
1269        v1::availability::PayloadId::BlockHash(h) => {
1270            let hash = h
1271                .parse()
1272                .map_err(|_| bad_request(format!("invalid block hash: {}", h)))?;
1273            Ok(HsBlockId::Hash(hash))
1274        },
1275    }
1276}
1277
1278fn classify_query_error(err: hotshot_query_service::QueryError) -> anyhow::Error {
1279    match err {
1280        QueryError::NotFound | QueryError::Missing => not_found(err.to_string()),
1281        QueryError::Error { .. } => anyhow::anyhow!(err.to_string()),
1282    }
1283}
1284
1285#[async_trait]
1286impl<D> v1::BlockStateApi for NodeApiStateImpl<D>
1287where
1288    D: Deref + Clone + Send + Sync + 'static,
1289    D::Target: hotshot_query_service::merklized_state::MerklizedStateDataSource<
1290            SeqTypes,
1291            espresso_types::BlockMerkleTree,
1292            { <espresso_types::BlockMerkleTree as jf_merkle_tree_compat::MerkleTreeScheme>::ARITY },
1293        > + hotshot_query_service::merklized_state::MerklizedStateHeightPersistence
1294        + Send
1295        + Sync,
1296{
1297    type MerkleProof = InternalMerkleProof<
1298        committable::Commitment<espresso_types::Header>,
1299        u64,
1300        jf_merkle_tree_compat::prelude::Sha3Node,
1301        3,
1302    >;
1303
1304    async fn get_block_state_path(
1305        &self,
1306        snapshot: v1::Snapshot,
1307        key: String,
1308    ) -> anyhow::Result<Self::MerkleProof> {
1309        let hs_snapshot = match snapshot {
1310            v1::Snapshot::Height(h) => HsSnapshot::Index(h),
1311            v1::Snapshot::Commit(c) => {
1312                let tb64: TaggedBase64 = c
1313                    .parse()
1314                    .map_err(|_| bad_request("failed to parse commit param"))?;
1315                let commit = (&tb64)
1316                    .try_into()
1317                    .map_err(|_| bad_request("failed to parse commit param"))?;
1318                HsSnapshot::Commit(commit)
1319            },
1320        };
1321        let key: u64 = key
1322            .parse()
1323            .map_err(|_| bad_request("failed to parse Key param"))?;
1324        let ds = &*self.data_source;
1325        MerklizedStateDataSource::<SeqTypes, espresso_types::BlockMerkleTree, _>::get_path(
1326            ds,
1327            hs_snapshot,
1328            key,
1329        )
1330        .await
1331        .map_err(classify_query_error)
1332    }
1333
1334    async fn get_block_state_height(&self) -> anyhow::Result<u64> {
1335        let ds = &*self.data_source;
1336        ds.get_last_state_height()
1337            .await
1338            .map(|h| h as u64)
1339            .map_err(classify_query_error)
1340    }
1341}
1342
1343#[async_trait]
1344impl<D> v1::FeeStateApi for NodeApiStateImpl<D>
1345where
1346    D: Deref + Clone + Send + Sync + 'static,
1347    D::Target: hotshot_query_service::merklized_state::MerklizedStateDataSource<
1348            SeqTypes,
1349            espresso_types::FeeMerkleTree,
1350            { <espresso_types::FeeMerkleTree as jf_merkle_tree_compat::MerkleTreeScheme>::ARITY },
1351        > + hotshot_query_service::merklized_state::MerklizedStateHeightPersistence
1352        + Send
1353        + Sync,
1354{
1355    type MerkleProof = InternalMerkleProof<
1356        espresso_types::FeeAmount,
1357        espresso_types::FeeAccount,
1358        jf_merkle_tree_compat::prelude::Sha3Node,
1359        256,
1360    >;
1361    type FeeAmount = espresso_types::FeeAmount;
1362
1363    async fn get_fee_state_path(
1364        &self,
1365        snapshot: v1::Snapshot,
1366        key: String,
1367    ) -> anyhow::Result<Self::MerkleProof> {
1368        let hs_snapshot = match snapshot {
1369            v1::Snapshot::Height(h) => HsSnapshot::Index(h),
1370            v1::Snapshot::Commit(c) => {
1371                let tb64: TaggedBase64 = c
1372                    .parse()
1373                    .map_err(|_| bad_request("failed to parse commit param"))?;
1374                let commit = (&tb64)
1375                    .try_into()
1376                    .map_err(|_| bad_request("failed to parse commit param"))?;
1377                HsSnapshot::Commit(commit)
1378            },
1379        };
1380        let key: espresso_types::FeeAccount = key
1381            .parse()
1382            .map_err(|_| bad_request("failed to parse Key param"))?;
1383        let ds = &*self.data_source;
1384        MerklizedStateDataSource::<SeqTypes, espresso_types::FeeMerkleTree, _>::get_path(
1385            ds,
1386            hs_snapshot,
1387            key,
1388        )
1389        .await
1390        .map_err(classify_query_error)
1391    }
1392
1393    async fn get_fee_state_height(&self) -> anyhow::Result<u64> {
1394        let ds = &*self.data_source;
1395        ds.get_last_state_height()
1396            .await
1397            .map(|h| h as u64)
1398            .map_err(classify_query_error)
1399    }
1400
1401    async fn get_fee_balance_latest(
1402        &self,
1403        address: String,
1404    ) -> anyhow::Result<Option<Self::FeeAmount>> {
1405        let key: espresso_types::FeeAccount = address
1406            .parse()
1407            .map_err(|_| bad_request("failed to parse address"))?;
1408        let ds = &*self.data_source;
1409        let height = ds
1410            .get_last_state_height()
1411            .await
1412            .map_err(classify_query_error)?;
1413        let path: JfMerkleProof<
1414            espresso_types::FeeAmount,
1415            espresso_types::FeeAccount,
1416            jf_merkle_tree_compat::prelude::Sha3Node,
1417            256,
1418        > = MerklizedStateDataSource::<SeqTypes, espresso_types::FeeMerkleTree, _>::get_path(
1419            ds,
1420            HsSnapshot::Index(height as u64),
1421            key,
1422        )
1423        .await
1424        .map_err(classify_query_error)?;
1425        Ok(path.elem().copied())
1426    }
1427}
1428
1429#[async_trait]
1430impl<D> v1::StatusApi for NodeApiStateImpl<D>
1431where
1432    D: Deref + Clone + Send + Sync + 'static,
1433    D::Target: hotshot_query_service::status::StatusDataSource + NodeKeysDataSource + Send + Sync,
1434{
1435    type Keys = NodePublicKeys;
1436
1437    async fn block_height(&self) -> anyhow::Result<u64> {
1438        let ds = &*self.data_source;
1439        let h = hotshot_query_service::status::StatusDataSource::block_height(ds)
1440            .await
1441            .map_err(|e| anyhow::anyhow!("{e}"))?;
1442        Ok(h as u64)
1443    }
1444
1445    async fn success_rate(&self) -> anyhow::Result<f64> {
1446        let ds = &*self.data_source;
1447        hotshot_query_service::status::StatusDataSource::success_rate(ds)
1448            .await
1449            .map_err(|e| anyhow::anyhow!("{e}"))
1450    }
1451
1452    async fn time_since_last_decide(&self) -> anyhow::Result<u64> {
1453        let ds = &*self.data_source;
1454        hotshot_query_service::status::StatusDataSource::elapsed_time_since_last_decide(ds)
1455            .await
1456            .map_err(|e| anyhow::anyhow!("{e}"))
1457    }
1458
1459    async fn metrics(&self) -> anyhow::Result<String> {
1460        let ds = &*self.data_source;
1461        // Standard prometheus text exposition of the registry.
1462        let mut buffer = Vec::new();
1463        prometheus::TextEncoder::new().encode(&ds.metrics().registry().gather(), &mut buffer)?;
1464        Ok(String::from_utf8(buffer)?)
1465    }
1466
1467    async fn keys(&self) -> anyhow::Result<NodePublicKeys> {
1468        Ok(self.data_source.node_public_keys().await)
1469    }
1470}
1471
1472#[tonic::async_trait]
1473impl<D> proto::status_service_server::StatusService for NodeApiStateImpl<D>
1474where
1475    D: Deref + Clone + Send + Sync + 'static,
1476    D::Target: hotshot_query_service::status::StatusDataSource + NodeKeysDataSource + Send + Sync,
1477{
1478    async fn get_block_height(
1479        &self,
1480        _request: tonic::Request<proto::GetBlockHeightRequest>,
1481    ) -> Result<tonic::Response<proto::BlockHeightResponse>, tonic::Status> {
1482        let height = <Self as v1::StatusApi>::block_height(self)
1483            .await
1484            .map_err(to_status)?;
1485        Ok(tonic::Response::new(proto::BlockHeightResponse { height }))
1486    }
1487
1488    async fn get_success_rate(
1489        &self,
1490        _request: tonic::Request<proto::GetSuccessRateRequest>,
1491    ) -> Result<tonic::Response<proto::SuccessRateResponse>, tonic::Status> {
1492        let rate = <Self as v1::StatusApi>::success_rate(self)
1493            .await
1494            .map_err(to_status)?;
1495        // A fresh node computes 0/0 and a restarted one height/0 until its first view tick
1496        // (the view gauge is in-memory, the height persisted). protoJSON cannot encode a
1497        // non-finite double and the generated deserializer rejects `null`, so clamp to zero.
1498        let rate = if rate.is_finite() { rate } else { 0. };
1499        Ok(tonic::Response::new(proto::SuccessRateResponse { rate }))
1500    }
1501
1502    async fn get_time_since_last_decide(
1503        &self,
1504        _request: tonic::Request<proto::GetTimeSinceLastDecideRequest>,
1505    ) -> Result<tonic::Response<proto::TimeSinceLastDecideResponse>, tonic::Status> {
1506        let seconds = <Self as v1::StatusApi>::time_since_last_decide(self)
1507            .await
1508            .map_err(to_status)?;
1509        Ok(tonic::Response::new(proto::TimeSinceLastDecideResponse {
1510            seconds,
1511        }))
1512    }
1513
1514    async fn get_node_keys(
1515        &self,
1516        _request: tonic::Request<proto::GetNodeKeysRequest>,
1517    ) -> Result<tonic::Response<proto::NodeKeysResponse>, tonic::Status> {
1518        let keys = <Self as v1::StatusApi>::keys(self)
1519            .await
1520            .map_err(to_status)?;
1521        Ok(tonic::Response::new(proto::NodeKeysResponse {
1522            eth_account: keys.eth_account.map(|account| format!("{account:#x}")),
1523            consensus_key: Some(proto::BlsPublicKey {
1524                key: keys.consensus_key.to_string(),
1525            }),
1526            state_ver_key: Some(proto::SchnorrPublicKey {
1527                key: keys.state_ver_key.to_string(),
1528            }),
1529            x25519_key: keys.x25519_key.as_ref().map(ToString::to_string),
1530        }))
1531    }
1532}
1533
1534#[async_trait]
1535impl<D> v1::ConfigApi for NodeApiStateImpl<D>
1536where
1537    D: Deref + Clone + Send + Sync + 'static,
1538    D::Target: HotShotConfigDataSource + Send + Sync,
1539{
1540    type HotShotConfig = espresso_types::config::PublicNetworkConfig;
1541    type RuntimeConfig = crate::options::PublicNodeConfig;
1542
1543    async fn hotshot_config(&self) -> anyhow::Result<Self::HotShotConfig> {
1544        let ds = &*self.data_source;
1545        Ok(ds.get_config().await)
1546    }
1547
1548    async fn env(&self) -> anyhow::Result<Vec<String>> {
1549        Ok((*self.env_vars).clone())
1550    }
1551
1552    async fn runtime_config(&self) -> anyhow::Result<Self::RuntimeConfig> {
1553        self.public_node_config.as_deref().cloned().ok_or_else(|| {
1554            espresso_api::error::AvailabilityError::NotFound(
1555                "runtime config not available".to_string(),
1556            )
1557            .into()
1558        })
1559    }
1560}
1561
1562#[async_trait]
1563impl<D> v1::NodeApi for NodeApiStateImpl<D>
1564where
1565    D: Deref + Clone + Send + Sync + 'static,
1566    D::Target: hotshot_query_service::node::NodeDataSource<SeqTypes>
1567        + StakeTableDataSource<SeqTypes>
1568        + PruningDataSource
1569        + Send
1570        + Sync,
1571{
1572    type VidShare = hotshot_types::data::VidShare;
1573    type SyncStatus = hotshot_query_service::node::SyncStatusQueryData;
1574    type HeaderWindow =
1575        hotshot_query_service::node::TimeWindowQueryData<hotshot_query_service::Header<SeqTypes>>;
1576    type Limits = hotshot_query_service::node::Limits;
1577    type StakeTable = Vec<hotshot_types::PeerConfig<SeqTypes>>;
1578    type StakeTableCurrent = StakeTableWithEpochNumber<SeqTypes>;
1579    type Validators = indexmap::IndexMap<
1580        alloy::primitives::Address,
1581        espresso_types::v0_3::AuthenticatedValidator<espresso_types::PubKey>,
1582    >;
1583    type AllValidators = Vec<espresso_types::v0_3::RegisteredValidator<espresso_types::PubKey>>;
1584    type Participation = std::collections::HashMap<espresso_types::PubKey, f64>;
1585    type BlockReward = Option<espresso_types::v0_3::RewardAmount>;
1586    type Block = hotshot_query_service::availability::BlockQueryData<SeqTypes>;
1587    type Leaf = hotshot_query_service::availability::LeafQueryData<SeqTypes>;
1588
1589    async fn block_height(&self) -> anyhow::Result<u64> {
1590        let ds = &*self.data_source;
1591        let h = hotshot_query_service::node::NodeDataSource::block_height(ds)
1592            .await
1593            .map_err(classify_query_error)?;
1594        Ok(h as u64)
1595    }
1596
1597    async fn count_transactions(
1598        &self,
1599        from: Option<u64>,
1600        to: Option<u64>,
1601        namespace: Option<u64>,
1602    ) -> anyhow::Result<u64> {
1603        let ds = &*self.data_source;
1604        let from = match from {
1605            Some(f) => Bound::Included(f as usize),
1606            None => Bound::Unbounded,
1607        };
1608        let to = match to {
1609            Some(t) => Bound::Included(t as usize),
1610            None => Bound::Unbounded,
1611        };
1612        let ns = namespace.map(espresso_types::NamespaceId::from);
1613        let count = ds
1614            .count_transactions_in_range((from, to), ns)
1615            .await
1616            .map_err(classify_query_error)?;
1617        Ok(count as u64)
1618    }
1619
1620    async fn payload_size(
1621        &self,
1622        from: Option<u64>,
1623        to: Option<u64>,
1624        namespace: Option<u64>,
1625    ) -> anyhow::Result<u64> {
1626        let ds = &*self.data_source;
1627        let from = match from {
1628            Some(f) => Bound::Included(f as usize),
1629            None => Bound::Unbounded,
1630        };
1631        let to = match to {
1632            Some(t) => Bound::Included(t as usize),
1633            None => Bound::Unbounded,
1634        };
1635        let ns = namespace.map(espresso_types::NamespaceId::from);
1636        let size = ds
1637            .payload_size_in_range((from, to), ns)
1638            .await
1639            .map_err(classify_query_error)?;
1640        Ok(size as u64)
1641    }
1642
1643    async fn get_vid_share(&self, id: v1::VidShareId) -> anyhow::Result<Self::VidShare> {
1644        let ds = &*self.data_source;
1645        let node_id: HsBlockId<SeqTypes> = match id {
1646            v1::VidShareId::Height(h) => HsBlockId::Number(h as usize),
1647            v1::VidShareId::Hash(h) => HsBlockId::Hash(
1648                h.parse()
1649                    .map_err(|_| bad_request(format!("invalid block hash: {h}")))?,
1650            ),
1651            v1::VidShareId::PayloadHash(h) => HsBlockId::PayloadHash(
1652                h.parse()
1653                    .map_err(|_| bad_request(format!("invalid payload hash: {h}")))?,
1654            ),
1655        };
1656        hotshot_query_service::node::NodeDataSource::vid_share(ds, node_id)
1657            .await
1658            .map_err(classify_query_error)
1659    }
1660
1661    async fn sync_status(&self) -> anyhow::Result<Self::SyncStatus> {
1662        let ds = &*self.data_source;
1663        hotshot_query_service::node::NodeDataSource::sync_status(ds)
1664            .await
1665            .map_err(classify_query_error)
1666    }
1667
1668    async fn get_header_window(
1669        &self,
1670        start: v1::HeaderWindowStart,
1671        end: u64,
1672    ) -> anyhow::Result<Self::HeaderWindow> {
1673        let ds = &*self.data_source;
1674        let start: WindowStart<SeqTypes> = match start {
1675            v1::HeaderWindowStart::Time(t) => WindowStart::Time(t),
1676            v1::HeaderWindowStart::Height(h) => WindowStart::Height(h),
1677            v1::HeaderWindowStart::Hash(h) => WindowStart::Hash(
1678                h.parse()
1679                    .map_err(|err| bad_request(format!("invalid block hash {h}: {err}")))?,
1680            ),
1681        };
1682        ds.get_header_window(start, end, node_window_limit())
1683            .await
1684            .map_err(classify_query_error)
1685    }
1686
1687    async fn limits(&self) -> anyhow::Result<Self::Limits> {
1688        Ok(hotshot_query_service::node::Limits {
1689            window_limit: node_window_limit(),
1690        })
1691    }
1692
1693    async fn stake_table(&self, epoch: u64) -> anyhow::Result<Self::StakeTable> {
1694        let ds = &*self.data_source;
1695        ds.get_stake_table(Some(hotshot_types::data::EpochNumber::new(epoch)))
1696            .await
1697    }
1698
1699    async fn stake_table_current(&self) -> anyhow::Result<Self::StakeTableCurrent> {
1700        let ds = &*self.data_source;
1701        ds.get_stake_table_current().await
1702    }
1703
1704    async fn da_stake_table(&self, epoch: u64) -> anyhow::Result<Self::StakeTable> {
1705        let ds = &*self.data_source;
1706        ds.get_da_stake_table(Some(hotshot_types::data::EpochNumber::new(epoch)))
1707            .await
1708    }
1709
1710    async fn da_stake_table_current(&self) -> anyhow::Result<Self::StakeTableCurrent> {
1711        let ds = &*self.data_source;
1712        ds.get_da_stake_table_current().await
1713    }
1714
1715    async fn get_validators(&self, epoch: u64) -> anyhow::Result<Self::Validators> {
1716        let ds = &*self.data_source;
1717        ds.get_validators(hotshot_types::data::EpochNumber::new(epoch))
1718            .await
1719    }
1720
1721    async fn get_all_validators(
1722        &self,
1723        epoch: u64,
1724        offset: u64,
1725        limit: u64,
1726    ) -> anyhow::Result<Self::AllValidators> {
1727        if limit > 1000 {
1728            return Err(anyhow::anyhow!("Limit cannot be greater than 1000"));
1729        }
1730        let ds = &*self.data_source;
1731        ds.get_all_validators(hotshot_types::data::EpochNumber::new(epoch), offset, limit)
1732            .await
1733    }
1734
1735    async fn current_proposal_participation(&self) -> anyhow::Result<Self::Participation> {
1736        let ds = &*self.data_source;
1737        Ok(ds.current_proposal_participation().await)
1738    }
1739
1740    async fn proposal_participation(&self, epoch: u64) -> anyhow::Result<Self::Participation> {
1741        let ds = &*self.data_source;
1742        Ok(ds
1743            .proposal_participation(hotshot_types::data::EpochNumber::new(epoch))
1744            .await)
1745    }
1746
1747    async fn current_vote_participation(&self) -> anyhow::Result<Self::Participation> {
1748        let ds = &*self.data_source;
1749        Ok(ds.current_vote_participation().await)
1750    }
1751
1752    async fn vote_participation(&self, epoch: u64) -> anyhow::Result<Self::Participation> {
1753        let ds = &*self.data_source;
1754        Ok(ds
1755            .vote_participation(hotshot_types::data::EpochNumber::new(epoch))
1756            .await)
1757    }
1758
1759    async fn get_block_reward(&self, epoch: Option<u64>) -> anyhow::Result<Self::BlockReward> {
1760        let ds = &*self.data_source;
1761        ds.get_block_reward(epoch.map(hotshot_types::data::EpochNumber::new))
1762            .await
1763    }
1764
1765    async fn get_oldest_block(&self) -> anyhow::Result<Option<Self::Block>> {
1766        let ds = &*self.data_source;
1767        ds.get_oldest_block().await
1768    }
1769
1770    async fn get_oldest_leaf(&self) -> anyhow::Result<Option<Self::Leaf>> {
1771        let ds = &*self.data_source;
1772        ds.get_oldest_leaf().await
1773    }
1774}
1775
1776fn node_window_limit() -> usize {
1777    hotshot_query_service::node::Options::default().window_limit
1778}
1779
1780#[async_trait]
1781impl<D> v1::CatchupApi for NodeApiStateImpl<D>
1782where
1783    D: Deref + Clone + Send + Sync + 'static,
1784    D::Target: CatchupDataSource + NodeStateDataSource + Send + Sync,
1785{
1786    type FeeAccount = espresso_types::FeeAccount;
1787    type RewardAccountV1 = espresso_types::v0_3::RewardAccountV1;
1788    type RewardAccountV2 = espresso_types::v0_4::RewardAccountV2;
1789
1790    type AccountQueryData = espresso_types::AccountQueryData;
1791    type FeeMerkleTree = espresso_types::FeeMerkleTree;
1792    type BlocksFrontier = super::BlocksFrontier;
1793    type ChainConfig = espresso_types::v0_3::ChainConfig;
1794    type LeafChain = Vec<espresso_types::Leaf2>;
1795    type Cert2 = espresso_types::Certificate2<SeqTypes>;
1796    type RewardAccountQueryDataV1 = espresso_types::v0_3::RewardAccountQueryDataV1;
1797    type RewardMerkleTreeV1 = espresso_types::v0_3::RewardMerkleTreeV1;
1798    type RewardAccountQueryDataV2 = espresso_types::v0_4::RewardAccountQueryDataV2;
1799    type RewardMerkleTreeV2Data = serde_json::Value;
1800    type StateCert =
1801        hotshot_types::simple_certificate::LightClientStateUpdateCertificateV2<SeqTypes>;
1802
1803    async fn get_account(
1804        &self,
1805        height: u64,
1806        view: u64,
1807        address: String,
1808    ) -> anyhow::Result<Self::AccountQueryData> {
1809        let ds = &*self.data_source;
1810        let view = hotshot_types::data::ViewNumber::new(view);
1811        let account: espresso_types::FeeAccount = address
1812            .parse()
1813            .map_err(|err| bad_request(format!("malformed fee account {address}: {err}")))?;
1814        let instance = ds.node_state().await;
1815        ds.get_account(&instance, height, view, account)
1816            .await
1817            .map_err(|err| not_found(format!("{err:#}")))
1818    }
1819
1820    async fn get_accounts(
1821        &self,
1822        height: u64,
1823        view: u64,
1824        accounts: Vec<Self::FeeAccount>,
1825    ) -> anyhow::Result<Self::FeeMerkleTree> {
1826        let ds = &*self.data_source;
1827        let view = hotshot_types::data::ViewNumber::new(view);
1828        let instance = ds.node_state().await;
1829        ds.get_accounts(&instance, height, view, &accounts)
1830            .await
1831            .map_err(|err| not_found(format!("{err:#}")))
1832    }
1833
1834    async fn get_blocks_frontier(
1835        &self,
1836        height: u64,
1837        view: u64,
1838    ) -> anyhow::Result<Self::BlocksFrontier> {
1839        let ds = &*self.data_source;
1840        let view = hotshot_types::data::ViewNumber::new(view);
1841        let instance = ds.node_state().await;
1842        ds.get_frontier(&instance, height, view)
1843            .await
1844            .map_err(|err| not_found(format!("{err:#}")))
1845    }
1846
1847    async fn get_chain_config(&self, commitment: String) -> anyhow::Result<Self::ChainConfig> {
1848        let ds = &*self.data_source;
1849        let parsed: committable::Commitment<espresso_types::v0_3::ChainConfig> = commitment
1850            .parse()
1851            .map_err(|err| bad_request(format!("malformed chain config commitment: {err}")))?;
1852        ds.get_chain_config(parsed)
1853            .await
1854            .map_err(|err| not_found(format!("{err:#}")))
1855    }
1856
1857    async fn get_leaf_chain(&self, height: u64) -> anyhow::Result<Self::LeafChain> {
1858        let ds = &*self.data_source;
1859        ds.get_leaf_chain(height)
1860            .await
1861            .map_err(|err| not_found(format!("{err:#}")))
1862    }
1863
1864    async fn get_cert2(&self, height: u64) -> anyhow::Result<Self::Cert2> {
1865        let ds = &*self.data_source;
1866        let response = ds
1867            .get_cert2(height)
1868            .await
1869            .map_err(|err| not_found(format!("{err:#}")))?;
1870        response.ok_or_else(|| not_found(format!("no cert2 available for height {height}")))
1871    }
1872
1873    async fn get_reward_account_v1(
1874        &self,
1875        height: u64,
1876        view: u64,
1877        address: String,
1878    ) -> anyhow::Result<Self::RewardAccountQueryDataV1> {
1879        let ds = &*self.data_source;
1880        let view = hotshot_types::data::ViewNumber::new(view);
1881        let account: espresso_types::v0_4::RewardAccountV2 = address
1882            .parse()
1883            .map_err(|err| bad_request(format!("malformed reward account {address}: {err}")))?;
1884        let instance = ds.node_state().await;
1885        ds.get_reward_account_v1(&instance, height, view, account.into())
1886            .await
1887            .map_err(|err| not_found(format!("{err:#}")))
1888    }
1889
1890    async fn get_reward_accounts_v1(
1891        &self,
1892        height: u64,
1893        view: u64,
1894        accounts: Vec<Self::RewardAccountV1>,
1895    ) -> anyhow::Result<Self::RewardMerkleTreeV1> {
1896        let ds = &*self.data_source;
1897        let view = hotshot_types::data::ViewNumber::new(view);
1898        let instance = ds.node_state().await;
1899        ds.get_reward_accounts_v1(&instance, height, view, &accounts)
1900            .await
1901            .map_err(|err| not_found(format!("{err:#}")))
1902    }
1903
1904    async fn get_reward_account_v2(
1905        &self,
1906        height: u64,
1907        view: u64,
1908        address: String,
1909    ) -> anyhow::Result<Self::RewardAccountQueryDataV2> {
1910        let ds = &*self.data_source;
1911        let view = hotshot_types::data::ViewNumber::new(view);
1912        let account: espresso_types::v0_4::RewardAccountV2 = address
1913            .parse()
1914            .map_err(|err| bad_request(format!("malformed reward account {address}: {err}")))?;
1915        let instance = ds.node_state().await;
1916        ds.get_reward_account_v2(&instance, height, view, account)
1917            .await
1918            .map_err(|err| not_found(format!("{err:#}")))
1919    }
1920
1921    async fn get_reward_merkle_tree_v2(
1922        &self,
1923        height: u64,
1924        view: u64,
1925    ) -> anyhow::Result<Self::RewardMerkleTreeV2Data> {
1926        let ds = &*self.data_source;
1927        let view = hotshot_types::data::ViewNumber::new(view);
1928        let bytes = ds
1929            .get_reward_merkle_tree_v2(height, view)
1930            .await
1931            .map_err(|err| not_found(format!("{err:#}")))?;
1932        // The wire format is the raw Vec<u8> from `get_reward_merkle_tree_v2` encoded as the
1933        // JSON body; keep it that way for existing clients.
1934        Ok(serde_json::to_value(bytes)?)
1935    }
1936
1937    async fn get_state_cert(&self, epoch: u64) -> anyhow::Result<Self::StateCert> {
1938        let ds = &*self.data_source;
1939        ds.get_state_cert(epoch)
1940            .await
1941            .map_err(|err| not_found(format!("{err:#}")))
1942    }
1943}
1944
1945#[async_trait]
1946impl<D> v1::SubmitApi for NodeApiStateImpl<D>
1947where
1948    D: Deref + Clone + Send + Sync + 'static,
1949    D::Target: SubmitDataSourceErased + Send + Sync,
1950{
1951    type Transaction = espresso_types::Transaction;
1952    type TxHash = committable::Commitment<espresso_types::Transaction>;
1953
1954    async fn submit(&self, tx: Self::Transaction) -> anyhow::Result<Self::TxHash> {
1955        let hash = tx.commit();
1956        let ds = &*self.data_source;
1957        ds.submit_erased(tx)
1958            .await
1959            .map_err(|err| anyhow::anyhow!("{err:#}"))?;
1960        Ok(hash)
1961    }
1962}
1963
1964/// Network-agnostic submit hook used by the axum wrapper. The original
1965/// `SubmitDataSource<N, P>` trait is parameterized by the network type; this
1966/// erased trait lets `NodeApiStateImpl` avoid carrying those parameters.
1967#[async_trait]
1968pub(crate) trait SubmitDataSourceErased {
1969    async fn submit_erased(&self, tx: espresso_types::Transaction) -> anyhow::Result<()>;
1970}
1971
1972#[async_trait]
1973impl<N, P, D> SubmitDataSourceErased
1974    for hotshot_query_service::data_source::ExtensibleDataSource<D, crate::api::ApiState<N, P>>
1975where
1976    N: hotshot_types::traits::network::ConnectedNetwork<espresso_types::PubKey>,
1977    P: espresso_types::v0::traits::SequencerPersistence,
1978    D: Send + Sync,
1979{
1980    async fn submit_erased(&self, tx: espresso_types::Transaction) -> anyhow::Result<()> {
1981        <Self as SubmitDataSource<N, P>>::submit(self, tx).await
1982    }
1983}
1984
1985// Bare mode (no query/status API) has no `ExtensibleDataSource` wrapper: the app state is
1986// `ApiState<N, P>` directly, so it needs its own erased forwarding impl.
1987#[async_trait]
1988impl<N, P> SubmitDataSourceErased for crate::api::ApiState<N, P>
1989where
1990    N: hotshot_types::traits::network::ConnectedNetwork<espresso_types::PubKey>,
1991    P: espresso_types::v0::traits::SequencerPersistence,
1992{
1993    async fn submit_erased(&self, tx: espresso_types::Transaction) -> anyhow::Result<()> {
1994        <Self as SubmitDataSource<N, P>>::submit(self, tx).await
1995    }
1996}
1997
1998#[async_trait]
1999impl<D> v1::StateSignatureApi for NodeApiStateImpl<D>
2000where
2001    D: Deref + Clone + Send + Sync + 'static,
2002    D::Target: StateSignatureDataSourceErased + Send + Sync,
2003{
2004    type Signature = hotshot_types::light_client::LCV3StateSignatureRequestBody;
2005
2006    async fn get_state_signature(&self, height: u64) -> anyhow::Result<Self::Signature> {
2007        let ds = &*self.data_source;
2008        ds.get_state_signature_erased(height)
2009            .await
2010            .ok_or_else(|| not_found("Signature not found."))
2011    }
2012}
2013
2014#[async_trait]
2015pub(crate) trait StateSignatureDataSourceErased {
2016    async fn get_state_signature_erased(
2017        &self,
2018        height: u64,
2019    ) -> Option<hotshot_types::light_client::LCV3StateSignatureRequestBody>;
2020}
2021
2022#[async_trait]
2023impl<N, P, D> StateSignatureDataSourceErased
2024    for hotshot_query_service::data_source::ExtensibleDataSource<D, crate::api::ApiState<N, P>>
2025where
2026    N: hotshot_types::traits::network::ConnectedNetwork<espresso_types::PubKey>,
2027    P: espresso_types::v0::traits::SequencerPersistence,
2028    D: Send + Sync,
2029{
2030    async fn get_state_signature_erased(
2031        &self,
2032        height: u64,
2033    ) -> Option<hotshot_types::light_client::LCV3StateSignatureRequestBody> {
2034        <Self as StateSignatureDataSource<N>>::get_state_signature(self, height).await
2035    }
2036}
2037
2038// Bare mode (no query/status API) has no `ExtensibleDataSource` wrapper: the app state is
2039// `ApiState<N, P>` directly, so it needs its own erased forwarding impl.
2040#[async_trait]
2041impl<N, P> StateSignatureDataSourceErased for crate::api::ApiState<N, P>
2042where
2043    N: hotshot_types::traits::network::ConnectedNetwork<espresso_types::PubKey>,
2044    P: espresso_types::v0::traits::SequencerPersistence,
2045{
2046    async fn get_state_signature_erased(
2047        &self,
2048        height: u64,
2049    ) -> Option<hotshot_types::light_client::LCV3StateSignatureRequestBody> {
2050        <Self as StateSignatureDataSource<N>>::get_state_signature(self, height).await
2051    }
2052}
2053
2054#[async_trait]
2055impl<D> v1::ExplorerApi for NodeApiStateImpl<D>
2056where
2057    D: Deref + Clone + Send + Sync + 'static,
2058    D::Target: hotshot_query_service::explorer::ExplorerDataSource<SeqTypes> + Send + Sync,
2059{
2060    type BlockDetail = hotshot_query_service::explorer::BlockDetailResponse<SeqTypes>;
2061    type BlockSummaries = hotshot_query_service::explorer::BlockSummaryResponse<SeqTypes>;
2062    type TransactionDetail = hotshot_query_service::explorer::TransactionDetailResponse<SeqTypes>;
2063    type TransactionSummaries =
2064        hotshot_query_service::explorer::TransactionSummariesResponse<SeqTypes>;
2065    type ExplorerSummary = hotshot_query_service::explorer::ExplorerSummaryResponse<SeqTypes>;
2066    type SearchResult = hotshot_query_service::explorer::SearchResultResponse<SeqTypes>;
2067
2068    async fn get_block_detail(&self, ident: v1::BlockIdent) -> anyhow::Result<Self::BlockDetail> {
2069        let ds = &*self.data_source;
2070        let target = match ident {
2071            v1::BlockIdent::Height(h) => BlockIdentifier::Height(h as usize),
2072            v1::BlockIdent::Hash(h) => BlockIdentifier::Hash(
2073                h.parse()
2074                    .map_err(|err| bad_request(format!("invalid block hash {h}: {err}")))?,
2075            ),
2076            v1::BlockIdent::Latest => BlockIdentifier::Latest,
2077        };
2078        ds.get_block_detail(target)
2079            .await
2080            .map(Into::into)
2081            .map_err(|err| anyhow::anyhow!("{err}"))
2082    }
2083
2084    async fn get_block_summaries(
2085        &self,
2086        target: v1::BlockIdent,
2087        limit: u64,
2088    ) -> anyhow::Result<Self::BlockSummaries> {
2089        let ds = &*self.data_source;
2090        let num_blocks = std::num::NonZeroUsize::new(limit as usize)
2091            .ok_or_else(|| bad_request("limit must be greater than 0"))?;
2092        if num_blocks.get() > 100 {
2093            return Err(bad_request("limit must be <= 100"));
2094        }
2095        let target = match target {
2096            v1::BlockIdent::Height(h) => BlockIdentifier::Height(h as usize),
2097            v1::BlockIdent::Hash(h) => BlockIdentifier::Hash(
2098                h.parse()
2099                    .map_err(|err| bad_request(format!("invalid block hash {h}: {err}")))?,
2100            ),
2101            v1::BlockIdent::Latest => BlockIdentifier::Latest,
2102        };
2103        ds.get_block_summaries(GetBlockSummariesRequest(BlockRange { target, num_blocks }))
2104            .await
2105            .map(Into::into)
2106            .map_err(|err| anyhow::anyhow!("{err}"))
2107    }
2108
2109    async fn get_transaction_detail(
2110        &self,
2111        ident: v1::TxIdent,
2112    ) -> anyhow::Result<Self::TransactionDetail> {
2113        let ds = &*self.data_source;
2114        let target = match ident {
2115            v1::TxIdent::HeightAndOffset(h, o) => {
2116                TransactionIdentifier::HeightAndOffset(h as usize, o as usize)
2117            },
2118            v1::TxIdent::Hash(h) => TransactionIdentifier::Hash(
2119                h.parse()
2120                    .map_err(|err| bad_request(format!("invalid tx hash {h}: {err}")))?,
2121            ),
2122            v1::TxIdent::Latest => TransactionIdentifier::Latest,
2123        };
2124        ds.get_transaction_detail(target)
2125            .await
2126            .map(Into::into)
2127            .map_err(|err| anyhow::anyhow!("{err}"))
2128    }
2129
2130    async fn get_transaction_summaries(
2131        &self,
2132        target: v1::TxIdent,
2133        limit: u64,
2134        filter: v1::TxSummaryFilter,
2135    ) -> anyhow::Result<Self::TransactionSummaries> {
2136        let ds = &*self.data_source;
2137        let num_transactions = std::num::NonZeroUsize::new(limit as usize)
2138            .ok_or_else(|| bad_request("limit must be greater than 0"))?;
2139        if num_transactions.get() > 100 {
2140            return Err(bad_request("limit must be <= 100"));
2141        }
2142        let target = match target {
2143            v1::TxIdent::HeightAndOffset(h, o) => {
2144                TransactionIdentifier::HeightAndOffset(h as usize, o as usize)
2145            },
2146            v1::TxIdent::Hash(h) => TransactionIdentifier::Hash(
2147                h.parse()
2148                    .map_err(|err| bad_request(format!("invalid tx hash {h}: {err}")))?,
2149            ),
2150            v1::TxIdent::Latest => TransactionIdentifier::Latest,
2151        };
2152        let filter = match filter {
2153            v1::TxSummaryFilter::None => TransactionSummaryFilter::None,
2154            v1::TxSummaryFilter::Block(b) => TransactionSummaryFilter::Block(b as usize),
2155            v1::TxSummaryFilter::Namespace(n) => TransactionSummaryFilter::RollUp(n.into()),
2156        };
2157        ds.get_transaction_summaries(GetTransactionSummariesRequest {
2158            range: TransactionRange {
2159                target,
2160                num_transactions,
2161            },
2162            filter,
2163        })
2164        .await
2165        .map(Into::into)
2166        .map_err(|err| anyhow::anyhow!("{err}"))
2167    }
2168
2169    async fn get_explorer_summary(&self) -> anyhow::Result<Self::ExplorerSummary> {
2170        let ds = &*self.data_source;
2171        ds.get_explorer_summary()
2172            .await
2173            .map(Into::into)
2174            .map_err(|err| anyhow::anyhow!("{err}"))
2175    }
2176
2177    async fn get_search_result(&self, query: String) -> anyhow::Result<Self::SearchResult> {
2178        let ds = &*self.data_source;
2179        let parsed: tagged_base64::TaggedBase64 = query
2180            .parse()
2181            .map_err(|err| bad_request(format!("invalid search query {query}: {err}")))?;
2182        ds.get_search_results(parsed)
2183            .await
2184            .map(Into::into)
2185            .map_err(|err| anyhow::anyhow!("{err}"))
2186    }
2187}
2188
2189#[async_trait]
2190impl<D> v1::LightClientApi for NodeApiStateImpl<D>
2191where
2192    D: Deref + Clone + Send + Sync + 'static,
2193    D::Target: AvailabilityDataSource<SeqTypes>
2194        + hotshot_query_service::merklized_state::MerklizedStateDataSource<
2195            SeqTypes,
2196            espresso_types::BlockMerkleTree,
2197            3,
2198        > + NodeStateDataSource
2199        + StakeTableDataSource<SeqTypes>
2200        + hotshot_query_service::data_source::VersionedDataSource
2201        + Sized
2202        + Send
2203        + Sync,
2204    for<'a> <D::Target as hotshot_query_service::data_source::VersionedDataSource>::ReadOnly<'a>:
2205        hotshot_query_service::data_source::storage::NodeStorage<SeqTypes>,
2206{
2207    type LeafProof = light_client::consensus::leaf::LeafProof;
2208    type HeaderProof = light_client::consensus::header::HeaderProof;
2209    type StakeTableEvents = Vec<espresso_types::v0_3::StakeTableEvent>;
2210    type PayloadProof = light_client::consensus::payload::PayloadProof;
2211    type NamespaceProof = light_client::consensus::namespace::NamespaceProof;
2212
2213    async fn get_leaf_proof(
2214        &self,
2215        query: v1::LeafQuery,
2216        finalized: Option<u64>,
2217    ) -> anyhow::Result<Self::LeafProof> {
2218        let ds = &*self.data_source;
2219        let fetch_timeout = FETCH_TIMEOUT;
2220
2221        let requested = match query {
2222            v1::LeafQuery::Height(h) => HsLeafId::Number(h as usize),
2223            v1::LeafQuery::Hash(h) => HsLeafId::Hash(
2224                h.parse()
2225                    .map_err(|err| bad_request(format!("invalid leaf hash {h}: {err}")))?,
2226            ),
2227            v1::LeafQuery::BlockHash(h) => {
2228                let parsed = h
2229                    .parse()
2230                    .map_err(|err| bad_request(format!("invalid block hash {h}: {err}")))?;
2231                let header = AvailabilityDataSource::get_header(ds, HsBlockId::Hash(parsed))
2232                    .await
2233                    .with_timeout(fetch_timeout)
2234                    .await
2235                    .ok_or_else(|| not_found(format!("unknown block hash {h}")))?;
2236                HsLeafId::Number(header.height() as usize)
2237            },
2238            v1::LeafQuery::PayloadHash(h) => {
2239                let parsed = h
2240                    .parse()
2241                    .map_err(|err| bad_request(format!("invalid payload hash {h}: {err}")))?;
2242                let header = AvailabilityDataSource::get_header(ds, HsBlockId::PayloadHash(parsed))
2243                    .await
2244                    .with_timeout(fetch_timeout)
2245                    .await
2246                    .ok_or_else(|| not_found(format!("unknown payload hash {h}")))?;
2247                HsLeafId::Number(header.height() as usize)
2248            },
2249        };
2250
2251        let requested_leaf = AvailabilityDataSource::get_leaf(ds, requested)
2252            .await
2253            .with_timeout(fetch_timeout)
2254            .await
2255            .ok_or_else(|| not_found(format!("unknown leaf {requested}")))?;
2256
2257        crate::api::light_client::get_leaf_proof(
2258            ds,
2259            requested_leaf,
2260            finalized.map(|f| f as usize),
2261            fetch_timeout,
2262            lc_leaf_proof_chain_limit(),
2263        )
2264        .await
2265        .map_err(|err| anyhow::anyhow!("{err}"))
2266    }
2267
2268    async fn get_header_proof(
2269        &self,
2270        root: u64,
2271        requested: v1::HeaderQuery,
2272    ) -> anyhow::Result<Self::HeaderProof> {
2273        let ds = &*self.data_source;
2274        let fetch_timeout = FETCH_TIMEOUT;
2275        let requested = match requested {
2276            v1::HeaderQuery::Height(h) => HsBlockId::Number(h as usize),
2277            v1::HeaderQuery::Hash(h) => HsBlockId::Hash(
2278                h.parse()
2279                    .map_err(|err| bad_request(format!("invalid block hash {h}: {err}")))?,
2280            ),
2281            v1::HeaderQuery::PayloadHash(h) => HsBlockId::PayloadHash(
2282                h.parse()
2283                    .map_err(|err| bad_request(format!("invalid payload hash {h}: {err}")))?,
2284            ),
2285        };
2286        crate::api::light_client::get_header_proof(ds, root, requested, fetch_timeout)
2287            .await
2288            .map_err(|err| anyhow::anyhow!("{err}"))
2289    }
2290
2291    async fn get_light_client_stake_table(
2292        &self,
2293        epoch: u64,
2294    ) -> anyhow::Result<Self::StakeTableEvents> {
2295        let ds = &*self.data_source;
2296        let fetch_timeout = FETCH_TIMEOUT;
2297
2298        let node_state = NodeStateDataSource::node_state(ds).await;
2299        let epoch_height = node_state
2300            .epoch_height
2301            .ok_or_else(|| anyhow::anyhow!("epoch state not set"))?;
2302        let first_epoch = epoch_from_block_number(node_state.epoch_start_block, epoch_height);
2303        if epoch < first_epoch + 2 {
2304            return Err(bad_request(format!(
2305                "epoch must be at least {}",
2306                first_epoch + 2
2307            )));
2308        }
2309
2310        let epoch_root_height = root_block_in_epoch(epoch - 2, epoch_height) as usize;
2311        let epoch_root = AvailabilityDataSource::get_header::<HsBlockId<SeqTypes>>(
2312            ds,
2313            HsBlockId::Number(epoch_root_height),
2314        )
2315        .await
2316        .with_timeout(fetch_timeout)
2317        .await
2318        .ok_or_else(|| not_found(format!("missing epoch root header {epoch_root_height}")))?;
2319        let to_l1_block = epoch_root
2320            .l1_finalized()
2321            .ok_or_else(|| anyhow::anyhow!("epoch root header is missing L1 finalized block"))?
2322            .number();
2323
2324        let from_l1_block = if epoch >= first_epoch + 3 {
2325            let prev_epoch_root_height = root_block_in_epoch(epoch - 3, epoch_height) as usize;
2326            let prev_epoch_root = AvailabilityDataSource::get_header::<HsBlockId<SeqTypes>>(
2327                ds,
2328                HsBlockId::Number(prev_epoch_root_height),
2329            )
2330            .await
2331            .with_timeout(fetch_timeout)
2332            .await
2333            .ok_or_else(|| {
2334                not_found(format!(
2335                    "missing previous epoch root header {prev_epoch_root_height}"
2336                ))
2337            })?;
2338            prev_epoch_root
2339                .l1_finalized()
2340                .ok_or_else(|| {
2341                    anyhow::anyhow!("previous epoch root header is missing L1 finalized block")
2342                })?
2343                .number()
2344                + 1
2345        } else {
2346            0
2347        };
2348
2349        StakeTableDataSource::stake_table_events(ds, from_l1_block, to_l1_block).await
2350    }
2351
2352    async fn get_payload_proof(&self, height: u64) -> anyhow::Result<Self::PayloadProof> {
2353        let ds = &*self.data_source;
2354        let fetch_timeout = FETCH_TIMEOUT;
2355        let height = height as usize;
2356        let payload = AvailabilityDataSource::get_payload(ds, height)
2357            .await
2358            .with_timeout(fetch_timeout)
2359            .await
2360            .ok_or_else(|| not_found(format!("missing payload {height}")))?;
2361        let vid_common = AvailabilityDataSource::get_vid_common(ds, height)
2362            .await
2363            .with_timeout(fetch_timeout)
2364            .await
2365            .ok_or_else(|| not_found(format!("missing VID common {height}")))?;
2366        Ok(light_client::consensus::payload::PayloadProof::new(
2367            payload.data().clone(),
2368            vid_common.common().clone(),
2369        ))
2370    }
2371
2372    async fn get_payload_proof_range(
2373        &self,
2374        start: u64,
2375        end: u64,
2376    ) -> anyhow::Result<Vec<Self::PayloadProof>> {
2377        let ds = &*self.data_source;
2378        let fetch_timeout = FETCH_TIMEOUT;
2379        let start = start as usize;
2380        let end = end as usize;
2381
2382        let payloads_stream = AvailabilityDataSource::get_payload_range(ds, start..end).await;
2383        let vid_stream = AvailabilityDataSource::get_vid_common_range(ds, start..end).await;
2384        let mut out = Vec::new();
2385        let mut payloads = payloads_stream.enumerate();
2386        let mut vid_commons = vid_stream.enumerate();
2387        loop {
2388            let (next_payload, next_vid) =
2389                futures::future::join(payloads.next(), vid_commons.next()).await;
2390            let (Some((i, payload_fut)), Some((_, vid_fut))) = (next_payload, next_vid) else {
2391                break;
2392            };
2393            let payload = payload_fut
2394                .with_timeout(fetch_timeout)
2395                .await
2396                .ok_or_else(|| not_found(format!("missing payload {}", start + i)))?;
2397            let vid_common = vid_fut
2398                .with_timeout(fetch_timeout)
2399                .await
2400                .ok_or_else(|| not_found(format!("missing VID common {}", start + i)))?;
2401            out.push(light_client::consensus::payload::PayloadProof::new(
2402                payload.data().clone(),
2403                vid_common.common().clone(),
2404            ));
2405        }
2406        Ok(out)
2407    }
2408
2409    async fn get_lc_namespace_proof(
2410        &self,
2411        height: u64,
2412        namespace: u64,
2413    ) -> anyhow::Result<Self::NamespaceProof> {
2414        let ds = &*self.data_source;
2415        let fetch_timeout = FETCH_TIMEOUT;
2416        let mut proofs = crate::api::light_client::get_namespace_proof_range(
2417            ds,
2418            height as usize,
2419            (height + 1) as usize,
2420            namespace,
2421            fetch_timeout,
2422            lc_large_object_range_limit(),
2423        )
2424        .await
2425        .map_err(lc_error)?;
2426        if proofs.len() != 1 {
2427            return Err(anyhow::anyhow!("internal consistency error"));
2428        }
2429        Ok(proofs.remove(0))
2430    }
2431
2432    async fn get_lc_namespace_proof_range(
2433        &self,
2434        start: u64,
2435        end: u64,
2436        namespace: u64,
2437    ) -> anyhow::Result<Vec<Self::NamespaceProof>> {
2438        let ds = &*self.data_source;
2439        let fetch_timeout = FETCH_TIMEOUT;
2440        crate::api::light_client::get_namespace_proof_range(
2441            ds,
2442            start as usize,
2443            end as usize,
2444            namespace,
2445            fetch_timeout,
2446            lc_large_object_range_limit(),
2447        )
2448        .await
2449        .map_err(lc_error)
2450    }
2451
2452    async fn get_lc_namespaces_proof_range(
2453        &self,
2454        start: u64,
2455        end: u64,
2456        namespaces: String,
2457    ) -> anyhow::Result<Vec<std::collections::HashMap<u64, Self::NamespaceProof>>> {
2458        let namespaces = crate::api::light_client::parse_namespaces_str(&namespaces)
2459            .map_err(|err| bad_request(err.to_string()))?;
2460        let ds = &*self.data_source;
2461        let fetch_timeout = FETCH_TIMEOUT;
2462        crate::api::light_client::get_namespaces_proof_range(
2463            ds,
2464            start as usize,
2465            end as usize,
2466            &namespaces,
2467            fetch_timeout,
2468            lc_large_object_range_limit(),
2469        )
2470        .await
2471        .map_err(lc_error)
2472    }
2473}
2474
2475fn lc_large_object_range_limit() -> usize {
2476    hotshot_query_service::availability::Options::default().large_object_range_limit
2477}
2478
2479/// Convert a query-service error to an [`AvailabilityError`]-carrying anyhow error so the HTTP
2480/// layer returns the status carried by the error (400/404) instead of 500.
2481pub(crate) fn lc_error(err: hotshot_query_service::Error) -> anyhow::Error {
2482    match err.status() {
2483        StatusCode::NOT_FOUND => not_found(err.to_string()),
2484        StatusCode::BAD_REQUEST => bad_request(err.to_string()),
2485        _ => anyhow::anyhow!("{err}"),
2486    }
2487}
2488
2489/// Bounds the leaves in a single leaf proof, and so the memory to build and serialize it.
2490///
2491/// Tracks the `hotshot_query_service` small-object range limit, so a dependency bump that
2492/// changes that default changes this bound too.
2493fn lc_leaf_proof_chain_limit() -> usize {
2494    hotshot_query_service::availability::Options::default().small_object_range_limit
2495}
2496
2497#[async_trait]
2498impl<D> v1::HotShotEventsApi for NodeApiStateImpl<D>
2499where
2500    D: Deref + Clone + Send + Sync + 'static,
2501    D::Target: hotshot_events_service::events_source::EventsSource<SeqTypes> + Send + Sync,
2502{
2503    type Event = std::sync::Arc<hotshot_types::event::Event<SeqTypes>>;
2504    type StartupInfo = hotshot_events_service::events_source::StartupInfo<SeqTypes>;
2505
2506    async fn startup_info(&self) -> anyhow::Result<Self::StartupInfo> {
2507        let ds = &*self.data_source;
2508        Ok(ds.get_startup_info().await)
2509    }
2510
2511    async fn events(&self) -> anyhow::Result<futures::stream::BoxStream<'static, Self::Event>> {
2512        let ds = &*self.data_source;
2513        let stream = ds.get_event_stream(None).await;
2514        Ok(Box::pin(stream))
2515    }
2516}
2517
2518#[async_trait]
2519impl<D> v1::TokenApi for NodeApiStateImpl<D>
2520where
2521    D: Deref + Clone + Send + Sync + 'static,
2522    D::Target: TokenDataSource<SeqTypes> + NodeStateDataSource + Send + Sync,
2523{
2524    async fn total_minted_supply(&self) -> anyhow::Result<String> {
2525        let ds = &*self.data_source;
2526        let value = ds
2527            .get_total_supply_l1()
2528            .await
2529            .map_err(|err| anyhow::anyhow!("failed to get total supply: {err:#}"))?;
2530        Ok(format_ether(value))
2531    }
2532
2533    async fn circulating_supply(&self) -> anyhow::Result<String> {
2534        let calc = fetch_supply_inputs(&*self.data_source).await?;
2535        Ok(format_ether(calc.circulating_supply()))
2536    }
2537
2538    async fn circulating_supply_ethereum(&self) -> anyhow::Result<String> {
2539        let calc = fetch_supply_inputs(&*self.data_source).await?;
2540        Ok(format_ether(calc.circulating_supply_ethereum()))
2541    }
2542
2543    async fn total_issued_supply(&self) -> anyhow::Result<String> {
2544        let calc = fetch_supply_inputs(&*self.data_source).await?;
2545        Ok(format_ether(calc.total_issued_supply()))
2546    }
2547
2548    async fn total_reward_distributed(&self) -> anyhow::Result<String> {
2549        let calc = fetch_supply_inputs(&*self.data_source).await?;
2550        Ok(format_ether(calc.total_reward_distributed()))
2551    }
2552}
2553
2554async fn fetch_supply_inputs<S>(
2555    ds: &S,
2556) -> anyhow::Result<crate::api::unlock_schedule::SupplyCalculator>
2557where
2558    S: TokenDataSource<SeqTypes> + NodeStateDataSource + Sync + ?Sized,
2559{
2560    let node_state = ds.node_state().await;
2561    let chain_id = node_state.chain_config.chain_id;
2562
2563    let header = ds.get_decided_header().await;
2564    let now_secs = header.timestamp_internal();
2565    let total_reward_distributed = header.total_reward_distributed();
2566
2567    let initial_supply = ds
2568        .get_initial_supply_l1()
2569        .await
2570        .map_err(|err| anyhow::anyhow!("failed to get initial supply: {err:#}"))?;
2571
2572    let total_supply_l1 = ds
2573        .get_total_supply_l1()
2574        .await
2575        .map_err(|err| anyhow::anyhow!("failed to get total supply: {err:#}"))?;
2576
2577    Ok(crate::api::unlock_schedule::SupplyCalculator::new(
2578        chain_id,
2579        now_secs,
2580        initial_supply,
2581        total_supply_l1,
2582        total_reward_distributed,
2583    ))
2584}
2585
2586#[tonic::async_trait]
2587impl<D> proto::token_service_server::TokenService for NodeApiStateImpl<D>
2588where
2589    D: Deref + Clone + Send + Sync + 'static,
2590    D::Target: TokenDataSource<SeqTypes> + NodeStateDataSource + Send + Sync,
2591{
2592    async fn get_total_minted_supply(
2593        &self,
2594        _request: tonic::Request<proto::GetTotalMintedSupplyRequest>,
2595    ) -> Result<tonic::Response<proto::TotalMintedSupplyResponse>, tonic::Status> {
2596        let amount = <Self as v1::TokenApi>::total_minted_supply(self)
2597            .await
2598            .map_err(to_status)?;
2599        Ok(tonic::Response::new(proto::TotalMintedSupplyResponse {
2600            amount,
2601        }))
2602    }
2603
2604    async fn get_circulating_supply(
2605        &self,
2606        _request: tonic::Request<proto::GetCirculatingSupplyRequest>,
2607    ) -> Result<tonic::Response<proto::CirculatingSupplyResponse>, tonic::Status> {
2608        let amount = <Self as v1::TokenApi>::circulating_supply(self)
2609            .await
2610            .map_err(to_status)?;
2611        Ok(tonic::Response::new(proto::CirculatingSupplyResponse {
2612            amount,
2613        }))
2614    }
2615
2616    async fn get_circulating_supply_ethereum(
2617        &self,
2618        _request: tonic::Request<proto::GetCirculatingSupplyEthereumRequest>,
2619    ) -> Result<tonic::Response<proto::CirculatingSupplyEthereumResponse>, tonic::Status> {
2620        let amount = <Self as v1::TokenApi>::circulating_supply_ethereum(self)
2621            .await
2622            .map_err(to_status)?;
2623        Ok(tonic::Response::new(
2624            proto::CirculatingSupplyEthereumResponse { amount },
2625        ))
2626    }
2627
2628    async fn get_total_issued_supply(
2629        &self,
2630        _request: tonic::Request<proto::GetTotalIssuedSupplyRequest>,
2631    ) -> Result<tonic::Response<proto::TotalIssuedSupplyResponse>, tonic::Status> {
2632        let amount = <Self as v1::TokenApi>::total_issued_supply(self)
2633            .await
2634            .map_err(to_status)?;
2635        Ok(tonic::Response::new(proto::TotalIssuedSupplyResponse {
2636            amount,
2637        }))
2638    }
2639
2640    async fn get_total_reward_distributed(
2641        &self,
2642        _request: tonic::Request<proto::GetTotalRewardDistributedRequest>,
2643    ) -> Result<tonic::Response<proto::TotalRewardDistributedResponse>, tonic::Status> {
2644        let amount = <Self as v1::TokenApi>::total_reward_distributed(self)
2645            .await
2646            .map_err(to_status)?;
2647        Ok(tonic::Response::new(
2648            proto::TotalRewardDistributedResponse { amount },
2649        ))
2650    }
2651}
2652
2653#[async_trait]
2654impl<D> v1::DatabaseApi for NodeApiStateImpl<D>
2655where
2656    D: Deref + Clone + Send + Sync + 'static,
2657    D::Target: DatabaseMetadataSource + Send + Sync,
2658{
2659    type TableSizes = Vec<TableSize>;
2660    type MigrationStatus = Vec<MigrationStatus>;
2661
2662    async fn get_table_sizes(&self) -> anyhow::Result<Self::TableSizes> {
2663        let ds = &*self.data_source;
2664        ds.get_table_sizes().await
2665    }
2666
2667    async fn get_migration_status(&self) -> anyhow::Result<Self::MigrationStatus> {
2668        let ds = &*self.data_source;
2669        ds.get_migration_status().await
2670    }
2671}
2672
2673#[cfg(test)]
2674mod tests {
2675    use super::*;
2676
2677    fn custom(status: StatusCode) -> hotshot_query_service::Error {
2678        hotshot_query_service::Error::Custom {
2679            message: "boom".into(),
2680            status,
2681        }
2682    }
2683
2684    // The only tests of the range limits since the query service's own API (and its
2685    // `test_range_limit`) was deleted: an in-limit range passes, one past the limit is a
2686    // RangeExceeded, which the HTTP layer serves as a 400.
2687    #[test]
2688    fn range_at_limit_is_allowed() {
2689        let limit = small_object_range_limit();
2690        enforce_range(0, limit, limit).unwrap();
2691        enforce_range(3, limit + 3, limit).unwrap();
2692    }
2693
2694    #[test]
2695    fn range_past_limit_is_rejected() {
2696        for limit in [small_object_range_limit(), large_object_range_limit()] {
2697            let err = enforce_range(0, limit + 1, limit).unwrap_err();
2698            assert!(matches!(
2699                err.downcast_ref::<AvailabilityError>(),
2700                Some(AvailabilityError::RangeExceeded(_))
2701            ));
2702        }
2703    }
2704
2705    // Tripwire: the enforced and advertised limits come from `hotshot_query_service`'s
2706    // `Options` defaults. If a dependency change moves them, this fails so the new bound is
2707    // adopted deliberately rather than silently.
2708    #[test]
2709    fn range_limits_track_known_defaults() {
2710        assert_eq!(small_object_range_limit(), 500);
2711        assert_eq!(large_object_range_limit(), 100);
2712        assert_eq!(node_window_limit(), 500);
2713    }
2714
2715    // Regression: the light-client trait methods used to map query-service errors through
2716    // `anyhow::anyhow!("{err}")`, erasing the status; every 400/404 became a 500.
2717    #[test]
2718    fn lc_error_preserves_bad_request() {
2719        let err = lc_error(custom(StatusCode::BAD_REQUEST));
2720        assert!(matches!(
2721            err.downcast_ref::<AvailabilityError>(),
2722            Some(AvailabilityError::BadRequest(_))
2723        ));
2724    }
2725
2726    #[test]
2727    fn lc_error_preserves_not_found() {
2728        let err = lc_error(custom(StatusCode::NOT_FOUND));
2729        assert!(matches!(
2730            err.downcast_ref::<AvailabilityError>(),
2731            Some(AvailabilityError::NotFound(_))
2732        ));
2733    }
2734
2735    #[test]
2736    fn lc_error_other_statuses_stay_internal() {
2737        let err = lc_error(custom(StatusCode::INTERNAL_SERVER_ERROR));
2738        assert!(err.downcast_ref::<AvailabilityError>().is_none());
2739    }
2740}