affinidi_did_resolver_cache_sdk/
lib.rs1#[cfg(all(feature = "network", target_arch = "wasm32"))]
16compile_error!("Cannot enable both features at the same time");
17
18use affinidi_did_common::Document;
19use config::DIDCacheConfig;
20use errors::DIDCacheError;
21use highway::{HighwayHash, HighwayHasher};
22use moka::future::Cache;
23#[cfg(feature = "network")]
24use networking::{
25 WSRequest,
26 network::{NetworkTask, WSCommands},
27};
28#[cfg(feature = "network")]
29use std::sync::Arc;
30use std::{fmt, time::Duration};
31#[cfg(feature = "network")]
32use tokio::sync::{Mutex, mpsc};
33use tracing::debug;
34use wasm_bindgen::JsValue;
35use wasm_bindgen::prelude::*;
36
37pub mod config;
38pub mod errors;
39#[cfg(feature = "network")]
40pub mod networking;
41mod resolver;
42
43#[derive(Clone, Debug, PartialEq, Eq, Hash)]
45#[wasm_bindgen]
46pub enum DIDMethod {
47 ETHR,
48 JWK,
49 KEY,
50 PEER,
51 PKH,
52 WEB,
53 WEBVH,
54 CHEQD,
55 SCID,
56 EXAMPLE,
57}
58
59impl fmt::Display for DIDMethod {
61 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62 match self {
63 DIDMethod::ETHR => write!(f, "ethr"),
64 DIDMethod::JWK => write!(f, "jwk"),
65 DIDMethod::KEY => write!(f, "key"),
66 DIDMethod::PEER => write!(f, "peer"),
67 DIDMethod::PKH => write!(f, "pkh"),
68 DIDMethod::WEB => write!(f, "web"),
69 DIDMethod::WEBVH => write!(f, "webvh"),
70 DIDMethod::CHEQD => write!(f, "cheqd"),
71 DIDMethod::SCID => write!(f, "scid"),
72 DIDMethod::EXAMPLE => write!(f, "example"),
73 }
74 }
75}
76
77impl TryFrom<String> for DIDMethod {
79 type Error = DIDCacheError;
80
81 fn try_from(value: String) -> Result<Self, Self::Error> {
82 value.as_str().try_into()
83 }
84}
85
86impl TryFrom<&str> for DIDMethod {
87 type Error = DIDCacheError;
88
89 fn try_from(value: &str) -> Result<Self, Self::Error> {
90 match value.to_lowercase().as_str() {
91 "ethr" => Ok(DIDMethod::ETHR),
92 "jwk" => Ok(DIDMethod::JWK),
93 "key" => Ok(DIDMethod::KEY),
94 "peer" => Ok(DIDMethod::PEER),
95 "pkh" => Ok(DIDMethod::PKH),
96 "web" => Ok(DIDMethod::WEB),
97 "webvh" => Ok(DIDMethod::WEBVH),
98 "cheqd" => Ok(DIDMethod::CHEQD),
99 "scid" => Ok(DIDMethod::SCID),
100 #[cfg(feature = "did_example")]
101 "example" => Ok(DIDMethod::EXAMPLE),
102 _ => Err(DIDCacheError::UnsupportedMethod(value.to_string())),
103 }
104 }
105}
106
107pub struct ResolveResponse {
108 pub did: String,
109 pub method: DIDMethod,
110 pub did_hash: [u64; 2],
111 pub doc: Document,
112 pub cache_hit: bool,
113}
114
115#[wasm_bindgen(getter_with_clone)]
123#[derive(Clone)]
124pub struct DIDCacheClient {
125 config: DIDCacheConfig,
126 cache: Cache<[u64; 2], Document>,
127 #[cfg(feature = "network")]
128 network_task_tx: Option<mpsc::Sender<WSCommands>>,
129 #[cfg(feature = "network")]
130 network_task_rx: Option<Arc<Mutex<mpsc::Receiver<WSCommands>>>>,
131 #[cfg(feature = "did_example")]
132 did_example_cache: did_example::DiDExampleCache,
133}
134
135impl DIDCacheClient {
136 pub async fn resolve(&self, did: &str) -> Result<ResolveResponse, DIDCacheError> {
142 use affinidi_did_common::DID;
143
144 if did.len() > self.config.max_did_size_in_bytes {
146 return Err(DIDCacheError::DIDError(format!(
147 "The DID size of {}bytes exceeds the limit of {1}. Please ensure the size is less than {1}.",
148 did.len(),
149 self.config.max_did_size_in_bytes
150 )));
151 }
152
153 let parts: Vec<&str> = did.split(':').collect();
154 if parts.len() < 3 {
155 return Err(DIDCacheError::DIDError(format!(
156 "did isn't to spec! did ({did})",
157 )));
158 }
159
160 let key_parts: Vec<&str> = parts.last().unwrap().split(".").collect();
161 if key_parts.len() > self.config.max_did_parts {
162 return Err(DIDCacheError::DIDError(format!(
163 "The total number of keys and/or services must be less than or equal to {:?}, but {:?} were found.",
164 self.config.max_did_parts,
165 parts.len()
166 )));
167 }
168
169 let parsed_did: DID = did
171 .parse()
172 .map_err(|e| DIDCacheError::DIDError(format!("Failed to parse DID: {e}")))?;
173
174 let hash = DIDCacheClient::hash_did(did);
175
176 #[cfg(feature = "did_example")]
177 if parts[1] == "example"
179 && let Some(doc) = self.did_example_cache.get(did)
180 {
181 return Ok(ResolveResponse {
182 did: did.to_string(),
183 method: parts[1].try_into()?,
184 did_hash: hash,
185 doc: doc.clone(),
186 cache_hit: true,
187 });
188 }
189
190 if let Some(doc) = self.cache.get(&hash).await {
192 debug!("found did ({}) in cache", did);
193 Ok(ResolveResponse {
194 did: did.to_string(),
195 method: parts[1].try_into()?,
196 did_hash: hash,
197 doc,
198 cache_hit: true,
199 })
200 } else {
201 debug!("did ({}) NOT in cache hash ({:#?})", did, hash);
202 #[cfg(feature = "network")]
204 let doc = {
205 if self.config.service_address.is_some() {
206 self.network_resolve(did, hash).await?
207 } else {
208 self.local_resolve(&parsed_did).await?
209 }
210 };
211
212 #[cfg(not(feature = "network"))]
213 let doc = self.local_resolve(&parsed_did).await?;
214
215 debug!("adding did ({}) to cache ({:#?})", did, hash);
216 self.cache.insert(hash, doc.clone()).await;
217 Ok(ResolveResponse {
218 did: did.to_string(),
219 method: parts[1].try_into()?,
220 did_hash: hash,
221 doc,
222 cache_hit: false,
223 })
224 }
225 }
226
227 pub fn get_cache(&self) -> Cache<[u64; 2], Document> {
231 self.cache.clone()
232 }
233
234 #[cfg(feature = "network")]
236 pub fn stop(&self) {
237 if let Some(tx) = self.network_task_tx.as_ref() {
238 let _ = tx.blocking_send(WSCommands::Exit);
239 }
240 }
241
242 pub async fn remove(&self, did: &str) -> Option<Document> {
245 self.cache.remove(&DIDCacheClient::hash_did(did)).await
246 }
247
248 pub async fn add_did_document(&mut self, did: &str, doc: Document) {
250 let hash = DIDCacheClient::hash_did(did);
251 debug!("manually adding did ({}) hash({:#?}) to cache", did, hash);
252 self.cache.insert(hash, doc).await;
253 }
254
255 pub fn hash_did(did: &str) -> [u64; 2] {
257 HighwayHasher::default().hash128(did.as_bytes())
259 }
260}
261
262#[wasm_bindgen]
264impl DIDCacheClient {
265 pub async fn new(config: DIDCacheConfig) -> Result<DIDCacheClient, DIDCacheError> {
273 let cache = Cache::builder()
275 .max_capacity(config.cache_capacity.into())
276 .time_to_live(Duration::from_secs(config.cache_ttl.into()))
277 .build();
278
279 #[cfg(feature = "network")]
280 let mut client = Self {
281 config,
282 cache,
283 network_task_tx: None,
284 network_task_rx: None,
285 #[cfg(feature = "did_example")]
286 did_example_cache: did_example::DiDExampleCache::new(),
287 };
288 #[cfg(not(feature = "network"))]
289 let client = Self {
290 config,
291 cache,
292 #[cfg(feature = "did_example")]
293 did_example_cache: did_example::DiDExampleCache::new(),
294 };
295
296 #[cfg(feature = "network")]
297 {
298 if client.config.service_address.is_some() {
299 let (sdk_tx, mut task_rx) = mpsc::channel(32);
303 let (task_tx, sdk_rx) = mpsc::channel(32);
305
306 client.network_task_tx = Some(sdk_tx);
307 client.network_task_rx = Some(Arc::new(Mutex::new(sdk_rx)));
308
309 let _config = client.config.clone();
311 tokio::spawn(async move {
312 let _ = NetworkTask::run(_config, &mut task_rx, &task_tx).await;
313 });
314
315 if let Some(arc_rx) = client.network_task_rx.as_ref() {
316 let mut rx = arc_rx.lock().await;
318 rx.recv().await.unwrap();
319 }
320 }
321 }
322
323 Ok(client)
324 }
325
326 pub async fn wasm_resolve(&self, did: &str) -> Result<JsValue, DIDCacheError> {
327 let response = self.resolve(did).await?;
328
329 match serde_wasm_bindgen::to_value(&response.doc) {
330 Ok(values) => Ok(values),
331 Err(err) => Err(DIDCacheError::DIDError(format!(
332 "Error serializing DID Document: {err}",
333 ))),
334 }
335 }
336
337 #[cfg(feature = "did_example")]
338 pub fn add_example_did(&mut self, doc: &str) -> Result<(), DIDCacheError> {
339 self.did_example_cache
340 .insert_from_string(doc)
341 .map_err(|e| DIDCacheError::DIDError(format!("Couldn't parse example DID: {e}")))
342 }
343}
344
345#[cfg(test)]
346mod tests {
347 use super::*;
348
349 const DID_KEY: &str = "did:key:z6MkiToqovww7vYtxm1xNM15u9JzqzUFZ1k7s7MazYJUyAxv";
350
351 async fn basic_local_client() -> DIDCacheClient {
352 let config = config::DIDCacheConfigBuilder::default().build();
353 DIDCacheClient::new(config).await.unwrap()
354 }
355
356 #[tokio::test]
357 async fn remove_existing_cached_did() {
358 let client = basic_local_client().await;
359
360 let response = client.resolve(DID_KEY).await.unwrap();
362 let removed_doc = client.remove(DID_KEY).await;
363 assert_eq!(removed_doc, Some(response.doc));
364 }
365
366 #[tokio::test]
367 async fn remove_non_existing_cached_did() {
368 let client = basic_local_client().await;
369
370 let removed_doc = client.remove(DID_KEY).await;
372 assert_eq!(removed_doc, None);
373 }
374}