identity_iota_core/rebased/client/
read_only.rs

1// Copyright 2020-2024 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4use std::collections::HashSet;
5use std::collections::VecDeque;
6use std::future::Future;
7use std::ops::Deref;
8use std::pin::Pin;
9use std::str::FromStr;
10
11use async_trait::async_trait;
12use futures::stream::FuturesUnordered;
13use futures::Stream;
14use futures::StreamExt as _;
15use futures::TryStreamExt as _;
16use identity_core::common::Url;
17use identity_did::DID;
18use iota_interaction::move_types::language_storage::StructTag;
19use iota_interaction::rpc_types::IotaObjectDataFilter;
20use iota_interaction::rpc_types::IotaObjectDataOptions;
21use iota_interaction::rpc_types::IotaObjectResponseQuery;
22use iota_interaction::types::base_types::IotaAddress;
23use iota_interaction::types::base_types::ObjectID;
24use iota_interaction::types::TypeTag;
25use iota_interaction::IotaClientTrait;
26use iota_interaction::MoveType;
27use product_common::core_client::CoreClientReadOnly;
28use product_common::network_name::NetworkName;
29
30use crate::iota_interaction_adapter::IotaClientAdapter;
31use crate::rebased::iota;
32use crate::rebased::migration::get_alias;
33use crate::rebased::migration::get_identity;
34use crate::rebased::migration::lookup;
35use crate::rebased::migration::ControllerCap;
36use crate::rebased::migration::ControllerToken;
37use crate::rebased::migration::DelegationToken;
38use crate::rebased::migration::Identity;
39use crate::rebased::Error;
40use crate::IotaDID;
41use crate::IotaDocument;
42
43#[cfg(not(target_arch = "wasm32"))]
44use iota_interaction::IotaClient;
45
46#[cfg(target_arch = "wasm32")]
47use iota_interaction_ts::bindings::WasmIotaClient;
48
49/// An [`IotaClient`] enriched with identity-related
50/// functionalities.
51#[derive(Clone)]
52pub struct IdentityClientReadOnly {
53  iota_client: IotaClientAdapter,
54  package_history: Vec<ObjectID>,
55  network: NetworkName,
56  chain_id: String,
57}
58
59impl Deref for IdentityClientReadOnly {
60  type Target = IotaClientAdapter;
61  fn deref(&self) -> &Self::Target {
62    &self.iota_client
63  }
64}
65
66impl IdentityClientReadOnly {
67  /// Returns `iota_identity`'s package ID.
68  /// The ID of the packages depends on the network
69  /// the client is connected to.
70  pub fn package_id(&self) -> ObjectID {
71    *self
72      .package_history
73      .last()
74      .expect("at least one package exists in history")
75  }
76
77  /// Returns the name of the network the client is
78  /// currently connected to.
79  pub const fn network(&self) -> &NetworkName {
80    &self.network
81  }
82
83  /// Returns the chain identifier for the network this client is connected to.
84  /// This method differs from [IdentityClientReadOnly::network] as it doesn't
85  /// return the human-readable network ID when available.
86  pub fn chain_id(&self) -> &str {
87    &self.chain_id
88  }
89
90  /// Attempts to create a new [`IdentityClientReadOnly`] from a given [`IotaClient`].
91  ///
92  /// # Failures
93  /// This function fails if the provided `iota_client` is connected to an unrecognized
94  /// network.
95  ///
96  /// # Notes
97  /// When trying to connect to a local or unofficial network, prefer using
98  /// [`IdentityClientReadOnly::new_with_pkg_id`].
99  pub async fn new(
100    #[cfg(target_arch = "wasm32")] iota_client: WasmIotaClient,
101    #[cfg(not(target_arch = "wasm32"))] iota_client: IotaClient,
102  ) -> Result<Self, Error> {
103    let client = IotaClientAdapter::new(iota_client);
104    let network = network_id(&client).await?;
105    Self::new_internal(client, network).await
106  }
107
108  async fn new_internal(iota_client: IotaClientAdapter, network: NetworkName) -> Result<Self, Error> {
109    let chain_id = network.as_ref().to_string();
110    let (network, package_history) = {
111      let package_registry = iota::package::identity_package_registry().await;
112      let package_history = package_registry
113        .history(&network)
114        .ok_or_else(|| {
115        Error::InvalidConfig(format!(
116          "no information for a published `iota_identity` package on network {network}; try to use `IdentityClientReadOnly::new_with_package_id`"
117        ))
118      })?
119      .to_vec();
120      let network = package_registry
121        .chain_alias(&chain_id)
122        .and_then(|alias| NetworkName::try_from(alias).ok())
123        .unwrap_or(network);
124
125      (network, package_history)
126    };
127    Ok(IdentityClientReadOnly {
128      iota_client,
129      package_history,
130      network,
131      chain_id,
132    })
133  }
134
135  /// Attempts to create a new [`IdentityClientReadOnly`] from the given IOTA client
136  /// and the ID of the IotaIdentity package published on the network the client is
137  /// connected to.
138  pub async fn new_with_pkg_id(
139    #[cfg(target_arch = "wasm32")] iota_client: WasmIotaClient,
140    #[cfg(not(target_arch = "wasm32"))] iota_client: IotaClient,
141    package_id: ObjectID,
142  ) -> Result<Self, Error> {
143    let client = IotaClientAdapter::new(iota_client);
144    let network = network_id(&client).await?;
145
146    // Use the passed pkg_id to force it at the end of the list or create a new env.
147    {
148      let mut registry = iota::package::identity_package_registry_mut().await;
149      registry.insert_new_package_version(&network, package_id);
150    }
151
152    Self::new_internal(client, network).await
153  }
154
155  /// Sets the migration registry ID for the current network.
156  /// # Notes
157  /// This is only needed when automatic retrieval of MigrationRegistry's ID fails.
158  pub fn set_migration_registry_id(&mut self, id: ObjectID) {
159    crate::rebased::migration::set_migration_registry_id(&self.chain_id, id);
160  }
161
162  /// Queries an [`IotaDocument`] DID Document through its `did`.
163  pub async fn resolve_did(&self, did: &IotaDID) -> Result<IotaDocument, Error> {
164    // Make sure `did` references a DID Document on the network
165    // this client is connected to.
166    let did_network = did.network_str();
167    let client_network = self.network.as_ref();
168    if did_network != client_network && did_network != self.chain_id() {
169      return Err(Error::DIDResolutionError(format!(
170        "provided DID `{did}` \
171        references a DID Document on network `{did_network}`, \
172        but this client is connected to network `{client_network}`"
173      )));
174    }
175    let identity = self.get_identity(get_object_id_from_did(did)?).await?;
176    let did_doc = identity.did_document(self.network())?;
177
178    match identity {
179      Identity::FullFledged(identity) if identity.has_deleted_did() => {
180        Err(Error::DIDResolutionError(format!("could not find DID Document {did}")))
181      }
182      _ => Ok(did_doc),
183    }
184  }
185
186  /// Resolves an [`Identity`] from its ID `object_id`.
187  pub async fn get_identity(&self, object_id: ObjectID) -> Result<Identity, Error> {
188    // spawn all checks
189    cfg_if::cfg_if! {
190      // Unfortunately the compiler runs into lifetime problems if we try to use a 'type ='
191      // instead of the below ugly platform specific code
192      if #[cfg(feature = "send-sync")] {
193        let all_futures = FuturesUnordered::<Pin<Box<dyn Future<Output = Result<Option<Identity>, Error>> + Send>>>::new();
194      } else {
195        let all_futures = FuturesUnordered::<Pin<Box<dyn Future<Output = Result<Option<Identity>, Error>>>>>::new();
196      }
197    }
198    all_futures.push(Box::pin(resolve_new(self, object_id)));
199    all_futures.push(Box::pin(resolve_migrated(self, object_id)));
200    all_futures.push(Box::pin(resolve_unmigrated(self, object_id)));
201
202    all_futures
203      .filter_map(|res| Box::pin(async move { res.ok().flatten() }))
204      .next()
205      .await
206      .ok_or_else(|| Error::DIDResolutionError(format!("could not find DID document for {object_id}")))
207  }
208
209  /// Returns a stream yielding the unique DIDs the given address can access as a controller.
210  /// # Notes
211  /// This is a streaming version of [dids_controlled_by](Self::dids_controlled_by).
212  /// # Errors
213  /// This stream might return a [QueryControlledDidsError] when the underlying RPC call fails.
214  /// When an error occurs, the stream might successfully yield a value if polled again, depending
215  /// on the actual RPC error.
216  /// [QueryControlledDidsError]'s source can be downcasted to [SDK's Error](iota_interaction::error::Error).
217  /// # Example
218  /// ```ignore
219  /// # use std::pin::pin;
220  /// # use identity_iota_core::rebased::client::IdentityClientReadOnly;
221  /// # use identity_iota_core::IotaDID;
222  /// # use iota_sdk::IotaClientBuilder;
223  /// # use futures::{Stream, StreamExt};
224  /// #
225  /// # #[tokio::main]
226  /// # async fn main() -> anyhow::Result<()> {
227  /// # let iota_client = IotaClientBuilder::default().build_testnet().await?;
228  /// # let identity_client = IdentityClientReadOnly::new(iota_client).await?;
229  /// #
230  /// let address = "0x666638f5118b8f894c4e60052f9bc47d6fcfb04fdb990c9afbb988848b79c475".parse()?;
231  /// let mut controlled_dids = pin!(identity_client.streamed_dids_controlled_by(address));
232  /// assert_eq!(
233  ///   controlled_dids.next().await.unwrap()?,
234  ///   IotaDID::parse(
235  ///     "did:iota:testnet:0x052cfb920024f7a640dc17f7f44c6042ea0038d26972c2cff5c7ba31c82fbb08"
236  ///   )?,
237  /// );
238  /// # Ok(())
239  /// # }
240  /// ```
241  pub(crate) fn streamed_dids_controlled_by(
242    &self,
243    address: IotaAddress,
244  ) -> impl Stream<Item = Result<IotaDID, QueryControlledDidsError>> + use<'_> {
245    // Create a filter that matches objects of type ControllerCap or DelegationToken with any package ID in history.
246    let all_struct_tags = history_type_tags::<ControllerCap>(&self.package_history)
247      .chain(history_type_tags::<DelegationToken>(&self.package_history))
248      .map(IotaObjectDataFilter::StructType)
249      .collect();
250    let query = IotaObjectResponseQuery::new(
251      Some(IotaObjectDataFilter::MatchAny(all_struct_tags)),
252      Some(IotaObjectDataOptions::default().with_bcs()),
253    );
254
255    // Create a stream that returns unique DIDs.
256    async_stream::try_stream! {
257      let mut page = self
258      .client_adapter()
259      .read_api()
260      .get_owned_objects(address, Some(query.clone()), None, None)
261      .await
262      .map_err(|e| QueryControlledDidsError { address, source: e.into() })?;
263      let mut identities = HashSet::new();
264
265      loop {
266        // Return data from the front of the current page until it is exhausted.
267        let mut data = VecDeque::from(std::mem::take(&mut page.data));
268        if let Some(obj_data) = data.pop_front() {
269          let bcs_content = obj_data.move_object_bcs().expect("bcs was requested").as_slice();
270          let token = bcs::from_bytes::<ControllerCap>(bcs_content)
271            .map(ControllerToken::Controller)
272            .or_else(|_| bcs::from_bytes::<DelegationToken>(bcs_content).map(ControllerToken::Delegate))
273            .expect("object is either a valid ControllerCap or DelegationToken");
274          if !identities.insert(token.controller_of()) {
275            continue;
276          }
277          yield IotaDID::new(&token.controller_of().into_bytes(), &self.network);
278        } else if page.has_next_page && page.next_cursor.is_some() {
279          // The page's content was exhausted, but a new page can be fetched.
280          page = self
281            .client_adapter()
282            .read_api()
283            .get_owned_objects(address, Some(query.clone()), page.next_cursor, None)
284            .await
285            .map_err(|e| QueryControlledDidsError { address, source: e.into() })?;
286        } else {
287          // End of content: current page is exhausted and no more pages are available.
288          break;
289        }
290      }
291    }
292  }
293
294  /// Returns the list of **all** unique DIDs the given address has access to as a controller.
295  /// # Notes
296  /// For a streaming version of this API see [dids_controlled_by_streamed](Self::dids_controlled_by_streamed).
297  /// # Errors
298  /// This method might return a [QueryControlledDidsError] when the underlying RPC call fails.
299  /// [QueryControlledDidsError]'s source can be downcasted to [SDK's Error](iota_interaction::error::Error)
300  /// in order to check whether calling this method again might return a successful result.
301  /// # Example
302  /// ```
303  /// # use identity_iota_core::rebased::client::IdentityClientReadOnly;
304  /// # use identity_iota_core::IotaDID;
305  /// # use iota_sdk::IotaClientBuilder;
306  /// #
307  /// # #[tokio::main]
308  /// # async fn main() -> anyhow::Result<()> {
309  /// # let iota_client = IotaClientBuilder::default().build_testnet().await?;
310  /// # let identity_client = IdentityClientReadOnly::new(iota_client).await?;
311  /// #
312  /// let address = "0x666638f5118b8f894c4e60052f9bc47d6fcfb04fdb990c9afbb988848b79c475".parse()?;
313  /// let controlled_dids = identity_client.dids_controlled_by(address).await?;
314  /// assert_eq!(
315  ///   controlled_dids,
316  ///   vec![IotaDID::parse(
317  ///     "did:iota:testnet:0x052cfb920024f7a640dc17f7f44c6042ea0038d26972c2cff5c7ba31c82fbb08"
318  ///   )?]
319  /// );
320  /// # Ok(())
321  /// # }
322  /// ```
323  pub async fn dids_controlled_by(&self, address: IotaAddress) -> Result<Vec<IotaDID>, QueryControlledDidsError> {
324    self.streamed_dids_controlled_by(address).try_collect().await
325  }
326}
327
328/// Error that might occur when querying an address for its controlled DIDs.
329#[derive(Debug, thiserror::Error)]
330#[error("failed to query the DIDs controlled by address `{address}`")]
331#[non_exhaustive]
332pub struct QueryControlledDidsError {
333  /// The queried address.
334  pub address: IotaAddress,
335  source: Box<dyn std::error::Error + Send + Sync>,
336}
337
338/// Returns the list of all type ID for a given move type where the package ID is taken from history.
339/// # Panics
340/// If type parameter T's move_type returns a TypeTag that is not TypeTag::Struct.
341fn history_type_tags<T: MoveType>(history: &[ObjectID]) -> impl Iterator<Item = StructTag> + use<'_, T> {
342  history.iter().copied().map(|pkg| {
343    let TypeTag::Struct(tag) = T::move_type(pkg) else {
344      panic!("T must be a Move struct")
345    };
346    *tag
347  })
348}
349
350async fn network_id(iota_client: &IotaClientAdapter) -> Result<NetworkName, Error> {
351  let network_id = iota_client
352    .read_api()
353    .get_chain_identifier()
354    .await
355    .map_err(|e| Error::RpcError(e.to_string()))?;
356  Ok(network_id.try_into().expect("chain ID is a valid network name"))
357}
358
359async fn resolve_new(client: &IdentityClientReadOnly, object_id: ObjectID) -> Result<Option<Identity>, Error> {
360  let onchain_identity = get_identity(client, object_id).await.map_err(|err| {
361    Error::DIDResolutionError(format!(
362      "could not get identity document for object id {object_id}; {err}"
363    ))
364  })?;
365  Ok(onchain_identity.map(Identity::FullFledged))
366}
367
368async fn resolve_migrated(client: &IdentityClientReadOnly, object_id: ObjectID) -> Result<Option<Identity>, Error> {
369  let onchain_identity = lookup(client, object_id).await.map_err(|err| {
370    Error::DIDResolutionError(format!(
371      "failed to look up object_id {object_id} in migration registry; {err}"
372    ))
373  })?;
374  let Some(mut onchain_identity) = onchain_identity else {
375    return Ok(None);
376  };
377  let object_id_str = object_id.to_string();
378  let queried_did = IotaDID::from_object_id(&object_id_str, &client.network);
379  let doc = onchain_identity.did_document_mut();
380  let identity_did = doc.id().clone();
381  // When querying a migrated identity we obtain a DID document with DID `identity_did` and the `alsoKnownAs`
382  // property containing `queried_did`. Since we are resolving `queried_did`, lets replace in the document these
383  // values. `queried_id` becomes the DID Document ID.
384  *doc.core_document_mut().id_mut_unchecked() = queried_did.clone().into();
385  // The DID Document `alsoKnownAs` property is cleaned of its `queried_did` entry,
386  // which gets replaced by `identity_did`.
387  doc
388    .also_known_as_mut()
389    .replace::<Url>(&queried_did.into_url().into(), identity_did.into_url().into());
390
391  Ok(Some(Identity::FullFledged(onchain_identity)))
392}
393
394async fn resolve_unmigrated(client: &IdentityClientReadOnly, object_id: ObjectID) -> Result<Option<Identity>, Error> {
395  let unmigrated_alias = get_alias(client, object_id)
396    .await
397    .map_err(|err| Error::DIDResolutionError(format!("could not query for object id {object_id}; {err}")))?;
398  Ok(unmigrated_alias.map(Identity::Legacy))
399}
400
401/// Extracts the object ID from the given `IotaDID`.
402///
403/// # Arguments
404///
405/// * `did` - A reference to the `IotaDID` to be converted.
406pub fn get_object_id_from_did(did: &IotaDID) -> Result<ObjectID, Error> {
407  ObjectID::from_str(did.tag_str())
408    .map_err(|err| Error::DIDResolutionError(format!("could not parse object id from did {did}; {err}")))
409}
410
411#[cfg_attr(feature = "send-sync", async_trait)]
412#[cfg_attr(not(feature = "send-sync"), async_trait(?Send))]
413impl CoreClientReadOnly for IdentityClientReadOnly {
414  fn package_id(&self) -> ObjectID {
415    self.package_id()
416  }
417
418  fn network_name(&self) -> &NetworkName {
419    &self.network
420  }
421
422  fn client_adapter(&self) -> &IotaClientAdapter {
423    &self.iota_client
424  }
425
426  fn package_history(&self) -> Vec<ObjectID> {
427    self.package_history.clone()
428  }
429}
430
431#[cfg(test)]
432mod tests {
433  use crate::IotaDID;
434
435  use super::IdentityClientReadOnly;
436  use iota_sdk::IotaClientBuilder;
437
438  #[tokio::test]
439  async fn resolution_of_a_did_for_a_different_network_fails() -> anyhow::Result<()> {
440    let iota_client = IotaClientBuilder::default().build_testnet().await?;
441    let identity_client = IdentityClientReadOnly::new(iota_client).await?;
442
443    let did = IotaDID::new(&[1; 32], &"unknown".parse().unwrap());
444    let error = identity_client.resolve_did(&did).await.unwrap_err();
445
446    assert!(matches!(error, crate::rebased::Error::DIDResolutionError(_)));
447
448    Ok(())
449  }
450}