1use 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
75pub(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#[derive(Clone)]
89pub enum Identity {
90 Legacy(UnmigratedAlias),
92 FullFledged(OnChainIdentity),
94}
95
96impl Identity {
97 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#[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 pub fn id(&self) -> ObjectID {
128 *self.id.object_id()
129 }
130
131 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 pub fn has_deleted_did(&self) -> bool {
146 self.deleted_did
147 }
148
149 pub fn is_shared(&self) -> bool {
151 self.multi_controller.controllers().len() > 1
152 }
153
154 pub fn proposals(&self) -> &HashSet<ObjectID> {
156 self.multi_controller.proposals()
157 }
158
159 pub fn controllers(&self) -> &HashMap<ObjectID, u64> {
161 self.multi_controller.controllers()
162 }
163
164 pub fn threshold(&self) -> u64 {
166 self.multi_controller.threshold()
167 }
168
169 pub fn controller_voting_power(&self, controller_id: ObjectID) -> Option<u64> {
171 self.multi_controller.controller_voting_power(controller_id)
172 }
173
174 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 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 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 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 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 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 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 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 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 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 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 last_version_value.clone()
304 } else {
305 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()); latest_version
317 };
318
319 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 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 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 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
361pub 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
379pub 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 let Some(data) = response.data else {
397 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 value.type_.module.as_ident_str().as_str() == MODULE && value.type_.name.as_ident_str().as_str() == NAME
445}
446
447pub(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 let created = {
493 let timestamp_ms: u64 = created.try_into().expect("Move string-encoded u64 are valid u64");
494 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::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#[derive(Debug)]
524pub struct IdentityBuilder {
525 did_doc: IotaDocument,
526 threshold: Option<u64>,
527 controllers: HashMap<IotaAddress, (u64, bool)>,
528}
529
530impl IdentityBuilder {
531 pub fn new(did_doc: IotaDocument) -> Self {
536 Self {
537 did_doc,
538 threshold: None,
539 controllers: HashMap::new(),
540 }
541 }
542
543 pub fn controller(mut self, address: IotaAddress, voting_power: u64) -> Self {
545 self.controllers.insert(address, (voting_power, false));
546 self
547 }
548
549 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 pub fn threshold(mut self, threshold: u64) -> Self {
558 self.threshold = Some(threshold);
559 self
560 }
561
562 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 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 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#[derive(Debug)]
608pub struct CreateIdentity {
609 builder: IdentityBuilder,
610 cached_ptb: OnceCell<ProgrammableTransaction>,
611}
612
613impl CreateIdentity {
614 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}