identity_iota_core/rebased/migration/
identity.rs

1// Copyright 2020-2024 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4use std::collections::HashMap;
5use std::collections::HashSet;
6use std::error::Error as StdError;
7
8use crate::rebased::iota::move_calls;
9
10use crate::rebased::iota::package::identity_package_id;
11use crate::rebased::proposals::AccessSubIdentityBuilder;
12use iota_interaction::types::error::IotaObjectResponseError;
13use iota_interaction::types::transaction::ProgrammableTransaction;
14use iota_interaction::IotaKeySignature;
15use iota_interaction::IotaTransactionBlockEffectsMutAPI as _;
16use iota_interaction::OptionalSync;
17use product_common::core_client::CoreClient;
18use product_common::core_client::CoreClientReadOnly;
19use product_common::network_name::NetworkName;
20use product_common::transaction::transaction_builder::Transaction;
21use product_common::transaction::transaction_builder::TransactionBuilder;
22use secret_storage::Signer;
23use tokio::sync::OnceCell;
24
25use crate::rebased::iota::types::Number;
26use crate::rebased::proposals::Upgrade;
27use crate::IotaDID;
28use crate::IotaDocument;
29
30use crate::StateMetadataDocument;
31use crate::StateMetadataEncoding;
32use async_trait::async_trait;
33use identity_core::common::Timestamp;
34use iota_interaction::ident_str;
35use iota_interaction::move_types::language_storage::StructTag;
36use iota_interaction::rpc_types::IotaExecutionStatus;
37use iota_interaction::rpc_types::IotaObjectData;
38use iota_interaction::rpc_types::IotaObjectDataOptions;
39use iota_interaction::rpc_types::IotaParsedData;
40use iota_interaction::rpc_types::IotaParsedMoveObject;
41use iota_interaction::rpc_types::IotaPastObjectResponse;
42use iota_interaction::rpc_types::IotaTransactionBlockEffects;
43use iota_interaction::rpc_types::IotaTransactionBlockEffectsAPI as _;
44use iota_interaction::types::base_types::IotaAddress;
45use iota_interaction::types::base_types::ObjectID;
46use iota_interaction::types::id::UID;
47use iota_interaction::types::object::Owner;
48use iota_interaction::types::TypeTag;
49use serde;
50use serde::Deserialize;
51use serde::Serialize;
52
53use crate::rebased::client::IdentityClientReadOnly;
54use crate::rebased::proposals::BorrowAction;
55use crate::rebased::proposals::ConfigChange;
56use crate::rebased::proposals::ControllerExecution;
57use crate::rebased::proposals::ProposalBuilder;
58use crate::rebased::proposals::SendAction;
59use crate::rebased::proposals::UpdateDidDocument;
60use crate::rebased::rebased_err;
61use crate::rebased::Error;
62use iota_interaction::IotaClientTrait;
63use iota_interaction::MoveType;
64
65use super::ControllerCap;
66use super::ControllerToken;
67use super::DelegationToken;
68use super::DelegationTokenRevocation;
69use super::DeleteDelegationToken;
70use super::Multicontroller;
71use super::UnmigratedAlias;
72
73const MODULE: &str = "identity";
74const NAME: &str = "Identity";
75const HISTORY_DEFAULT_PAGE_SIZE: usize = 10;
76
77/// The data stored in an on-chain identity.
78pub(crate) struct IdentityData {
79  pub(crate) id: UID,
80  pub(crate) multicontroller: Multicontroller<Option<Vec<u8>>>,
81  pub(crate) legacy_id: Option<ObjectID>,
82  pub(crate) created: Timestamp,
83  pub(crate) updated: Timestamp,
84  pub(crate) version: u64,
85  pub(crate) deleted: bool,
86  pub(crate) deleted_did: bool,
87}
88
89/// An on-chain object holding a DID Document.
90#[derive(Clone)]
91pub enum Identity {
92  /// A legacy IOTA Stardust's Identity.
93  Legacy(UnmigratedAlias),
94  /// An on-chain Identity.
95  FullFledged(OnChainIdentity),
96}
97
98impl Identity {
99  /// Returns the [`IotaDocument`] DID Document stored inside this [`Identity`].
100  pub fn did_document(&self, network: &NetworkName) -> Result<IotaDocument, Error> {
101    match self {
102      Self::FullFledged(onchain_identity) => Ok(onchain_identity.did_doc.clone()),
103      Self::Legacy(alias) => {
104        let state_metadata = alias.state_metadata.as_deref().ok_or_else(|| {
105          Error::DidDocParsingFailed("legacy stardust alias doesn't contain a DID Document".to_string())
106        })?;
107        let did = IotaDID::from_object_id(*alias.id.object_id(), network);
108        StateMetadataDocument::unpack(state_metadata)
109          .and_then(|state_metadata_doc| state_metadata_doc.into_iota_document(&did))
110          .map_err(|e| Error::DidDocParsingFailed(e.to_string()))
111      }
112    }
113  }
114}
115
116/// An on-chain entity that wraps an optional DID Document.
117#[derive(Debug, Clone, Serialize)]
118pub struct OnChainIdentity {
119  id: UID,
120  multi_controller: Multicontroller<Option<Vec<u8>>>,
121  pub(crate) did_doc: IotaDocument,
122  version: u64,
123  deleted: bool,
124  deleted_did: bool,
125}
126
127impl OnChainIdentity {
128  /// Returns the [`ObjectID`] of this [`OnChainIdentity`].
129  pub fn id(&self) -> ObjectID {
130    *self.id.object_id()
131  }
132
133  /// Returns the [`IotaDocument`] contained in this [`OnChainIdentity`].
134  pub fn did_document(&self) -> &IotaDocument {
135    &self.did_doc
136  }
137
138  pub(crate) fn did_document_mut(&mut self) -> &mut IotaDocument {
139    &mut self.did_doc
140  }
141
142  /// Returns whether the [IotaDocument] contained in this [OnChainIdentity] has been deleted.
143  /// Once a DID Document is deleted, it cannot be reactivated.
144  ///
145  /// When calling [OnChainIdentity::did_document] on an Identity whose DID Document
146  /// had been deleted, an *empty* and *deactivated* [IotaDocument] will be returned.
147  pub fn has_deleted_did(&self) -> bool {
148    self.deleted_did
149  }
150
151  /// Returns true if this [`OnChainIdentity`] is shared between multiple controllers.
152  pub fn is_shared(&self) -> bool {
153    self.multi_controller.controllers().len() > 1
154  }
155
156  /// Returns this [`OnChainIdentity`]'s list of active proposals.
157  pub fn proposals(&self) -> &HashSet<ObjectID> {
158    self.multi_controller.proposals()
159  }
160
161  /// Returns this [`OnChainIdentity`]'s controllers as the map: `controller_id -> controller_voting_power`.
162  pub fn controllers(&self) -> &HashMap<ObjectID, u64> {
163    self.multi_controller.controllers()
164  }
165
166  /// Returns the threshold required by this [`OnChainIdentity`] for executing a proposal.
167  pub fn threshold(&self) -> u64 {
168    self.multi_controller.threshold()
169  }
170
171  /// Returns the voting power of controller with ID `controller_id`, if any.
172  pub fn controller_voting_power(&self, controller_id: ObjectID) -> Option<u64> {
173    self.multi_controller.controller_voting_power(controller_id)
174  }
175
176  /// Returns a [ControllerToken] owned by `address` that grants access to this Identity.
177  /// ## Notes
178  /// [None] is returned if `address` doesn't own a valid [ControllerToken].
179  pub async fn get_controller_token_for_address(
180    &self,
181    address: IotaAddress,
182    client: &(impl CoreClientReadOnly + OptionalSync),
183  ) -> Result<Option<ControllerToken>, Error> {
184    let maybe_controller_cap = client
185      .find_object_for_address::<ControllerCap, _>(address, |token| token.controller_of() == self.id())
186      .await;
187
188    if let Ok(Some(controller_cap)) = maybe_controller_cap {
189      return Ok(Some(controller_cap.into()));
190    }
191
192    client
193      .find_object_for_address::<DelegationToken, _>(address, |token| token.controller_of() == self.id())
194      .await
195      .map(|maybe_delegate| maybe_delegate.map(ControllerToken::from))
196      .map_err(|e| Error::RpcError(format!("{e:#}")))
197  }
198
199  /// Returns a [ControllerToken], owned by `client`'s sender address, that grants access to this Identity.
200  /// ## Notes
201  /// [None] is returned if `client`'s sender address doesn't own a valid [ControllerToken].
202  pub async fn get_controller_token<S: Signer<IotaKeySignature> + OptionalSync>(
203    &self,
204    client: &(impl CoreClient<S> + OptionalSync),
205  ) -> Result<Option<ControllerToken>, Error> {
206    self
207      .get_controller_token_for_address(client.sender_address(), client)
208      .await
209  }
210
211  pub(crate) fn multicontroller(&self) -> &Multicontroller<Option<Vec<u8>>> {
212    &self.multi_controller
213  }
214
215  /// Updates this [`OnChainIdentity`]'s DID Document.
216  pub fn update_did_document<'i, 'c>(
217    &'i mut self,
218    updated_doc: IotaDocument,
219    controller_token: &'c ControllerToken,
220  ) -> ProposalBuilder<'i, 'c, UpdateDidDocument> {
221    ProposalBuilder::new(self, controller_token, UpdateDidDocument::new(updated_doc))
222  }
223
224  /// Updates this [`OnChainIdentity`]'s configuration.
225  pub fn update_config<'i, 'c>(
226    &'i mut self,
227    controller_token: &'c ControllerToken,
228  ) -> ProposalBuilder<'i, 'c, ConfigChange> {
229    ProposalBuilder::new(self, controller_token, ConfigChange::default())
230  }
231
232  /// Deactivates the DID Document represented by this [`OnChainIdentity`].
233  pub fn deactivate_did<'i, 'c>(
234    &'i mut self,
235    controller_token: &'c ControllerToken,
236  ) -> ProposalBuilder<'i, 'c, UpdateDidDocument> {
237    ProposalBuilder::new(self, controller_token, UpdateDidDocument::deactivate())
238  }
239
240  /// Deletes the DID Document contained in this [OnChainIdentity].
241  pub fn delete_did<'i, 'c>(
242    &'i mut self,
243    controller_token: &'c ControllerToken,
244  ) -> ProposalBuilder<'i, 'c, UpdateDidDocument> {
245    ProposalBuilder::new(self, controller_token, UpdateDidDocument::delete())
246  }
247
248  /// Upgrades this [`OnChainIdentity`]'s version to match the package's.
249  pub fn upgrade_version<'i, 'c>(
250    &'i mut self,
251    controller_token: &'c ControllerToken,
252  ) -> ProposalBuilder<'i, 'c, Upgrade> {
253    ProposalBuilder::new(self, controller_token, Upgrade)
254  }
255
256  /// Sends assets owned by this [`OnChainIdentity`] to other addresses.
257  pub fn send_assets<'i, 'c>(
258    &'i mut self,
259    controller_token: &'c ControllerToken,
260  ) -> ProposalBuilder<'i, 'c, SendAction> {
261    ProposalBuilder::new(self, controller_token, SendAction::default())
262  }
263
264  /// Borrows assets owned by this [`OnChainIdentity`] to use them in a custom transaction.
265  pub fn borrow_assets<'i, 'c>(
266    &'i mut self,
267    controller_token: &'c ControllerToken,
268  ) -> ProposalBuilder<'i, 'c, BorrowAction> {
269    ProposalBuilder::new(self, controller_token, BorrowAction::default())
270  }
271
272  /// Borrows a `ControllerCap` with ID `controller_cap` owned by this identity in a transaction.
273  /// This proposal is used to perform operation on a sub-identity controlled
274  /// by this one.
275  #[deprecated = "use `OnChainIdentity::access_sub_identity` instead."]
276  pub fn controller_execution<'i, 'c>(
277    &'i mut self,
278    controller_cap: ObjectID,
279    controller_token: &'c ControllerToken,
280  ) -> ProposalBuilder<'i, 'c, ControllerExecution> {
281    let action = ControllerExecution::new(controller_cap, self);
282    ProposalBuilder::new(self, controller_token, action)
283  }
284
285  /// Perform an action on an Identity that is controlled by this Identity.
286  pub fn access_sub_identity<'i, 'sub>(
287    &'i mut self,
288    sub_identity: &'sub mut OnChainIdentity,
289    controller_token: &ControllerToken,
290  ) -> AccessSubIdentityBuilder<'i, 'sub> {
291    AccessSubIdentityBuilder::new(self, sub_identity, controller_token)
292  }
293
294  /// Returns historical data for this [`OnChainIdentity`].
295  pub async fn get_history(
296    &self,
297    client: &IdentityClientReadOnly,
298    last_version: Option<&IotaObjectData>,
299    page_size: Option<usize>,
300  ) -> Result<Vec<IotaObjectData>, Error> {
301    let identity_ref = client
302      .get_object_ref_by_id(self.id())
303      .await?
304      .ok_or_else(|| Error::InvalidIdentityHistory("no reference to identity loaded".to_string()))?;
305    let object_id = identity_ref.object_id();
306
307    let mut history: Vec<IotaObjectData> = vec![];
308    let mut current_version = if let Some(last_version_value) = last_version {
309      // starting version given, this will be skipped in paging
310      last_version_value.clone()
311    } else {
312      // no version given, this version will be included in history
313      let version = identity_ref.version();
314      let response = client.get_past_object(object_id, version).await.map_err(rebased_err)?;
315      let latest_version = if let IotaPastObjectResponse::VersionFound(response_value) = response {
316        response_value
317      } else {
318        return Err(Error::InvalidIdentityHistory(format!(
319          "could not find current version {version} of object {object_id}, response {response:?}"
320        )));
321      };
322      history.push(latest_version.clone()); // include current version in history if we start from now
323      latest_version
324    };
325
326    // limit lookup count to prevent locking on large histories
327    let page_size = page_size.unwrap_or(HISTORY_DEFAULT_PAGE_SIZE);
328    while history.len() < page_size {
329      let lookup = get_previous_version(client, current_version).await?;
330      if let Some(value) = lookup {
331        current_version = value;
332        history.push(current_version.clone());
333      } else {
334        break;
335      }
336    }
337
338    Ok(history)
339  }
340
341  /// Returns a [Transaction] to revoke a [DelegationToken].
342  pub fn revoke_delegation_token(
343    &self,
344    controller_capability: &ControllerCap,
345    delegation_token: &DelegationToken,
346  ) -> Result<TransactionBuilder<DelegationTokenRevocation>, Error> {
347    DelegationTokenRevocation::revoke(self, controller_capability, delegation_token).map(TransactionBuilder::new)
348  }
349
350  /// Returns a [Transaction] to *un*revoke a [DelegationToken].
351  pub fn unrevoke_delegation_token(
352    &self,
353    controller_capability: &ControllerCap,
354    delegation_token: &DelegationToken,
355  ) -> Result<TransactionBuilder<DelegationTokenRevocation>, Error> {
356    DelegationTokenRevocation::unrevoke(self, controller_capability, delegation_token).map(TransactionBuilder::new)
357  }
358
359  /// Returns a [Transaction] to delete a [DelegationToken].
360  pub fn delete_delegation_token(
361    &self,
362    delegation_token: DelegationToken,
363  ) -> Result<TransactionBuilder<DeleteDelegationToken>, Error> {
364    DeleteDelegationToken::new(self, delegation_token).map(TransactionBuilder::new)
365  }
366}
367
368/// Returns the previous version of the given `history_item`.
369pub fn has_previous_version(history_item: &IotaObjectData) -> Result<bool, Error> {
370  if let Some(Owner::Shared { initial_shared_version }) = history_item.owner {
371    Ok(history_item.version != initial_shared_version)
372  } else {
373    Err(Error::InvalidIdentityHistory(format!(
374      "provided history item does not seem to be a valid identity; {history_item}"
375    )))
376  }
377}
378
379async fn get_previous_version(
380  client: &IdentityClientReadOnly,
381  iod: IotaObjectData,
382) -> Result<Option<IotaObjectData>, Error> {
383  client.get_previous_version(iod).await.map_err(rebased_err)
384}
385
386/// Returns the [`OnChainIdentity`] having ID `object_id`, if it exists.
387pub async fn get_identity(
388  client: &impl CoreClientReadOnly,
389  object_id: ObjectID,
390) -> Result<Option<OnChainIdentity>, Error> {
391  use IdentityResolutionErrorKind::NotFound;
392
393  match get_identity_impl(client, object_id).await {
394    Ok(identity) => Ok(Some(identity)),
395    Err(IdentityResolutionError { kind: NotFound, .. }) => Ok(None),
396    Err(e) => {
397      // Use anyhow to format the error in such a way that all its causes are displayed too.
398      let formatted_err_msg = format!("{:#}", anyhow::Error::new(e));
399      Err(Error::ObjectLookup(formatted_err_msg))
400    }
401  }
402}
403
404pub(crate) async fn get_identity_impl(
405  client: &impl CoreClientReadOnly,
406  object_id: ObjectID,
407) -> Result<OnChainIdentity, IdentityResolutionError> {
408  let response = client
409    .client_adapter()
410    .read_api()
411    .get_object_with_options(object_id, IotaObjectDataOptions::new().with_content())
412    .await
413    .map_err(|e| IdentityResolutionError {
414      kind: IdentityResolutionErrorKind::RpcError(e.into()),
415      resolving: object_id,
416    })?;
417
418  if let Some(response_error) = response.error {
419    match response_error {
420      IotaObjectResponseError::NotExists { .. } | IotaObjectResponseError::Deleted { .. } => {
421        return Err(IdentityResolutionError {
422          resolving: object_id,
423          kind: IdentityResolutionErrorKind::NotFound,
424        })
425      }
426      _ => unreachable!(),
427    }
428  }
429
430  let data = response.data.expect("already handled errors in response");
431  let network = client.network_name();
432  let did = IotaDID::from_object_id(object_id, network);
433  let IdentityData {
434    id,
435    multicontroller,
436    legacy_id,
437    created,
438    updated,
439    version,
440    deleted,
441    deleted_did,
442  } = unpack_identity_data(data)?;
443  let legacy_did = legacy_id.map(|legacy_id| IotaDID::from_object_id(legacy_id, client.network_name()));
444
445  let did_doc = multicontroller
446    .controlled_value()
447    .as_deref()
448    .map(|did_doc_bytes| IotaDocument::from_iota_document_data(did_doc_bytes, true, &did, legacy_did, created, updated))
449    .transpose()
450    .map_err(|e| IdentityResolutionError {
451      resolving: object_id,
452      kind: IdentityResolutionErrorKind::InvalidDidDocument(e.into()),
453    })?
454    .unwrap_or_else(|| {
455      let mut empty_did_doc = IotaDocument::new(network);
456      empty_did_doc.metadata.deactivated = Some(true);
457
458      empty_did_doc
459    });
460
461  Ok(OnChainIdentity {
462    id,
463    multi_controller: multicontroller,
464    did_doc,
465    version,
466    deleted,
467    deleted_did,
468  })
469}
470
471/// Type of failures that can be encountered when resolving an Identity.
472#[derive(Debug, thiserror::Error)]
473#[non_exhaustive]
474pub enum IdentityResolutionErrorKind {
475  /// RPC request to an IOTA Node failed.
476  #[error("object lookup RPC request failed")]
477  RpcError(#[source] Box<dyn StdError + Send + Sync>),
478  /// The queried object ID doesn't exist on-chain.
479  #[error("Identity does not exist")]
480  NotFound,
481  /// Type.
482  #[error("invalid object type: expected `iota_identity::identity::Identity`, found `{0}`")]
483  InvalidType(String),
484  /// Malformed DID Document.
485  #[error("invalid or malformed DID Document")]
486  InvalidDidDocument(#[source] Box<dyn StdError + Send + Sync>),
487  /// Malformed Identity object
488  #[error("malformed Identity object")]
489  Malformed(#[source] Box<dyn StdError + Send + Sync>),
490}
491
492/// Failed to resolve an Identity by its ID.
493#[derive(Debug, thiserror::Error)]
494#[error("failed to resolve Identity `{resolving}`")]
495#[non_exhaustive]
496pub struct IdentityResolutionError {
497  /// The Identity's ID.
498  pub resolving: ObjectID,
499  /// Specific type of failure for this error.
500  #[source]
501  pub kind: IdentityResolutionErrorKind,
502}
503
504fn is_identity(value: &IotaParsedMoveObject) -> bool {
505  // if available we might also check if object stems from expected module
506  // but how would this act upon package updates?
507  value.type_.module.as_ident_str().as_str() == MODULE && value.type_.name.as_ident_str().as_str() == NAME
508}
509
510/// Unpack identity data from given `IotaObjectData`
511///
512/// # Errors:
513/// * in case given data for DID is not an object
514/// * parsing identity data from object fails
515pub(crate) fn unpack_identity_data(data: IotaObjectData) -> Result<IdentityData, IdentityResolutionError> {
516  let content = data.content.ok_or_else(|| IdentityResolutionError {
517    resolving: data.object_id,
518    kind: IdentityResolutionErrorKind::RpcError("no content in RPC response".into()),
519  })?;
520
521  let IotaParsedData::MoveObject(value) = content else {
522    return Err(IdentityResolutionError {
523      resolving: data.object_id,
524      kind: IdentityResolutionErrorKind::InvalidType("Move Package".to_owned()),
525    });
526  };
527
528  if !is_identity(&value) {
529    return Err(IdentityResolutionError {
530      resolving: data.object_id,
531      kind: IdentityResolutionErrorKind::InvalidType(value.type_.to_canonical_string(true)),
532    });
533  }
534
535  #[derive(Deserialize)]
536  struct TempOnChainIdentity {
537    id: UID,
538    did_doc: Multicontroller<Option<Vec<u8>>>,
539    legacy_id: Option<ObjectID>,
540    created: Number<u64>,
541    updated: Number<u64>,
542    version: Number<u64>,
543    deleted: bool,
544    deleted_did: bool,
545  }
546
547  let TempOnChainIdentity {
548    id,
549    did_doc: multicontroller,
550    legacy_id,
551    created,
552    updated,
553    version,
554    deleted,
555    deleted_did,
556  } = serde_json::from_value::<TempOnChainIdentity>(value.fields.to_json_value()).map_err(|err| {
557    IdentityResolutionError {
558      resolving: data.object_id,
559      kind: IdentityResolutionErrorKind::Malformed(err.into()),
560    }
561  })?;
562
563  // Parse DID document timestamps
564  let created = {
565    let timestamp_ms: u64 = created.try_into().expect("Move string-encoded u64 are valid u64");
566    // `Timestamp` requires a timestamp expressed in seconds.
567    Timestamp::from_unix(timestamp_ms as i64 / 1000).expect("On-chain clock produces valid timestamps")
568  };
569  let updated = {
570    let timestamp_ms: u64 = updated.try_into().expect("Move string-encoded u64 are valid u64");
571    // `Timestamp` requires a timestamp expressed in seconds.
572    Timestamp::from_unix(timestamp_ms as i64 / 1000).expect("On-chain clock produces valid timestamps")
573  };
574  let version = version.try_into().expect("Move string-encoded u64 are valid u64");
575
576  Ok(IdentityData {
577    id,
578    multicontroller,
579    legacy_id,
580    created,
581    updated,
582    version,
583    deleted,
584    deleted_did,
585  })
586}
587
588impl From<OnChainIdentity> for IotaDocument {
589  fn from(identity: OnChainIdentity) -> Self {
590    identity.did_doc
591  }
592}
593
594/// Builder-style struct to create a new [`OnChainIdentity`].
595#[derive(Debug)]
596pub struct IdentityBuilder {
597  did_doc: IotaDocument,
598  threshold: Option<u64>,
599  controllers: HashMap<IotaAddress, (u64, bool)>,
600}
601
602impl IdentityBuilder {
603  /// Initializes a new builder for an [`OnChainIdentity`], where the passed `did_doc` will be
604  /// used as the identity's DID Document.
605  /// ## Warning
606  /// Validation of `did_doc` is deferred to [CreateIdentity].
607  pub fn new(did_doc: IotaDocument) -> Self {
608    Self {
609      did_doc,
610      threshold: None,
611      controllers: HashMap::new(),
612    }
613  }
614
615  /// Gives `address` the capability to act as a controller with voting power `voting_power`.
616  pub fn controller(mut self, address: IotaAddress, voting_power: u64) -> Self {
617    self.controllers.insert(address, (voting_power, false));
618    self
619  }
620
621  /// Gives `address` the capability to act as a controller with voting power `voting_power` and
622  /// the ability to delegate its access to third parties.
623  pub fn controller_with_delegation(mut self, address: IotaAddress, voting_power: u64) -> Self {
624    self.controllers.insert(address, (voting_power, true));
625    self
626  }
627
628  /// Sets the identity's threshold.
629  pub fn threshold(mut self, threshold: u64) -> Self {
630    self.threshold = Some(threshold);
631    self
632  }
633
634  /// Sets multiple controllers in a single step. See [`IdentityBuilder::controller`].
635  pub fn controllers<I>(self, controllers: I) -> Self
636  where
637    I: IntoIterator<Item = (IotaAddress, u64)>,
638  {
639    controllers
640      .into_iter()
641      .fold(self, |builder, (addr, vp)| builder.controller(addr, vp))
642  }
643
644  /// Sets multiple controllers in a single step.
645  /// Differently from [IdentityBuilder::controllers], this method requires
646  /// the controller's data to be passed as the triple `(address, voting power, delegate-ability)`.
647  /// A `true` value as the tuple's third value means the controller *CAN* delegate its access.
648  pub fn controllers_with_delegation<I>(self, controllers: I) -> Self
649  where
650    I: IntoIterator<Item = (IotaAddress, u64, bool)>,
651  {
652    controllers.into_iter().fold(self, |builder, (addr, vp, can_delegate)| {
653      if can_delegate {
654        builder.controller_with_delegation(addr, vp)
655      } else {
656        builder.controller(addr, vp)
657      }
658    })
659  }
660
661  /// Turns this builder into a [`Transaction`], ready to be executed.
662  pub fn finish(self) -> TransactionBuilder<CreateIdentity> {
663    TransactionBuilder::new(CreateIdentity::new(self))
664  }
665}
666
667impl MoveType for OnChainIdentity {
668  fn move_type(package: ObjectID) -> TypeTag {
669    TypeTag::Struct(Box::new(StructTag {
670      address: package.into(),
671      module: ident_str!("identity").into(),
672      name: ident_str!("Identity").into(),
673      type_params: vec![],
674    }))
675  }
676}
677
678/// A [`Transaction`] for creating a new [`OnChainIdentity`] from an [`IdentityBuilder`].
679#[derive(Debug)]
680pub struct CreateIdentity {
681  builder: IdentityBuilder,
682  cached_ptb: OnceCell<ProgrammableTransaction>,
683}
684
685impl CreateIdentity {
686  /// Returns a new [CreateIdentity] [Transaction] from an [IdentityBuilder]
687  pub fn new(builder: IdentityBuilder) -> CreateIdentity {
688    Self {
689      builder,
690      cached_ptb: OnceCell::new(),
691    }
692  }
693
694  async fn make_ptb(&self, client: &impl CoreClientReadOnly) -> Result<ProgrammableTransaction, Error> {
695    let IdentityBuilder {
696      did_doc,
697      threshold,
698      controllers,
699    } = &self.builder;
700    let package = identity_package_id(client).await?;
701    let did_doc = StateMetadataDocument::from(did_doc.clone())
702      .pack(StateMetadataEncoding::default())
703      .map_err(|e| Error::DidDocSerialization(e.to_string()))?;
704    let pt_bcs = if controllers.is_empty() {
705      move_calls::identity::new_identity(Some(&did_doc), package).await?
706    } else {
707      let threshold = match threshold {
708        Some(t) => t,
709        None if controllers.len() == 1 => {
710          &controllers
711            .values()
712            .next()
713            .ok_or_else(|| Error::Identity("could not get controller".to_string()))?
714            .0
715        }
716        None => {
717          return Err(Error::TransactionBuildingFailed(
718            "Missing field `threshold` in identity creation".to_owned(),
719          ))
720        }
721      };
722      let controllers = controllers
723        .iter()
724        .map(|(addr, (vp, can_delegate))| (*addr, *vp, *can_delegate));
725      move_calls::identity::new_with_controllers(Some(&did_doc), controllers, *threshold, package).await?
726    };
727
728    Ok(bcs::from_bytes(&pt_bcs)?)
729  }
730}
731
732#[cfg_attr(not(feature = "send-sync"), async_trait(?Send))]
733#[cfg_attr(feature = "send-sync", async_trait)]
734impl Transaction for CreateIdentity {
735  type Output = OnChainIdentity;
736  type Error = Error;
737
738  async fn build_programmable_transaction<C>(&self, client: &C) -> Result<ProgrammableTransaction, Self::Error>
739  where
740    C: CoreClientReadOnly + OptionalSync,
741  {
742    self.cached_ptb.get_or_try_init(|| self.make_ptb(client)).await.cloned()
743  }
744
745  async fn apply<C>(
746    mut self,
747    effects: &mut IotaTransactionBlockEffects,
748    client: &C,
749  ) -> Result<Self::Output, Self::Error>
750  where
751    C: CoreClientReadOnly + OptionalSync,
752  {
753    if let IotaExecutionStatus::Failure { error } = effects.status() {
754      return Err(Error::TransactionUnexpectedResponse(error.clone()));
755    }
756
757    let created_objects = effects
758      .created()
759      .iter()
760      .enumerate()
761      .filter(|(_, elem)| matches!(elem.owner, Owner::Shared { .. }))
762      .map(|(i, obj)| (i, obj.object_id()));
763
764    let target_did_bytes = StateMetadataDocument::from(self.builder.did_doc)
765      .pack(StateMetadataEncoding::Json)
766      .map_err(|e| Error::DidDocSerialization(e.to_string()))?;
767
768    let is_target_identity = |identity: &OnChainIdentity| -> bool {
769      let did_bytes = identity
770        .multicontroller()
771        .controlled_value()
772        .as_deref()
773        .unwrap_or_default();
774      target_did_bytes == did_bytes && self.builder.threshold.unwrap_or(1) == identity.threshold()
775    };
776
777    let mut target_identity_pos = None;
778    let mut target_identity = None;
779    for (i, obj_id) in created_objects {
780      match get_identity(client, obj_id).await {
781        Ok(Some(identity)) if is_target_identity(&identity) => {
782          target_identity_pos = Some(i);
783          target_identity = Some(identity);
784          break;
785        }
786        _ => continue,
787      }
788    }
789
790    let (Some(i), Some(identity)) = (target_identity_pos, target_identity) else {
791      return Err(Error::TransactionUnexpectedResponse(
792        "failed to find the correct identity in this transaction's effects".to_owned(),
793      ));
794    };
795
796    effects.created_mut().swap_remove(i);
797
798    Ok(identity)
799  }
800}