did_resolver_cheqd/resolution/
resolver.rs1use std::{cmp::Ordering, collections::HashMap};
2
3use chrono::{DateTime, Utc};
4use tokio::sync::Mutex;
5use tonic::transport::{Channel, ClientTlsConfig, Endpoint};
6
7use crate::{
9 error::{DidCheqdError, DidCheqdResult},
10 proto::cheqd::{
11 did::v2::{
12 QueryDidDocRequest, QueryDidDocVersionRequest,
13 query_client::QueryClient as DidQueryClient,
14 },
15 resource::v2::{
16 Metadata as CheqdResourceMetadata, QueryCollectionResourcesRequest,
17 QueryResourceRequest, query_client::QueryClient as ResourceQueryClient,
18 },
19 },
20 resolution::parser::DidCheqdParsed,
21};
22
23pub const MAINNET_NAMESPACE: &str = "mainnet";
25pub const MAINNET_DEFAULT_GRPC: &str = "https://grpc.cheqd.net:443";
27pub const TESTNET_NAMESPACE: &str = "testnet";
29pub const TESTNET_DEFAULT_GRPC: &str = "https://grpc.cheqd.network:443";
31
32pub struct DidCheqdResolverConfiguration {
34 pub networks: Vec<NetworkConfiguration>,
36}
37
38impl Default for DidCheqdResolverConfiguration {
39 fn default() -> Self {
40 Self {
41 networks: vec![
42 NetworkConfiguration::mainnet(),
43 NetworkConfiguration::testnet(),
44 ],
45 }
46 }
47}
48
49pub struct NetworkConfiguration {
51 pub grpc_url: String,
53 pub namespace: String,
55}
56
57impl Clone for NetworkConfiguration {
58 fn clone(&self) -> Self {
59 Self {
60 grpc_url: self.grpc_url.clone(),
61 namespace: self.namespace.clone(),
62 }
63 }
64}
65
66impl Clone for DidCheqdResolverConfiguration {
67 fn clone(&self) -> Self {
68 Self {
69 networks: self.networks.clone(),
70 }
71 }
72}
73
74impl NetworkConfiguration {
75 pub fn mainnet() -> Self {
77 Self {
78 grpc_url: String::from(MAINNET_DEFAULT_GRPC),
79 namespace: String::from(MAINNET_NAMESPACE),
80 }
81 }
82
83 pub fn testnet() -> Self {
85 Self {
86 grpc_url: String::from(TESTNET_DEFAULT_GRPC),
87 namespace: String::from(TESTNET_NAMESPACE),
88 }
89 }
90}
91
92#[derive(Clone)]
93struct CheqdGrpcClient {
94 did: DidQueryClient<Channel>,
95 resources: ResourceQueryClient<Channel>,
96}
97
98pub struct DidCheqdResolver {
99 networks: Vec<NetworkConfiguration>,
100 network_clients: Mutex<HashMap<String, CheqdGrpcClient>>,
101}
102
103impl DidCheqdResolver {
109 pub fn new(configuration: DidCheqdResolverConfiguration) -> Self {
114 Self {
115 networks: configuration.networks,
116 network_clients: Default::default(),
117 }
118 }
119
120 async fn client_for_network(&self, network: &str) -> DidCheqdResult<CheqdGrpcClient> {
122 let mut lock = self.network_clients.lock().await;
123 if let Some(client) = lock.get(network) {
124 return Ok(client.clone());
125 }
126
127 let network_config = self
128 .networks
129 .iter()
130 .find(|n| n.namespace == network)
131 .ok_or(DidCheqdError::NetworkNotSupported(network.to_owned()))?;
132
133 let endpoint = Endpoint::new(network_config.grpc_url.to_string())
134 .map_err(|_e| DidCheqdError::BadConfiguration("Failed to parse GRPC url".to_string()))?
135 .tls_config(ClientTlsConfig::new().with_webpki_roots())
136 .map_err(|e| DidCheqdError::TransportError(Box::new(e)))?;
137
138 let channel = endpoint
140 .connect()
141 .await
142 .map_err(|e| DidCheqdError::TransportError(Box::new(e)))?;
143
144 let did_client = DidQueryClient::new(channel.clone());
145 let resource_client = ResourceQueryClient::new(channel);
146
147 let client = CheqdGrpcClient {
148 did: did_client,
149 resources: resource_client,
150 };
151
152 lock.insert(network.to_owned(), client.clone());
153
154 Ok(client)
155 }
156
157 pub async fn query_did_doc_by_str(
160 &self,
161 _did_str: &str,
162 parsed_did: DidCheqdParsed,
163 ) -> DidCheqdResult<(
164 crate::proto::cheqd::did::v2::DidDoc,
165 Option<crate::proto::cheqd::did::v2::Metadata>,
166 )> {
167 let network = parsed_did.namespace.as_str();
169 let mut client = self.client_for_network(network).await?;
170
171 if parsed_did.version.is_some() {
172 let request = tonic::Request::new(QueryDidDocVersionRequest {
173 id: parsed_did.did.to_string(),
174 version: parsed_did.version.unwrap(),
175 });
176 let response = client
177 .did
178 .did_doc_version(request)
179 .await
180 .map_err(|e| DidCheqdError::NonSuccessResponse(Box::new(e)))?;
181 let query_response = response.into_inner();
182 let query_doc_res = query_response.value.ok_or(DidCheqdError::InvalidResponse(
183 "DIDDoc query did version not return a value".into(),
184 ))?;
185 let query_doc = query_doc_res.did_doc.ok_or(DidCheqdError::InvalidResponse(
186 "DIDDoc query did version not return a DIDDoc".into(),
187 ))?;
188
189 Ok((query_doc, query_doc_res.metadata))
190 } else {
191 let request = tonic::Request::new(QueryDidDocRequest {
192 id: parsed_did.did.to_string(),
193 });
194 let response = client
195 .did
196 .did_doc(request)
197 .await
198 .map_err(|e| DidCheqdError::NonSuccessResponse(Box::new(e)))?;
199 let query_response = response.into_inner();
200 let query_doc_res = query_response.value.ok_or(DidCheqdError::InvalidResponse(
201 "DIDDoc query did not return a value".into(),
202 ))?;
203 let query_doc = query_doc_res.did_doc.ok_or(DidCheqdError::InvalidResponse(
204 "DIDDoc query did not return a DIDDoc".into(),
205 ))?;
206
207 Ok((query_doc, query_doc_res.metadata))
208 }
209 }
210
211 pub async fn query_resource_by_str(
216 &self,
217 did_url: &str,
218 parsed_did: DidCheqdParsed,
219 ) -> DidCheqdResult<(Vec<u8>, Option<String>)> {
220 let network = parsed_did.namespace.as_str();
222 let did_id = parsed_did.id.as_str();
223
224 if let Some(ref qmap) = parsed_did.query {
226 if let Some(resource_id) = qmap.get("resourceId") {
227 return self
228 .resolve_resource_by_id(did_id, resource_id.as_str(), network)
229 .await;
230 }
231 }
232
233 if let Some(qmap) = parsed_did.query {
235 let resource_name = qmap.get("resourceName");
236 let resource_type = qmap.get("resourceType");
237 let version_time = qmap.get("resourceVersionTime");
238
239 let (Some(resource_name), Some(resource_type)) = (resource_name, resource_type) else {
240 return Err(DidCheqdError::InvalidDidUrl(format!(
241 "Resolver can only resolve by exact resource ID or name+type combination {did_url}"
242 )));
243 };
244
245 let version_time = match version_time {
246 Some(v) => DateTime::parse_from_rfc3339(v)
247 .map_err(|e| DidCheqdError::InvalidDidUrl(e.to_string()))?
248 .to_utc(),
249 None => Utc::now(),
250 };
251
252 return self
253 .resolve_resource_by_name_type_and_time(
254 did_id,
255 resource_name.as_str(),
256 resource_type.as_str(),
257 version_time,
258 network,
259 )
260 .await;
261 }
262
263 Err(DidCheqdError::InvalidDidUrl(format!(
264 "No resource path or query present: {did_url}"
265 )))
266 }
267
268 async fn resolve_resource_by_id(
270 &self,
271 did_id: &str,
272 resource_id: &str,
273 network: &str,
274 ) -> DidCheqdResult<(Vec<u8>, Option<String>)> {
275 let mut client = self.client_for_network(network).await?;
276 let request = QueryResourceRequest {
277 collection_id: did_id.to_owned(),
278 id: resource_id.to_owned(),
279 };
280 let response = client
281 .resources
282 .resource(request)
283 .await
284 .map_err(|e| DidCheqdError::NonSuccessResponse(Box::new(e)))?;
285
286 let query_response = response.into_inner();
287 let query_response = query_response
288 .resource
289 .ok_or(DidCheqdError::InvalidResponse(
290 "Resource query did not return a value".into(),
291 ))?;
292 let query_resource = query_response
293 .resource
294 .ok_or(DidCheqdError::InvalidResponse(
295 "Resource query did not return a resource".into(),
296 ))?;
297 let query_metadata = query_response
298 .metadata
299 .ok_or(DidCheqdError::InvalidResponse(
300 "Resource query did not return metadata".into(),
301 ))?;
302
303 let media_type =
304 (!query_metadata.media_type.trim().is_empty()).then_some(query_metadata.media_type);
305
306 Ok((query_resource.data, media_type))
307 }
308
309 async fn resolve_resource_by_name_type_and_time(
312 &self,
313 did_id: &str,
314 name: &str,
315 rtyp: &str,
316 time: DateTime<Utc>,
317 network: &str,
318 ) -> DidCheqdResult<(Vec<u8>, Option<String>)> {
319 let mut client = self.client_for_network(network).await?;
320
321 let response = client
322 .resources
323 .collection_resources(QueryCollectionResourcesRequest {
324 collection_id: did_id.to_owned(),
325 pagination: None,
327 })
328 .await
329 .map_err(|e| DidCheqdError::NonSuccessResponse(Box::new(e)))?;
330
331 let query_response = response.into_inner();
332 let resources = query_response.resources;
333 let mut filtered: Vec<_> =
334 filter_resources_by_name_and_type(resources.iter(), name, rtyp).collect();
335 filtered.sort_by(|a, b| desc_chronological_sort_resources(a, b));
336
337 let resource_meta = find_resource_just_before_time(filtered.into_iter(), time);
338
339 let Some(meta) = resource_meta else {
340 return Err(DidCheqdError::ResourceNotFound(format!(
341 "network: {network}, collection: {did_id}, name: {name}, type: {rtyp}, time: \
342 {time}"
343 )));
344 };
345
346 let (data, media) = self
347 .resolve_resource_by_id(did_id, &meta.id, network)
348 .await?;
349 Ok((data, media))
350 }
351}
352
353fn filter_resources_by_name_and_type<'a>(
355 resources: impl Iterator<Item = &'a CheqdResourceMetadata> + 'a,
356 name: &'a str,
357 rtyp: &'a str,
358) -> impl Iterator<Item = &'a CheqdResourceMetadata> + 'a {
359 resources.filter(move |r| r.name == name && r.resource_type == rtyp)
360}
361
362fn desc_chronological_sort_resources(
364 b: &CheqdResourceMetadata,
365 a: &CheqdResourceMetadata,
366) -> Ordering {
367 let (a_secs, a_ns) = a
368 .created
369 .map(|v| {
370 let v = v.normalized();
371 (v.seconds, v.nanos)
372 })
373 .unwrap_or((0, 0));
374 let (b_secs, b_ns) = b
375 .created
376 .map(|v| {
377 let v = v.normalized();
378 (v.seconds, v.nanos)
379 })
380 .unwrap_or((0, 0));
381
382 match a_secs.cmp(&b_secs) {
383 Ordering::Equal => a_ns.cmp(&b_ns),
384 res => res,
385 }
386}
387
388fn find_resource_just_before_time<'a>(
402 resources: impl Iterator<Item = &'a CheqdResourceMetadata>,
403 before_time: DateTime<Utc>,
404) -> Option<&'a CheqdResourceMetadata> {
405 let before_epoch = before_time.timestamp();
406
407 for r in resources {
408 let Some(created) = r.created else {
409 continue;
410 };
411
412 let created_epoch = created.normalized().seconds;
413 if created_epoch < before_epoch {
414 return Some(r);
415 }
416 }
417
418 None
419}
420
421#[cfg(test)]
422mod unit_tests {
423 use crate::resolution::parser::DidCheqdParser;
424
425 use super::*;
426
427 #[tokio::test]
428 async fn test_resolve_fails_if_no_network_config() {
429 let did = "did:cheqd:devnet:Ps1ysXP2Ae6GBfxNhNQNKN";
430 let resolver = DidCheqdResolver::new(Default::default());
431 let e = resolver
432 .query_did_doc_by_str(did, DidCheqdParser::parse(did).unwrap())
433 .await
434 .unwrap_err();
435 assert!(matches!(e, DidCheqdError::NetworkNotSupported(_)));
436 }
437
438 #[tokio::test]
439 async fn test_resolve_fails_if_bad_network_uri() {
440 let did = "did:cheqd:devnet:Ps1ysXP2Ae6GBfxNhNQNKN";
441 let config = DidCheqdResolverConfiguration {
442 networks: vec![NetworkConfiguration {
443 grpc_url: "@baduri://.".into(),
444 namespace: "devnet".into(),
445 }],
446 };
447
448 let resolver = DidCheqdResolver::new(config);
449 let e = resolver
450 .query_did_doc_by_str(did, DidCheqdParser::parse(did).unwrap())
451 .await
452 .unwrap_err();
453 assert!(matches!(e, DidCheqdError::BadConfiguration(_)));
454 }
455
456 #[tokio::test]
457 async fn test_resolve_resource_fails_if_no_query() {
458 let url = "did:cheqd:mainnet:zF7rhDBfUt9d1gJPjx7s1J";
459 let resolver = DidCheqdResolver::new(Default::default());
460 let e = resolver
461 .query_resource_by_str(url, DidCheqdParser::parse(url).unwrap())
462 .await
463 .unwrap_err();
464 assert!(matches!(e, DidCheqdError::InvalidDidUrl(_)));
465 }
466
467 #[tokio::test]
468 async fn test_resolve_resource_fails_if_incomplete_query() {
469 let url = "did:cheqd:mainnet:zF7rhDBfUt9d1gJPjx7s1j?resourceName=asdf";
470 let resolver = DidCheqdResolver::new(Default::default());
471 let e = resolver
472 .query_resource_by_str(url, DidCheqdParser::parse(url).unwrap())
473 .await
474 .unwrap_err();
475 assert!(matches!(e, DidCheqdError::InvalidDidUrl(_)));
476 }
477
478 #[tokio::test]
479 async fn test_resolve_resource_fails_if_invalid_resource_time() {
480 let url = "did:cheqd:mainnet:zF7rhDBfUt9d1gJPjx7s1J?resourceName=asdf&resourceType=fdsa&resourceVersionTime=12341234";
482 let resolver = DidCheqdResolver::new(Default::default());
483 let e = resolver
484 .query_resource_by_str(url, DidCheqdParser::parse(url).unwrap())
485 .await
486 .unwrap_err();
487 assert!(matches!(e, DidCheqdError::InvalidDidUrl(_)));
488 }
489
490 #[tokio::test]
491 async fn test_resolve_did_success() {
492 let did = "did:cheqd:testnet:f5101dd8-447f-40a7-a9b8-700abeba389a".to_string();
494 let resolver = DidCheqdResolver::new(Default::default());
495 let res = resolver
496 .query_did_doc_by_str(&did, DidCheqdParser::parse(&did).unwrap())
497 .await;
498 println!("res: {:#?}", res);
499 assert!(res.is_ok());
500 }
501
502 #[tokio::test]
503 async fn test_resolve_resource_id_success() {
504 let did_url = "did:cheqd:testnet:f5101dd8-447f-40a7-a9b8-700abeba389a/resources/6155f8bc-d9c9-4e83-a1bb-453744fe5438".to_string();
506 let resolver = DidCheqdResolver::new(Default::default());
507 let res = resolver
508 .query_resource_by_str(&did_url, DidCheqdParser::parse(&did_url).unwrap())
509 .await;
510 println!("res: {res:?}");
511 assert!(res.is_ok());
512 }
513
514 #[tokio::test]
515 async fn test_resolve_resource_query_success() {
516 let did_url = "did:cheqd:testnet:f5101dd8-447f-40a7-a9b8-700abeba389a?resourceName=Patient ID 85905-Schema&resourceType=anonCredsSchema".to_string();
518 let resolver = DidCheqdResolver::new(Default::default());
519 let res = resolver
520 .query_resource_by_str(&did_url, DidCheqdParser::parse(&did_url).unwrap())
521 .await;
522 println!("res: {res:?}");
523 assert!(res.is_ok());
524 }
525
526 #[tokio::test]
527 async fn test_resolve_did_version_id() {
528 let did = "did:cheqd:testnet:ac2b9027-ec1a-4ee2-aad1-1e316e7d6f59/versions/ff82cc93-25fd-493a-8896-9303a9c8383d".to_string();
530 let resolver = DidCheqdResolver::new(Default::default());
531 let res = resolver
532 .query_did_doc_by_str(&did, DidCheqdParser::parse(&did).unwrap())
533 .await;
534 println!("res: {res:?}");
535 assert!(res.is_ok());
536 }
537}