use std::{cmp::Ordering, collections::HashMap};
use chrono::{DateTime, Utc};
use tokio::sync::Mutex;
use tonic::transport::{Channel, ClientTlsConfig, Endpoint};
use crate::{
error::{DidCheqdError, DidCheqdResult},
proto::cheqd::{
did::v2::{
QueryDidDocRequest, QueryDidDocVersionRequest,
query_client::QueryClient as DidQueryClient,
},
resource::v2::{
Metadata as CheqdResourceMetadata, QueryCollectionResourcesRequest,
QueryResourceRequest, query_client::QueryClient as ResourceQueryClient,
},
},
resolution::parser::DidCheqdParsed,
};
pub const MAINNET_NAMESPACE: &str = "mainnet";
pub const MAINNET_DEFAULT_GRPC: &str = "https://grpc.cheqd.net:443";
pub const TESTNET_NAMESPACE: &str = "testnet";
pub const TESTNET_DEFAULT_GRPC: &str = "https://grpc.cheqd.network:443";
pub struct DidCheqdResolverConfiguration {
pub networks: Vec<NetworkConfiguration>,
}
impl Default for DidCheqdResolverConfiguration {
fn default() -> Self {
Self {
networks: vec![
NetworkConfiguration::mainnet(),
NetworkConfiguration::testnet(),
],
}
}
}
pub struct NetworkConfiguration {
pub grpc_url: String,
pub namespace: String,
}
impl Clone for NetworkConfiguration {
fn clone(&self) -> Self {
Self {
grpc_url: self.grpc_url.clone(),
namespace: self.namespace.clone(),
}
}
}
impl Clone for DidCheqdResolverConfiguration {
fn clone(&self) -> Self {
Self {
networks: self.networks.clone(),
}
}
}
impl NetworkConfiguration {
pub fn mainnet() -> Self {
Self {
grpc_url: String::from(MAINNET_DEFAULT_GRPC),
namespace: String::from(MAINNET_NAMESPACE),
}
}
pub fn testnet() -> Self {
Self {
grpc_url: String::from(TESTNET_DEFAULT_GRPC),
namespace: String::from(TESTNET_NAMESPACE),
}
}
}
#[derive(Clone)]
struct CheqdGrpcClient {
did: DidQueryClient<Channel>,
resources: ResourceQueryClient<Channel>,
}
pub struct DidCheqdResolver {
networks: Vec<NetworkConfiguration>,
network_clients: Mutex<HashMap<String, CheqdGrpcClient>>,
}
impl DidCheqdResolver {
pub fn new(configuration: DidCheqdResolverConfiguration) -> Self {
Self {
networks: configuration.networks,
network_clients: Default::default(),
}
}
async fn client_for_network(&self, network: &str) -> DidCheqdResult<CheqdGrpcClient> {
let mut lock = self.network_clients.lock().await;
if let Some(client) = lock.get(network) {
return Ok(client.clone());
}
let network_config = self
.networks
.iter()
.find(|n| n.namespace == network)
.ok_or(DidCheqdError::NetworkNotSupported(network.to_owned()))?;
let endpoint = Endpoint::new(network_config.grpc_url.to_string())
.map_err(|_e| DidCheqdError::BadConfiguration("Failed to parse GRPC url".to_string()))?
.tls_config(ClientTlsConfig::new().with_webpki_roots())
.map_err(|e| DidCheqdError::TransportError(Box::new(e)))?;
let channel = endpoint
.connect()
.await
.map_err(|e| DidCheqdError::TransportError(Box::new(e)))?;
let did_client = DidQueryClient::new(channel.clone());
let resource_client = ResourceQueryClient::new(channel);
let client = CheqdGrpcClient {
did: did_client,
resources: resource_client,
};
lock.insert(network.to_owned(), client.clone());
Ok(client)
}
pub async fn query_did_doc_by_str(
&self,
_did_str: &str,
parsed_did: DidCheqdParsed,
) -> DidCheqdResult<(
crate::proto::cheqd::did::v2::DidDoc,
Option<crate::proto::cheqd::did::v2::Metadata>,
)> {
let network = parsed_did.namespace.as_str();
let mut client = self.client_for_network(network).await?;
if parsed_did.version.is_some() {
let request = tonic::Request::new(QueryDidDocVersionRequest {
id: parsed_did.did.to_string(),
version: parsed_did.version.unwrap(),
});
let response = client
.did
.did_doc_version(request)
.await
.map_err(|e| DidCheqdError::NonSuccessResponse(Box::new(e)))?;
let query_response = response.into_inner();
let query_doc_res = query_response.value.ok_or(DidCheqdError::InvalidResponse(
"DIDDoc query did version not return a value".into(),
))?;
let query_doc = query_doc_res.did_doc.ok_or(DidCheqdError::InvalidResponse(
"DIDDoc query did version not return a DIDDoc".into(),
))?;
Ok((query_doc, query_doc_res.metadata))
} else {
let request = tonic::Request::new(QueryDidDocRequest {
id: parsed_did.did.to_string(),
});
let response = client
.did
.did_doc(request)
.await
.map_err(|e| DidCheqdError::NonSuccessResponse(Box::new(e)))?;
let query_response = response.into_inner();
let query_doc_res = query_response.value.ok_or(DidCheqdError::InvalidResponse(
"DIDDoc query did not return a value".into(),
))?;
let query_doc = query_doc_res.did_doc.ok_or(DidCheqdError::InvalidResponse(
"DIDDoc query did not return a DIDDoc".into(),
))?;
Ok((query_doc, query_doc_res.metadata))
}
}
pub async fn query_resource_by_str(
&self,
did_url: &str,
parsed_did: DidCheqdParsed,
) -> DidCheqdResult<(Vec<u8>, Option<String>)> {
let network = parsed_did.namespace.as_str();
let did_id = parsed_did.id.as_str();
if let Some(ref qmap) = parsed_did.query {
if let Some(resource_id) = qmap.get("resourceId") {
return self
.resolve_resource_by_id(did_id, resource_id.as_str(), network)
.await;
}
}
if let Some(qmap) = parsed_did.query {
let resource_name = qmap.get("resourceName");
let resource_type = qmap.get("resourceType");
let version_time = qmap.get("resourceVersionTime");
let (Some(resource_name), Some(resource_type)) = (resource_name, resource_type) else {
return Err(DidCheqdError::InvalidDidUrl(format!(
"Resolver can only resolve by exact resource ID or name+type combination {did_url}"
)));
};
let version_time = match version_time {
Some(v) => DateTime::parse_from_rfc3339(v)
.map_err(|e| DidCheqdError::InvalidDidUrl(e.to_string()))?
.to_utc(),
None => Utc::now(),
};
return self
.resolve_resource_by_name_type_and_time(
did_id,
resource_name.as_str(),
resource_type.as_str(),
version_time,
network,
)
.await;
}
Err(DidCheqdError::InvalidDidUrl(format!(
"No resource path or query present: {did_url}"
)))
}
async fn resolve_resource_by_id(
&self,
did_id: &str,
resource_id: &str,
network: &str,
) -> DidCheqdResult<(Vec<u8>, Option<String>)> {
let mut client = self.client_for_network(network).await?;
let request = QueryResourceRequest {
collection_id: did_id.to_owned(),
id: resource_id.to_owned(),
};
let response = client
.resources
.resource(request)
.await
.map_err(|e| DidCheqdError::NonSuccessResponse(Box::new(e)))?;
let query_response = response.into_inner();
let query_response = query_response
.resource
.ok_or(DidCheqdError::InvalidResponse(
"Resource query did not return a value".into(),
))?;
let query_resource = query_response
.resource
.ok_or(DidCheqdError::InvalidResponse(
"Resource query did not return a resource".into(),
))?;
let query_metadata = query_response
.metadata
.ok_or(DidCheqdError::InvalidResponse(
"Resource query did not return metadata".into(),
))?;
let media_type =
(!query_metadata.media_type.trim().is_empty()).then_some(query_metadata.media_type);
Ok((query_resource.data, media_type))
}
async fn resolve_resource_by_name_type_and_time(
&self,
did_id: &str,
name: &str,
rtyp: &str,
time: DateTime<Utc>,
network: &str,
) -> DidCheqdResult<(Vec<u8>, Option<String>)> {
let mut client = self.client_for_network(network).await?;
let response = client
.resources
.collection_resources(QueryCollectionResourcesRequest {
collection_id: did_id.to_owned(),
pagination: None,
})
.await
.map_err(|e| DidCheqdError::NonSuccessResponse(Box::new(e)))?;
let query_response = response.into_inner();
let resources = query_response.resources;
let mut filtered: Vec<_> =
filter_resources_by_name_and_type(resources.iter(), name, rtyp).collect();
filtered.sort_by(|a, b| desc_chronological_sort_resources(a, b));
let resource_meta = find_resource_just_before_time(filtered.into_iter(), time);
let Some(meta) = resource_meta else {
return Err(DidCheqdError::ResourceNotFound(format!(
"network: {network}, collection: {did_id}, name: {name}, type: {rtyp}, time: \
{time}"
)));
};
let (data, media) = self
.resolve_resource_by_id(did_id, &meta.id, network)
.await?;
Ok((data, media))
}
}
fn filter_resources_by_name_and_type<'a>(
resources: impl Iterator<Item = &'a CheqdResourceMetadata> + 'a,
name: &'a str,
rtyp: &'a str,
) -> impl Iterator<Item = &'a CheqdResourceMetadata> + 'a {
resources.filter(move |r| r.name == name && r.resource_type == rtyp)
}
fn desc_chronological_sort_resources(
b: &CheqdResourceMetadata,
a: &CheqdResourceMetadata,
) -> Ordering {
let (a_secs, a_ns) = a
.created
.map(|v| {
let v = v.normalized();
(v.seconds, v.nanos)
})
.unwrap_or((0, 0));
let (b_secs, b_ns) = b
.created
.map(|v| {
let v = v.normalized();
(v.seconds, v.nanos)
})
.unwrap_or((0, 0));
match a_secs.cmp(&b_secs) {
Ordering::Equal => a_ns.cmp(&b_ns),
res => res,
}
}
fn find_resource_just_before_time<'a>(
resources: impl Iterator<Item = &'a CheqdResourceMetadata>,
before_time: DateTime<Utc>,
) -> Option<&'a CheqdResourceMetadata> {
let before_epoch = before_time.timestamp();
for r in resources {
let Some(created) = r.created else {
continue;
};
let created_epoch = created.normalized().seconds;
if created_epoch < before_epoch {
return Some(r);
}
}
None
}
#[cfg(test)]
mod unit_tests {
use crate::resolution::parser::DidCheqdParser;
use super::*;
#[tokio::test]
async fn test_resolve_fails_if_no_network_config() {
let did = "did:cheqd:devnet:Ps1ysXP2Ae6GBfxNhNQNKN";
let resolver = DidCheqdResolver::new(Default::default());
let e = resolver
.query_did_doc_by_str(did, DidCheqdParser::parse(did).unwrap())
.await
.unwrap_err();
assert!(matches!(e, DidCheqdError::NetworkNotSupported(_)));
}
#[tokio::test]
async fn test_resolve_fails_if_bad_network_uri() {
let did = "did:cheqd:devnet:Ps1ysXP2Ae6GBfxNhNQNKN";
let config = DidCheqdResolverConfiguration {
networks: vec![NetworkConfiguration {
grpc_url: "@baduri://.".into(),
namespace: "devnet".into(),
}],
};
let resolver = DidCheqdResolver::new(config);
let e = resolver
.query_did_doc_by_str(did, DidCheqdParser::parse(did).unwrap())
.await
.unwrap_err();
assert!(matches!(e, DidCheqdError::BadConfiguration(_)));
}
#[tokio::test]
async fn test_resolve_resource_fails_if_no_query() {
let url = "did:cheqd:mainnet:zF7rhDBfUt9d1gJPjx7s1J";
let resolver = DidCheqdResolver::new(Default::default());
let e = resolver
.query_resource_by_str(url, DidCheqdParser::parse(url).unwrap())
.await
.unwrap_err();
assert!(matches!(e, DidCheqdError::InvalidDidUrl(_)));
}
#[tokio::test]
async fn test_resolve_resource_fails_if_incomplete_query() {
let url = "did:cheqd:mainnet:zF7rhDBfUt9d1gJPjx7s1j?resourceName=asdf";
let resolver = DidCheqdResolver::new(Default::default());
let e = resolver
.query_resource_by_str(url, DidCheqdParser::parse(url).unwrap())
.await
.unwrap_err();
assert!(matches!(e, DidCheqdError::InvalidDidUrl(_)));
}
#[tokio::test]
async fn test_resolve_resource_fails_if_invalid_resource_time() {
let url = "did:cheqd:mainnet:zF7rhDBfUt9d1gJPjx7s1J?resourceName=asdf&resourceType=fdsa&resourceVersionTime=12341234";
let resolver = DidCheqdResolver::new(Default::default());
let e = resolver
.query_resource_by_str(url, DidCheqdParser::parse(url).unwrap())
.await
.unwrap_err();
assert!(matches!(e, DidCheqdError::InvalidDidUrl(_)));
}
#[tokio::test]
async fn test_resolve_did_success() {
let did = "did:cheqd:testnet:f5101dd8-447f-40a7-a9b8-700abeba389a".to_string();
let resolver = DidCheqdResolver::new(Default::default());
let res = resolver
.query_did_doc_by_str(&did, DidCheqdParser::parse(&did).unwrap())
.await;
println!("res: {:#?}", res);
assert!(res.is_ok());
}
#[tokio::test]
async fn test_resolve_resource_id_success() {
let did_url = "did:cheqd:testnet:f5101dd8-447f-40a7-a9b8-700abeba389a/resources/6155f8bc-d9c9-4e83-a1bb-453744fe5438".to_string();
let resolver = DidCheqdResolver::new(Default::default());
let res = resolver
.query_resource_by_str(&did_url, DidCheqdParser::parse(&did_url).unwrap())
.await;
println!("res: {res:?}");
assert!(res.is_ok());
}
#[tokio::test]
async fn test_resolve_resource_query_success() {
let did_url = "did:cheqd:testnet:f5101dd8-447f-40a7-a9b8-700abeba389a?resourceName=Patient ID 85905-Schema&resourceType=anonCredsSchema".to_string();
let resolver = DidCheqdResolver::new(Default::default());
let res = resolver
.query_resource_by_str(&did_url, DidCheqdParser::parse(&did_url).unwrap())
.await;
println!("res: {res:?}");
assert!(res.is_ok());
}
#[tokio::test]
async fn test_resolve_did_version_id() {
let did = "did:cheqd:testnet:ac2b9027-ec1a-4ee2-aad1-1e316e7d6f59/versions/ff82cc93-25fd-493a-8896-9303a9c8383d".to_string();
let resolver = DidCheqdResolver::new(Default::default());
let res = resolver
.query_did_doc_by_str(&did, DidCheqdParser::parse(&did).unwrap())
.await;
println!("res: {res:?}");
assert!(res.is_ok());
}
}