1use 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
77pub(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#[derive(Clone)]
91pub enum Identity {
92 Legacy(UnmigratedAlias),
94 FullFledged(OnChainIdentity),
96}
97
98impl Identity {
99 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#[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 pub fn id(&self) -> ObjectID {
130 *self.id.object_id()
131 }
132
133 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 pub fn has_deleted_did(&self) -> bool {
148 self.deleted_did
149 }
150
151 pub fn is_shared(&self) -> bool {
153 self.multi_controller.controllers().len() > 1
154 }
155
156 pub fn proposals(&self) -> &HashSet<ObjectID> {
158 self.multi_controller.proposals()
159 }
160
161 pub fn controllers(&self) -> &HashMap<ObjectID, u64> {
163 self.multi_controller.controllers()
164 }
165
166 pub fn threshold(&self) -> u64 {
168 self.multi_controller.threshold()
169 }
170
171 pub fn controller_voting_power(&self, controller_id: ObjectID) -> Option<u64> {
173 self.multi_controller.controller_voting_power(controller_id)
174 }
175
176 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 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 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 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 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 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 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 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 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 #[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 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 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 last_version_value.clone()
311 } else {
312 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()); latest_version
324 };
325
326 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 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 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 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
368pub 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
386pub 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 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#[derive(Debug, thiserror::Error)]
473#[non_exhaustive]
474pub(crate) enum IdentityResolutionErrorKind {
475 #[error("object lookup RPC request failed")]
477 RpcError(#[source] Box<dyn StdError + Send + Sync>),
478 #[error("Identity does not exist")]
480 NotFound,
481 #[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 value.type_.module.as_ident_str().as_str() == MODULE && value.type_.name.as_ident_str().as_str() == NAME
503}
504
505pub(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 let created = {
560 let timestamp_ms: u64 = created.try_into().expect("Move string-encoded u64 are valid u64");
561 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::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#[derive(Debug)]
591pub struct IdentityBuilder {
592 did_doc: IotaDocument,
593 threshold: Option<u64>,
594 controllers: HashMap<IotaAddress, (u64, bool)>,
595}
596
597impl IdentityBuilder {
598 pub fn new(did_doc: IotaDocument) -> Self {
603 Self {
604 did_doc,
605 threshold: None,
606 controllers: HashMap::new(),
607 }
608 }
609
610 pub fn controller(mut self, address: IotaAddress, voting_power: u64) -> Self {
612 self.controllers.insert(address, (voting_power, false));
613 self
614 }
615
616 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 pub fn threshold(mut self, threshold: u64) -> Self {
625 self.threshold = Some(threshold);
626 self
627 }
628
629 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 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 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#[derive(Debug)]
675pub struct CreateIdentity {
676 builder: IdentityBuilder,
677 cached_ptb: OnceCell<ProgrammableTransaction>,
678}
679
680impl CreateIdentity {
681 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}