1use 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#[async_trait]
21pub trait TrustchainRootHTTP {
22 async fn root_candidates(
24 date: NaiveDate,
25 root_candidates: &RwLock<HashMap<NaiveDate, RootCandidatesResult>>,
26 ) -> Result<RootCandidatesResult, TrustchainHTTPError>;
27 async fn block_timestamp(height: u64) -> Result<TimestampResult, TrustchainHTTPError>;
29}
30
31pub 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 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 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: {:?}", ×tamp);
65 Ok(TimestampResult { timestamp })
66 }
67}
68
69#[derive(Deserialize, Debug)]
70pub struct RootEventYear {
72 year: i32,
73}
74
75#[derive(Deserialize, Debug)]
76pub struct RootEventMonth {
78 month: u32,
79}
80
81#[derive(Deserialize, Debug)]
82pub struct RootEventDay {
84 day: u32,
85}
86
87impl TrustchainRootHTTPHandler {
88 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 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")]
130pub 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")]
147pub 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 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 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 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 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 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}