identity_iota_core/rebased/client/
read_only.rs

1// Copyright 2020-2024 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4use std::future::Future;
5use std::ops::Deref;
6use std::pin::Pin;
7use std::str::FromStr;
8
9use async_trait::async_trait;
10use futures::stream::FuturesUnordered;
11use futures::StreamExt as _;
12use identity_core::common::Url;
13use identity_did::DID;
14use iota_interaction::types::base_types::ObjectID;
15use iota_interaction::IotaClientTrait;
16use product_common::core_client::CoreClientReadOnly;
17use product_common::network_name::NetworkName;
18
19use crate::iota_interaction_adapter::IotaClientAdapter;
20use crate::rebased::iota;
21use crate::rebased::migration::get_alias;
22use crate::rebased::migration::get_identity;
23use crate::rebased::migration::lookup;
24use crate::rebased::migration::Identity;
25use crate::rebased::Error;
26use crate::IotaDID;
27use crate::IotaDocument;
28
29#[cfg(not(target_arch = "wasm32"))]
30use iota_interaction::IotaClient;
31
32#[cfg(target_arch = "wasm32")]
33use iota_interaction_ts::bindings::WasmIotaClient;
34
35/// An [`IotaClient`] enriched with identity-related
36/// functionalities.
37#[derive(Clone)]
38pub struct IdentityClientReadOnly {
39  iota_client: IotaClientAdapter,
40  package_history: Vec<ObjectID>,
41  network: NetworkName,
42  chain_id: String,
43}
44
45impl Deref for IdentityClientReadOnly {
46  type Target = IotaClientAdapter;
47  fn deref(&self) -> &Self::Target {
48    &self.iota_client
49  }
50}
51
52impl IdentityClientReadOnly {
53  /// Returns `iota_identity`'s package ID.
54  /// The ID of the packages depends on the network
55  /// the client is connected to.
56  pub fn package_id(&self) -> ObjectID {
57    *self
58      .package_history
59      .last()
60      .expect("at least one package exists in history")
61  }
62
63  /// Returns the name of the network the client is
64  /// currently connected to.
65  pub const fn network(&self) -> &NetworkName {
66    &self.network
67  }
68
69  /// Returns the chain identifier for the network this client is connected to.
70  /// This method differs from [IdentityClientReadOnly::network] as it doesn't
71  /// return the human-readable network ID when available.
72  pub fn chain_id(&self) -> &str {
73    &self.chain_id
74  }
75
76  /// Attempts to create a new [`IdentityClientReadOnly`] from a given [`IotaClient`].
77  ///
78  /// # Failures
79  /// This function fails if the provided `iota_client` is connected to an unrecognized
80  /// network.
81  ///
82  /// # Notes
83  /// When trying to connect to a local or unofficial network, prefer using
84  /// [`IdentityClientReadOnly::new_with_pkg_id`].
85  pub async fn new(
86    #[cfg(target_arch = "wasm32")] iota_client: WasmIotaClient,
87    #[cfg(not(target_arch = "wasm32"))] iota_client: IotaClient,
88  ) -> Result<Self, Error> {
89    let client = IotaClientAdapter::new(iota_client);
90    let network = network_id(&client).await?;
91    Self::new_internal(client, network).await
92  }
93
94  async fn new_internal(iota_client: IotaClientAdapter, network: NetworkName) -> Result<Self, Error> {
95    let chain_id = network.as_ref().to_string();
96    let (network, package_history) = {
97      let package_registry = iota::package::identity_package_registry().await;
98      let package_history = package_registry
99        .history(&network)
100        .ok_or_else(|| {
101        Error::InvalidConfig(format!(
102          "no information for a published `iota_identity` package on network {network}; try to use `IdentityClientReadOnly::new_with_package_id`"
103        ))
104      })?
105      .to_vec();
106      let network = package_registry
107        .chain_alias(&chain_id)
108        .and_then(|alias| NetworkName::try_from(alias).ok())
109        .unwrap_or(network);
110
111      (network, package_history)
112    };
113    Ok(IdentityClientReadOnly {
114      iota_client,
115      package_history,
116      network,
117      chain_id,
118    })
119  }
120
121  /// Attempts to create a new [`IdentityClientReadOnly`] from the given IOTA client
122  /// and the ID of the IotaIdentity package published on the network the client is
123  /// connected to.
124  pub async fn new_with_pkg_id(
125    #[cfg(target_arch = "wasm32")] iota_client: WasmIotaClient,
126    #[cfg(not(target_arch = "wasm32"))] iota_client: IotaClient,
127    package_id: ObjectID,
128  ) -> Result<Self, Error> {
129    let client = IotaClientAdapter::new(iota_client);
130    let network = network_id(&client).await?;
131
132    // Use the passed pkg_id to force it at the end of the list or create a new env.
133    {
134      let mut registry = iota::package::identity_package_registry_mut().await;
135      registry.insert_new_package_version(&network, package_id);
136    }
137
138    Self::new_internal(client, network).await
139  }
140
141  /// Sets the migration registry ID for the current network.
142  /// # Notes
143  /// This is only needed when automatic retrival of MigrationRegistry's ID fails.
144  pub fn set_migration_registry_id(&mut self, id: ObjectID) {
145    crate::rebased::migration::set_migration_registry_id(&self.chain_id, id);
146  }
147
148  /// Queries an [`IotaDocument`] DID Document through its `did`.
149  pub async fn resolve_did(&self, did: &IotaDID) -> Result<IotaDocument, Error> {
150    let identity = self.get_identity(get_object_id_from_did(did)?).await?;
151    let did_doc = identity.did_document(self.network())?;
152
153    match identity {
154      Identity::FullFledged(identity) if identity.has_deleted_did() => {
155        Err(Error::DIDResolutionError(format!("could not find DID Document {did}")))
156      }
157      _ => Ok(did_doc),
158    }
159  }
160
161  /// Resolves an [`Identity`] from its ID `object_id`.
162  pub async fn get_identity(&self, object_id: ObjectID) -> Result<Identity, Error> {
163    // spawn all checks
164    cfg_if::cfg_if! {
165      // Unfortunately the compiler runs into lifetime problems if we try to use a 'type ='
166      // instead of the below ugly platform specific code
167      if #[cfg(feature = "send-sync")] {
168        let all_futures = FuturesUnordered::<Pin<Box<dyn Future<Output = Result<Option<Identity>, Error>> + Send>>>::new();
169      } else {
170        let all_futures = FuturesUnordered::<Pin<Box<dyn Future<Output = Result<Option<Identity>, Error>>>>>::new();
171      }
172    }
173    all_futures.push(Box::pin(resolve_new(self, object_id)));
174    all_futures.push(Box::pin(resolve_migrated(self, object_id)));
175    all_futures.push(Box::pin(resolve_unmigrated(self, object_id)));
176
177    all_futures
178      .filter_map(|res| Box::pin(async move { res.ok().flatten() }))
179      .next()
180      .await
181      .ok_or_else(|| Error::DIDResolutionError(format!("could not find DID document for {object_id}")))
182  }
183}
184
185async fn network_id(iota_client: &IotaClientAdapter) -> Result<NetworkName, Error> {
186  let network_id = iota_client
187    .read_api()
188    .get_chain_identifier()
189    .await
190    .map_err(|e| Error::RpcError(e.to_string()))?;
191  Ok(network_id.try_into().expect("chain ID is a valid network name"))
192}
193
194async fn resolve_new(client: &IdentityClientReadOnly, object_id: ObjectID) -> Result<Option<Identity>, Error> {
195  let onchain_identity = get_identity(client, object_id).await.map_err(|err| {
196    Error::DIDResolutionError(format!(
197      "could not get identity document for object id {object_id}; {err}"
198    ))
199  })?;
200  Ok(onchain_identity.map(Identity::FullFledged))
201}
202
203async fn resolve_migrated(client: &IdentityClientReadOnly, object_id: ObjectID) -> Result<Option<Identity>, Error> {
204  let onchain_identity = lookup(client, object_id).await.map_err(|err| {
205    Error::DIDResolutionError(format!(
206      "failed to look up object_id {object_id} in migration registry; {err}"
207    ))
208  })?;
209  let Some(mut onchain_identity) = onchain_identity else {
210    return Ok(None);
211  };
212  let object_id_str = object_id.to_string();
213  let queried_did = IotaDID::from_object_id(&object_id_str, &client.network);
214  let doc = onchain_identity.did_document_mut();
215  let identity_did = doc.id().clone();
216  // When querying a migrated identity we obtain a DID document with DID `identity_did` and the `alsoKnownAs`
217  // property containing `queried_did`. Since we are resolving `queried_did`, lets replace in the document these
218  // values. `queried_id` becomes the DID Document ID.
219  *doc.core_document_mut().id_mut_unchecked() = queried_did.clone().into();
220  // The DID Document `alsoKnownAs` property is cleaned of its `queried_did` entry,
221  // which gets replaced by `identity_did`.
222  doc
223    .also_known_as_mut()
224    .replace::<Url>(&queried_did.into_url().into(), identity_did.into_url().into());
225
226  Ok(Some(Identity::FullFledged(onchain_identity)))
227}
228
229async fn resolve_unmigrated(client: &IdentityClientReadOnly, object_id: ObjectID) -> Result<Option<Identity>, Error> {
230  let unmigrated_alias = get_alias(client, object_id)
231    .await
232    .map_err(|err| Error::DIDResolutionError(format!("could  no query for object id {object_id}; {err}")))?;
233  Ok(unmigrated_alias.map(Identity::Legacy))
234}
235
236/// Extracts the object ID from the given `IotaDID`.
237///
238/// # Arguments
239///
240/// * `did` - A reference to the `IotaDID` to be converted.
241pub fn get_object_id_from_did(did: &IotaDID) -> Result<ObjectID, Error> {
242  ObjectID::from_str(did.tag_str())
243    .map_err(|err| Error::DIDResolutionError(format!("could not parse object id from did {did}; {err}")))
244}
245
246#[cfg_attr(feature = "send-sync", async_trait)]
247#[cfg_attr(not(feature = "send-sync"), async_trait(?Send))]
248impl CoreClientReadOnly for IdentityClientReadOnly {
249  fn package_id(&self) -> ObjectID {
250    self.package_id()
251  }
252
253  fn network_name(&self) -> &NetworkName {
254    &self.network
255  }
256
257  fn client_adapter(&self) -> &IotaClientAdapter {
258    &self.iota_client
259  }
260
261  fn package_history(&self) -> Vec<ObjectID> {
262    self.package_history.clone()
263  }
264}