identity_resolver/resolution/
resolver.rs

1// Copyright 2020-2025 IOTA Stiftung, Fondazione LINKS
2// SPDX-License-Identifier: Apache-2.0
3
4use core::future::Future;
5use futures::stream::FuturesUnordered;
6use futures::TryStreamExt;
7use identity_did::DIDCompositeJwk;
8use identity_did::DIDJwk;
9use identity_did::DID;
10use std::collections::HashSet;
11
12use identity_document::document::CoreDocument;
13use std::collections::HashMap;
14use std::marker::PhantomData;
15
16use crate::Error;
17use crate::ErrorCause;
18use crate::Result;
19
20use super::commands::Command;
21use super::commands::SendSyncCommand;
22use super::commands::SingleThreadedCommand;
23
24/// Convenience type for resolving DID documents from different DID methods.
25///
26/// # Configuration
27///
28/// The resolver will only be able to resolve DID documents for methods it has been configured for. This is done by
29/// attaching method specific handlers with [`Self::attach_handler`](Self::attach_handler()).
30pub struct Resolver<DOC = CoreDocument, CMD = SendSyncCommand<DOC>>
31where
32  CMD: for<'r> Command<'r, Result<DOC>>,
33{
34  command_map: HashMap<String, CMD>,
35  _required: PhantomData<DOC>,
36}
37
38impl<M, DOC> Resolver<DOC, M>
39where
40  M: for<'r> Command<'r, Result<DOC>>,
41{
42  /// Constructs a new [`Resolver`].
43  ///
44  /// # Example
45  ///
46  /// Construct a `Resolver` that resolves DID documents of type
47  /// [`CoreDocument`](::identity_document::document::CoreDocument).
48  ///  ```
49  /// # use identity_resolver::Resolver;
50  /// # use identity_document::document::CoreDocument;
51  ///
52  /// let mut resolver = Resolver::<CoreDocument>::new();
53  /// // Now attach some handlers whose output can be converted to a `CoreDocument`.
54  /// ```
55  pub fn new() -> Self {
56    Self {
57      command_map: HashMap::new(),
58      _required: PhantomData::<DOC>,
59    }
60  }
61
62  /// Fetches the DID Document of the given DID.
63  ///
64  /// # Errors
65  ///
66  /// Errors if the resolver has not been configured to handle the method corresponding to the given DID or the
67  /// resolution process itself fails.
68  ///
69  /// ## Example
70  ///
71  /// ```
72  /// # use identity_resolver::Resolver;
73  /// # use identity_did::CoreDID;
74  /// # use identity_document::document::CoreDocument;
75  ///
76  /// async fn configure_and_resolve(
77  ///   did: CoreDID,
78  /// ) -> std::result::Result<CoreDocument, Box<dyn std::error::Error>> {
79  ///   let resolver: Resolver = configure_resolver(Resolver::new());
80  ///   let resolved_doc: CoreDocument = resolver.resolve(&did).await?;
81  ///   Ok(resolved_doc)
82  /// }
83  ///
84  /// fn configure_resolver(mut resolver: Resolver) -> Resolver {
85  ///   resolver.attach_handler("foo".to_owned(), resolve_foo);
86  ///   // Attach handlers for other DID methods we are interested in.
87  ///   resolver
88  /// }
89  ///
90  /// async fn resolve_foo(did: CoreDID) -> std::result::Result<CoreDocument, std::io::Error> {
91  ///   todo!()
92  /// }
93  /// ```
94  pub async fn resolve<D: DID>(&self, did: &D) -> Result<DOC> {
95    let method: &str = did.method();
96    let delegate: &M = self
97      .command_map
98      .get(method)
99      .ok_or_else(|| ErrorCause::UnsupportedMethodError {
100        method: method.to_owned(),
101      })
102      .map_err(Error::new)?;
103
104    delegate.apply(did.as_str()).await
105  }
106
107  /// Concurrently fetches the DID Documents of the multiple given DIDs.
108  ///
109  /// # Errors
110  /// * If the resolver has not been configured to handle the method of any of the given DIDs.
111  /// * If the resolution process of any DID fails.
112  ///
113  /// ## Note
114  /// * If `dids` contains duplicates, these will be resolved only once.
115  pub async fn resolve_multiple<D: DID>(&self, dids: &[D]) -> Result<HashMap<D, DOC>> {
116    let futures = FuturesUnordered::new();
117
118    // Create set to remove duplicates to avoid unnecessary resolution.
119    let dids_set: HashSet<D> = dids.iter().cloned().collect();
120    for did in dids_set {
121      futures.push(async move {
122        let doc = self.resolve(&did).await;
123        doc.map(|doc| (did, doc))
124      });
125    }
126
127    let documents: HashMap<D, DOC> = futures.try_collect().await?;
128
129    Ok(documents)
130  }
131}
132
133impl<DOC: 'static> Resolver<DOC, SendSyncCommand<DOC>> {
134  /// Attach a new handler responsible for resolving DIDs of the given DID method.
135  ///
136  /// The `handler` is expected to be a closure taking an owned DID and asynchronously returning a DID Document
137  /// which can be converted to the type this [`Resolver`] is parametrized over. The `handler` is required to be
138  /// [`Clone`], [`Send`], [`Sync`] and `'static` hence all captured variables must satisfy these bounds. In this regard
139  /// the `move` keyword and (possibly) wrapping values in an [`Arc`](std::sync::Arc) may come in handy (see the example
140  /// below).
141  ///
142  /// NOTE: If there already exists a handler for this method then it will be replaced with the new handler.
143  /// In the case where one would like to have a "backup handler" for the same DID method, one can achieve this with
144  /// composition.
145  ///
146  /// # Example
147  /// ```
148  /// # use identity_resolver::Resolver;
149  /// # use identity_did::CoreDID;
150  /// # use identity_document::document::CoreDocument;
151  ///
152  ///    // A client that can resolve DIDs of our invented "foo" method.
153  ///    struct Client;
154  ///
155  ///    impl Client {
156  ///      // Resolves some of the DIDs we are interested in.
157  ///      async fn resolve(&self, _did: &CoreDID) -> std::result::Result<CoreDocument, std::io::Error> {
158  ///        todo!()
159  ///      }
160  ///    }
161  ///
162  ///    // This way we can essentially produce (cheap) clones of our client.
163  ///    let client = std::sync::Arc::new(Client {});
164  ///
165  ///    // Get a clone we can move into a handler.
166  ///    let client_clone = client.clone();
167  ///
168  ///    // Construct a resolver that resolves documents of type `CoreDocument`.
169  ///    let mut resolver = Resolver::<CoreDocument>::new();
170  ///
171  ///    // Now we want to attach a handler that uses the client to resolve DIDs whose method is "foo".
172  ///    resolver.attach_handler("foo".to_owned(), move |did: CoreDID| {
173  ///      // We want to resolve the did asynchronously, but since we do not know when it will be awaited we
174  ///      // let the future take ownership of the client by moving a clone into the asynchronous block.
175  ///      let future_client = client_clone.clone();
176  ///      async move { future_client.resolve(&did).await }
177  ///    });
178  /// ```
179  pub fn attach_handler<D, F, Fut, DOCUMENT, E, DIDERR>(&mut self, method: String, handler: F)
180  where
181    D: DID + Send + for<'r> TryFrom<&'r str, Error = DIDERR> + 'static,
182    DOCUMENT: 'static + Into<DOC>,
183    F: Fn(D) -> Fut + 'static + Clone + Send + Sync,
184    Fut: Future<Output = std::result::Result<DOCUMENT, E>> + Send,
185    E: Into<Box<dyn std::error::Error + Send + Sync + 'static>>,
186    DIDERR: Into<Box<dyn std::error::Error + Send + Sync + 'static>>,
187  {
188    let command = SendSyncCommand::new(handler);
189    self.command_map.insert(method, command);
190  }
191}
192
193impl<DOC: 'static> Resolver<DOC, SingleThreadedCommand<DOC>> {
194  /// Attach a new handler responsible for resolving DIDs of the given DID method.
195  ///
196  /// The `handler` is expected to be a closure taking an owned DID and asynchronously returning a DID Document
197  /// which can be converted to the type this [`Resolver`] is parametrized over. The `handler` is required to be
198  /// [`Clone`] and `'static`  hence all captured variables must satisfy these bounds. In this regard the
199  /// `move` keyword and (possibly) wrapping values in an [`std::rc::Rc`] may come in handy (see the example below).
200  ///
201  /// NOTE: If there already exists a handler for this method then it will be replaced with the new handler.
202  /// In the case where one would like to have a "backup handler" for the same DID method, one can achieve this with
203  /// composition.
204  ///
205  /// # Example
206  /// ```
207  /// # use identity_resolver::SingleThreadedResolver;
208  /// # use identity_did::CoreDID;
209  /// # use identity_document::document::CoreDocument;
210  ///
211  ///    // A client that can resolve DIDs of our invented "foo" method.
212  ///    struct Client;
213  ///
214  ///    impl Client {
215  ///      // Resolves some of the DIDs we are interested in.
216  ///      async fn resolve(&self, _did: &CoreDID) -> std::result::Result<CoreDocument, std::io::Error> {
217  ///        todo!()
218  ///      }
219  ///    }
220  ///
221  ///    // This way we can essentially produce (cheap) clones of our client.
222  ///    let client = std::rc::Rc::new(Client {});
223  ///
224  ///    // Get a clone we can move into a handler.
225  ///    let client_clone = client.clone();
226  ///
227  ///    // Construct a resolver that resolves documents of type `CoreDocument`.
228  ///    let mut resolver = SingleThreadedResolver::<CoreDocument>::new();
229  ///
230  ///    // Now we want to attach a handler that uses the client to resolve DIDs whose method is "foo".
231  ///    resolver.attach_handler("foo".to_owned(), move |did: CoreDID| {
232  ///      // We want to resolve the did asynchronously, but since we do not know when it will be awaited we
233  ///      // let the future take ownership of the client by moving a clone into the asynchronous block.
234  ///      let future_client = client_clone.clone();
235  ///      async move { future_client.resolve(&did).await }
236  ///    });
237  /// ```
238  pub fn attach_handler<D, F, Fut, DOCUMENT, E, DIDERR>(&mut self, method: String, handler: F)
239  where
240    D: DID + for<'r> TryFrom<&'r str, Error = DIDERR> + 'static,
241    DOCUMENT: 'static + Into<DOC>,
242    F: Fn(D) -> Fut + 'static + Clone,
243    Fut: Future<Output = std::result::Result<DOCUMENT, E>>,
244    E: Into<Box<dyn std::error::Error + Send + Sync + 'static>>,
245    DIDERR: Into<Box<dyn std::error::Error + Send + Sync + 'static>>,
246  {
247    let command = SingleThreadedCommand::new(handler);
248    self.command_map.insert(method, command);
249  }
250}
251
252impl<DOC: From<CoreDocument> + 'static> Resolver<DOC, SingleThreadedCommand<DOC>> {
253  /// Attaches a handler capable of resolving `did:jwk` DIDs.
254  pub fn attach_did_jwk_handler(&mut self) {
255    let handler = |did_jwk: DIDJwk| async move { CoreDocument::expand_did_jwk(did_jwk) };
256    self.attach_handler(DIDJwk::METHOD.to_string(), handler)
257  }
258}
259
260impl<DOC: From<CoreDocument> + 'static> Resolver<DOC, SendSyncCommand<DOC>> {
261  /// Attaches a handler capable of resolving `did:jwk` DIDs.
262  pub fn attach_did_jwk_handler(&mut self) {
263    let handler = |did_jwk: DIDJwk| async move { CoreDocument::expand_did_jwk(did_jwk) };
264    self.attach_handler(DIDJwk::METHOD.to_string(), handler)
265  }
266}
267
268impl<DOC: From<CoreDocument> + 'static> Resolver<DOC, SingleThreadedCommand<DOC>> {
269  /// Attaches a handler capable of resolving `did:compositejwk` DIDs.
270  pub fn attach_did_compositejwk_handler(&mut self) {
271    let handler =
272      |did_compositejwk: DIDCompositeJwk| async move { CoreDocument::expand_did_compositejwk(did_compositejwk) };
273    self.attach_handler(DIDCompositeJwk::METHOD.to_string(), handler)
274  }
275}
276
277impl<DOC: From<CoreDocument> + 'static> Resolver<DOC, SendSyncCommand<DOC>> {
278  /// Attaches a handler capable of resolving `did:compositejwk` DIDs.
279  pub fn attach_did_compositejwk_handler(&mut self) {
280    let handler =
281      |did_compositejwk: DIDCompositeJwk| async move { CoreDocument::expand_did_compositejwk(did_compositejwk) };
282    self.attach_handler(DIDCompositeJwk::METHOD.to_string(), handler)
283  }
284}
285
286#[cfg(all(feature = "iota", not(target_arch = "wasm32")))]
287mod iota_handler {
288  use crate::ErrorCause;
289
290  use super::Resolver;
291  use identity_document::document::CoreDocument;
292  use identity_iota_core::IotaDID;
293  use identity_iota_core::IotaDocument;
294  use std::sync::Arc;
295
296  mod iota_specific {
297    use identity_iota_core::DidResolutionHandler;
298    use std::collections::HashMap;
299
300    use super::*;
301
302    impl<DOC> Resolver<DOC>
303    where
304      DOC: From<IotaDocument> + AsRef<CoreDocument> + 'static,
305    {
306      /// Convenience method for attaching a new handler responsible for resolving IOTA DIDs.
307      ///
308      /// See also [`attach_handler`](Self::attach_handler).
309      pub fn attach_iota_handler<CLI>(&mut self, client: CLI)
310      where
311        CLI: DidResolutionHandler + Send + Sync + 'static,
312      {
313        let arc_client: Arc<CLI> = Arc::new(client);
314
315        let handler = move |did: IotaDID| {
316          let future_client = arc_client.clone();
317          async move { future_client.resolve_did(&did).await }
318        };
319
320        self.attach_handler(IotaDID::METHOD.to_owned(), handler);
321      }
322
323      /// Convenience method for attaching multiple handlers responsible for resolving IOTA DIDs
324      /// on multiple networks.
325      ///
326      ///
327      /// # Arguments
328      ///
329      /// * `clients` - A collection of tuples where each tuple contains the name of the network name and its
330      ///   corresponding client.
331      ///
332      /// # Examples
333      ///
334      /// ```ignore
335      /// // Assume `client1` and `client2` are instances of identity clients `IdentityClientReadOnly`.
336      /// attach_multiple_iota_handlers(vec![("client1", client1), ("client2", client2)]);
337      /// ```
338      ///
339      /// # See Also
340      /// - [`attach_handler`](Self::attach_handler).
341      ///
342      /// # Note
343      ///
344      /// - Using `attach_iota_handler` or `attach_handler` for the IOTA method would override all previously added
345      ///   clients.
346      /// - This function does not validate the provided configuration. Ensure that the provided network name
347      ///   corresponds with the client, possibly by using `client.network_name()`.
348      pub fn attach_multiple_iota_handlers<CLI, I>(&mut self, clients: I)
349      where
350        CLI: DidResolutionHandler + Send + Sync + 'static,
351        I: IntoIterator<Item = (&'static str, CLI)>,
352      {
353        let arc_clients = Arc::new(clients.into_iter().collect::<HashMap<&'static str, CLI>>());
354
355        let handler = move |did: IotaDID| {
356          let future_client = arc_clients.clone();
357          async move {
358            let did_network = did.network_str();
359            let client: &CLI =
360              future_client
361                .get(did_network)
362                .ok_or(crate::Error::new(ErrorCause::UnsupportedNetwork(
363                  did_network.to_string(),
364                )))?;
365            client
366              .resolve_did(&did)
367              .await
368              .map_err(|err| crate::Error::new(ErrorCause::HandlerError { source: Box::new(err) }))
369          }
370        };
371
372        self.attach_handler(IotaDID::METHOD.to_owned(), handler);
373      }
374    }
375  }
376}
377
378impl<CMD, DOC> Default for Resolver<DOC, CMD>
379where
380  CMD: for<'r> Command<'r, Result<DOC>>,
381  DOC: AsRef<CoreDocument>,
382{
383  fn default() -> Self {
384    Self::new()
385  }
386}
387
388impl<CMD, DOC> std::fmt::Debug for Resolver<DOC, CMD>
389where
390  CMD: for<'r> Command<'r, Result<DOC>>,
391  DOC: AsRef<CoreDocument>,
392{
393  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
394    f.debug_struct("Resolver")
395      .field("command_map", &self.command_map)
396      .finish()
397  }
398}
399
400#[cfg(test)]
401mod tests {
402  use identity_did::CoreDID;
403  use identity_iota_core::DidResolutionHandler;
404  use identity_iota_core::IotaDID;
405  use identity_iota_core::IotaDocument;
406
407  use super::*;
408
409  struct DummyClient(IotaDocument);
410
411  #[async_trait::async_trait]
412  impl DidResolutionHandler for DummyClient {
413    async fn resolve_did(&self, did: &IotaDID) -> identity_iota_core::Result<IotaDocument> {
414      if self.0.id().as_str() == did.as_str() {
415        Ok(self.0.clone())
416      } else {
417        Err(identity_iota_core::Error::DIDResolutionError(
418          "DID not found".to_string(),
419        ))
420      }
421    }
422  }
423
424  #[cfg(feature = "iota")]
425  #[tokio::test]
426  async fn test_multiple_handlers() {
427    let did1 =
428      IotaDID::parse("did:iota:smr:0x0101010101010101010101010101010101010101010101010101010101010101").unwrap();
429    let document = IotaDocument::new_with_id(did1.clone());
430    let dummy_smr_client = DummyClient(document);
431
432    let did2 = IotaDID::parse("did:iota:0x0101010101010101010101010101010101010101010101010101010101010101").unwrap();
433    let document = IotaDocument::new_with_id(did2.clone());
434    let dummy_iota_client = DummyClient(document);
435
436    let mut resolver = Resolver::<IotaDocument>::new();
437    resolver.attach_multiple_iota_handlers(vec![("iota", dummy_iota_client), ("smr", dummy_smr_client)]);
438
439    let doc = resolver.resolve(&did1).await.unwrap();
440    assert_eq!(doc.id(), &did1);
441
442    let doc = resolver.resolve(&did2).await.unwrap();
443    assert_eq!(doc.id(), &did2);
444  }
445
446  #[tokio::test]
447  async fn test_did_jwk_resolution() {
448    let mut resolver = Resolver::<CoreDocument>::new();
449    resolver.attach_did_jwk_handler();
450
451    let did_jwk = "did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9".parse::<DIDJwk>().unwrap();
452    let expected_did: &CoreDID = did_jwk.as_ref();
453
454    let doc = resolver.resolve(&did_jwk).await.unwrap();
455    assert_eq!(doc.id(), expected_did);
456  }
457}