use std::sync::Arc;
use pasta_curves::Fp;
use vote_commitment_tree::sync_api::{BlockCommitmentsPage, TreeState, TreeSyncApi};
use crate::transport::{Transport, TransportError, TransportResponse};
use crate::types::{
QueryCommitmentLeavesResponse, QueryCommitmentTreeResponse, QueryLatestTreeResponse,
};
#[derive(Debug, thiserror::Error)]
pub enum HttpSyncError {
#[error("transport request failed: {0}")]
Transport(#[from] TransportError),
#[error("HTTP request to {url} returned status {status}: {body}")]
HttpStatus {
url: String,
status: u16,
body: String,
},
#[error("JSON parse error: {0}")]
Json(#[from] serde_json::Error),
#[error("parse error: {0}")]
Parse(#[from] crate::types::ParseError),
#[error("server returned no tree state")]
NoTreeState,
}
pub struct HttpTreeSyncApi {
transport: Arc<dyn Transport>,
base_url: String,
round_id: String,
}
impl HttpTreeSyncApi {
pub fn new(
base_url: impl Into<String>,
round_id: impl Into<String>,
transport: Arc<dyn Transport>,
) -> Self {
Self {
base_url: base_url.into(),
round_id: round_id.into(),
transport,
}
}
fn get_json<T: serde::de::DeserializeOwned>(&self, url: String) -> Result<T, HttpSyncError> {
let response = self.transport.get(&url)?;
let body = self.success_body(&url, response)?;
serde_json::from_slice(&body).map_err(HttpSyncError::Json)
}
fn success_body(
&self,
url: &str,
response: TransportResponse,
) -> Result<Vec<u8>, HttpSyncError> {
if (200..300).contains(&response.status) {
Ok(response.body)
} else {
Err(HttpSyncError::HttpStatus {
url: url.to_string(),
status: response.status,
body: String::from_utf8_lossy(&response.body).into_owned(),
})
}
}
}
impl TreeSyncApi for HttpTreeSyncApi {
type Error = HttpSyncError;
fn get_tree_state(&self) -> Result<TreeState, Self::Error> {
let url = format!(
"{}/shielded-vote/v1/commitment-tree/{}/latest",
self.base_url, self.round_id
);
let resp: QueryLatestTreeResponse = self.get_json(url)?;
resp.tree
.ok_or(HttpSyncError::NoTreeState)?
.into_tree_state()
.map_err(HttpSyncError::Parse)
}
fn get_root_at_height(&self, height: u32) -> Result<Option<Fp>, Self::Error> {
let url = format!(
"{}/shielded-vote/v1/commitment-tree/{}/{}",
self.base_url, self.round_id, height
);
let resp: QueryCommitmentTreeResponse = self.get_json(url)?;
match resp.tree {
Some(state) => {
let ts = state.into_tree_state().map_err(HttpSyncError::Parse)?;
Ok(Some(ts.root))
}
None => Ok(None),
}
}
fn get_block_commitments(
&self,
from_height: u32,
to_height: u32,
) -> Result<BlockCommitmentsPage, Self::Error> {
let url = format!(
"{}/shielded-vote/v1/commitment-tree/{}/leaves?from_height={}&to_height={}",
self.base_url, self.round_id, from_height, to_height
);
let resp: QueryCommitmentLeavesResponse = self.get_json(url)?;
resp.into_block_commitments_page()
.map_err(HttpSyncError::Parse)
}
}