trustchain_http/
root.rs

1//! Handler and trait for identifying root DID candidates from a naive date.
2use crate::state::AppState;
3use async_trait::async_trait;
4use axum::extract::{Path, Query, State};
5use axum::http::StatusCode;
6use axum::response::IntoResponse;
7use axum::Json;
8use chrono::NaiveDate;
9use log::debug;
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12use std::sync::{Arc, RwLock};
13use trustchain_core::verifier::Timestamp;
14use trustchain_ion::root::{root_did_candidates, RootCandidate, TrustchainRootError};
15use trustchain_ion::utils::time_at_block_height;
16
17use crate::errors::TrustchainHTTPError;
18
19/// An HTTP API for identifying candidate root DIDs.
20#[async_trait]
21pub trait TrustchainRootHTTP {
22    /// Gets a vector of root DID candidates timestamped on a given date.
23    async fn root_candidates(
24        date: NaiveDate,
25        root_candidates: &RwLock<HashMap<NaiveDate, RootCandidatesResult>>,
26    ) -> Result<RootCandidatesResult, TrustchainHTTPError>;
27    /// Gets a unix timestamp for a given Bitcoin transaction ID.
28    async fn block_timestamp(height: u64) -> Result<TimestampResult, TrustchainHTTPError>;
29}
30
31/// Type for implementing the TrustchainIssuerHTTP trait that will contain additional handler methods.
32pub struct TrustchainRootHTTPHandler {}
33
34#[async_trait]
35impl TrustchainRootHTTP for TrustchainRootHTTPHandler {
36    async fn root_candidates(
37        date: NaiveDate,
38        root_candidates: &RwLock<HashMap<NaiveDate, RootCandidatesResult>>,
39    ) -> Result<RootCandidatesResult, TrustchainHTTPError> {
40        debug!("Getting root candidates for {0}", date);
41        {
42            let read_guard = root_candidates.read().unwrap();
43            // Return the cached vector of root DID candidates, if available.
44            if read_guard.contains_key(&date) {
45                return Ok(read_guard.get(&date).cloned().unwrap());
46            }
47        }
48        let result = RootCandidatesResult::new(date, root_did_candidates(date).await?);
49        debug!("Got root candidates: {:?}", &result);
50
51        // Add the result to the cache.
52        root_candidates
53            .write()
54            .unwrap()
55            .insert(date, result.clone());
56        Ok(result)
57    }
58
59    async fn block_timestamp(height: u64) -> Result<TimestampResult, TrustchainHTTPError> {
60        debug!("Getting unix timestamp for block height: {0}", height);
61
62        let timestamp = time_at_block_height(height, None)
63            .map_err(|err| TrustchainRootError::FailedToParseBlockHeight(err.to_string()))?;
64        debug!("Got block timestamp: {:?}", &timestamp);
65        Ok(TimestampResult { timestamp })
66    }
67}
68
69#[derive(Deserialize, Debug)]
70/// Struct for deserializing root event `year` from handler's query param.
71pub struct RootEventYear {
72    year: i32,
73}
74
75#[derive(Deserialize, Debug)]
76/// Struct for deserializing root event `month` from handler's query param.
77pub struct RootEventMonth {
78    month: u32,
79}
80
81#[derive(Deserialize, Debug)]
82/// Struct for deserializing root event `day` from handler's query param.
83pub struct RootEventDay {
84    day: u32,
85}
86
87impl TrustchainRootHTTPHandler {
88    /// Handles a GET request for root DID candidates.
89    pub async fn get_root_candidates(
90        Query(year): Query<RootEventYear>,
91        Query(month): Query<RootEventMonth>,
92        Query(day): Query<RootEventDay>,
93        State(app_state): State<Arc<AppState>>,
94    ) -> impl IntoResponse {
95        debug!(
96            "Received date for root DID candidates: {:?}-{:?}-{:?}",
97            year, month, day
98        );
99
100        let date = chrono::NaiveDate::from_ymd_opt(year.year, month.month, day.day);
101        if date.is_none() {
102            return Err(TrustchainHTTPError::RootError(
103                TrustchainRootError::InvalidDate(year.year, month.month, day.day),
104            ));
105        }
106        TrustchainRootHTTPHandler::root_candidates(date.unwrap(), &app_state.root_candidates)
107            .await
108            .map(|vec| (StatusCode::OK, Json(vec)))
109    }
110
111    /// Handles a GET request for a transaction timestamp.
112    pub async fn get_block_timestamp(Path(height): Path<String>) -> impl IntoResponse {
113        debug!("Received block height for timestamp: {:?}", height.as_str());
114        let block_height = height.parse::<u64>();
115
116        if block_height.is_err() {
117            return Err(TrustchainHTTPError::RootError(
118                TrustchainRootError::FailedToParseBlockHeight(height),
119            ));
120        }
121
122        TrustchainRootHTTPHandler::block_timestamp(block_height.unwrap())
123            .await
124            .map(|result| (StatusCode::OK, Json(result)))
125    }
126}
127
128#[derive(Debug, Serialize, Deserialize, Clone)]
129#[serde(rename_all = "camelCase")]
130/// Serializable type representing the result of a request for root DID candidates on a given date.
131pub struct RootCandidatesResult {
132    date: NaiveDate,
133    root_candidates: Vec<RootCandidate>,
134}
135
136impl RootCandidatesResult {
137    pub fn new(date: NaiveDate, root_candidates: Vec<RootCandidate>) -> Self {
138        Self {
139            date,
140            root_candidates,
141        }
142    }
143}
144
145#[derive(Debug, Serialize, Deserialize, Clone)]
146#[serde(rename_all = "camelCase")]
147/// Serializable type representing the result of a request for root DID candidates on a given date.
148pub struct TimestampResult {
149    timestamp: Timestamp,
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155    use crate::{config::HTTPConfig, server::TrustchainRouter};
156    use axum_test_helper::TestClient;
157    use itertools::Itertools;
158
159    #[tokio::test]
160    #[ignore = "requires MongoDB and Bitcoin RPC"]
161    async fn test_root_candidates() {
162        let app = TrustchainRouter::from(HTTPConfig::default()).into_router();
163        let client = TestClient::new(app);
164
165        // Invalid date in request:
166        let uri = "/root?year=2022&month=10&day=40".to_string();
167        let response = client.get(&uri).send().await;
168        assert_eq!(response.status(), StatusCode::BAD_REQUEST);
169        assert_eq!(
170            response.text().await,
171            r#"{"error":"Trustchain root error: Invalid date: 2022-10-40"}"#.to_string()
172        );
173
174        // Valid request:
175        let uri = "/root?year=2022&month=10&day=20".to_string();
176        let response = client.get(&uri).send().await;
177        assert_eq!(response.status(), StatusCode::OK);
178
179        let result: RootCandidatesResult = serde_json::from_str(&response.text().await).unwrap();
180
181        assert_eq!(result.date, NaiveDate::from_ymd_opt(2022, 10, 20).unwrap());
182        let sorted_root_candidates = result.root_candidates.into_iter().sorted().collect_vec();
183        assert_eq!(
184            sorted_root_candidates[26].did,
185            "did:ion:test:EiCClfEdkTv_aM3UnBBhlOV89LlGhpQAbfeZLFdFxVFkEg"
186        );
187        assert_eq!(
188            sorted_root_candidates[26].txid,
189            "9dc43cca950d923442445340c2e30bc57761a62ef3eaf2417ec5c75784ea9c2c"
190        );
191    }
192
193    #[tokio::test]
194    #[ignore = "requires MongoDB and Bitcoin RPC"]
195    async fn test_block_timestamp() {
196        let app = TrustchainRouter::from(HTTPConfig::default()).into_router();
197        let client = TestClient::new(app);
198
199        // Invalid block height in request:
200        let uri = "/root/timestamp/2377xyz".to_string();
201        let response = client.get(&uri).send().await;
202        assert_eq!(response.status(), StatusCode::BAD_REQUEST);
203        assert_eq!(
204            response.text().await,
205            r#"{"error":"Trustchain root error: Failed to parse block height: 2377xyz"}"#
206                .to_string()
207        );
208
209        // Invalid block height in request:
210        let uri = "/root/timestamp/237744522222".to_string();
211        let response = client.get(&uri).send().await;
212        assert_eq!(response.status(), StatusCode::BAD_REQUEST);
213        assert!(response.text().await.contains("integer out of range"));
214
215        // Valid request:
216        let uri = "/root/timestamp/2377445".to_string();
217        let response = client.get(&uri).send().await;
218        assert_eq!(response.status(), StatusCode::OK);
219
220        let result: TimestampResult = serde_json::from_str(&response.text().await).unwrap();
221
222        assert_eq!(result.timestamp, 1666265405);
223    }
224}