Skip to main content

vote_commitment_tree_client/
http_sync_api.rs

1//! HTTP implementation of [`TreeSyncApi`] for connecting to a running Zally chain node.
2//!
3//! Maps the three trait methods to per-round REST endpoints:
4//! - `get_tree_state()`        → `GET /shielded-vote/v1/commitment-tree/{round_id}/latest`
5//! - `get_root_at_height(h)`   → `GET /shielded-vote/v1/commitment-tree/{round_id}/{h}`
6//! - `get_block_commitments()` → `GET /shielded-vote/v1/commitment-tree/{round_id}/leaves?from_height=X&to_height=Y`
7
8use pasta_curves::Fp;
9
10use vote_commitment_tree::sync_api::{BlockCommitments, TreeState, TreeSyncApi};
11
12use crate::types::{
13    QueryCommitmentLeavesResponse, QueryCommitmentTreeResponse, QueryLatestTreeResponse,
14};
15
16// ---------------------------------------------------------------------------
17// Error
18// ---------------------------------------------------------------------------
19
20/// Errors from the HTTP sync API.
21#[derive(Debug, thiserror::Error)]
22pub enum HttpSyncError {
23    #[error("HTTP request failed: {0}")]
24    Http(#[from] reqwest::Error),
25
26    #[error("parse error: {0}")]
27    Parse(#[from] crate::types::ParseError),
28
29    #[error("server returned no tree state")]
30    NoTreeState,
31}
32
33// ---------------------------------------------------------------------------
34// HttpTreeSyncApi
35// ---------------------------------------------------------------------------
36
37/// HTTP-based implementation of [`TreeSyncApi`] for remote chain sync.
38///
39/// Uses `reqwest::blocking::Client` for synchronous HTTP calls, matching the
40/// synchronous `TreeSyncApi` trait signature. Each instance is scoped to a
41/// specific voting round via `round_id` (hex-encoded in URL paths).
42pub struct HttpTreeSyncApi {
43    client: reqwest::blocking::Client,
44    /// Base URL of the chain's REST API (e.g. `http://localhost:1317`).
45    base_url: String,
46    /// Hex-encoded round ID for per-round tree endpoints.
47    round_id: String,
48}
49
50impl HttpTreeSyncApi {
51    /// Create a new HTTP sync API client for a specific voting round.
52    ///
53    /// `base_url` should be the root of the chain's REST API, without a trailing
54    /// slash (e.g. `http://localhost:1317`). `round_id` is the hex-encoded
55    /// voting round identifier.
56    pub fn new(base_url: impl Into<String>, round_id: impl Into<String>) -> Self {
57        Self {
58            client: reqwest::blocking::Client::new(),
59            base_url: base_url.into(),
60            round_id: round_id.into(),
61        }
62    }
63
64    /// Create with an existing reqwest client (for custom timeouts, etc.).
65    pub fn with_client(
66        client: reqwest::blocking::Client,
67        base_url: impl Into<String>,
68        round_id: impl Into<String>,
69    ) -> Self {
70        Self {
71            client,
72            base_url: base_url.into(),
73            round_id: round_id.into(),
74        }
75    }
76}
77
78impl TreeSyncApi for HttpTreeSyncApi {
79    type Error = HttpSyncError;
80
81    fn get_tree_state(&self) -> Result<TreeState, Self::Error> {
82        let url = format!(
83            "{}/shielded-vote/v1/commitment-tree/{}/latest",
84            self.base_url, self.round_id
85        );
86        let resp: QueryLatestTreeResponse = self.client.get(&url).send()?.json()?;
87        resp.tree
88            .ok_or(HttpSyncError::NoTreeState)?
89            .into_tree_state()
90            .map_err(HttpSyncError::Parse)
91    }
92
93    fn get_root_at_height(&self, height: u32) -> Result<Option<Fp>, Self::Error> {
94        let url = format!(
95            "{}/shielded-vote/v1/commitment-tree/{}/{}",
96            self.base_url, self.round_id, height
97        );
98        let resp: QueryCommitmentTreeResponse = self.client.get(&url).send()?.json()?;
99        match resp.tree {
100            Some(state) => {
101                let ts = state.into_tree_state().map_err(HttpSyncError::Parse)?;
102                Ok(Some(ts.root))
103            }
104            None => Ok(None),
105        }
106    }
107
108    fn get_block_commitments(
109        &self,
110        from_height: u32,
111        to_height: u32,
112    ) -> Result<Vec<BlockCommitments>, Self::Error> {
113        let url = format!(
114            "{}/shielded-vote/v1/commitment-tree/{}/leaves?from_height={}&to_height={}",
115            self.base_url, self.round_id, from_height, to_height
116        );
117        let resp: QueryCommitmentLeavesResponse = self.client.get(&url).send()?.json()?;
118        resp.blocks
119            .into_iter()
120            .map(|b| b.into_block_commitments().map_err(HttpSyncError::Parse))
121            .collect()
122    }
123}