From 3d5c9252f0d5059c06fd4332a497fdd754599e5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eike=20Ha=C3=9F?= Date: Fri, 29 Aug 2025 10:45:37 +0200 Subject: [PATCH 1/7] fix windows longpaths (#1714) --- .github/workflows/build-and-test.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index c28cc91f1..64c6b9354 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -100,6 +100,11 @@ jobs: }} steps: + + + - name: git configure long path + if: matrix.os == 'windows-latest' + run: git config --global core.longpaths true - uses: actions/checkout@v3 - name: Ensure, OpenSSL is available in Windows From 3a2b08dc5f7850cb1a968044c14b0e6c1b58536c Mon Sep 17 00:00:00 2001 From: Enrico Marconi <31142849+UMR1352@users.noreply.github.com> Date: Fri, 29 Aug 2025 13:53:24 +0200 Subject: [PATCH 2/7] Fix `SdJwtVcPresentationBuilder::conceal` (#1713) * fix `SdJwtVcPresentationBuilder` * update docs --- .../src/sd_jwt_vc/presentation.rs | 15 ++++-- .../src/sd_jwt_vc/tests/mod.rs | 1 + .../src/sd_jwt_vc/tests/presentation.rs | 48 +++++++++++++++++++ 3 files changed, 60 insertions(+), 4 deletions(-) create mode 100644 identity_credential/src/sd_jwt_vc/tests/presentation.rs diff --git a/identity_credential/src/sd_jwt_vc/presentation.rs b/identity_credential/src/sd_jwt_vc/presentation.rs index 06a2d2fea..e2ef6ca1b 100644 --- a/identity_credential/src/sd_jwt_vc/presentation.rs +++ b/identity_credential/src/sd_jwt_vc/presentation.rs @@ -23,9 +23,13 @@ impl SdJwtVcPresentationBuilder { /// Prepare a presentation for a given [`SdJwtVc`]. pub fn new(token: SdJwtVc, hasher: &dyn Hasher) -> Result { let SdJwtVc { - sd_jwt, - parsed_claims: vc_claims, + mut sd_jwt, + parsed_claims: mut vc_claims, } = token; + // Make sure to set the parsed claims back into the SD-JWT Token. + // The reason we do this is to make sure that the underlying SdJwtPresetationBuilder + // that operates on the wrapped SdJwt token can handle the claims. + std::mem::swap(sd_jwt.claims_mut(), &mut vc_claims.sd_jwt_claims); let builder = sd_jwt.into_presentation(hasher).map_err(Error::SdJwt)?; Ok(Self { vc_claims, builder }) @@ -47,8 +51,11 @@ impl SdJwtVcPresentationBuilder { } /// Returns the resulting [`SdJwtVc`] together with all removed disclosures. - pub fn finish(self) -> Result<(SdJwtVc, Vec)> { - let (sd_jwt, disclosures) = self.builder.finish()?; + pub fn finish(mut self) -> Result<(SdJwtVc, Vec)> { + let (mut sd_jwt, disclosures) = self.builder.finish()?; + // Move the token's claim back into parsed VC claims. + std::mem::swap(sd_jwt.claims_mut(), &mut self.vc_claims.sd_jwt_claims); + Ok((SdJwtVc::new(sd_jwt, self.vc_claims), disclosures)) } } diff --git a/identity_credential/src/sd_jwt_vc/tests/mod.rs b/identity_credential/src/sd_jwt_vc/tests/mod.rs index f93fe2078..989a2b365 100644 --- a/identity_credential/src/sd_jwt_vc/tests/mod.rs +++ b/identity_credential/src/sd_jwt_vc/tests/mod.rs @@ -21,6 +21,7 @@ use serde_json::Value; use super::resolver; use super::Resolver; +mod presentation; mod validation; pub(crate) const ISSUER_SECRET: &[u8] = b"0123456789ABCDEF0123456789ABCDEF"; diff --git a/identity_credential/src/sd_jwt_vc/tests/presentation.rs b/identity_credential/src/sd_jwt_vc/tests/presentation.rs new file mode 100644 index 000000000..5ab98f873 --- /dev/null +++ b/identity_credential/src/sd_jwt_vc/tests/presentation.rs @@ -0,0 +1,48 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use identity_core::common::Timestamp; +use identity_core::common::Url; +use sd_jwt_payload_rework::Sha256Hasher; +use serde_json::json; + +use crate::sd_jwt_vc::tests::TestSigner; +use crate::sd_jwt_vc::SdJwtVcBuilder; + +#[tokio::test] +async fn test_sd_jwt_presentation_builder() -> anyhow::Result<()> { + let credential = SdJwtVcBuilder::new(json!({ + "name": "John Doe", + "address": { + "street_address": "A random street", + "number": "3a" + }, + "degree": [] + }))? + .header(std::iter::once(("kid".to_string(), serde_json::Value::String("key1".to_string()))).collect()) + .vct("https://example.com/education_credential".parse::()?) + .iat(Timestamp::now_utc()) + .iss("https://example.com".parse()?) + .make_concealable("/address/street_address")? + .make_concealable("/address")? + .finish(&TestSigner, "HS256") + .await?; + + let (concealed_address_credential, conceiled_disclosures) = credential + .into_presentation(&Sha256Hasher::new())? + .conceal("/address")? + .finish()?; + + // Object "address" has been omitted from the credential. + assert!(!concealed_address_credential.claims().contains_key("address")); + // Concealable "address" and its sub-property "street_address" have both been concealed. + assert_eq!( + conceiled_disclosures + .iter() + .map(|disclosure| disclosure.claim_name.as_deref().unwrap()) + .collect::>(), + vec!["street_address", "address"] + ); + + Ok(()) +} From 8cbb162b5b571a16bbecd4f89c7f79d117995950 Mon Sep 17 00:00:00 2001 From: umr1352 Date: Wed, 13 Aug 2025 13:47:57 +0200 Subject: [PATCH 3/7] first draft without error handling --- identity_iota_core/Cargo.toml | 2 + .../src/rebased/client/full_client.rs | 17 +++ .../src/rebased/client/read_only.rs | 138 +++++++++++++++++- .../src/rebased/migration/controller_token.rs | 28 +++- 4 files changed, 182 insertions(+), 3 deletions(-) diff --git a/identity_iota_core/Cargo.toml b/identity_iota_core/Cargo.toml index 25e5d0dc3..29d39159d 100644 --- a/identity_iota_core/Cargo.toml +++ b/identity_iota_core/Cargo.toml @@ -40,6 +40,7 @@ iota-crypto = { version = "0.23", optional = true } itertools = { version = "0.13.0", optional = true } phf = { version = "0.11.2", features = ["macros"] } +async-stream = { version = "0.3", optional = true } rand = { version = "0.8.5", optional = true } secret-storage = { git = "https://github.com/iotaledger/secret-storage.git", tag = "v0.3.0", default-features = false, optional = true } serde-aux = { version = "4.5.0", optional = true } @@ -98,6 +99,7 @@ iota-client = [ "dep:secret-storage", "dep:serde-aux", "product_common/transaction", + "dep:async-stream", ] # Enables an high level integration with IOTA Gas Station. gas-station = ["product_common/gas-station"] diff --git a/identity_iota_core/src/rebased/client/full_client.rs b/identity_iota_core/src/rebased/client/full_client.rs index 60fc5660e..d06c6b24f 100644 --- a/identity_iota_core/src/rebased/client/full_client.rs +++ b/identity_iota_core/src/rebased/client/full_client.rs @@ -12,6 +12,7 @@ use crate::IotaDocument; use crate::StateMetadataDocument; use crate::StateMetadataEncoding; use async_trait::async_trait; +use futures::Stream; use identity_verification::jwk::Jwk; use iota_interaction::move_types::language_storage::StructTag; use iota_interaction::rpc_types::IotaObjectData; @@ -122,6 +123,22 @@ impl IdentityClient { { AuthenticatedAssetBuilder::new(content) } + + /// Returns the [IotaAddress] wrapped by this client. + #[inline(always)] + pub fn address(&self) -> IotaAddress { + IotaAddress::from(&self.public_key) + } + + /// Returns the list of **all** unique DIDs the address wrapped by this client can access as a controller. + pub async fn controlled_dids(&self) -> Vec { + self.dids_controlled_by(self.address()).await + } + + /// Returns a stream yielding the unique DIDs the address wrapped by this client can access as a controller. + pub fn controlled_dids_streamed(&self) -> impl Stream + use<'_, S> { + self.streamed_dids_controlled_by(self.address()) + } } impl IdentityClient diff --git a/identity_iota_core/src/rebased/client/read_only.rs b/identity_iota_core/src/rebased/client/read_only.rs index 3e299d243..9a852daf8 100644 --- a/identity_iota_core/src/rebased/client/read_only.rs +++ b/identity_iota_core/src/rebased/client/read_only.rs @@ -1,6 +1,8 @@ // Copyright 2020-2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +use std::collections::HashSet; +use std::collections::VecDeque; use std::future::Future; use std::ops::Deref; use std::pin::Pin; @@ -8,11 +10,19 @@ use std::str::FromStr; use async_trait::async_trait; use futures::stream::FuturesUnordered; +use futures::Stream; use futures::StreamExt as _; use identity_core::common::Url; use identity_did::DID; +use iota_interaction::rpc_types::IotaObjectDataFilter; +use iota_interaction::rpc_types::IotaObjectDataOptions; +use iota_interaction::rpc_types::IotaObjectResponseQuery; +use iota_interaction::types::base_types::IotaAddress; use iota_interaction::types::base_types::ObjectID; +use iota_interaction::types::TypeTag; use iota_interaction::IotaClientTrait; +use iota_interaction::MoveType; +use move_core_types::language_storage::StructTag; use product_common::core_client::CoreClientReadOnly; use product_common::network_name::NetworkName; @@ -21,6 +31,9 @@ use crate::rebased::iota; use crate::rebased::migration::get_alias; use crate::rebased::migration::get_identity; use crate::rebased::migration::lookup; +use crate::rebased::migration::ControllerCap; +use crate::rebased::migration::ControllerToken; +use crate::rebased::migration::DelegationToken; use crate::rebased::migration::Identity; use crate::rebased::Error; use crate::IotaDID; @@ -140,7 +153,7 @@ impl IdentityClientReadOnly { /// Sets the migration registry ID for the current network. /// # Notes - /// This is only needed when automatic retrival of MigrationRegistry's ID fails. + /// This is only needed when automatic retrieval of MigrationRegistry's ID fails. pub fn set_migration_registry_id(&mut self, id: ObjectID) { crate::rebased::migration::set_migration_registry_id(&self.chain_id, id); } @@ -191,6 +204,127 @@ impl IdentityClientReadOnly { .await .ok_or_else(|| Error::DIDResolutionError(format!("could not find DID document for {object_id}"))) } + + /// Returns a stream yielding the unique DIDs the given address can access as a controller. + /// # Notes + /// This is a streaming version of [dids_controlled_by](Self::dids_controlled_by). + /// # Example + /// ``` + /// # use identity_iota_core::rebased::client::IdentityClientReadOnly; + /// # use identity_iota_core::IotaDID; + /// # use iota_sdk::IotaClientBuilder; + /// # use futures::{Stream, StreamExt}; + /// # + /// # #[tokio::main] + /// # async fn main() -> anyhow::Result<()> { + /// # let iota_client = IotaClientBuilder::default().build_testnet().await?; + /// # let identity_client = IdentityClientReadOnly::new(iota_client).await?; + /// # + /// let address = "0x666638f5118b8f894c4e60052f9bc47d6fcfb04fdb990c9afbb988848b79c475".parse()?; + /// let mut controlled_dids = identity_client.streamed_dids_controlled_by(address); + /// assert_eq!( + /// controlled_dids.next().await, + /// Some(IotaDID::parse( + /// "did:iota:testnet:0x052cfb920024f7a640dc17f7f44c6042ea0038d26972c2cff5c7ba31c82fbb08" + /// )?) + /// ); + /// # Ok(()) + /// # } + /// ``` + pub fn streamed_dids_controlled_by(&self, address: IotaAddress) -> impl Stream + use<'_> { + // Create a filter that matches objects of type ControllerCap or DelegationToken with any package ID in history. + let all_struct_tags = history_type_tags::(&self.package_history) + .chain(history_type_tags::(&self.package_history)) + .map(IotaObjectDataFilter::StructType) + .collect(); + let query = IotaObjectResponseQuery::new( + Some(IotaObjectDataFilter::MatchAny(all_struct_tags)), + Some(IotaObjectDataOptions::default().with_bcs()), + ); + + // Create a stream that returns unique DIDs. + let stream = async_stream::stream! { + let mut page = self + .client_adapter() + .read_api() + .get_owned_objects(address, Some(query.clone()), None, None) + .await + .unwrap(); // TODO. + let mut identities = HashSet::new(); + + loop { + // Return data from the front of the current page until it is exhausted. + let mut data = VecDeque::from(std::mem::take(&mut page.data)); + if let Some(obj_data) = data.pop_front() { + let bcs_content = obj_data.move_object_bcs().expect("bcs was requested").as_slice(); + let token = bcs::from_bytes::(bcs_content) + .map(ControllerToken::Controller) + .or_else(|_| bcs::from_bytes::(bcs_content).map(ControllerToken::Delegate)) + .expect("object is either a valid ControllerCap or DelegationToken"); + if !identities.insert(token.controller_of()) { + continue; + } + yield IotaDID::new(&token.controller_of().into_bytes(), &self.network); + } else if page.has_next_page && page.next_cursor.is_some() { + // The page's content was exhausted, but a new page can be fetched. + page = self + .client_adapter() + .read_api() + .get_owned_objects(address, Some(query.clone()), page.next_cursor, None) + .await + .unwrap(); + } else { + // End of content: current page is exhausted and no more pages are available. + break; + } + } + }; + + // Pin the stream on the heap so that callers can consume it directly without having + // to pin it themselves. + Box::pin(stream) + } + + /// Returns the list of **all** unique DIDs the given address has access to as a controller. + /// # Notes + /// For a streaming version of this API see [dids_controlled_by_streamed](Self::dids_controlled_by_streamed). + /// # Example + /// ``` + /// # use identity_iota_core::rebased::client::IdentityClientReadOnly; + /// # use identity_iota_core::IotaDID; + /// # use iota_sdk::IotaClientBuilder; + /// # + /// # #[tokio::main] + /// # async fn main() -> anyhow::Result<()> { + /// # let iota_client = IotaClientBuilder::default().build_testnet().await?; + /// # let identity_client = IdentityClientReadOnly::new(iota_client).await?; + /// # + /// let address = "0x666638f5118b8f894c4e60052f9bc47d6fcfb04fdb990c9afbb988848b79c475".parse()?; + /// let controlled_dids = identity_client.dids_controlled_by(address).await; + /// assert_eq!( + /// controlled_dids, + /// vec![IotaDID::parse( + /// "did:iota:testnet:0x052cfb920024f7a640dc17f7f44c6042ea0038d26972c2cff5c7ba31c82fbb08" + /// )?] + /// ); + /// # Ok(()) + /// # } + /// ``` + pub async fn dids_controlled_by(&self, address: IotaAddress) -> Vec { + self.streamed_dids_controlled_by(address).collect().await + } +} + +/// Returns the list of all type ID for a given move type where the package ID is taken from history. +/// # Panics +/// If type parameter T's move_type returns a TypeTag that is not TypeTag::Struct. +fn history_type_tags(history: &[ObjectID]) -> impl Iterator + use<'_, T> { + history.iter().copied().map(|pkg| { + let TypeTag::Struct(tag) = T::move_type(pkg) else { + panic!("T must be a Move struct") + }; + *tag + }) } async fn network_id(iota_client: &IotaClientAdapter) -> Result { @@ -240,7 +374,7 @@ async fn resolve_migrated(client: &IdentityClientReadOnly, object_id: ObjectID) async fn resolve_unmigrated(client: &IdentityClientReadOnly, object_id: ObjectID) -> Result, Error> { let unmigrated_alias = get_alias(client, object_id) .await - .map_err(|err| Error::DIDResolutionError(format!("could no query for object id {object_id}; {err}")))?; + .map_err(|err| Error::DIDResolutionError(format!("could not query for object id {object_id}; {err}")))?; Ok(unmigrated_alias.map(Identity::Legacy)) } diff --git a/identity_iota_core/src/rebased/migration/controller_token.rs b/identity_iota_core/src/rebased/migration/controller_token.rs index 7b2d54530..e868d9f01 100644 --- a/identity_iota_core/src/rebased/migration/controller_token.rs +++ b/identity_iota_core/src/rebased/migration/controller_token.rs @@ -142,8 +142,8 @@ impl ControllerToken { /// A token that authenticates its bearer as a controller of a specific shared object. #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(from = "IotaControllerCap")] pub struct ControllerCap { - #[serde(deserialize_with = "deserialize_from_uid")] id: ObjectID, controller_of: ObjectID, can_delegate: bool, @@ -208,6 +208,32 @@ impl From for ControllerToken { } } +#[derive(Debug, Deserialize)] +struct IotaControllerCap { + id: UID, + controller_of: ObjectID, + can_delegate: bool, + #[allow(unused)] + access_token: Referent, +} + +impl From for ControllerCap { + fn from(value: IotaControllerCap) -> Self { + Self { + id: *value.id.object_id(), + controller_of: value.controller_of, + can_delegate: value.can_delegate, + } + } +} + +#[derive(Debug, Deserialize)] +#[allow(unused)] +struct Referent { + id: IotaAddress, + value: Option, +} + /// A token minted by a controller that allows another entity to act in /// its stead - with full or reduced permissions. #[derive(Debug, Clone, Serialize, Deserialize)] From 2f98728d7967ff5ee83c128b913301b765fa9547 Mon Sep 17 00:00:00 2001 From: umr1352 Date: Wed, 13 Aug 2025 16:32:44 +0200 Subject: [PATCH 4/7] controlled_dids with error handling --- .../src/rebased/client/full_client.rs | 5 ++- .../src/rebased/client/read_only.rs | 43 ++++++++++++++----- 2 files changed, 36 insertions(+), 12 deletions(-) diff --git a/identity_iota_core/src/rebased/client/full_client.rs b/identity_iota_core/src/rebased/client/full_client.rs index d06c6b24f..536af1992 100644 --- a/identity_iota_core/src/rebased/client/full_client.rs +++ b/identity_iota_core/src/rebased/client/full_client.rs @@ -4,6 +4,7 @@ use std::ops::Deref; use crate::iota_interaction_adapter::IotaClientAdapter; +use crate::rebased::client::QueryControlledDidsError; use crate::rebased::iota::move_calls; use crate::rebased::iota::package::identity_package_id; use crate::rebased::migration::CreateIdentity; @@ -131,12 +132,12 @@ impl IdentityClient { } /// Returns the list of **all** unique DIDs the address wrapped by this client can access as a controller. - pub async fn controlled_dids(&self) -> Vec { + pub async fn controlled_dids(&self) -> Result, QueryControlledDidsError> { self.dids_controlled_by(self.address()).await } /// Returns a stream yielding the unique DIDs the address wrapped by this client can access as a controller. - pub fn controlled_dids_streamed(&self) -> impl Stream + use<'_, S> { + pub fn controlled_dids_streamed(&self) -> impl Stream> + use<'_, S> { self.streamed_dids_controlled_by(self.address()) } } diff --git a/identity_iota_core/src/rebased/client/read_only.rs b/identity_iota_core/src/rebased/client/read_only.rs index 9a852daf8..6bc43725c 100644 --- a/identity_iota_core/src/rebased/client/read_only.rs +++ b/identity_iota_core/src/rebased/client/read_only.rs @@ -12,6 +12,7 @@ use async_trait::async_trait; use futures::stream::FuturesUnordered; use futures::Stream; use futures::StreamExt as _; +use futures::TryStreamExt as _; use identity_core::common::Url; use identity_did::DID; use iota_interaction::rpc_types::IotaObjectDataFilter; @@ -208,6 +209,11 @@ impl IdentityClientReadOnly { /// Returns a stream yielding the unique DIDs the given address can access as a controller. /// # Notes /// This is a streaming version of [dids_controlled_by](Self::dids_controlled_by). + /// # Errors + /// This stream might return a [QueryControlledDidsError] when the underlying RPC call fails. + /// When an error occurs, the stream might successfully yield a value if polled again, depending + /// on the actual RPC error. + /// [QueryControlledDidsError]'s source can be downcasted to [SDK's Error](iota_interaction::error::Error). /// # Example /// ``` /// # use identity_iota_core::rebased::client::IdentityClientReadOnly; @@ -223,15 +229,18 @@ impl IdentityClientReadOnly { /// let address = "0x666638f5118b8f894c4e60052f9bc47d6fcfb04fdb990c9afbb988848b79c475".parse()?; /// let mut controlled_dids = identity_client.streamed_dids_controlled_by(address); /// assert_eq!( - /// controlled_dids.next().await, - /// Some(IotaDID::parse( + /// controlled_dids.next().await.unwrap()?, + /// IotaDID::parse( /// "did:iota:testnet:0x052cfb920024f7a640dc17f7f44c6042ea0038d26972c2cff5c7ba31c82fbb08" - /// )?) + /// )?, /// ); /// # Ok(()) /// # } /// ``` - pub fn streamed_dids_controlled_by(&self, address: IotaAddress) -> impl Stream + use<'_> { + pub fn streamed_dids_controlled_by( + &self, + address: IotaAddress, + ) -> impl Stream> + use<'_> { // Create a filter that matches objects of type ControllerCap or DelegationToken with any package ID in history. let all_struct_tags = history_type_tags::(&self.package_history) .chain(history_type_tags::(&self.package_history)) @@ -243,13 +252,13 @@ impl IdentityClientReadOnly { ); // Create a stream that returns unique DIDs. - let stream = async_stream::stream! { + let stream = async_stream::try_stream! { let mut page = self .client_adapter() .read_api() .get_owned_objects(address, Some(query.clone()), None, None) .await - .unwrap(); // TODO. + .map_err(|e| QueryControlledDidsError { address, source: e.into() })?; let mut identities = HashSet::new(); loop { @@ -272,7 +281,7 @@ impl IdentityClientReadOnly { .read_api() .get_owned_objects(address, Some(query.clone()), page.next_cursor, None) .await - .unwrap(); + .map_err(|e| QueryControlledDidsError { address, source: e.into() })?; } else { // End of content: current page is exhausted and no more pages are available. break; @@ -288,6 +297,10 @@ impl IdentityClientReadOnly { /// Returns the list of **all** unique DIDs the given address has access to as a controller. /// # Notes /// For a streaming version of this API see [dids_controlled_by_streamed](Self::dids_controlled_by_streamed). + /// # Errors + /// This method might return a [QueryControlledDidsError] when the underlying RPC call fails. + /// [QueryControlledDidsError]'s source can be downcasted to [SDK's Error](iota_interaction::error::Error) + /// in order to check whether calling this method again might return a successfull result. /// # Example /// ``` /// # use identity_iota_core::rebased::client::IdentityClientReadOnly; @@ -300,7 +313,7 @@ impl IdentityClientReadOnly { /// # let identity_client = IdentityClientReadOnly::new(iota_client).await?; /// # /// let address = "0x666638f5118b8f894c4e60052f9bc47d6fcfb04fdb990c9afbb988848b79c475".parse()?; - /// let controlled_dids = identity_client.dids_controlled_by(address).await; + /// let controlled_dids = identity_client.dids_controlled_by(address).await?; /// assert_eq!( /// controlled_dids, /// vec![IotaDID::parse( @@ -310,11 +323,21 @@ impl IdentityClientReadOnly { /// # Ok(()) /// # } /// ``` - pub async fn dids_controlled_by(&self, address: IotaAddress) -> Vec { - self.streamed_dids_controlled_by(address).collect().await + pub async fn dids_controlled_by(&self, address: IotaAddress) -> Result, QueryControlledDidsError> { + self.streamed_dids_controlled_by(address).try_collect().await } } +/// Error that might occur when querying an address for its controlled DIDs. +#[derive(Debug, thiserror::Error)] +#[error("failed to query the DIDs controlled by address `{address}`")] +#[non_exhaustive] +pub struct QueryControlledDidsError { + /// The queried address. + pub address: IotaAddress, + source: Box, +} + /// Returns the list of all type ID for a given move type where the package ID is taken from history. /// # Panics /// If type parameter T's move_type returns a TypeTag that is not TypeTag::Struct. From f257880e89459ce7e172aeac91493ea587ee3bf9 Mon Sep 17 00:00:00 2001 From: umr1352 Date: Thu, 14 Aug 2025 12:23:14 +0200 Subject: [PATCH 5/7] wasm bindings --- .../wasm/identity_wasm/examples/src/util.ts | 9 ++++ .../wasm/identity_wasm/src/iota/iota_did.rs | 4 +- .../identity_wasm/src/rebased/identity.rs | 4 +- .../src/rebased/wasm_identity_client.rs | 17 +++++++ .../rebased/wasm_identity_client_read_only.rs | 51 +++++++++++++++++++ .../src/rebased/client/read_only.rs | 4 +- 6 files changed, 83 insertions(+), 6 deletions(-) diff --git a/bindings/wasm/identity_wasm/examples/src/util.ts b/bindings/wasm/identity_wasm/examples/src/util.ts index 3cf9dd656..c84d97ff7 100644 --- a/bindings/wasm/identity_wasm/examples/src/util.ts +++ b/bindings/wasm/identity_wasm/examples/src/util.ts @@ -16,6 +16,7 @@ import { import { CoreClientReadOnly } from "@iota/iota-interaction-ts/node/core_client"; import { IotaClient, TransactionEffects } from "@iota/iota-sdk/client"; import { getFaucetHost, requestIotaFromFaucetV0 } from "@iota/iota-sdk/faucet"; +import { IotaEvent } from "@iota/iota-sdk/src/client/types/generated"; import { Transaction as SdkTransaction } from "@iota/iota-sdk/transactions"; export const IOTA_IDENTITY_PKG_ID = globalThis?.process?.env?.IOTA_IDENTITY_PKG_ID || ""; @@ -113,4 +114,12 @@ export class SendZeroCoinTx implements Transaction { async apply(effects: TransactionEffects, client: CoreClientReadOnly): Promise { return effects.created![0].reference.objectId; } + + async applyWithEvents( + effects: TransactionEffects, + _events: IotaEvent[], + client: CoreClientReadOnly, + ): Promise { + return this.apply(effects, client); + } } diff --git a/bindings/wasm/identity_wasm/src/iota/iota_did.rs b/bindings/wasm/identity_wasm/src/iota/iota_did.rs index 7a8aca4b7..e456a76a9 100644 --- a/bindings/wasm/identity_wasm/src/iota/iota_did.rs +++ b/bindings/wasm/identity_wasm/src/iota/iota_did.rs @@ -152,8 +152,8 @@ impl WasmIotaDID { WasmDIDUrl::from(self.0.to_url()) } - /// Returns the hex-encoded AliasId with a '0x' prefix, from the DID tag. - #[wasm_bindgen(js_name = toAliasId)] + /// Returns the hex-encoded ObjectID with a '0x' prefix, from the DID tag. + #[wasm_bindgen(js_name = toObjectID)] pub fn to_object_id(&self) -> String { self.0.to_string() } diff --git a/bindings/wasm/identity_wasm/src/rebased/identity.rs b/bindings/wasm/identity_wasm/src/rebased/identity.rs index 685adb421..e2d4513b5 100644 --- a/bindings/wasm/identity_wasm/src/rebased/identity.rs +++ b/bindings/wasm/identity_wasm/src/rebased/identity.rs @@ -222,8 +222,8 @@ impl WasmOnChainIdentity { transfer_map: Vec, expiration_epoch: Option, ) -> Result { - let tx = WasmCreateSendProposal::new(self, controller_token, transfer_map, expiration_epoch) - .map_err(|e| WasmError::from(e))?; + let tx = + WasmCreateSendProposal::new(self, controller_token, transfer_map, expiration_epoch).map_err(WasmError::from)?; Ok(WasmTransactionBuilder::new(JsValue::from(tx).unchecked_into())) } diff --git a/bindings/wasm/identity_wasm/src/rebased/wasm_identity_client.rs b/bindings/wasm/identity_wasm/src/rebased/wasm_identity_client.rs index e94198ae7..a97de2571 100644 --- a/bindings/wasm/identity_wasm/src/rebased/wasm_identity_client.rs +++ b/bindings/wasm/identity_wasm/src/rebased/wasm_identity_client.rs @@ -138,6 +138,23 @@ impl WasmIdentityClient { Ok(WasmIotaDocument(Rc::new(IotaDocumentLock::new(document)))) } + /// Returns the list of DIDs the given address can access as a controller. + /// # Errors + /// @throws {QueryControlledDidsError} when the underlying RPC calls fail; + /// @throws {Error} when the passed `address` string is not a valid IOTA address. + #[wasm_bindgen(js_name = didsControlledBy)] + pub async fn dids_controlled_by(&self, address: &str) -> std::result::Result, js_sys::Error> { + self.read_only().dids_controlled_by(address).await + } + + /// Returns the list of DIDs the address wrapped by this client can access as a controller. + /// # Errors + /// @throws {QueryControlledDidsError} when the underlying RPC calls fail; + #[wasm_bindgen(js_name = controlledDids)] + pub async fn controlled_dids(&self) -> std::result::Result, js_sys::Error> { + self.dids_controlled_by(&self.address().to_string()).await + } + #[wasm_bindgen( js_name = publishDidDocument, unchecked_return_type = "TransactionBuilder" diff --git a/bindings/wasm/identity_wasm/src/rebased/wasm_identity_client_read_only.rs b/bindings/wasm/identity_wasm/src/rebased/wasm_identity_client_read_only.rs index fd27d108f..6f5e0110d 100644 --- a/bindings/wasm/identity_wasm/src/rebased/wasm_identity_client_read_only.rs +++ b/bindings/wasm/identity_wasm/src/rebased/wasm_identity_client_read_only.rs @@ -1,12 +1,14 @@ // Copyright 2020-2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +use std::error::Error; use std::rc::Rc; use std::str::FromStr; use identity_iota::iota::rebased::client::IdentityClientReadOnly; use identity_iota::iota::rebased::migration::Identity; use identity_iota::iota_interaction::types::base_types::ObjectID; +use iota_interaction::types::base_types::IotaAddress; use iota_interaction_ts::bindings::WasmIotaClient; use product_common::core_client::CoreClientReadOnly as _; use wasm_bindgen::prelude::*; @@ -113,4 +115,53 @@ impl WasmIdentityClientReadOnly { .map_err(|err| JsError::new(&format!("failed to resolve identity by object id; {err:?}")))?; Ok(IdentityContainer(inner_value)) } + + /// Returns the list of DIDs the given address can access as a controller. + /// # Errors + /// @throws {QueryControlledDidsError} when the underlying RPC calls fail; + /// @throws {Error} when the passed `address` string is not a valid IOTA address. + #[wasm_bindgen(js_name = didsControlledBy)] + pub async fn dids_controlled_by(&self, address: &str) -> Result, js_sys::Error> { + let address = IotaAddress::from_str(address).map_err(|e| js_sys::Error::new(&format!("{e:#}")))?; + let dids = self + .0 + .dids_controlled_by(address) + .await + .map_err(|e| { + let address = e.address.to_string(); + let source = js_sys::Error::new(&format!("{:#}", e.source().unwrap())); + WasmQueryControlledDidsError::new(&address, source) + })? + .into_iter() + .map(WasmIotaDID) + .collect(); + + Ok(dids) + } +} + +#[wasm_bindgen(typescript_custom_section)] +const WASM_QUERY_CONTROLLED_DIDS_ERROR: &str = r#" +/** + * Error that may occur when querying the DIDs controlled by a given address. + * @extends Error + */ +export class QueryControlledDidsError extends Error { + /** The IOTA address that was being queried */ + address: string; + /** @costructor */ + constructor(address: string, source: Error) { + const msg = `failed to query the DIDs controlled by address \`${address}\``; + this.address = address; + super(msg, { cause: source }); + } +} +"#; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = QueryControlledDidsError, extends = js_sys::Error)] + pub type WasmQueryControlledDidsError; + #[wasm_bindgen(constructor)] + pub fn new(address: &str, source: js_sys::Error) -> WasmQueryControlledDidsError; } diff --git a/identity_iota_core/src/rebased/client/read_only.rs b/identity_iota_core/src/rebased/client/read_only.rs index 6bc43725c..c48db5f85 100644 --- a/identity_iota_core/src/rebased/client/read_only.rs +++ b/identity_iota_core/src/rebased/client/read_only.rs @@ -15,6 +15,7 @@ use futures::StreamExt as _; use futures::TryStreamExt as _; use identity_core::common::Url; use identity_did::DID; +use iota_interaction::move_types::language_storage::StructTag; use iota_interaction::rpc_types::IotaObjectDataFilter; use iota_interaction::rpc_types::IotaObjectDataOptions; use iota_interaction::rpc_types::IotaObjectResponseQuery; @@ -23,7 +24,6 @@ use iota_interaction::types::base_types::ObjectID; use iota_interaction::types::TypeTag; use iota_interaction::IotaClientTrait; use iota_interaction::MoveType; -use move_core_types::language_storage::StructTag; use product_common::core_client::CoreClientReadOnly; use product_common::network_name::NetworkName; @@ -240,7 +240,7 @@ impl IdentityClientReadOnly { pub fn streamed_dids_controlled_by( &self, address: IotaAddress, - ) -> impl Stream> + use<'_> { + ) -> impl Stream> + Unpin + use<'_> { // Create a filter that matches objects of type ControllerCap or DelegationToken with any package ID in history. let all_struct_tags = history_type_tags::(&self.package_history) .chain(history_type_tags::(&self.package_history)) From d2246b03575efeaf553cfc7252bff9ed66f0a959 Mon Sep 17 00:00:00 2001 From: umr1352 Date: Thu, 14 Aug 2025 12:28:46 +0200 Subject: [PATCH 6/7] fmt --- .../wasm/identity_wasm/src/rebased/wasm_identity_client.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bindings/wasm/identity_wasm/src/rebased/wasm_identity_client.rs b/bindings/wasm/identity_wasm/src/rebased/wasm_identity_client.rs index a97de2571..2e26952bd 100644 --- a/bindings/wasm/identity_wasm/src/rebased/wasm_identity_client.rs +++ b/bindings/wasm/identity_wasm/src/rebased/wasm_identity_client.rs @@ -147,9 +147,9 @@ impl WasmIdentityClient { self.read_only().dids_controlled_by(address).await } - /// Returns the list of DIDs the address wrapped by this client can access as a controller. - /// # Errors - /// @throws {QueryControlledDidsError} when the underlying RPC calls fail; + /// Returns the list of DIDs the address wrapped by this client can access as a controller. + /// # Errors + /// @throws {QueryControlledDidsError} when the underlying RPC calls fail; #[wasm_bindgen(js_name = controlledDids)] pub async fn controlled_dids(&self) -> std::result::Result, js_sys::Error> { self.dids_controlled_by(&self.address().to_string()).await From 0262250ae8409ccfa5654249bbc3c9a92ec262b6 Mon Sep 17 00:00:00 2001 From: umr1352 Date: Fri, 15 Aug 2025 12:04:23 +0200 Subject: [PATCH 7/7] remove the pinning of the stream on the heap --- identity_iota_core/src/rebased/client/read_only.rs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/identity_iota_core/src/rebased/client/read_only.rs b/identity_iota_core/src/rebased/client/read_only.rs index c48db5f85..8f4690401 100644 --- a/identity_iota_core/src/rebased/client/read_only.rs +++ b/identity_iota_core/src/rebased/client/read_only.rs @@ -216,6 +216,7 @@ impl IdentityClientReadOnly { /// [QueryControlledDidsError]'s source can be downcasted to [SDK's Error](iota_interaction::error::Error). /// # Example /// ``` + /// # use std::pin::pin; /// # use identity_iota_core::rebased::client::IdentityClientReadOnly; /// # use identity_iota_core::IotaDID; /// # use iota_sdk::IotaClientBuilder; @@ -227,7 +228,7 @@ impl IdentityClientReadOnly { /// # let identity_client = IdentityClientReadOnly::new(iota_client).await?; /// # /// let address = "0x666638f5118b8f894c4e60052f9bc47d6fcfb04fdb990c9afbb988848b79c475".parse()?; - /// let mut controlled_dids = identity_client.streamed_dids_controlled_by(address); + /// let mut controlled_dids = pin!(identity_client.streamed_dids_controlled_by(address)); /// assert_eq!( /// controlled_dids.next().await.unwrap()?, /// IotaDID::parse( @@ -240,7 +241,7 @@ impl IdentityClientReadOnly { pub fn streamed_dids_controlled_by( &self, address: IotaAddress, - ) -> impl Stream> + Unpin + use<'_> { + ) -> impl Stream> + use<'_> { // Create a filter that matches objects of type ControllerCap or DelegationToken with any package ID in history. let all_struct_tags = history_type_tags::(&self.package_history) .chain(history_type_tags::(&self.package_history)) @@ -252,7 +253,7 @@ impl IdentityClientReadOnly { ); // Create a stream that returns unique DIDs. - let stream = async_stream::try_stream! { + async_stream::try_stream! { let mut page = self .client_adapter() .read_api() @@ -287,11 +288,7 @@ impl IdentityClientReadOnly { break; } } - }; - - // Pin the stream on the heap so that callers can consume it directly without having - // to pin it themselves. - Box::pin(stream) + } } /// Returns the list of **all** unique DIDs the given address has access to as a controller.