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