-
Notifications
You must be signed in to change notification settings - Fork 5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Root event date #125
Root event date #125
Changes from 17 commits
fa801d4
be9b628
a99ff5a
dfc3242
0550963
18ee41b
0e46a98
ccb433f
61a3d2d
f48b4c7
23c66ca
bc931a3
1fb6333
fad5cbc
77b5dbc
9997e0b
6326fec
cb106e5
22a03a8
717c9b5
92fafb0
e5bc179
07ab3c3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -8,6 +8,7 @@ | |||||||||||||
use ssi::jwk::JWK; | ||||||||||||||
use ssi::one_or_many::OneOrMany; | ||||||||||||||
use std::path::{Path, PathBuf}; | ||||||||||||||
use std::str::FromStr; | ||||||||||||||
use std::sync::Once; | ||||||||||||||
|
||||||||||||||
/// Gets the type of an object as a String. For diagnostic purposes (debugging) only. | ||||||||||||||
|
@@ -99,6 +100,15 @@ | |||||||||||||
did.split(':').last().unwrap() | ||||||||||||||
} | ||||||||||||||
|
||||||||||||||
/// Converts a short-form DID into a complete DID. | ||||||||||||||
pub fn get_did_from_suffix(did_suffix: &str, method: &str) -> String { | ||||||||||||||
thobson88 marked this conversation as resolved.
Show resolved
Hide resolved
sgreenbury marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||
let mut did = String::from_str("did:").unwrap(); | ||||||||||||||
did.push_str(method); | ||||||||||||||
did.push_str(":"); | ||||||||||||||
Check warning on line 107 in trustchain-core/src/utils.rs GitHub Actions / clippycalling `push_str()` using a single-character string literal
|
||||||||||||||
did.push_str(did_suffix); | ||||||||||||||
did | ||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||
} | ||||||||||||||
|
||||||||||||||
/// [`JSON_CANONICALIZATION_SCHEME`](https://identity.foundation/sidetree/spec/v1.0.0/#json-canonicalization-scheme) | ||||||||||||||
pub fn canonicalize<T: Serialize + ?Sized>(value: &T) -> Result<String, serde_json::Error> { | ||||||||||||||
serde_jcs::to_string(value) | ||||||||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,6 +6,7 @@ use trustchain_core::{ | |
commitment::CommitmentError, issuer::IssuerError, resolver::ResolverError, vc::CredentialError, | ||
verifier::VerifierError, vp::PresentationError, | ||
}; | ||
use trustchain_ion::root::TrustchainRootError; | ||
|
||
// TODO: refine and add doc comments for error variants | ||
#[derive(Error, Debug)] | ||
|
@@ -20,6 +21,8 @@ pub enum TrustchainHTTPError { | |
ResolverError(ResolverError), | ||
#[error("Trustchain issuer error: {0}")] | ||
IssuerError(IssuerError), | ||
#[error("Trustchain root error: {0}")] | ||
RootError(TrustchainRootError), | ||
#[error("Trustchain presentation error: {0}")] | ||
PresentationError(PresentationError), | ||
#[error("Credential does not exist.")] | ||
|
@@ -55,12 +58,19 @@ impl From<VerifierError> for TrustchainHTTPError { | |
TrustchainHTTPError::VerifierError(err) | ||
} | ||
} | ||
|
||
impl From<IssuerError> for TrustchainHTTPError { | ||
fn from(err: IssuerError) -> Self { | ||
TrustchainHTTPError::IssuerError(err) | ||
} | ||
} | ||
|
||
impl From<TrustchainRootError> for TrustchainHTTPError { | ||
fn from(err: TrustchainRootError) -> Self { | ||
TrustchainHTTPError::RootError(err) | ||
} | ||
} | ||
|
||
impl From<PresentationError> for TrustchainHTTPError { | ||
fn from(err: PresentationError) -> Self { | ||
TrustchainHTTPError::PresentationError(err) | ||
|
@@ -108,6 +118,7 @@ impl IntoResponse for TrustchainHTTPError { | |
err @ TrustchainHTTPError::NoCredentialIssuer => { | ||
(StatusCode::BAD_REQUEST, err.to_string()) | ||
} | ||
err @ TrustchainHTTPError::RootError(_) => (StatusCode::BAD_REQUEST, err.to_string()), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Check the HTTP status |
||
err @ TrustchainHTTPError::FailedToVerifyCredential => { | ||
(StatusCode::INTERNAL_SERVER_ERROR, err.to_string()) | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,218 @@ | ||
use crate::state::AppState; | ||
use async_trait::async_trait; | ||
use axum::extract::{Path, Query, State}; | ||
use axum::http::StatusCode; | ||
use axum::response::IntoResponse; | ||
use axum::Json; | ||
use chrono::NaiveDate; | ||
use log::debug; | ||
use serde::{Deserialize, Serialize}; | ||
use std::collections::HashMap; | ||
use std::sync::{Arc, Mutex}; | ||
use trustchain_core::verifier::Timestamp; | ||
use trustchain_ion::root::{root_did_candidates, RootCandidate, TrustchainRootError}; | ||
use trustchain_ion::utils::time_at_block_height; | ||
|
||
use crate::errors::TrustchainHTTPError; | ||
|
||
/// An HTTP API for identifying candidate root DIDs. | ||
#[async_trait] | ||
pub trait TrustchainRootHTTP { | ||
/// Gets a vector of root DID candidates timestamped on a given date. | ||
async fn root_candidates( | ||
date: NaiveDate, | ||
root_candidates: &Mutex<HashMap<NaiveDate, RootCandidatesResult>>, | ||
) -> Result<RootCandidatesResult, TrustchainHTTPError>; | ||
/// Gets a unix timestamp for a given Bitcoin transaction ID. | ||
async fn block_timestamp(height: u64) -> Result<TimestampResult, TrustchainHTTPError>; | ||
} | ||
|
||
/// Type for implementing the TrustchainIssuerHTTP trait that will contain additional handler methods. | ||
pub struct TrustchainRootHTTPHandler {} | ||
|
||
#[async_trait] | ||
impl TrustchainRootHTTP for TrustchainRootHTTPHandler { | ||
async fn root_candidates( | ||
date: NaiveDate, | ||
root_candidates: &Mutex<HashMap<NaiveDate, RootCandidatesResult>>, | ||
) -> Result<RootCandidatesResult, TrustchainHTTPError> { | ||
debug!("Getting root candidates for {0}", date); | ||
|
||
// Return the cached vector of root DID candidates, if available. | ||
if root_candidates.lock().unwrap().contains_key(&date) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Perhaps create a single lock to use throughout function? |
||
return Ok(root_candidates.lock().unwrap().get(&date).cloned().unwrap()); | ||
} | ||
|
||
let result = RootCandidatesResult::new(date, root_did_candidates(date).await?); | ||
debug!("Got root candidates: {:?}", &result); | ||
|
||
// Add the result to the cache. | ||
root_candidates.lock().unwrap().insert(date, result.clone()); | ||
Ok(result) | ||
} | ||
|
||
async fn block_timestamp(height: u64) -> Result<TimestampResult, TrustchainHTTPError> { | ||
debug!("Getting unix timestamp for block height: {0}", height); | ||
|
||
let timestamp = time_at_block_height(height, None) | ||
.map_err(|err| TrustchainRootError::FailedToParseBlockHeight(err.to_string()))?; | ||
debug!("Got block timestamp: {:?}", ×tamp); | ||
Ok(TimestampResult { timestamp }) | ||
} | ||
} | ||
|
||
#[derive(Deserialize, Debug)] | ||
/// Struct for deserializing root event `year` from handler's query param. | ||
pub struct RootEventYear { | ||
year: i32, | ||
} | ||
|
||
#[derive(Deserialize, Debug)] | ||
/// Struct for deserializing root event `month` from handler's query param. | ||
pub struct RootEventMonth { | ||
month: u32, | ||
} | ||
|
||
#[derive(Deserialize, Debug)] | ||
/// Struct for deserializing root event `day` from handler's query param. | ||
pub struct RootEventDay { | ||
day: u32, | ||
} | ||
|
||
impl TrustchainRootHTTPHandler { | ||
/// Handles a GET request for root DID candidates. | ||
pub async fn get_root_candidates( | ||
Query(year): Query<RootEventYear>, | ||
Query(month): Query<RootEventMonth>, | ||
Query(day): Query<RootEventDay>, | ||
State(app_state): State<Arc<AppState>>, | ||
) -> impl IntoResponse { | ||
debug!( | ||
"Received date for root DID candidates: {:?}-{:?}-{:?}", | ||
year, month, day | ||
); | ||
|
||
let date = chrono::NaiveDate::from_ymd_opt(year.year, month.month, day.day); | ||
if date.is_none() { | ||
return Err(TrustchainHTTPError::RootError( | ||
TrustchainRootError::InvalidDate(year.year, month.month, day.day), | ||
)); | ||
} | ||
TrustchainRootHTTPHandler::root_candidates(date.unwrap(), &app_state.root_candidates) | ||
.await | ||
.map(|vec| (StatusCode::OK, Json(vec))) | ||
} | ||
|
||
/// Handles a GET request for a transaction timestamp. | ||
pub async fn get_block_timestamp(Path(height): Path<String>) -> impl IntoResponse { | ||
debug!("Received block height for timestamp: {:?}", height.as_str()); | ||
let block_height = height.parse::<u64>(); | ||
|
||
if block_height.is_err() { | ||
return Err(TrustchainHTTPError::RootError( | ||
TrustchainRootError::FailedToParseBlockHeight(height), | ||
)); | ||
} | ||
|
||
TrustchainRootHTTPHandler::block_timestamp(block_height.unwrap()) | ||
.await | ||
.map(|result| (StatusCode::OK, Json(result))) | ||
} | ||
} | ||
|
||
#[derive(Debug, Serialize, Deserialize, Clone)] | ||
#[serde(rename_all = "camelCase")] | ||
/// Serializable type representing the result of a request for root DID candidates on a given date. | ||
pub struct RootCandidatesResult { | ||
date: NaiveDate, | ||
root_candidates: Vec<RootCandidate>, | ||
} | ||
|
||
impl RootCandidatesResult { | ||
pub fn new(date: NaiveDate, root_candidates: Vec<RootCandidate>) -> Self { | ||
Self { | ||
date, | ||
root_candidates, | ||
} | ||
} | ||
} | ||
|
||
#[derive(Debug, Serialize, Deserialize, Clone)] | ||
#[serde(rename_all = "camelCase")] | ||
/// Serializable type representing the result of a request for root DID candidates on a given date. | ||
pub struct TimestampResult { | ||
timestamp: Timestamp, | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use super::*; | ||
use crate::{config::HTTPConfig, server::TrustchainRouter}; | ||
use axum_test_helper::TestClient; | ||
|
||
#[tokio::test] | ||
#[ignore = "requires MongoDB and Bitcoin RPC"] | ||
async fn test_root_candidates() { | ||
let app = TrustchainRouter::from(HTTPConfig::default()).into_router(); | ||
let client = TestClient::new(app); | ||
|
||
// Invalid date in request: | ||
let uri = "/root?year=2022&month=10&day=40".to_string(); | ||
let response = client.get(&uri).send().await; | ||
assert_eq!(response.status(), StatusCode::BAD_REQUEST); | ||
assert_eq!( | ||
response.text().await, | ||
r#"{"error":"Trustchain root error: Invalid date: 2022-10-40"}"#.to_string() | ||
); | ||
|
||
// Valid request: | ||
let uri = "/root?year=2022&month=10&day=20".to_string(); | ||
let response = client.get(&uri).send().await; | ||
assert_eq!(response.status(), StatusCode::OK); | ||
|
||
let result: RootCandidatesResult = serde_json::from_str(&response.text().await).unwrap(); | ||
|
||
assert_eq!(result.date, NaiveDate::from_ymd_opt(2022, 10, 20).unwrap()); | ||
|
||
assert_eq!( | ||
result.root_candidates[16].did, | ||
"did:ion:test:EiCClfEdkTv_aM3UnBBhlOV89LlGhpQAbfeZLFdFxVFkEg" | ||
); | ||
assert_eq!( | ||
result.root_candidates[16].txid, | ||
"9dc43cca950d923442445340c2e30bc57761a62ef3eaf2417ec5c75784ea9c2c" | ||
); | ||
} | ||
|
||
#[tokio::test] | ||
#[ignore = "requires MongoDB and Bitcoin RPC"] | ||
async fn test_block_timestamp() { | ||
let app = TrustchainRouter::from(HTTPConfig::default()).into_router(); | ||
let client = TestClient::new(app); | ||
|
||
// Invalid block height in request: | ||
let uri = "/root/timestamp/2377xyz".to_string(); | ||
let response = client.get(&uri).send().await; | ||
assert_eq!(response.status(), StatusCode::BAD_REQUEST); | ||
assert_eq!( | ||
response.text().await, | ||
r#"{"error":"Trustchain root error: Failed to parse block height: 2377xyz"}"# | ||
.to_string() | ||
); | ||
|
||
// Invalid block height in request: | ||
let uri = "/root/timestamp/237744522222".to_string(); | ||
let response = client.get(&uri).send().await; | ||
assert_eq!(response.status(), StatusCode::BAD_REQUEST); | ||
assert!(response.text().await.contains("integer out of range")); | ||
|
||
// Valid request: | ||
let uri = "/root/timestamp/2377445".to_string(); | ||
let response = client.get(&uri).send().await; | ||
assert_eq!(response.status(), StatusCode::OK); | ||
|
||
let result: TimestampResult = serde_json::from_str(&response.text().await).unwrap(); | ||
|
||
assert_eq!(result.timestamp, 1666265405); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove
into()
(clippy)