identity_iota_core/rebased/migration/
alias.rs

1// Copyright 2020-2024 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4use async_trait::async_trait;
5use identity_core::common::Url;
6use identity_did::DID as _;
7use iota_interaction::rpc_types::IotaExecutionStatus;
8use iota_interaction::rpc_types::IotaObjectDataOptions;
9use iota_interaction::rpc_types::IotaTransactionBlockEffects;
10use iota_interaction::rpc_types::IotaTransactionBlockEffectsAPI as _;
11use iota_interaction::types::base_types::IotaAddress;
12use iota_interaction::types::base_types::ObjectID;
13use iota_interaction::types::id::UID;
14use iota_interaction::types::transaction::ProgrammableTransaction;
15use iota_interaction::types::TypeTag;
16use iota_interaction::types::STARDUST_PACKAGE_ID;
17use iota_interaction::IotaTransactionBlockEffectsMutAPI as _;
18use iota_interaction::OptionalSync;
19
20use product_common::core_client::CoreClientReadOnly;
21use product_common::transaction::transaction_builder::Transaction;
22use serde;
23use serde::Deserialize;
24use serde::Serialize;
25use tokio::sync::OnceCell;
26
27use crate::rebased::client::IdentityClientReadOnly;
28
29use crate::rebased::iota::move_calls;
30use crate::rebased::iota::package::identity_package_id;
31use crate::rebased::Error;
32use crate::IotaDID;
33use iota_interaction::IotaClientTrait;
34use iota_interaction::MoveType;
35
36use super::get_identity;
37use super::migration_registry_id;
38use super::Identity;
39use super::OnChainIdentity;
40
41/// A legacy IOTA Stardust Output type, used to store DID Documents.
42#[derive(Clone, Debug, Deserialize, Serialize)]
43pub struct UnmigratedAlias {
44  /// The ID of the Alias = hash of the Output ID that created the Alias Output in Stardust.
45  /// This is the AliasID from Stardust.
46  pub id: UID,
47
48  /// The last State Controller address assigned before the migration.
49  pub legacy_state_controller: Option<IotaAddress>,
50  /// A counter increased by 1 every time the alias was state transitioned.
51  pub state_index: u32,
52  /// State metadata that can be used to store additional information.
53  pub state_metadata: Option<Vec<u8>>,
54
55  /// The sender feature.
56  pub sender: Option<IotaAddress>,
57
58  /// The immutable issuer feature.
59  pub immutable_issuer: Option<IotaAddress>,
60  /// The immutable metadata feature.
61  pub immutable_metadata: Option<Vec<u8>>,
62}
63
64impl MoveType for UnmigratedAlias {
65  fn move_type(_: ObjectID) -> TypeTag {
66    format!("{STARDUST_PACKAGE_ID}::alias::Alias")
67      .parse()
68      .expect("valid move type")
69  }
70}
71
72/// Resolves an [`UnmigratedAlias`] given its ID `object_id`.
73pub async fn get_alias(client: &IdentityClientReadOnly, object_id: ObjectID) -> Result<Option<UnmigratedAlias>, Error> {
74  match client.get_object_by_id(object_id).await {
75    Ok(Some(alias)) => Ok(Some(alias)),
76    Ok(None) => Ok(None),
77    Err(e) => Err(e.into()),
78  }
79}
80
81/// A [Transaction] that migrates a legacy Identity to
82/// a new [OnChainIdentity].
83pub struct MigrateLegacyIdentity {
84  alias: UnmigratedAlias,
85  cached_ptb: OnceCell<ProgrammableTransaction>,
86}
87
88impl MigrateLegacyIdentity {
89  /// Returns a new [MigrateLegacyIdentity] transaction.
90  pub fn new(alias: UnmigratedAlias) -> Self {
91    Self {
92      alias,
93      cached_ptb: OnceCell::new(),
94    }
95  }
96
97  async fn make_ptb<C>(&self, client: &C) -> Result<ProgrammableTransaction, Error>
98  where
99    C: CoreClientReadOnly + OptionalSync,
100  {
101    // Try to parse a StateMetadataDocument out of this alias.
102    let identity = Identity::Legacy(self.alias.clone());
103    let did_doc = identity.did_document(client.network_name())?;
104    let Identity::Legacy(alias) = identity else {
105      unreachable!("alias was wrapped by us")
106    };
107    // Get the ID of the `AliasOutput` that owns this `Alias`.
108    let dynamic_field_wrapper = client
109      .client_adapter()
110      .read_api()
111      .get_object_with_options(*alias.id.object_id(), IotaObjectDataOptions::new().with_owner())
112      .await
113      .map_err(|e| Error::RpcError(e.to_string()))?
114      .owner()
115      .expect("owner was requested")
116      .get_owner_address()
117      .expect("alias is a dynamic field")
118      .into();
119    let alias_output_id = client
120      .client_adapter()
121      .read_api()
122      .get_object_with_options(dynamic_field_wrapper, IotaObjectDataOptions::new().with_owner())
123      .await
124      .map_err(|e| Error::RpcError(e.to_string()))?
125      .owner()
126      .expect("owner was requested")
127      .get_owner_address()
128      .expect("alias is owned by an alias_output")
129      .into();
130    // Get alias_output's ref.
131    let alias_output_ref = client
132      .client_adapter()
133      .read_api()
134      .get_object_with_options(alias_output_id, IotaObjectDataOptions::default())
135      .await
136      .map_err(|e| Error::RpcError(e.to_string()))?
137      .object_ref_if_exists()
138      .expect("alias_output exists");
139    // Get migration registry ref.
140    let migration_registry_id = migration_registry_id(client)
141      .await
142      .map_err(Error::MigrationRegistryNotFound)?;
143    let migration_registry_ref = client
144      .get_object_ref_by_id(migration_registry_id)
145      .await?
146      .expect("migration registry exists");
147
148    // Extract creation metadata
149    let created = did_doc
150      .metadata
151      .created
152      // `to_unix` returns the seconds since EPOCH; we need milliseconds.
153      .map(|timestamp| timestamp.to_unix() as u64 * 1000);
154
155    let package = identity_package_id(client).await?;
156
157    // Build migration tx.
158    let tx = move_calls::migration::migrate_did_output(alias_output_ref, created, migration_registry_ref, package)
159      .map_err(|e| Error::TransactionBuildingFailed(e.to_string()))?;
160
161    Ok(bcs::from_bytes(&tx)?)
162  }
163}
164
165#[cfg_attr(not(feature = "send-sync"), async_trait(?Send))]
166#[cfg_attr(feature = "send-sync", async_trait)]
167impl Transaction for MigrateLegacyIdentity {
168  type Output = OnChainIdentity;
169  type Error = Error;
170
171  async fn build_programmable_transaction<C>(&self, client: &C) -> Result<ProgrammableTransaction, Self::Error>
172  where
173    C: CoreClientReadOnly + OptionalSync,
174  {
175    self.cached_ptb.get_or_try_init(|| self.make_ptb(client)).await.cloned()
176  }
177
178  async fn apply<C>(self, effects: &mut IotaTransactionBlockEffects, client: &C) -> Result<Self::Output, Self::Error>
179  where
180    C: CoreClientReadOnly + OptionalSync,
181  {
182    if let IotaExecutionStatus::Failure { error } = effects.status() {
183      return Err(Error::TransactionUnexpectedResponse(error.to_string()));
184    }
185
186    let legacy_did: Url = IotaDID::new(&self.alias.id.object_id().into_bytes(), client.network_name())
187      .to_url()
188      .into();
189    let is_target_identity =
190      |identity: &OnChainIdentity| -> bool { identity.did_document().also_known_as().contains(&legacy_did) };
191
192    let created_objects = effects
193      .created()
194      .iter()
195      .enumerate()
196      .filter(|(_, obj_ref)| obj_ref.owner.is_shared())
197      .map(|(i, obj_ref)| (i, obj_ref.object_id()));
198
199    let mut target_identity_pos = None;
200    let mut target_identity = None;
201    for (i, obj_id) in created_objects {
202      match get_identity(client, obj_id).await {
203        Ok(Some(identity)) if is_target_identity(&identity) => {
204          target_identity_pos = Some(i);
205          target_identity = Some(identity);
206          break;
207        }
208        _ => continue,
209      }
210    }
211
212    let (Some(i), Some(identity)) = (target_identity_pos, target_identity) else {
213      return Err(Error::TransactionUnexpectedResponse(
214        "failed to find the correct identity in this transaction's effects".to_owned(),
215      ));
216    };
217
218    effects.created_mut().swap_remove(i);
219
220    Ok(identity)
221  }
222}