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().to_string(), 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.to_string(), 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.to_string(), 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(crate) 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  #[error("invalid or malformed DID Document")]
485  InvalidDidDocument(#[source] Box<dyn StdError + Send + Sync>),
486  #[error("malformed Identity object")]
487  Malformed(#[source] Box<dyn StdError + Send + Sync>),
488}
489
490#[derive(Debug, thiserror::Error)]
491#[non_exhaustive]
492#[error("failed to resolve Identity `{resolving}`")]
493pub(crate) struct IdentityResolutionError {
494  pub resolving: ObjectID,
495  #[source]
496  pub kind: IdentityResolutionErrorKind,
497}
498
499fn is_identity(value: &IotaParsedMoveObject) -> bool {
500  // if available we might also check if object stems from expected module
501  // but how would this act upon package updates?
502  value.type_.module.as_ident_str().as_str() == MODULE && value.type_.name.as_ident_str().as_str() == NAME
503}
504
505/// Unpack identity data from given `IotaObjectData`
506///
507/// # Errors:
508/// * in case given data for DID is not an object
509/// * parsing identity data from object fails
510pub(crate) fn unpack_identity_data(data: IotaObjectData) -> Result<IdentityData, IdentityResolutionError> {
511  let content = data.content.ok_or_else(|| IdentityResolutionError {
512    resolving: data.object_id,
513    kind: IdentityResolutionErrorKind::RpcError("no content in RPC response".into()),
514  })?;
515
516  let IotaParsedData::MoveObject(value) = content else {
517    return Err(IdentityResolutionError {
518      resolving: data.object_id,
519      kind: IdentityResolutionErrorKind::InvalidType("Move Package".to_owned()),
520    });
521  };
522
523  if !is_identity(&value) {
524    return Err(IdentityResolutionError {
525      resolving: data.object_id,
526      kind: IdentityResolutionErrorKind::InvalidType(value.type_.to_canonical_string(true)),
527    });
528  }
529
530  #[derive(Deserialize)]
531  struct TempOnChainIdentity {
532    id: UID,
533    did_doc: Multicontroller<Option<Vec<u8>>>,
534    legacy_id: Option<ObjectID>,
535    created: Number<u64>,
536    updated: Number<u64>,
537    version: Number<u64>,
538    deleted: bool,
539    deleted_did: bool,
540  }
541
542  let TempOnChainIdentity {
543    id,
544    did_doc: multicontroller,
545    legacy_id,
546    created,
547    updated,
548    version,
549    deleted,
550    deleted_did,
551  } = serde_json::from_value::<TempOnChainIdentity>(value.fields.to_json_value()).map_err(|err| {
552    IdentityResolutionError {
553      resolving: data.object_id,
554      kind: IdentityResolutionErrorKind::Malformed(err.into()),
555    }
556  })?;
557
558  // Parse DID document timestamps
559  let created = {
560    let timestamp_ms: u64 = created.try_into().expect("Move string-encoded u64 are valid u64");
561    // `Timestamp` requires a timestamp expressed in seconds.
562    Timestamp::from_unix(timestamp_ms as i64 / 1000).expect("On-chain clock produces valid timestamps")
563  };
564  let updated = {
565    let timestamp_ms: u64 = updated.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 version = version.try_into().expect("Move string-encoded u64 are valid u64");
570
571  Ok(IdentityData {
572    id,
573    multicontroller,
574    legacy_id,
575    created,
576    updated,
577    version,
578    deleted,
579    deleted_did,
580  })
581}
582
583impl From<OnChainIdentity> for IotaDocument {
584  fn from(identity: OnChainIdentity) -> Self {
585    identity.did_doc
586  }
587}
588
589/// Builder-style struct to create a new [`OnChainIdentity`].
590#[derive(Debug)]
591pub struct IdentityBuilder {
592  did_doc: IotaDocument,
593  threshold: Option<u64>,
594  controllers: HashMap<IotaAddress, (u64, bool)>,
595}
596
597impl IdentityBuilder {
598  /// Initializes a new builder for an [`OnChainIdentity`], where the passed `did_doc` will be
599  /// used as the identity's DID Document.
600  /// ## Warning
601  /// Validation of `did_doc` is deferred to [CreateIdentity].
602  pub fn new(did_doc: IotaDocument) -> Self {
603    Self {
604      did_doc,
605      threshold: None,
606      controllers: HashMap::new(),
607    }
608  }
609
610  /// Gives `address` the capability to act as a controller with voting power `voting_power`.
611  pub fn controller(mut self, address: IotaAddress, voting_power: u64) -> Self {
612    self.controllers.insert(address, (voting_power, false));
613    self
614  }
615
616  /// Gives `address` the capability to act as a controller with voting power `voting_power` and
617  /// the ability to delegate its access to third parties.
618  pub fn controller_with_delegation(mut self, address: IotaAddress, voting_power: u64) -> Self {
619    self.controllers.insert(address, (voting_power, true));
620    self
621  }
622
623  /// Sets the identity's threshold.
624  pub fn threshold(mut self, threshold: u64) -> Self {
625    self.threshold = Some(threshold);
626    self
627  }
628
629  /// Sets multiple controllers in a single step. See [`IdentityBuilder::controller`].
630  pub fn controllers<I>(self, controllers: I) -> Self
631  where
632    I: IntoIterator<Item = (IotaAddress, u64)>,
633  {
634    controllers
635      .into_iter()
636      .fold(self, |builder, (addr, vp)| builder.controller(addr, vp))
637  }
638
639  /// Sets multiple controllers in a single step.
640  /// Differently from [IdentityBuilder::controllers], this method requires
641  /// the controller's data to be passed as the triple `(address, voting power, delegate-ability)`.
642  /// A `true` value as the tuple's third value means the controller *CAN* delegate its access.
643  pub fn controllers_with_delegation<I>(self, controllers: I) -> Self
644  where
645    I: IntoIterator<Item = (IotaAddress, u64, bool)>,
646  {
647    controllers.into_iter().fold(self, |builder, (addr, vp, can_delegate)| {
648      if can_delegate {
649        builder.controller_with_delegation(addr, vp)
650      } else {
651        builder.controller(addr, vp)
652      }
653    })
654  }
655
656  /// Turns this builder into a [`Transaction`], ready to be executed.
657  pub fn finish(self) -> TransactionBuilder<CreateIdentity> {
658    TransactionBuilder::new(CreateIdentity::new(self))
659  }
660}
661
662impl MoveType for OnChainIdentity {
663  fn move_type(package: ObjectID) -> TypeTag {
664    TypeTag::Struct(Box::new(StructTag {
665      address: package.into(),
666      module: ident_str!("identity").into(),
667      name: ident_str!("Identity").into(),
668      type_params: vec![],
669    }))
670  }
671}
672
673/// A [`Transaction`] for creating a new [`OnChainIdentity`] from an [`IdentityBuilder`].
674#[derive(Debug)]
675pub struct CreateIdentity {
676  builder: IdentityBuilder,
677  cached_ptb: OnceCell<ProgrammableTransaction>,
678}
679
680impl CreateIdentity {
681  /// Returns a new [CreateIdentity] [Transaction] from an [IdentityBuilder]
682  pub fn new(builder: IdentityBuilder) -> CreateIdentity {
683    Self {
684      builder,
685      cached_ptb: OnceCell::new(),
686    }
687  }
688
689  async fn make_ptb(&self, client: &impl CoreClientReadOnly) -> Result<ProgrammableTransaction, Error> {
690    let IdentityBuilder {
691      did_doc,
692      threshold,
693      controllers,
694    } = &self.builder;
695    let package = identity_package_id(client).await?;
696    let did_doc = StateMetadataDocument::from(did_doc.clone())
697      .pack(StateMetadataEncoding::default())
698      .map_err(|e| Error::DidDocSerialization(e.to_string()))?;
699    let pt_bcs = if controllers.is_empty() {
700      move_calls::identity::new_identity(Some(&did_doc), package).await?
701    } else {
702      let threshold = match threshold {
703        Some(t) => t,
704        None if controllers.len() == 1 => {
705          &controllers
706            .values()
707            .next()
708            .ok_or_else(|| Error::Identity("could not get controller".to_string()))?
709            .0
710        }
711        None => {
712          return Err(Error::TransactionBuildingFailed(
713            "Missing field `threshold` in identity creation".to_owned(),
714          ))
715        }
716      };
717      let controllers = controllers
718        .iter()
719        .map(|(addr, (vp, can_delegate))| (*addr, *vp, *can_delegate));
720      move_calls::identity::new_with_controllers(Some(&did_doc), controllers, *threshold, package).await?
721    };
722
723    Ok(bcs::from_bytes(&pt_bcs)?)
724  }
725}
726
727#[cfg_attr(not(feature = "send-sync"), async_trait(?Send))]
728#[cfg_attr(feature = "send-sync", async_trait)]
729impl Transaction for CreateIdentity {
730  type Output = OnChainIdentity;
731  type Error = Error;
732
733  async fn build_programmable_transaction<C>(&self, client: &C) -> Result<ProgrammableTransaction, Self::Error>
734  where
735    C: CoreClientReadOnly + OptionalSync,
736  {
737    self.cached_ptb.get_or_try_init(|| self.make_ptb(client)).await.cloned()
738  }
739
740  async fn apply<C>(
741    mut self,
742    effects: &mut IotaTransactionBlockEffects,
743    client: &C,
744  ) -> Result<Self::Output, Self::Error>
745  where
746    C: CoreClientReadOnly + OptionalSync,
747  {
748    if let IotaExecutionStatus::Failure { error } = effects.status() {
749      return Err(Error::TransactionUnexpectedResponse(error.clone()));
750    }
751
752    let created_objects = effects
753      .created()
754      .iter()
755      .enumerate()
756      .filter(|(_, elem)| matches!(elem.owner, Owner::Shared { .. }))
757      .map(|(i, obj)| (i, obj.object_id()));
758
759    let target_did_bytes = StateMetadataDocument::from(self.builder.did_doc)
760      .pack(StateMetadataEncoding::Json)
761      .map_err(|e| Error::DidDocSerialization(e.to_string()))?;
762
763    let is_target_identity = |identity: &OnChainIdentity| -> bool {
764      let did_bytes = identity
765        .multicontroller()
766        .controlled_value()
767        .as_deref()
768        .unwrap_or_default();
769      target_did_bytes == did_bytes && self.builder.threshold.unwrap_or(1) == identity.threshold()
770    };
771
772    let mut target_identity_pos = None;
773    let mut target_identity = None;
774    for (i, obj_id) in created_objects {
775      match get_identity(client, obj_id).await {
776        Ok(Some(identity)) if is_target_identity(&identity) => {
777          target_identity_pos = Some(i);
778          target_identity = Some(identity);
779          break;
780        }
781        _ => continue,
782      }
783    }
784
785    let (Some(i), Some(identity)) = (target_identity_pos, target_identity) else {
786      return Err(Error::TransactionUnexpectedResponse(
787        "failed to find the correct identity in this transaction's effects".to_owned(),
788      ));
789    };
790
791    effects.created_mut().swap_remove(i);
792
793    Ok(identity)
794  }
795}