From 5eabeff8863a8f7c7ef5682a4ad2a32cda7cd612 Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Mon, 17 Jul 2023 13:22:45 +0200 Subject: [PATCH] Polish `identity_iota_core` (#1203) * Remove unused error variants * Link to shimmer spec * Polish document signatures and docs * Rename `tag` to `tag_str` * Update Wasm bindings compatibly * Add lints and add docs --- bindings/wasm/lib/iota_identity_client.ts | 4 +- bindings/wasm/src/iota/iota_did.rs | 6 +- bindings/wasm/src/iota/iota_document.rs | 4 +- .../wasm/src/iota/iota_document_metadata.rs | 2 +- bindings/wasm/tests/iota.ts | 6 +- .../src/document/core_document.rs | 74 ++++++++------- identity_iota_core/Cargo.toml | 2 +- identity_iota_core/README.md | 2 +- .../src/client/identity_client.rs | 5 +- identity_iota_core/src/client/iota_client.rs | 2 - identity_iota_core/src/did/iota_did.rs | 26 ++--- .../src/document/iota_document.rs | 94 ++++++++++++++----- .../src/document/iota_document_metadata.rs | 16 +++- identity_iota_core/src/error.rs | 31 ++++-- identity_iota_core/src/lib.rs | 13 ++- .../src/network/network_name.rs | 1 + .../src/state_metadata/document.rs | 3 +- .../src/state_metadata/encoding.rs | 1 + .../src/state_metadata/version.rs | 2 +- 19 files changed, 194 insertions(+), 100 deletions(-) diff --git a/bindings/wasm/lib/iota_identity_client.ts b/bindings/wasm/lib/iota_identity_client.ts index 954cb6a1d6..4f6d70029a 100644 --- a/bindings/wasm/lib/iota_identity_client.ts +++ b/bindings/wasm/lib/iota_identity_client.ts @@ -149,10 +149,10 @@ export class IotaIdentityClient implements IIotaIdentityClient { */ async deleteDidOutput(secretManager: SecretManager, address: AddressTypes, did: IotaDID) { const networkHrp = await this.getNetworkHrp(); - if (networkHrp !== did.networkStr()) { + if (networkHrp !== did.network()) { throw new Error( "deleteDidOutput: DID network mismatch, client expected `" + networkHrp + "`, DID network is `" - + did.networkStr() + "`", + + did.network() + "`", ); } diff --git a/bindings/wasm/src/iota/iota_did.rs b/bindings/wasm/src/iota/iota_did.rs index 6192014c68..93a1b35431 100644 --- a/bindings/wasm/src/iota/iota_did.rs +++ b/bindings/wasm/src/iota/iota_did.rs @@ -80,15 +80,15 @@ impl WasmIotaDID { // =========================================================================== /// Returns the Tangle network name of the `IotaDID`. - #[wasm_bindgen(js_name = networkStr)] - pub fn network_str(&self) -> String { + #[wasm_bindgen] + pub fn network(&self) -> String { self.0.network_str().to_owned() } /// Returns a copy of the unique tag of the `IotaDID`. #[wasm_bindgen] pub fn tag(&self) -> String { - self.0.tag().to_owned() + self.0.tag_str().to_owned() } #[wasm_bindgen(js_name = toCoreDid)] diff --git a/bindings/wasm/src/iota/iota_document.rs b/bindings/wasm/src/iota/iota_document.rs index ec62e707e6..160dbbbd7f 100644 --- a/bindings/wasm/src/iota/iota_document.rs +++ b/bindings/wasm/src/iota/iota_document.rs @@ -548,10 +548,10 @@ impl WasmIotaDocument { let value: Option = value.into_serde().wasm_result()?; match value { Some(value) => { - self.0.blocking_write().metadata.properties.insert(key, value); + self.0.blocking_write().metadata.properties_mut().insert(key, value); } None => { - self.0.blocking_write().metadata.properties.remove(&key); + self.0.blocking_write().metadata.properties_mut().remove(&key); } } Ok(()) diff --git a/bindings/wasm/src/iota/iota_document_metadata.rs b/bindings/wasm/src/iota/iota_document_metadata.rs index 2638bf7ac4..4e77c699a8 100644 --- a/bindings/wasm/src/iota/iota_document_metadata.rs +++ b/bindings/wasm/src/iota/iota_document_metadata.rs @@ -49,7 +49,7 @@ impl WasmIotaDocumentMetadata { /// Returns a copy of the custom metadata properties. #[wasm_bindgen] pub fn properties(&self) -> Result { - MapStringAny::try_from(&self.0.properties) + MapStringAny::try_from(self.0.properties()) } } diff --git a/bindings/wasm/tests/iota.ts b/bindings/wasm/tests/iota.ts index 52ec5750c1..b32279c3ae 100644 --- a/bindings/wasm/tests/iota.ts +++ b/bindings/wasm/tests/iota.ts @@ -33,7 +33,7 @@ describe("IotaDID", function() { assert.deepStrictEqual(did.toString(), "did:" + IotaDID.METHOD + ":" + networkName + ":" + aliasIdHex); assert.deepStrictEqual(did.tag(), aliasIdHex); assert.deepStrictEqual(did.method(), IotaDID.METHOD); - assert.deepStrictEqual(did.networkStr(), networkName); + assert.deepStrictEqual(did.network(), networkName); assert.deepStrictEqual(did.authority(), IotaDID.METHOD + ":" + networkName + ":" + aliasIdHex); assert.deepStrictEqual(did.methodId(), networkName + ":" + aliasIdHex); assert.deepStrictEqual(did.scheme(), "did"); @@ -55,7 +55,7 @@ describe("IotaDID", function() { assert.deepStrictEqual(did.toString(), "did:" + IotaDID.METHOD + ":" + networkName + ":" + aliasIdHex); assert.deepStrictEqual(did.tag(), aliasIdHex); assert.deepStrictEqual(did.method(), IotaDID.METHOD); - assert.deepStrictEqual(did.networkStr(), networkName); + assert.deepStrictEqual(did.network(), networkName); assert.deepStrictEqual(did.authority(), IotaDID.METHOD + ":" + networkName + ":" + aliasIdHex); assert.deepStrictEqual(did.methodId(), networkName + ":" + aliasIdHex); assert.deepStrictEqual(did.scheme(), "did"); @@ -68,7 +68,7 @@ describe("IotaDID", function() { assert.deepStrictEqual(did.toString(), "did:" + IotaDID.METHOD + ":" + networkName + ":" + expectedTag); assert.deepStrictEqual(did.tag(), expectedTag); assert.deepStrictEqual(did.method(), IotaDID.METHOD); - assert.deepStrictEqual(did.networkStr(), networkName); + assert.deepStrictEqual(did.network(), networkName); assert.deepStrictEqual(did.authority(), IotaDID.METHOD + ":" + networkName + ":" + expectedTag); assert.deepStrictEqual(did.methodId(), networkName + ":" + expectedTag); assert.deepStrictEqual(did.scheme(), "did"); diff --git a/identity_document/src/document/core_document.rs b/identity_document/src/document/core_document.rs index cbf5239068..128140763e 100644 --- a/identity_document/src/document/core_document.rs +++ b/identity_document/src/document/core_document.rs @@ -437,14 +437,14 @@ impl CoreDocument { Ok(()) } - /// Removes and returns the [`VerificationMethod`] from the document. + /// Removes and returns the [`VerificationMethod`] identified by `did_url` from the document. /// /// # Note /// /// All _references to the method_ found in the document will be removed. /// This includes cases where the reference is to a method contained in another DID document. - pub fn remove_method(&mut self, did: &DIDUrl) -> Option { - self.remove_method_and_scope(did).map(|(method, _scope)| method) + pub fn remove_method(&mut self, did_url: &DIDUrl) -> Option { + self.remove_method_and_scope(did_url).map(|(method, _scope)| method) } /// Removes and returns the [`VerificationMethod`] from the document. The [`MethodScope`] under which the method was @@ -454,33 +454,33 @@ impl CoreDocument { /// /// All _references to the method_ found in the document will be removed. /// This includes cases where the reference is to a method contained in another DID document. - pub fn remove_method_and_scope(&mut self, did: &DIDUrl) -> Option<(VerificationMethod, MethodScope)> { + pub fn remove_method_and_scope(&mut self, did_url: &DIDUrl) -> Option<(VerificationMethod, MethodScope)> { for (method_ref, scope) in [ - self.data.authentication.remove(did).map(|method_ref| { + self.data.authentication.remove(did_url).map(|method_ref| { ( method_ref, MethodScope::VerificationRelationship(MethodRelationship::Authentication), ) }), - self.data.assertion_method.remove(did).map(|method_ref| { + self.data.assertion_method.remove(did_url).map(|method_ref| { ( method_ref, MethodScope::VerificationRelationship(MethodRelationship::AssertionMethod), ) }), - self.data.key_agreement.remove(did).map(|method_ref| { + self.data.key_agreement.remove(did_url).map(|method_ref| { ( method_ref, MethodScope::VerificationRelationship(MethodRelationship::KeyAgreement), ) }), - self.data.capability_delegation.remove(did).map(|method_ref| { + self.data.capability_delegation.remove(did_url).map(|method_ref| { ( method_ref, MethodScope::VerificationRelationship(MethodRelationship::CapabilityDelegation), ) }), - self.data.capability_invocation.remove(did).map(|method_ref| { + self.data.capability_invocation.remove(did_url).map(|method_ref| { ( method_ref, MethodScope::VerificationRelationship(MethodRelationship::CapabilityInvocation), @@ -500,7 +500,7 @@ impl CoreDocument { self .data .verification_method - .remove(did) + .remove(did_url) .map(|method| (method, MethodScope::VerificationMethod)) } @@ -526,6 +526,7 @@ impl CoreDocument { pub fn remove_service(&mut self, id: &DIDUrl) -> Option { self.data.service.remove(id) } + /// Attaches the relationship to the method resolved by `method_query`. /// /// # Errors @@ -565,6 +566,7 @@ impl CoreDocument { } /// Detaches the relationship from the method resolved by `method_query`. + /// Returns `true` if the relationship was found and removed, `false` otherwise. /// /// # Errors /// @@ -680,10 +682,14 @@ impl CoreDocument { } /// Returns the first [`VerificationMethod`] with an `id` property matching the - /// provided `query` and the verification relationship specified by `scope` if present. + /// provided `method_query` and the verification relationship specified by `scope` if present. // NOTE: This method demonstrates unexpected behaviour in the edge cases where the document contains methods // whose ids are of the form #. - pub fn resolve_method<'query, 'me, Q>(&'me self, query: Q, scope: Option) -> Option<&VerificationMethod> + pub fn resolve_method<'query, 'me, Q>( + &'me self, + method_query: Q, + scope: Option, + ) -> Option<&VerificationMethod> where Q: Into>, { @@ -692,38 +698,40 @@ impl CoreDocument { let resolve_ref_helper = |method_ref: &'me MethodRef| self.resolve_method_ref(method_ref); match scope { - MethodScope::VerificationMethod => self.data.verification_method.query(query.into()), + MethodScope::VerificationMethod => self.data.verification_method.query(method_query.into()), MethodScope::VerificationRelationship(MethodRelationship::Authentication) => self .data .authentication - .query(query.into()) + .query(method_query.into()) .and_then(resolve_ref_helper), MethodScope::VerificationRelationship(MethodRelationship::AssertionMethod) => self .data .assertion_method - .query(query.into()) + .query(method_query.into()) + .and_then(resolve_ref_helper), + MethodScope::VerificationRelationship(MethodRelationship::KeyAgreement) => self + .data + .key_agreement + .query(method_query.into()) .and_then(resolve_ref_helper), - MethodScope::VerificationRelationship(MethodRelationship::KeyAgreement) => { - self.data.key_agreement.query(query.into()).and_then(resolve_ref_helper) - } MethodScope::VerificationRelationship(MethodRelationship::CapabilityDelegation) => self .data .capability_delegation - .query(query.into()) + .query(method_query.into()) .and_then(resolve_ref_helper), MethodScope::VerificationRelationship(MethodRelationship::CapabilityInvocation) => self .data .capability_invocation - .query(query.into()) + .query(method_query.into()) .and_then(resolve_ref_helper), } } - None => self.resolve_method_inner(query.into()), + None => self.resolve_method_inner(method_query.into()), } } /// Returns a mutable reference to the first [`VerificationMethod`] with an `id` property - /// matching the provided `query`. + /// matching the provided `method_query`. /// /// # Warning /// @@ -732,7 +740,7 @@ impl CoreDocument { // whose ids are of the form #. pub fn resolve_method_mut<'query, 'me, Q>( &'me mut self, - query: Q, + method_query: Q, scope: Option, ) -> Option<&'me mut VerificationMethod> where @@ -740,35 +748,35 @@ impl CoreDocument { { match scope { Some(scope) => match scope { - MethodScope::VerificationMethod => self.data.verification_method.query_mut(query.into()), + MethodScope::VerificationMethod => self.data.verification_method.query_mut(method_query.into()), MethodScope::VerificationRelationship(MethodRelationship::Authentication) => { - method_ref_mut_helper!(self, authentication, query) + method_ref_mut_helper!(self, authentication, method_query) } MethodScope::VerificationRelationship(MethodRelationship::AssertionMethod) => { - method_ref_mut_helper!(self, assertion_method, query) + method_ref_mut_helper!(self, assertion_method, method_query) } MethodScope::VerificationRelationship(MethodRelationship::KeyAgreement) => { - method_ref_mut_helper!(self, key_agreement, query) + method_ref_mut_helper!(self, key_agreement, method_query) } MethodScope::VerificationRelationship(MethodRelationship::CapabilityDelegation) => { - method_ref_mut_helper!(self, capability_delegation, query) + method_ref_mut_helper!(self, capability_delegation, method_query) } MethodScope::VerificationRelationship(MethodRelationship::CapabilityInvocation) => { - method_ref_mut_helper!(self, capability_invocation, query) + method_ref_mut_helper!(self, capability_invocation, method_query) } }, - None => self.resolve_method_mut_inner(query.into()), + None => self.resolve_method_mut_inner(method_query.into()), } } - /// Returns the first [`Service`] with an `id` property matching the provided `query`, if present. + /// Returns the first [`Service`] with an `id` property matching the provided `service_query`, if present. // NOTE: This method demonstrates unexpected behaviour in the edge cases where the document contains // services whose ids are of the form #. - pub fn resolve_service<'query, 'me, Q>(&'me self, query: Q) -> Option<&Service> + pub fn resolve_service<'query, 'me, Q>(&'me self, service_query: Q) -> Option<&Service> where Q: Into>, { - self.service().query(query.into()) + self.service().query(service_query.into()) } #[doc(hidden)] diff --git a/identity_iota_core/Cargo.toml b/identity_iota_core/Cargo.toml index a43049fc6a..191da9042b 100644 --- a/identity_iota_core/Cargo.toml +++ b/identity_iota_core/Cargo.toml @@ -13,7 +13,7 @@ description = "An IOTA Ledger integration for the IOTA DID Method." [dependencies] async-trait = { version = "0.1.56", default-features = false, optional = true } -futures = { version = "0.3" } +futures = { version = "0.3", default-features = false } identity_core = { version = "=0.7.0-alpha.6", path = "../identity_core", default-features = false } identity_credential = { version = "=0.7.0-alpha.6", path = "../identity_credential", default-features = false, features = ["validator"] } identity_did = { version = "=0.7.0-alpha.6", path = "../identity_did", default-features = false } diff --git a/identity_iota_core/README.md b/identity_iota_core/README.md index 74576bb24e..6bfab1fa53 100644 --- a/identity_iota_core/README.md +++ b/identity_iota_core/README.md @@ -1,4 +1,4 @@ IOTA Identity === -This crate provides the core data structures for the [IOTA DID Method Specification](https://wiki.iota.org/identity.rs/specs/did/iota_did_method_spec). It provides interfaces for publishing and resolving DID Documents to and from the Tangle according to the IOTA DID Method Specification. +This crate provides the core data structures for the [IOTA DID Method Specification](https://wiki.iota.org/shimmer/identity.rs/specs/did/iota_did_method_spec). It provides interfaces for publishing and resolving DID Documents to and from the Tangle according to the IOTA DID Method Specification. diff --git a/identity_iota_core/src/client/identity_client.rs b/identity_iota_core/src/client/identity_client.rs index 2cddd2cc2f..3febbeedf6 100644 --- a/identity_iota_core/src/client/identity_client.rs +++ b/identity_iota_core/src/client/identity_client.rs @@ -35,7 +35,6 @@ pub trait IotaIdentityClient { /// /// This trait is not intended to be implemented directly, a blanket implementation is /// provided for [`IotaIdentityClient`] implementers. - #[cfg_attr(feature = "send-sync-client-ext", async_trait::async_trait)] #[cfg_attr(not(feature = "send-sync-client-ext"), async_trait::async_trait(?Send))] pub trait IotaIdentityClientExt: IotaIdentityClient { @@ -46,7 +45,7 @@ pub trait IotaIdentityClientExt: IotaIdentityClient { /// `rent_structure`, which will be fetched from the node if not provided. /// The returned Alias Output can be further customised before publication, if desired. /// - /// NOTE: this does *not* publish the Alias Output. + /// NOTE: This does *not* publish the Alias Output. /// /// # Errors /// @@ -83,7 +82,7 @@ pub trait IotaIdentityClientExt: IotaIdentityClient { /// The storage deposit on the output is left unchanged. If the size of the document increased, /// the amount should be increased manually. /// - /// NOTE: this does *not* publish the updated Alias Output. + /// NOTE: This does *not* publish the updated Alias Output. /// /// # Errors /// diff --git a/identity_iota_core/src/client/iota_client.rs b/identity_iota_core/src/client/iota_client.rs index 42d5faa0a2..57df37333d 100644 --- a/identity_iota_core/src/client/iota_client.rs +++ b/identity_iota_core/src/client/iota_client.rs @@ -53,8 +53,6 @@ pub trait IotaClientExt: IotaIdentityClient { async fn delete_did_output(&self, secret_manager: &SecretManager, address: Address, did: &IotaDID) -> Result<()>; } -/// An extension trait for [`Client`] that provides helper functions for publication -/// and deletion of DID documents in Alias Outputs. #[cfg_attr(feature = "send-sync-client-ext", async_trait::async_trait)] #[cfg_attr(not(feature = "send-sync-client-ext"), async_trait::async_trait(?Send))] impl IotaClientExt for Client { diff --git a/identity_iota_core/src/did/iota_did.rs b/identity_iota_core/src/did/iota_did.rs index 76aa5bf67f..2dfbf5b1a8 100644 --- a/identity_iota_core/src/did/iota_did.rs +++ b/identity_iota_core/src/did/iota_did.rs @@ -19,7 +19,8 @@ use serde::Serialize; use crate::NetworkName; -pub type Result = std::result::Result; +/// Alias for a `Result` with the error type [`DIDError`]. +type Result = std::result::Result; /// A DID conforming to the IOTA DID method specification. /// @@ -76,15 +77,15 @@ impl IotaDID { /// # /// let did = IotaDID::new(&[1;32], &NetworkName::try_from("smr").unwrap()); /// assert_eq!(did.as_str(), "did:iota:smr:0x0101010101010101010101010101010101010101010101010101010101010101"); - pub fn new(bytes: &[u8; 32], network_name: &NetworkName) -> Self { - let tag = prefix_hex::encode(bytes); + pub fn new(bytes: &[u8; Self::TAG_BYTES_LEN], network_name: &NetworkName) -> Self { + let tag: String = prefix_hex::encode(bytes); let did: String = format!("did:{}:{}:{}", Self::METHOD, network_name, tag); Self::parse(did).expect("DIDs constructed with new should be valid") } /// Constructs a new [`IotaDID`] from a hex representation of an Alias Id and the given - /// network name. + /// `network_name`. pub fn from_alias_id(alias_id: &str, network_name: &NetworkName) -> Self { let did: String = format!("did:{}:{}:{}", Self::METHOD, network_name, alias_id); Self::parse(did).expect("DIDs constructed with new should be valid") @@ -118,7 +119,7 @@ impl IotaDID { /// let placeholder = IotaDID::placeholder(&NetworkName::try_from("smr").unwrap()); /// assert!(placeholder.is_placeholder()); pub fn is_placeholder(&self) -> bool { - self.tag() == Self::PLACEHOLDER_TAG + self.tag_str() == Self::PLACEHOLDER_TAG } /// Parses an [`IotaDID`] from the given `input`. @@ -151,7 +152,7 @@ impl IotaDID { } /// Returns the tag of the `DID`, which is a hex-encoded Alias ID. - pub fn tag(&self) -> &str { + pub fn tag_str(&self) -> &str { Self::denormalized_components(self.method_id()).1 } @@ -295,6 +296,7 @@ impl From for String { impl TryFrom for IotaDID { type Error = DIDError; + fn try_from(value: CoreDID) -> std::result::Result { Self::try_from_core(value) } @@ -332,7 +334,7 @@ mod __iota_did_client { impl From<&IotaDID> for AliasId { /// Creates an [`AliasId`] from the DID tag. fn from(did: &IotaDID) -> Self { - let tag_bytes: [u8; IotaDID::TAG_BYTES_LEN] = prefix_hex::decode(did.tag()) + let tag_bytes: [u8; IotaDID::TAG_BYTES_LEN] = prefix_hex::decode(did.tag_str()) .expect("being able to successfully decode the tag should be checked during DID creation"); AliasId::new(tag_bytes) } @@ -648,7 +650,7 @@ mod tests { fn property_based_alias_id_string_representation_roundtrip(alias_id in arbitrary_alias_id()) { for network_name in VALID_NETWORK_NAMES.iter().map(|name| NetworkName::try_from(*name).unwrap()) { assert_eq!( - iota_sdk::types::block::output::AliasId::from_str(IotaDID::new(&alias_id, &network_name).tag()).unwrap(), + iota_sdk::types::block::output::AliasId::from_str(IotaDID::new(&alias_id, &network_name).tag_str()).unwrap(), alias_id ); } @@ -756,7 +758,7 @@ mod tests { fn test_tag() { let execute_assertions = |valid_alias_id: &str| { let did: IotaDID = format!("did:{}:{}", IotaDID::METHOD, valid_alias_id).parse().unwrap(); - assert_eq!(did.tag(), valid_alias_id); + assert_eq!(did.tag_str(), valid_alias_id); let did: IotaDID = format!( "did:{}:{}:{}", @@ -766,17 +768,17 @@ mod tests { ) .parse() .unwrap(); - assert_eq!(did.tag(), valid_alias_id); + assert_eq!(did.tag_str(), valid_alias_id); let did: IotaDID = format!("did:{}:dev:{}", IotaDID::METHOD, valid_alias_id) .parse() .unwrap(); - assert_eq!(did.tag(), valid_alias_id); + assert_eq!(did.tag_str(), valid_alias_id); let did: IotaDID = format!("did:{}:custom:{}", IotaDID::METHOD, valid_alias_id) .parse() .unwrap(); - assert_eq!(did.tag(), valid_alias_id); + assert_eq!(did.tag_str(), valid_alias_id); }; execute_assertions(IotaDID::PLACEHOLDER_TAG); execute_assertions(VALID_ALIAS_ID_STR); diff --git a/identity_iota_core/src/document/iota_document.rs b/identity_iota_core/src/document/iota_document.rs index 526b9e77d3..247c00acd6 100644 --- a/identity_iota_core/src/document/iota_document.rs +++ b/identity_iota_core/src/document/iota_document.rs @@ -35,8 +35,8 @@ use crate::NetworkName; use crate::StateMetadataDocument; use crate::StateMetadataEncoding; -#[derive(Debug, Deserialize)] /// Struct used internally when deserializing [`IotaDocument`]. +#[derive(Debug, Deserialize)] struct ProvisionalIotaDocument { #[serde(rename = "doc")] document: CoreDocument, @@ -55,6 +55,7 @@ impl TryFrom for IotaDocument { None, ) })?; + for controller_id in document .controller() .map(|controller_set| controller_set.iter()) @@ -68,17 +69,21 @@ impl TryFrom for IotaDocument { ) })?; } + Ok(IotaDocument { document, metadata }) } } + /// A DID Document adhering to the IOTA DID method specification. /// /// This extends [`CoreDocument`]. #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] #[serde(try_from = "ProvisionalIotaDocument")] pub struct IotaDocument { + /// The DID document. #[serde(rename = "doc")] pub(crate) document: CoreDocument, + /// The metadata of an IOTA DID document. #[serde(rename = "meta")] pub metadata: IotaDocumentMetadata, } @@ -91,7 +96,6 @@ impl IotaDocument { /// Constructs an empty DID Document with a [`IotaDID::placeholder`] identifier /// for the given `network`. // TODO: always take Option or `new_with_options` for a particular network? - // TODO: store the network in the serialized state metadata? Currently it's lost during packing. pub fn new(network: &NetworkName) -> Self { Self::new_with_id(IotaDID::placeholder(network)) } @@ -208,7 +212,7 @@ impl IotaDocument { /// Returns a `Vec` of verification method references whose verification relationship matches `scope`. /// - /// If `scope` is `None`, an iterator over all **embedded** methods is returned. + /// If `scope` is `None`, all **embedded** methods are returned. pub fn methods(&self, scope: Option) -> Vec<&VerificationMethod> { self.document.methods(scope) } @@ -225,76 +229,114 @@ impl IotaDocument { .map_err(Error::InvalidDoc) } - /// Removes all references to the specified [`VerificationMethod`]. + /// Removes and returns the [`VerificationMethod`] identified by `did_url` from the document. /// - /// # Errors + /// # Note /// - /// Returns None if the method does not exist. + /// All _references to the method_ found in the document will be removed. + /// This includes cases where the reference is to a method contained in another DID document. pub fn remove_method(&mut self, did_url: &DIDUrl) -> Option { self.core_document_mut().remove_method(did_url) } - /// Similar to [`Self::remove_method`](Self::remove_method()), but appends the scope where the method was found - /// to the second position of the returned tuple. + /// Removes and returns the [`VerificationMethod`] from the document. The [`MethodScope`] under which the method was + /// found is appended to the second position of the returned tuple. + /// + /// # Note + /// + /// All _references to the method_ found in the document will be removed. + /// This includes cases where the reference is to a method contained in another DID document. pub fn remove_method_and_scope(&mut self, did_url: &DIDUrl) -> Option<(VerificationMethod, MethodScope)> { self.core_document_mut().remove_method_and_scope(did_url) } - /// Attaches the relationship to the given method, if the method exists. + /// Attaches the relationship to the method resolved by `method_query`. + /// + /// # Errors /// - /// Note: The method needs to be in the set of verification methods, - /// so it cannot be an embedded one. - pub fn attach_method_relationship(&mut self, did_url: &DIDUrl, relationship: MethodRelationship) -> Result { + /// Returns an error if the method does not exist or if it is embedded. + /// To convert an embedded method into a generic verification method, remove it first + /// and insert it with [`MethodScope::VerificationMethod`]. + pub fn attach_method_relationship<'query, Q>( + &mut self, + method_query: Q, + relationship: MethodRelationship, + ) -> Result + where + Q: Into>, + { self .core_document_mut() - .attach_method_relationship(did_url, relationship) + .attach_method_relationship(method_query, relationship) .map_err(Error::InvalidDoc) } - /// Detaches the given relationship from the given method, if the method exists. - pub fn detach_method_relationship(&mut self, did_url: &DIDUrl, relationship: MethodRelationship) -> Result { + /// Detaches the `relationship` from the method identified by `did_url`. + /// Returns `true` if the relationship was found and removed, `false` otherwise. + /// + /// # Errors + /// + /// Returns an error if the method does not exist or is embedded. + /// To remove an embedded method, use [`Self::remove_method`]. + /// + /// # Note + /// + /// If the method is referenced in the given scope, but the document does not contain the referenced verification + /// method, then the reference will persist in the document (i.e. it is not removed). + pub fn detach_method_relationship<'query, Q>( + &mut self, + method_query: Q, + relationship: MethodRelationship, + ) -> Result + where + Q: Into>, + { self .core_document_mut() - .detach_method_relationship(did_url, relationship) + .detach_method_relationship(method_query, relationship) .map_err(Error::InvalidDoc) } /// Returns the first [`VerificationMethod`] with an `id` property matching the - /// provided `query` and the verification relationship specified by `scope` if present. + /// provided `method_query` and the verification relationship specified by `scope` if present. /// /// # Warning /// /// Incorrect use of this method can lead to distinct document resources being identified by the same DID URL. pub fn resolve_method_mut<'query, Q>( &mut self, - query: Q, + method_query: Q, scope: Option, ) -> Option<&mut VerificationMethod> where Q: Into>, { - self.document.resolve_method_mut(query, scope) + self.document.resolve_method_mut(method_query, scope) } - /// Returns the first [`Service`] with an `id` property matching the provided `query`, if present. + /// Returns the first [`Service`] with an `id` property matching the provided `service_query`, if present. // NOTE: This method demonstrates unexpected behaviour in the edge cases where the document contains // services whose ids are of the form #. - pub fn resolve_service<'query, 'me, Q>(&'me self, query: Q) -> Option<&Service> + pub fn resolve_service<'query, 'me, Q>(&'me self, service_query: Q) -> Option<&Service> where Q: Into>, { - self.document.resolve_service(query) + self.document.resolve_service(service_query) } /// Returns the first [`VerificationMethod`] with an `id` property matching the - /// provided `query` and the verification relationship specified by `scope` if present. + /// provided `method_query` and the verification relationship specified by `scope` if present. // NOTE: This method demonstrates unexpected behaviour in the edge cases where the document contains methods // whose ids are of the form #. - pub fn resolve_method<'query, 'me, Q>(&'me self, query: Q, scope: Option) -> Option<&VerificationMethod> + pub fn resolve_method<'query, 'me, Q>( + &'me self, + method_query: Q, + scope: Option, + ) -> Option<&VerificationMethod> where Q: Into>, { - self.document.resolve_method(query, scope) + self.document.resolve_method(method_query, scope) } // =========================================================================== @@ -554,7 +596,7 @@ mod tests { let placeholder: IotaDID = IotaDID::placeholder(&network); let doc1: IotaDocument = IotaDocument::new(&network); assert_eq!(doc1.id().network_str(), network.as_ref()); - assert_eq!(doc1.id().tag(), placeholder.tag()); + assert_eq!(doc1.id().tag_str(), placeholder.tag_str()); assert_eq!(doc1.id(), &placeholder); assert_eq!(doc1.methods(None).len(), 0); assert!(doc1.service().is_empty()); diff --git a/identity_iota_core/src/document/iota_document_metadata.rs b/identity_iota_core/src/document/iota_document_metadata.rs index 29d70e5a99..bf9aef565f 100644 --- a/identity_iota_core/src/document/iota_document_metadata.rs +++ b/identity_iota_core/src/document/iota_document_metadata.rs @@ -14,11 +14,13 @@ use serde::Serialize; /// Additional attributes related to a [`IotaDocument`][crate::IotaDocument]. #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] pub struct IotaDocumentMetadata { - // TODO: store created in the immutable metadata, if possible? + /// The timestamp of document creation. #[serde(skip_serializing_if = "Option::is_none")] pub created: Option, + /// The timestamp of the last update to the document. #[serde(skip_serializing_if = "Option::is_none")] pub updated: Option, + /// Signals whether the document is deactivated. #[serde(skip_serializing_if = "Option::is_none")] pub deactivated: Option, /// Bech32-encoded address of the governor unlock condition. @@ -28,7 +30,7 @@ pub struct IotaDocumentMetadata { #[serde(rename = "stateControllerAddress", skip_serializing_if = "Option::is_none")] pub state_controller_address: Option, #[serde(flatten)] - pub properties: Object, + properties: Object, } impl IotaDocumentMetadata { @@ -45,6 +47,16 @@ impl IotaDocumentMetadata { properties: Object::default(), } } + + /// Returns a reference to the custom metadata properties. + pub fn properties(&self) -> &Object { + &self.properties + } + + /// Returns a mutable reference to the custom metadata properties. + pub fn properties_mut(&mut self) -> &mut Object { + &mut self.properties + } } impl Default for IotaDocumentMetadata { diff --git a/identity_iota_core/src/error.rs b/identity_iota_core/src/error.rs index e7a996fa0c..2a5e16ef34 100644 --- a/identity_iota_core/src/error.rs +++ b/identity_iota_core/src/error.rs @@ -1,57 +1,76 @@ // Copyright 2020-2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +/// Alias for a `Result` with the error type [`Error`]. pub type Result = core::result::Result; +/// This type represents errors that can occur when constructing credentials and presentations or their serializations. #[derive(Debug, thiserror::Error, strum::IntoStaticStr)] #[non_exhaustive] pub enum Error { + /// Caused by a failure to serialize or deserialize. #[error("serialization error: {0}")] SerializationError(&'static str, #[source] Option), + /// Caused by an invalid DID. #[error("invalid did")] DIDSyntaxError(#[source] identity_did::Error), + /// Caused by an invalid DID document. #[error("invalid document")] InvalidDoc(#[source] identity_document::Error), #[cfg(feature = "iota-client")] + /// Caused by a client failure during publishing. #[error("DID update: {0}")] DIDUpdateError(&'static str, #[source] Option>), #[cfg(feature = "iota-client")] + /// Caused by a client failure during resolution. #[error("DID resolution failed")] DIDResolutionError(#[source] iota_sdk::client::error::Error), #[cfg(feature = "iota-client")] + /// Caused by an error when building a basic output. #[error("basic output build error")] BasicOutputBuildError(#[source] iota_sdk::types::block::Error), + /// Caused by an invalid network name. #[error("\"{0}\" is not a valid network name in the context of the `iota` did method")] InvalidNetworkName(String), #[cfg(feature = "iota-client")] + /// Caused by a failure to retrieve the token supply. #[error("unable to obtain the token supply from the client")] TokenSupplyError(#[source] iota_sdk::client::Error), + /// Caused by a mismatch of the DID's network and the network the client is connected with. #[error("unable to resolve a `{expected}` DID on network `{actual}`")] - NetworkMismatch { expected: String, actual: String }, + NetworkMismatch { + /// The network of the DID. + expected: String, + /// The network the client is connected with. + actual: String, + }, #[cfg(feature = "iota-client")] + /// Caused by an error when fetching protocol parameters from a node. #[error("could not fetch protocol parameters")] ProtocolParametersError(#[source] iota_sdk::client::Error), + /// Caused by an attempt to read state metadata that does not adhere to the IOTA DID method specification. #[error("invalid state metadata {0}")] InvalidStateMetadata(&'static str), #[cfg(feature = "revocation-bitmap")] + /// Caused by a failure during (un)revocation of credentials. #[error("credential revocation error")] RevocationError(#[source] identity_credential::revocation::RevocationError), #[cfg(feature = "client")] + /// Caused by an error when building an alias output. #[error("alias output build error")] AliasOutputBuildError(#[source] crate::block::Error), #[cfg(feature = "iota-client")] + /// Caused by retrieving an output that is expected to be an alias output but is not. #[error("output with id `{0}` is not an alias output")] NotAnAliasOutput(iota_sdk::types::block::output::OutputId), - #[cfg(feature = "iota-client")] - #[error("converting a DTO to an output failed")] - OutputConversionError(#[source] iota_sdk::types::block::Error), + /// Caused by an error when constructing an output id. #[error("conversion to an OutputId failed: {0}")] OutputIdConversionError(String), #[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))] + /// Caused by an error in the Wasm bindings. #[error("JavaScript function threw an exception: {0}")] JsError(String), - #[error("could not sign the data")] - SigningError(#[source] Box), + /// Caused by an error during JSON Web Signature verification. #[error("jws signature verification failed")] JwsVerificationError(#[source] identity_document::Error), } diff --git a/identity_iota_core/src/lib.rs b/identity_iota_core/src/lib.rs index a305e27575..dbf1af9093 100644 --- a/identity_iota_core/src/lib.rs +++ b/identity_iota_core/src/lib.rs @@ -1,6 +1,17 @@ -// Copyright 2020-2022 IOTA Stiftung +// Copyright 2020-2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +#![doc = include_str!("./../README.md")] +#![warn( + rust_2018_idioms, + unreachable_pub, + missing_docs, + rustdoc::missing_crate_level_docs, + rustdoc::broken_intra_doc_links, + rustdoc::private_intra_doc_links, + rustdoc::private_doc_tests, + clippy::missing_safety_doc +)] #![allow(clippy::upper_case_acronyms)] #[cfg(feature = "iota-client")] diff --git a/identity_iota_core/src/network/network_name.rs b/identity_iota_core/src/network/network_name.rs index e1f9b2516c..97ce562a95 100644 --- a/identity_iota_core/src/network/network_name.rs +++ b/identity_iota_core/src/network/network_name.rs @@ -21,6 +21,7 @@ use crate::error::Result; pub struct NetworkName(Cow<'static, str>); impl NetworkName { + /// The maximum length of a network name. pub const MAX_LENGTH: usize = 6; /// Creates a new [`NetworkName`] if the name passes validation. diff --git a/identity_iota_core/src/state_metadata/document.rs b/identity_iota_core/src/state_metadata/document.rs index fb1236d12f..d15f0d8d26 100644 --- a/identity_iota_core/src/state_metadata/document.rs +++ b/identity_iota_core/src/state_metadata/document.rs @@ -39,18 +39,19 @@ impl StateMetadataDocument { /// Transforms the document into a [`IotaDocument`] by replacing all placeholders with `original_did`. pub fn into_iota_document(self, original_did: &IotaDID) -> Result { let Self { document, metadata } = self; + // Transform identifiers: Replace placeholder identifiers, and ensure that `id` and `controller` adhere to the // specification. let replace_placeholder_with_method_check = |did: CoreDID| -> Result { if did == PLACEHOLDER_DID.as_ref() { Ok(CoreDID::from(original_did.clone())) } else { - // TODO: Consider introducing better error variant IotaDID::check_validity(&did).map_err(Error::DIDSyntaxError)?; Ok(did) } }; let [id_update, controller_update] = [replace_placeholder_with_method_check; 2]; + // Methods and services are not required to be IOTA UTXO DIDs, but we still want to replace placeholders let replace_placeholder = |did: CoreDID| -> Result { if did == PLACEHOLDER_DID.as_ref() { diff --git a/identity_iota_core/src/state_metadata/encoding.rs b/identity_iota_core/src/state_metadata/encoding.rs index 9c7dd4b863..9ac7452b7f 100644 --- a/identity_iota_core/src/state_metadata/encoding.rs +++ b/identity_iota_core/src/state_metadata/encoding.rs @@ -8,6 +8,7 @@ use crate::Error; /// Indicates the encoding of a DID document in state metadata. #[derive(Copy, Clone, Debug, Default, Ord, PartialOrd, Eq, PartialEq, num_derive::FromPrimitive)] pub enum StateMetadataEncoding { + /// State Metadata encoded as JSON. #[default] Json = 0, } diff --git a/identity_iota_core/src/state_metadata/version.rs b/identity_iota_core/src/state_metadata/version.rs index 82ddc02407..7f31f4f40d 100644 --- a/identity_iota_core/src/state_metadata/version.rs +++ b/identity_iota_core/src/state_metadata/version.rs @@ -12,7 +12,7 @@ pub(crate) enum StateMetadataVersion { } impl StateMetadataVersion { - pub const CURRENT: Self = Self::V1; + pub(crate) const CURRENT: Self = Self::V1; } impl TryFrom for StateMetadataVersion {