identity_iota_core/rebased/client/
read_only.rs1use 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#[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 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 pub const fn network(&self) -> &NetworkName {
80 &self.network
81 }
82
83 pub fn chain_id(&self) -> &str {
87 &self.chain_id
88 }
89
90 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 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 {
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 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 pub async fn resolve_did(&self, did: &IotaDID) -> Result<IotaDocument, Error> {
164 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 pub async fn get_identity(&self, object_id: ObjectID) -> Result<Identity, Error> {
188 cfg_if::cfg_if! {
190 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 pub(crate) fn streamed_dids_controlled_by(
242 &self,
243 address: IotaAddress,
244 ) -> impl Stream<Item = Result<IotaDID, QueryControlledDidsError>> + use<'_> {
245 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 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 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 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 break;
289 }
290 }
291 }
292 }
293
294 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#[derive(Debug, thiserror::Error)]
330#[error("failed to query the DIDs controlled by address `{address}`")]
331#[non_exhaustive]
332pub struct QueryControlledDidsError {
333 pub address: IotaAddress,
335 source: Box<dyn std::error::Error + Send + Sync>,
336}
337
338fn 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 *doc.core_document_mut().id_mut_unchecked() = queried_did.clone().into();
385 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
401pub 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}