diff --git a/examples/account/lazy.rs b/examples/account/lazy.rs index 995275364f..5356f7f058 100644 --- a/examples/account/lazy.rs +++ b/examples/account/lazy.rs @@ -4,7 +4,6 @@ //! cargo run --example account_lazy use identity::account::Account; -use identity::account::Command; use identity::account::IdentityCreate; use identity::account::IdentitySnapshot; use identity::account::Result; @@ -27,28 +26,36 @@ async fn main() -> Result<()> { // Retrieve the DID from the newly created Identity state. let did: &IotaDID = snapshot.identity().try_did()?; - let command: Command = Command::create_service() + account + .update_identity(did) + .create_service() .fragment("example-service") .type_("LinkedDomains") .endpoint(Url::parse("https://example.org")?) - .finish()?; - account.update_identity(did, command).await?; + .apply() + .await?; // Publish the newly created DID document, // including the new service, to the tangle. account.publish_updates(did).await?; // Add another service. - let command: Command = Command::create_service() + account + .update_identity(did) + .create_service() .fragment("another-service") .type_("LinkedDomains") .endpoint(Url::parse("https://example.org")?) - .finish()?; - account.update_identity(did, command).await?; + .apply() + .await?; // Delete the previously added service. - let command: Command = Command::delete_service().fragment("example-service").finish()?; - account.update_identity(did, command).await?; + account + .update_identity(did) + .delete_service() + .fragment("example-service") + .apply() + .await?; // Publish the updates as one message to the tangle. account.publish_updates(did).await?; diff --git a/examples/account/methods.rs b/examples/account/methods.rs index 97a7901758..4b22ea29f5 100644 --- a/examples/account/methods.rs +++ b/examples/account/methods.rs @@ -4,7 +4,6 @@ //! cargo run --example account_methods use identity::account::Account; -use identity::account::Command; use identity::account::IdentityCreate; use identity::account::IdentitySnapshot; use identity::account::Result; @@ -24,15 +23,15 @@ async fn main() -> Result<()> { // Retrieve the DID from the newly created Identity state. let did: &IotaDID = snapshot.identity().try_did()?; - // Add a new Ed25519 (defualt) verification method to the identity - the + // Add a new Ed25519 (default) verification method to the identity - the // verification method is included as an embedded authentication method. - let command: Command = Command::create_method() + account + .update_identity(did) + .create_method() .scope(MethodScope::Authentication) .fragment("my-auth-key") - .finish()?; - - // Process the command and update the identity state. - account.update_identity(did, command).await?; + .apply() + .await?; // Fetch and log the DID Document from the Tangle // @@ -43,20 +42,22 @@ async fn main() -> Result<()> { ); // Add another Ed25519 verification method to the identity - let command: Command = Command::create_method().fragment("my-next-key").finish()?; - - // Process the command and update the identity state. - account.update_identity(did, command).await?; + account + .update_identity(did) + .create_method() + .fragment("my-next-key") + .apply() + .await?; // Associate the newly created method with additional verification relationships - let command: Command = Command::attach_method() + account + .update_identity(did) + .attach_method() .fragment("my-next-key") .scope(MethodScope::CapabilityDelegation) .scope(MethodScope::CapabilityInvocation) - .finish()?; - - // Process the command and update the identity state. - account.update_identity(did, command).await?; + .apply() + .await?; // Fetch and log the DID Document from the Tangle // @@ -67,10 +68,12 @@ async fn main() -> Result<()> { ); // Remove the original Ed25519 verification method - let command: Command = Command::delete_method().fragment("my-auth-key").finish()?; - - // Process the command and update the identity state. - account.update_identity(did, command).await?; + account + .update_identity(did) + .delete_method() + .fragment("my-auth-key") + .apply() + .await?; // Fetch and log the DID Document from the Tangle // diff --git a/examples/account/services.rs b/examples/account/services.rs index b457a98dea..cca96d07d1 100644 --- a/examples/account/services.rs +++ b/examples/account/services.rs @@ -4,7 +4,6 @@ //! cargo run --example account_services use identity::account::Account; -use identity::account::Command; use identity::account::IdentityCreate; use identity::account::IdentitySnapshot; use identity::account::Result; @@ -24,14 +23,15 @@ async fn main() -> Result<()> { // Retrieve the DID from the newly created Identity state. let did: &IotaDID = snapshot.identity().try_did()?; - let command: Command = Command::create_service() + // Add a new service to the identity. + account + .update_identity(did) + .create_service() .fragment("my-service-1") .type_("MyCustomService") .endpoint(Url::parse("https://example.com")?) - .finish()?; - - // Process the command and update the identity state. - account.update_identity(did, command).await?; + .apply() + .await?; // Fetch and log the DID Document from the Tangle // diff --git a/examples/account/signing.rs b/examples/account/signing.rs index 247547bf1e..bdf0736181 100644 --- a/examples/account/signing.rs +++ b/examples/account/signing.rs @@ -4,7 +4,6 @@ //! cargo run --example account_signing use identity::account::Account; -use identity::account::Command; use identity::account::IdentityCreate; use identity::account::IdentitySnapshot; use identity::account::Result; @@ -34,10 +33,12 @@ async fn main() -> Result<()> { println!("[Example] Local Document = {:#?}", snapshot.identity().to_document()?); // Add a new Ed25519 Verification Method to the identity - let command: Command = Command::create_method().fragment("key-1").finish()?; - - // Process the command and update the identity state. - account.update_identity(did, command).await?; + account + .update_identity(did) + .create_method() + .fragment("key-1") + .apply() + .await?; // Create a subject DID for the recipient of a `UniversityDegree` credential. let subject_key: KeyPair = KeyPair::new_ed25519()?; diff --git a/identity-account/src/account/account.rs b/identity-account/src/account/account.rs index 91515fdcd0..3f0e335e07 100644 --- a/identity-account/src/account/account.rs +++ b/identity-account/src/account/account.rs @@ -38,6 +38,7 @@ use crate::identity::IdentityLock; use crate::identity::IdentitySnapshot; use crate::identity::IdentityState; use crate::identity::IdentityTag; +use crate::identity::IdentityUpdater; use crate::identity::TinyMethod; use crate::storage::Storage; use crate::types::Generation; @@ -112,7 +113,7 @@ impl Account { /// Finds and returns the state snapshot for the identity specified by given `key`. pub async fn find_identity(&self, key: K) -> Result> { - match self.resolve_id(key).await { + match self.resolve_id(&key).await { Some(identity) => self.load_snapshot(identity).await.map(Some), None => Ok(None), } @@ -155,15 +156,15 @@ impl Account { Ok(snapshot) } - /// Updates the identity specified by the given `key` with the given `command`. - pub async fn update_identity(&self, key: K, command: Command) -> Result<()> { - // Hold on to an `IdentityId`s individual lock until we've finished processing the update. - let identity_lock = self.try_resolve_id_lock(key).await?; - let identity: RwLockWriteGuard<'_, IdentityId> = identity_lock.write().await; - - self.process(*identity, command, true).await?; - - Ok(()) + /// Returns the `IdentityUpdater` for the given `key`. + /// + /// On this type, various operations can be executed + /// that modify an identity, such as creating services or methods. + pub fn update_identity<'account, 'key, K: IdentityKey>( + &'account self, + key: &'key K, + ) -> IdentityUpdater<'account, 'key, K> { + IdentityUpdater::new(self, key) } /// Removes the identity specified by the given `key`. @@ -190,7 +191,7 @@ impl Account { /// Resolves the DID Document associated with the specified `key`. pub async fn resolve_identity(&self, key: K) -> Result { - let identity: IdentityId = self.try_resolve_id(key).await?; + let identity: IdentityId = self.try_resolve_id(&key).await?; let snapshot: IdentitySnapshot = self.load_snapshot(identity).await?; let document: &IotaDID = snapshot.identity().try_did()?; @@ -204,7 +205,7 @@ impl Account { K: IdentityKey, U: Serialize + SetSignature, { - let identity: IdentityId = self.try_resolve_id(key).await?; + let identity: IdentityId = self.try_resolve_id(&key).await?; let snapshot: IdentitySnapshot = self.load_snapshot(identity).await?; let state: &IdentityState = snapshot.identity(); @@ -217,15 +218,15 @@ impl Account { Ok(()) } - async fn resolve_id(&self, key: K) -> Option { + async fn resolve_id(&self, key: &K) -> Option { self.index.read().await.get(key) } - async fn try_resolve_id(&self, key: K) -> Result { + async fn try_resolve_id(&self, key: &K) -> Result { self.resolve_id(key).await.ok_or(Error::IdentityNotFound) } - async fn try_resolve_id_lock(&self, key: K) -> Result { + async fn try_resolve_id_lock(&self, key: &K) -> Result { self.index.write().await.get_lock(key).ok_or(Error::IdentityNotFound) } @@ -233,8 +234,18 @@ impl Account { // Misc. Private // =========================================================================== - #[doc(hidden)] - pub async fn process(&self, id: IdentityId, command: Command, persist: bool) -> Result<()> { + /// Updates the identity specified by the given `key` with the given `command`. + pub(crate) async fn apply_command(&self, key: &K, command: Command) -> Result<()> { + // Hold on to an `IdentityId`s individual lock until we've finished processing the update. + let identity_lock = self.try_resolve_id_lock(key).await?; + let identity: RwLockWriteGuard<'_, IdentityId> = identity_lock.write().await; + + self.process(*identity, command, true).await?; + + Ok(()) + } + + pub(crate) async fn process(&self, id: IdentityId, command: Command, persist: bool) -> Result<()> { // Load the latest state snapshot from storage let root: IdentitySnapshot = self.load_snapshot(id).await?; @@ -437,7 +448,7 @@ impl Account { /// Push all unpublished changes for the given identity to the tangle in a single message. pub async fn publish_updates(&self, key: K) -> Result<()> { - let identity_lock: IdentityLock = self.try_resolve_id_lock(key).await?; + let identity_lock: IdentityLock = self.try_resolve_id_lock(&key).await?; let identity: RwLockWriteGuard<'_, IdentityId> = identity_lock.write().await; // Get the last commit generation that was published to the tangle. diff --git a/identity-account/src/error.rs b/identity-account/src/error.rs index ce131b4f48..cabd863626 100644 --- a/identity-account/src/error.rs +++ b/identity-account/src/error.rs @@ -88,9 +88,9 @@ pub enum Error { /// Caused by attempting to find a service that does not exist. #[error("Service not found")] ServiceNotFound, - /// Caused by attempting to perform a command in an invalid context. - #[error("Command Error: {0}")] - CommandError(#[from] crate::events::CommandError), + /// Caused by attempting to perform an upate in an invalid context. + #[error("Update Error: {0}")] + UpdateError(#[from] crate::events::UpdateError), #[error("Invalid Secret Key: {0}")] InvalidSecretKey(String), } diff --git a/identity-account/src/events/command.rs b/identity-account/src/events/command.rs index e4e8f84abd..439b8cca6f 100644 --- a/identity-account/src/events/command.rs +++ b/identity-account/src/events/command.rs @@ -11,12 +11,14 @@ use identity_did::verification::MethodScope; use identity_did::verification::MethodType; use identity_iota::did::IotaDID; +use crate::account::Account; use crate::error::Result; -use crate::events::CommandError; use crate::events::Context; use crate::events::Event; use crate::events::EventData; +use crate::events::UpdateError; use crate::identity::IdentityId; +use crate::identity::IdentityKey; use crate::identity::IdentityState; use crate::identity::TinyMethod; use crate::identity::TinyService; @@ -29,7 +31,7 @@ use crate::types::MethodSecret; const AUTH_TYPES: &[MethodType] = &[MethodType::Ed25519VerificationKey2018]; #[derive(Clone, Debug)] -pub enum Command { +pub(crate) enum Command { CreateIdentity { network: Option, method_secret: Option, @@ -64,7 +66,7 @@ pub enum Command { } impl Command { - pub async fn process(self, context: Context<'_>) -> Result>> { + pub(crate) async fn process(self, context: Context<'_>) -> Result>> { let state: &IdentityState = context.state(); let store: &dyn Storage = context.store(); @@ -79,12 +81,12 @@ impl Command { authentication, } => { // The state must not be initialized - ensure!(state.did().is_none(), CommandError::DocumentAlreadyExists); + ensure!(state.did().is_none(), UpdateError::DocumentAlreadyExists); // The authentication method type must be valid ensure!( AUTH_TYPES.contains(&authentication), - CommandError::InvalidMethodType(authentication) + UpdateError::InvalidMethodType(authentication) ); let generation: Generation = state.auth_generation(); @@ -94,7 +96,7 @@ impl Command { // TODO: config: strict ensure!( !store.key_exists(state.id(), &location).await?, - CommandError::DuplicateKeyLocation(location) + UpdateError::DuplicateKeyLocation(location) ); let public: PublicKey = if let Some(method_secret_key) = method_secret { @@ -123,27 +125,27 @@ impl Command { method_secret, } => { // The state must be initialized - ensure!(state.did().is_some(), CommandError::DocumentNotFound); + ensure!(state.did().is_some(), UpdateError::DocumentNotFound); let location: KeyLocation = state.key_location(type_, fragment)?; // The key location must not be an authentication location ensure!( !location.is_authentication(), - CommandError::InvalidMethodFragment("reserved") + UpdateError::InvalidMethodFragment("reserved") ); // The key location must be available // TODO: config: strict ensure!( !store.key_exists(state.id(), &location).await?, - CommandError::DuplicateKeyLocation(location) + UpdateError::DuplicateKeyLocation(location) ); // The verification method must not exist ensure!( !state.methods().contains(location.fragment()), - CommandError::DuplicateKeyFragment(location.fragment.clone()), + UpdateError::DuplicateKeyFragment(location.fragment.clone()), ); let public: PublicKey = if let Some(method_secret_key) = method_secret { @@ -159,52 +161,52 @@ impl Command { } Self::DeleteMethod { fragment } => { // The state must be initialized - ensure!(state.did().is_some(), CommandError::DocumentNotFound); + ensure!(state.did().is_some(), UpdateError::DocumentNotFound); let fragment: Fragment = Fragment::new(fragment); // The fragment must not be an authentication location ensure!( !KeyLocation::is_authentication_fragment(&fragment), - CommandError::InvalidMethodFragment("reserved") + UpdateError::InvalidMethodFragment("reserved") ); // The verification method must exist - ensure!(state.methods().contains(fragment.name()), CommandError::MethodNotFound); + ensure!(state.methods().contains(fragment.name()), UpdateError::MethodNotFound); Ok(Some(vec![Event::new(EventData::MethodDeleted(fragment))])) } Self::AttachMethod { fragment, scopes } => { // The state must be initialized - ensure!(state.did().is_some(), CommandError::DocumentNotFound); + ensure!(state.did().is_some(), UpdateError::DocumentNotFound); let fragment: Fragment = Fragment::new(fragment); // The fragment must not be an authentication location ensure!( !KeyLocation::is_authentication_fragment(&fragment), - CommandError::InvalidMethodFragment("reserved") + UpdateError::InvalidMethodFragment("reserved") ); // The verification method must exist - ensure!(state.methods().contains(fragment.name()), CommandError::MethodNotFound); + ensure!(state.methods().contains(fragment.name()), UpdateError::MethodNotFound); Ok(Some(vec![Event::new(EventData::MethodAttached(fragment, scopes))])) } Self::DetachMethod { fragment, scopes } => { // The state must be initialized - ensure!(state.did().is_some(), CommandError::DocumentNotFound); + ensure!(state.did().is_some(), UpdateError::DocumentNotFound); let fragment: Fragment = Fragment::new(fragment); // The fragment must not be an authentication location ensure!( !KeyLocation::is_authentication_fragment(&fragment), - CommandError::InvalidMethodFragment("reserved") + UpdateError::InvalidMethodFragment("reserved") ); // The verification method must exist - ensure!(state.methods().contains(fragment.name()), CommandError::MethodNotFound); + ensure!(state.methods().contains(fragment.name()), UpdateError::MethodNotFound); Ok(Some(vec![Event::new(EventData::MethodDetached(fragment, scopes))])) } @@ -215,12 +217,12 @@ impl Command { properties, } => { // The state must be initialized - ensure!(state.did().is_some(), CommandError::DocumentNotFound); + ensure!(state.did().is_some(), UpdateError::DocumentNotFound); // The service must not exist ensure!( !state.services().contains(&fragment), - CommandError::DuplicateServiceFragment(fragment), + UpdateError::DuplicateServiceFragment(fragment), ); let service: TinyService = TinyService::new(fragment, type_, endpoint, properties); @@ -229,15 +231,12 @@ impl Command { } Self::DeleteService { fragment } => { // The state must be initialized - ensure!(state.did().is_some(), CommandError::DocumentNotFound); + ensure!(state.did().is_some(), UpdateError::DocumentNotFound); let fragment: Fragment = Fragment::new(fragment); // The service must exist - ensure!( - state.services().contains(fragment.name()), - CommandError::ServiceNotFound - ); + ensure!(state.services().contains(fragment.name()), UpdateError::ServiceNotFound); Ok(Some(vec![Event::new(EventData::ServiceDeleted(fragment))])) } @@ -256,7 +255,7 @@ async fn insert_method_secret( MethodSecret::Ed25519(secret_key) => { ensure!( secret_key.as_ref().len() == ed25519::SECRET_KEY_LENGTH, - CommandError::InvalidMethodSecret(format!( + UpdateError::InvalidMethodSecret(format!( "an ed25519 secret key requires {} bytes, found {}", ed25519::SECRET_KEY_LENGTH, secret_key.as_ref().len() @@ -265,7 +264,7 @@ async fn insert_method_secret( ensure!( matches!(method_type, MethodType::Ed25519VerificationKey2018), - CommandError::InvalidMethodSecret( + UpdateError::InvalidMethodSecret( "MethodType::Ed25519VerificationKey2018 can only be used with an ed25519 method secret".to_owned(), ) ); @@ -275,7 +274,7 @@ async fn insert_method_secret( MethodSecret::MerkleKeyCollection(_) => { ensure!( matches!(method_type, MethodType::MerkleKeyCollection2021), - CommandError::InvalidMethodSecret( + UpdateError::InvalidMethodSecret( "MethodType::MerkleKeyCollection2021 can only be used with a MerkleKeyCollection method secret".to_owned(), ) ); @@ -289,54 +288,86 @@ async fn insert_method_secret( // Command Builders // ============================================================================= -impl_command_builder!(CreateIdentity { - @optional network String, - @optional method_secret MethodSecret, - @defaulte authentication MethodType = Ed25519VerificationKey2018, -}); - -impl_command_builder!(CreateMethod { +impl_command_builder!( +/// Create a new method on an identity. +/// +/// # Parameters +/// - `type_`: the type of the method, defaults to [`MethodType::Ed25519VerificationKey2018`]. +/// - `scope`: the scope of the method, defaults to [`MethodScope::default`]. +/// - `fragment`: the identifier of the method in the document, required. +/// - `method_secret`: the secret key to use for the method, optional. Will be generated when omitted. +CreateMethod { @defaulte type_ MethodType = Ed25519VerificationKey2018, @default scope MethodScope, @required fragment String, @optional method_secret MethodSecret }); -impl_command_builder!(DeleteMethod { +impl_command_builder!( +/// Delete a method on an identity. +/// +/// # Parameters +/// - `fragment`: the identifier of the method in the document, required. +DeleteMethod { @required fragment String, }); -impl_command_builder!(AttachMethod { +impl_command_builder!( +/// Attach one or more verification relationships to a method on an identity. +/// +/// # Parameters +/// - `scopes`: the scopes to add, defaults to an empty [`Vec`]. +/// - `fragment`: the identifier of the method in the document, required. +AttachMethod { @required fragment String, @default scopes Vec, }); -impl AttachMethodBuilder { +impl<'account, 'key, K: IdentityKey> AttachMethodBuilder<'account, 'key, K> { pub fn scope(mut self, value: MethodScope) -> Self { self.scopes.get_or_insert_with(Default::default).push(value); self } } -impl_command_builder!(DetachMethod { +impl_command_builder!( +/// Detaches one or more verification relationships from a method on an identity. +/// +/// # Parameters +/// - `scopes`: the scopes to remove, defaults to an empty [`Vec`]. +/// - `fragment`: the identifier of the method in the document, required. +DetachMethod { @required fragment String, @default scopes Vec, }); -impl DetachMethodBuilder { +impl<'account, 'key, K: IdentityKey> DetachMethodBuilder<'account, 'key, K> { pub fn scope(mut self, value: MethodScope) -> Self { self.scopes.get_or_insert_with(Default::default).push(value); self } } -impl_command_builder!(CreateService { +impl_command_builder!( +/// Create a new service on an identity. +/// +/// # Parameters +/// - `type_`: the type of the service, e.g. `"LinkedDomains"`, required. +/// - `fragment`: the identifier of the service in the document, required. +/// - `endpoint`: the url of the service, required. +/// - `properties`: additional properties of the service, optional. +CreateService { @required fragment String, @required type_ String, @required endpoint Url, @optional properties Object, }); -impl_command_builder!(DeleteService { +impl_command_builder!( +/// Delete a service on an identity. +/// +/// # Parameters +/// - `fragment`: the identifier of the service in the document, required. +DeleteService { @required fragment String, }); diff --git a/identity-account/src/events/error.rs b/identity-account/src/events/error.rs index 39c3f7fbb3..5333edab80 100644 --- a/identity-account/src/events/error.rs +++ b/identity-account/src/events/error.rs @@ -6,9 +6,9 @@ use identity_did::verification::MethodType; use crate::types::KeyLocation; -/// Errors than may occur while processing a [Command][crate::events::Command]. +/// Errors than may occur while processing an update in the [`Account`][crate::account::Account]. #[derive(Debug, thiserror::Error)] -pub enum CommandError { +pub enum UpdateError { #[error("document already exists")] DocumentAlreadyExists, #[error("document not found")] diff --git a/identity-account/src/events/macros.rs b/identity-account/src/events/macros.rs index cc5f071e89..b47621aeef 100644 --- a/identity-account/src/events/macros.rs +++ b/identity-account/src/events/macros.rs @@ -4,7 +4,7 @@ macro_rules! ensure { ($cond:expr, $error:expr $(,)?) => { if !$cond { - return Err($crate::Error::CommandError($error)); + return Err($crate::Error::UpdateError($error)); } }; } @@ -25,21 +25,24 @@ macro_rules! impl_command_builder { (@finish $this:ident required $field:ident $ty:ty) => { match $this.$field { Some(value) => value, - None => return Err($crate::Error::CommandError( - $crate::events::CommandError::MissingRequiredField(stringify!($field)), + None => return Err($crate::Error::UpdateError( + $crate::events::UpdateError::MissingRequiredField(stringify!($field)), )), } }; - ($ident:ident { $(@ $requirement:ident $field:ident $ty:ty $(= $value:expr)?),* $(,)* }) => { + ($(#[$doc:meta])* $ident:ident { $(@ $requirement:ident $field:ident $ty:ty $(= $value:expr)?),* $(,)* }) => { paste::paste! { + $(#[$doc])* #[derive(Clone, Debug)] - pub struct [<$ident Builder>] { + pub struct [<$ident Builder>]<'account, 'key, K: $crate::identity::IdentityKey> { + account: &'account Account, + key: &'key K, $( $field: Option<$ty>, )* } - impl [<$ident Builder>] { + impl<'account, 'key, K: $crate::identity::IdentityKey> [<$ident Builder>]<'account, 'key, K> { $( pub fn $field>(mut self, value: VALUE) -> Self { self.$field = Some(value.into()); @@ -47,32 +50,31 @@ macro_rules! impl_command_builder { } )* - pub fn new() -> [<$ident Builder>] { + pub fn new(account: &'account Account, key: &'key K) -> [<$ident Builder>]<'account, 'key, K> { [<$ident Builder>] { + account, + key, $( $field: None, )* } } - pub fn finish(self) -> $crate::Result<$crate::events::Command> { - Ok($crate::events::Command::$ident { + pub async fn apply(self) -> $crate::Result<()> { + let update = $crate::events::Command::$ident { $( $field: impl_command_builder!(@finish self $requirement $field $ty $(= $value)?), )* - }) - } - } + }; - impl Default for [<$ident Builder>] { - fn default() -> Self { - Self::new() + self.account.apply_command(self.key, update).await } } - impl $crate::events::Command { - pub fn [<$ident:snake>]() -> [<$ident Builder>] { - [<$ident Builder>]::new() + impl<'account, 'key, K: $crate::identity::IdentityKey> $crate::identity::IdentityUpdater<'account, 'key, K> { + /// Creates a new builder to modify the identity. See the documentation of the return type for details. + pub fn [<$ident:snake>](&self) -> [<$ident Builder>]<'account, 'key, K> { + [<$ident Builder>]::new(self.account, self.key) } } } diff --git a/identity-account/src/identity/identity_index.rs b/identity-account/src/identity/identity_index.rs index 81945fb5b0..bcc5299a9a 100644 --- a/identity-account/src/identity/identity_index.rs +++ b/identity-account/src/identity/identity_index.rs @@ -49,7 +49,7 @@ impl IdentityIndex { } /// Returns the id of the identity matching the given `key`. - pub fn get(&self, key: K) -> Option { + pub fn get(&self, key: &K) -> Option { key.scan(self.data.iter()) } diff --git a/identity-account/src/identity/identity_updater.rs b/identity-account/src/identity/identity_updater.rs new file mode 100644 index 0000000000..e5e9f9f6fb --- /dev/null +++ b/identity-account/src/identity/identity_updater.rs @@ -0,0 +1,20 @@ +// Copyright 2020-2021 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use crate::account::Account; + +use super::IdentityKey; + +/// A struct created by the [`Account::update_identity`] method, that +/// allows executing various updates on the identity it was created on. +#[derive(Debug, Clone)] +pub struct IdentityUpdater<'account, 'key, K: IdentityKey> { + pub(crate) account: &'account Account, + pub(crate) key: &'key K, +} + +impl<'account, 'key, K: IdentityKey> IdentityUpdater<'account, 'key, K> { + pub(crate) fn new(account: &'account Account, key: &'key K) -> Self { + Self { account, key } + } +} diff --git a/identity-account/src/identity/mod.rs b/identity-account/src/identity/mod.rs index d6251e8ab2..246933ce9e 100644 --- a/identity-account/src/identity/mod.rs +++ b/identity-account/src/identity/mod.rs @@ -9,6 +9,7 @@ mod identity_name; mod identity_snapshot; mod identity_state; mod identity_tag; +mod identity_updater; pub use self::identity_create::*; pub use self::identity_id::*; @@ -18,3 +19,4 @@ pub use self::identity_name::*; pub use self::identity_snapshot::*; pub use self::identity_state::*; pub use self::identity_tag::*; +pub use self::identity_updater::*; diff --git a/identity-account/src/lib.rs b/identity-account/src/lib.rs index f3416f5d78..200829ca78 100644 --- a/identity-account/src/lib.rs +++ b/identity-account/src/lib.rs @@ -28,6 +28,8 @@ pub mod identity; pub mod storage; #[cfg(feature = "stronghold")] pub mod stronghold; +#[cfg(test)] +mod tests; pub mod types; pub mod utils; diff --git a/identity-account/tests/commands.rs b/identity-account/src/tests/commands.rs similarity index 70% rename from identity-account/tests/commands.rs rename to identity-account/src/tests/commands.rs index 6da5436a7b..8fc4891153 100644 --- a/identity-account/tests/commands.rs +++ b/identity-account/src/tests/commands.rs @@ -1,24 +1,25 @@ // Copyright 2020-2021 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -use identity_account::account::Account; -use identity_account::account::Config; -use identity_account::error::Error; -use identity_account::error::Result; -use identity_account::events::Command; -use identity_account::events::CommandError; -use identity_account::identity::IdentityCreate; -use identity_account::identity::IdentityId; -use identity_account::identity::IdentitySnapshot; -use identity_account::identity::TinyMethod; -use identity_account::storage::MemStore; -use identity_account::types::Generation; -use identity_account::types::MethodSecret; +use crate::account::Account; +use crate::account::Config; +use crate::error::Error; +use crate::error::Result; +use crate::events::Command; +use crate::events::UpdateError; +use crate::identity::IdentityCreate; +use crate::identity::IdentityId; +use crate::identity::IdentitySnapshot; +use crate::identity::TinyMethod; +use crate::storage::MemStore; +use crate::types::Generation; +use crate::types::MethodSecret; use identity_core::common::UnixTimestamp; use identity_core::crypto::KeyCollection; use identity_core::crypto::KeyPair; use identity_core::crypto::KeyType; use identity_core::crypto::SecretKey; +use identity_did::verification::MethodScope; use identity_did::verification::MethodType; async fn new_account() -> Result { @@ -40,9 +41,13 @@ async fn test_create_identity() -> Result<()> { assert_eq!(snapshot.identity().created(), UnixTimestamp::EPOCH); assert_eq!(snapshot.identity().updated(), UnixTimestamp::EPOCH); - account - .process(identity, Command::create_identity().finish().unwrap(), false) - .await?; + let command: Command = Command::CreateIdentity { + network: None, + method_secret: None, + authentication: MethodType::Ed25519VerificationKey2018, + }; + + account.process(identity, command, false).await?; let snapshot: IdentitySnapshot = account.load_snapshot(identity).await?; @@ -67,12 +72,17 @@ async fn test_create_identity_invalid_method() -> Result<()> { assert_eq!(snapshot.sequence(), Generation::new()); for type_ in TYPES.iter().copied() { - let command: Command = Command::create_identity().authentication(type_).finish().unwrap(); + let command: Command = Command::CreateIdentity { + network: None, + method_secret: None, + authentication: type_, + }; + let output: Result<()> = account.process(identity, command, false).await; assert!(matches!( output.unwrap_err(), - Error::CommandError(CommandError::InvalidMethodType(_)) + Error::UpdateError(UpdateError::InvalidMethodType(_)) )); let snapshot: IdentitySnapshot = account.load_snapshot(identity).await?; @@ -127,10 +137,11 @@ async fn test_create_identity_already_exists() -> Result<()> { // initial snapshot version = 0 assert_eq!(snapshot.sequence(), Generation::new()); - let command: Command = Command::create_identity() - .authentication(MethodType::Ed25519VerificationKey2018) - .finish() - .unwrap(); + let command: Command = Command::CreateIdentity { + network: None, + method_secret: None, + authentication: MethodType::Ed25519VerificationKey2018, + }; account.process(identity, command.clone(), false).await?; @@ -143,7 +154,7 @@ async fn test_create_identity_already_exists() -> Result<()> { assert!(matches!( output.unwrap_err(), - Error::CommandError(CommandError::DocumentAlreadyExists), + Error::UpdateError(UpdateError::DocumentAlreadyExists), )); let snapshot: IdentitySnapshot = account.load_snapshot(identity).await?; @@ -193,7 +204,7 @@ async fn test_create_identity_from_invalid_secret_key() -> Result<()> { let err = account.create_identity(id_create).await.unwrap_err(); - assert!(matches!(err, Error::CommandError(CommandError::InvalidMethodSecret(_)))); + assert!(matches!(err, Error::UpdateError(UpdateError::InvalidMethodSecret(_)))); Ok(()) } @@ -203,18 +214,20 @@ async fn test_create_method() -> Result<()> { let account: Account = new_account().await?; let identity: IdentityId = IdentityId::from_u32(1); - let command: Command = Command::create_identity() - .authentication(MethodType::Ed25519VerificationKey2018) - .finish() - .unwrap(); + let command: Command = Command::CreateIdentity { + network: None, + method_secret: None, + authentication: MethodType::Ed25519VerificationKey2018, + }; account.process(identity, command, false).await?; - let command: Command = Command::create_method() - .type_(MethodType::Ed25519VerificationKey2018) - .fragment("key-1") - .finish() - .unwrap(); + let command: Command = Command::CreateMethod { + scope: MethodScope::default(), + method_secret: None, + type_: MethodType::Ed25519VerificationKey2018, + fragment: "key-1".to_owned(), + }; account.process(identity, command, false).await?; @@ -240,18 +253,20 @@ async fn test_create_method_reserved_fragment() -> Result<()> { let account: Account = new_account().await?; let identity: IdentityId = IdentityId::from_u32(1); - let command: Command = Command::create_identity() - .authentication(MethodType::Ed25519VerificationKey2018) - .finish() - .unwrap(); + let command: Command = Command::CreateIdentity { + network: None, + method_secret: None, + authentication: MethodType::Ed25519VerificationKey2018, + }; account.process(identity, command, false).await?; - let command: Command = Command::create_method() - .type_(MethodType::Ed25519VerificationKey2018) - .fragment("_sign-123") - .finish() - .unwrap(); + let command: Command = Command::CreateMethod { + scope: MethodScope::default(), + method_secret: None, + type_: MethodType::Ed25519VerificationKey2018, + fragment: "_sign-123".to_owned(), + }; let snapshot: IdentitySnapshot = account.load_snapshot(identity).await?; @@ -262,7 +277,7 @@ async fn test_create_method_reserved_fragment() -> Result<()> { assert!(matches!( output.unwrap_err(), - Error::CommandError(CommandError::InvalidMethodFragment(_)), + Error::UpdateError(UpdateError::InvalidMethodFragment(_)), )); let snapshot: IdentitySnapshot = account.load_snapshot(identity).await?; @@ -278,18 +293,20 @@ async fn test_create_method_duplicate_fragment() -> Result<()> { let account: Account = new_account().await?; let identity: IdentityId = IdentityId::from_u32(1); - let command: Command = Command::create_identity() - .authentication(MethodType::Ed25519VerificationKey2018) - .finish() - .unwrap(); + let command: Command = Command::CreateIdentity { + network: None, + method_secret: None, + authentication: MethodType::Ed25519VerificationKey2018, + }; account.process(identity, command, false).await?; - let command: Command = Command::create_method() - .type_(MethodType::Ed25519VerificationKey2018) - .fragment("key-1") - .finish() - .unwrap(); + let command: Command = Command::CreateMethod { + scope: MethodScope::default(), + method_secret: None, + type_: MethodType::Ed25519VerificationKey2018, + fragment: "key-1".to_owned(), + }; let snapshot: IdentitySnapshot = account.load_snapshot(identity).await?; assert_eq!(snapshot.sequence(), Generation::from_u32(3)); @@ -303,7 +320,7 @@ async fn test_create_method_duplicate_fragment() -> Result<()> { assert!(matches!( output.unwrap_err(), - Error::CommandError(CommandError::DuplicateKeyFragment(_)), + Error::UpdateError(UpdateError::DuplicateKeyFragment(_)), )); let snapshot: IdentitySnapshot = account.load_snapshot(identity).await?; @@ -317,21 +334,22 @@ async fn test_create_method_from_secret_key() -> Result<()> { let account: Account = new_account().await?; let identity: IdentityId = IdentityId::from_u32(1); - let command: Command = Command::create_identity() - .authentication(MethodType::Ed25519VerificationKey2018) - .finish() - .unwrap(); + let command: Command = Command::CreateIdentity { + network: None, + method_secret: None, + authentication: MethodType::Ed25519VerificationKey2018, + }; account.process(identity, command, false).await?; let keypair = KeyPair::new_ed25519()?; - let command: Command = Command::create_method() - .type_(MethodType::Ed25519VerificationKey2018) - .fragment("key-1") - .method_secret(MethodSecret::Ed25519(keypair.secret().clone())) - .finish() - .unwrap(); + let command: Command = Command::CreateMethod { + scope: MethodScope::default(), + method_secret: Some(MethodSecret::Ed25519(keypair.secret().clone())), + type_: MethodType::Ed25519VerificationKey2018, + fragment: "key-1".to_owned(), + }; account.process(identity, command, false).await?; @@ -351,26 +369,27 @@ async fn test_create_method_from_invalid_secret_key() -> Result<()> { let account: Account = new_account().await?; let identity: IdentityId = IdentityId::from_u32(1); - let command: Command = Command::create_identity() - .authentication(MethodType::Ed25519VerificationKey2018) - .finish() - .unwrap(); + let command: Command = Command::CreateIdentity { + network: None, + method_secret: None, + authentication: MethodType::Ed25519VerificationKey2018, + }; account.process(identity, command, false).await?; let secret_bytes: Box<[u8]> = Box::new([0; 33]); let secret_key = SecretKey::from(secret_bytes); - let command: Command = Command::create_method() - .type_(MethodType::Ed25519VerificationKey2018) - .fragment("key-1") - .method_secret(MethodSecret::Ed25519(secret_key)) - .finish() - .unwrap(); + let command: Command = Command::CreateMethod { + scope: MethodScope::default(), + method_secret: Some(MethodSecret::Ed25519(secret_key)), + type_: MethodType::Ed25519VerificationKey2018, + fragment: "key-1".to_owned(), + }; let err = account.process(identity, command, false).await.unwrap_err(); - assert!(matches!(err, Error::CommandError(CommandError::InvalidMethodSecret(_)))); + assert!(matches!(err, Error::UpdateError(UpdateError::InvalidMethodSecret(_)))); Ok(()) } @@ -380,39 +399,40 @@ async fn test_create_method_with_type_secret_mismatch() -> Result<()> { let account: Account = new_account().await?; let identity: IdentityId = IdentityId::from_u32(1); - let command: Command = Command::create_identity() - .authentication(MethodType::Ed25519VerificationKey2018) - .finish() - .unwrap(); + let command: Command = Command::CreateIdentity { + network: None, + method_secret: None, + authentication: MethodType::Ed25519VerificationKey2018, + }; account.process(identity, command, false).await?; let secret_bytes: Box<[u8]> = Box::new([0; 32]); let secret_key = SecretKey::from(secret_bytes); - let command: Command = Command::create_method() - .type_(MethodType::MerkleKeyCollection2021) - .fragment("key-1") - .method_secret(MethodSecret::Ed25519(secret_key)) - .finish() - .unwrap(); + let command: Command = Command::CreateMethod { + scope: MethodScope::default(), + method_secret: Some(MethodSecret::Ed25519(secret_key)), + type_: MethodType::MerkleKeyCollection2021, + fragment: "key-1".to_owned(), + }; let err = account.process(identity, command, false).await.unwrap_err(); - assert!(matches!(err, Error::CommandError(CommandError::InvalidMethodSecret(_)))); + assert!(matches!(err, Error::UpdateError(UpdateError::InvalidMethodSecret(_)))); let key_collection = KeyCollection::new_ed25519(4).unwrap(); - let command: Command = Command::create_method() - .type_(MethodType::Ed25519VerificationKey2018) - .fragment("key-2") - .method_secret(MethodSecret::MerkleKeyCollection(key_collection)) - .finish() - .unwrap(); + let command: Command = Command::CreateMethod { + scope: MethodScope::default(), + method_secret: Some(MethodSecret::MerkleKeyCollection(key_collection)), + type_: MethodType::Ed25519VerificationKey2018, + fragment: "key-2".to_owned(), + }; let err = account.process(identity, command, false).await.unwrap_err(); - assert!(matches!(err, Error::CommandError(CommandError::InvalidMethodSecret(_)))); + assert!(matches!(err, Error::UpdateError(UpdateError::InvalidMethodSecret(_)))); Ok(()) } @@ -422,18 +442,20 @@ async fn test_delete_method() -> Result<()> { let account: Account = new_account().await?; let identity: IdentityId = IdentityId::from_u32(1); - let command: Command = Command::create_identity() - .authentication(MethodType::Ed25519VerificationKey2018) - .finish() - .unwrap(); + let command: Command = Command::CreateIdentity { + network: None, + method_secret: None, + authentication: MethodType::Ed25519VerificationKey2018, + }; account.process(identity, command, false).await?; - let command: Command = Command::create_method() - .type_(MethodType::Ed25519VerificationKey2018) - .fragment("key-1") - .finish() - .unwrap(); + let command: Command = Command::CreateMethod { + scope: MethodScope::default(), + method_secret: None, + type_: MethodType::Ed25519VerificationKey2018, + fragment: "key-1".to_owned(), + }; let snapshot: IdentitySnapshot = account.load_snapshot(identity).await?; assert_eq!(snapshot.sequence(), Generation::from_u32(3)); @@ -448,7 +470,9 @@ async fn test_delete_method() -> Result<()> { assert!(snapshot.identity().methods().get("key-1").is_some()); assert!(snapshot.identity().methods().fetch("key-1").is_ok()); - let command: Command = Command::delete_method().fragment("key-1").finish().unwrap(); + let command: Command = Command::DeleteMethod { + fragment: "key-1".to_owned(), + }; account.process(identity, command, false).await?; diff --git a/identity-account/tests/lazy.rs b/identity-account/src/tests/lazy.rs similarity index 81% rename from identity-account/tests/lazy.rs rename to identity-account/src/tests/lazy.rs index e935280fea..c64bd78cdf 100644 --- a/identity-account/tests/lazy.rs +++ b/identity-account/src/tests/lazy.rs @@ -3,11 +3,10 @@ use std::pin::Pin; +use crate::account::Account; +use crate::identity::{IdentityCreate, IdentitySnapshot, IdentityUpdater}; +use crate::{Error as AccountError, Result}; use futures::Future; -use identity_account::account::Account; -use identity_account::events::Command; -use identity_account::identity::{IdentityCreate, IdentitySnapshot}; -use identity_account::{Error as AccountError, Result}; use identity_core::common::Url; use identity_iota::chain::DocumentHistory; use identity_iota::did::{IotaDID, IotaVerificationMethod}; @@ -35,19 +34,23 @@ async fn test_lazy_updates() -> Result<()> { let did: &IotaDID = snapshot.identity().try_did()?; - let command: Command = Command::create_service() + let did_updater: IdentityUpdater<'_, '_, _> = account.update_identity(did); + + did_updater + .create_service() .fragment("my-service") - .type_("url") + .type_("LinkedDomains") .endpoint(Url::parse("https://example.org").unwrap()) - .finish()?; - account.update_identity(did, command).await?; + .apply() + .await?; - let command: Command = Command::create_service() + did_updater + .create_service() .fragment("my-other-service") - .type_("url") + .type_("LinkedDomains") .endpoint(Url::parse("https://example.org").unwrap()) - .finish()?; - account.update_identity(did, command).await?; + .apply() + .await?; account.publish_updates(did).await?; @@ -73,14 +76,15 @@ async fn test_lazy_updates() -> Result<()> { // More updates to the identity // =========================================================================== - let command: Command = Command::delete_service().fragment("my-service").finish()?; - account.update_identity(did, command).await?; + did_updater.delete_service().fragment("my-service").apply().await?; - let command: Command = Command::delete_service().fragment("my-other-service").finish()?; - account.update_identity(did, command).await?; + did_updater + .delete_service() + .fragment("my-other-service") + .apply() + .await?; - let command: Command = Command::create_method().fragment("new-method").finish()?; - account.update_identity(did, command).await?; + did_updater.create_method().fragment("new-method").apply().await?; account.publish_updates(did).await?; diff --git a/identity-account/src/tests/mod.rs b/identity-account/src/tests/mod.rs new file mode 100644 index 0000000000..4cc77c616c --- /dev/null +++ b/identity-account/src/tests/mod.rs @@ -0,0 +1,5 @@ +// Copyright 2020-2021 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +mod commands; +mod lazy;