identity_iota_core/rebased/migration/
controller_token.rs

1// Copyright 2020-2025 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4use super::OnChainIdentity;
5
6use crate::rebased::iota::move_calls;
7
8use crate::rebased::iota::move_calls::ControllerTokenRef;
9use crate::rebased::iota::package::identity_package_id;
10use crate::rebased::Error;
11use async_trait::async_trait;
12use iota_interaction::move_types::language_storage::TypeTag;
13use iota_interaction::rpc_types::IotaExecutionStatus;
14use iota_interaction::rpc_types::IotaTransactionBlockEffects;
15use iota_interaction::rpc_types::IotaTransactionBlockEffectsAPI;
16use iota_interaction::types::base_types::IotaAddress;
17use iota_interaction::types::base_types::ObjectID;
18use iota_interaction::types::id::UID;
19use iota_interaction::types::object::Owner;
20use iota_interaction::types::transaction::ProgrammableTransaction;
21use iota_interaction::IotaTransactionBlockEffectsMutAPI;
22use iota_interaction::MoveType;
23use iota_interaction::OptionalSync;
24use itertools::Itertools as _;
25use product_common::core_client::CoreClientReadOnly;
26use product_common::transaction::transaction_builder::Transaction;
27use product_common::transaction::transaction_builder::TransactionBuilder;
28use serde::Deserialize;
29use serde::Deserializer;
30use serde::Serialize;
31use std::ops::BitAnd;
32use std::ops::BitAndAssign;
33use std::ops::BitOr;
34use std::ops::BitOrAssign;
35use std::ops::BitXor;
36use std::ops::BitXorAssign;
37use std::ops::Not;
38/// A token that proves ownership over an object.
39#[derive(Debug, Clone, Serialize, Deserialize)]
40#[serde(untagged)]
41pub enum ControllerToken {
42  /// A Controller Capability.
43  Controller(ControllerCap),
44  /// A Delegation Token.
45  Delegate(DelegationToken),
46}
47
48impl ControllerToken {
49  /// Returns the ID of this [ControllerToken].
50  pub fn id(&self) -> ObjectID {
51    match self {
52      Self::Controller(controller) => controller.id,
53      Self::Delegate(delegate) => delegate.id,
54    }
55  }
56
57  /// Returns the ID of the this token's controller.
58  /// For [ControllerToken::Controller] this is the same as its ID, but
59  /// for [ControllerToken::Delegate] this is [DelegationToken::controller].
60  pub fn controller_id(&self) -> ObjectID {
61    match self {
62      Self::Controller(controller) => controller.id,
63      Self::Delegate(delegate) => delegate.controller,
64    }
65  }
66
67  /// Returns the ID of the object this token controls.
68  pub fn controller_of(&self) -> ObjectID {
69    match self {
70      Self::Controller(controller) => controller.controller_of,
71      Self::Delegate(delegate) => delegate.controller_of,
72    }
73  }
74
75  /// Returns a reference to [ControllerCap], if this token is a [ControllerCap].
76  pub fn as_controller(&self) -> Option<&ControllerCap> {
77    match self {
78      Self::Controller(controller) => Some(controller),
79      Self::Delegate(_) => None,
80    }
81  }
82
83  /// Attepts to return the [ControllerToken::Controller] variant of this [ControllerToken].
84  pub fn try_controller(self) -> Option<ControllerCap> {
85    match self {
86      Self::Controller(controller) => Some(controller),
87      Self::Delegate(_) => None,
88    }
89  }
90
91  /// Returns a reference to [DelegationToken], if this token is a [DelegationToken].
92  pub fn as_delegate(&self) -> Option<&DelegationToken> {
93    match self {
94      Self::Controller(_) => None,
95      Self::Delegate(delegate) => Some(delegate),
96    }
97  }
98
99  /// Attepts to return the [ControllerToken::Delegate] variant of this [ControllerToken].
100  pub fn try_delegate(self) -> Option<DelegationToken> {
101    match self {
102      Self::Controller(_) => None,
103      Self::Delegate(delegate) => Some(delegate),
104    }
105  }
106
107  /// Returns the Move type of this token.
108  pub fn move_type(&self, package: ObjectID) -> TypeTag {
109    match self {
110      Self::Controller(_) => ControllerCap::move_type(package),
111      Self::Delegate(_) => DelegationToken::move_type(package),
112    }
113  }
114
115  pub(crate) async fn controller_ref<C>(&self, client: &C) -> Result<ControllerTokenRef, Error>
116  where
117    C: CoreClientReadOnly + OptionalSync,
118  {
119    let obj_ref = client
120      .get_object_ref_by_id(self.id())
121      .await?
122      .expect("token exists on-chain")
123      .reference
124      .to_object_ref();
125
126    Ok(match self {
127      Self::Controller(_) => ControllerTokenRef::Controller(obj_ref),
128      Self::Delegate(_) => ControllerTokenRef::Delegate(obj_ref),
129    })
130  }
131}
132
133/// A token that authenticates its bearer as a controller of a specific shared object.
134#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct ControllerCap {
136  #[serde(deserialize_with = "deserialize_from_uid")]
137  id: ObjectID,
138  controller_of: ObjectID,
139  can_delegate: bool,
140}
141
142fn deserialize_from_uid<'de, D>(deserializer: D) -> Result<ObjectID, D::Error>
143where
144  D: Deserializer<'de>,
145{
146  UID::deserialize(deserializer).map(|uid| *uid.object_id())
147}
148
149impl MoveType for ControllerCap {
150  fn move_type(package: ObjectID) -> TypeTag {
151    format!("{package}::controller::ControllerCap")
152      .parse()
153      .expect("valid Move type")
154  }
155}
156
157impl ControllerCap {
158  /// Returns the ID of this [ControllerCap].
159  pub fn id(&self) -> ObjectID {
160    self.id
161  }
162
163  /// Returns the ID of the object this token controls.
164  pub fn controller_of(&self) -> ObjectID {
165    self.controller_of
166  }
167
168  /// Returns whether this controller is allowed to delegate
169  /// its access to the controlled object.
170  pub fn can_delegate(&self) -> bool {
171    self.can_delegate
172  }
173
174  /// If this token can be delegated, this function will return
175  /// a [DelegateTransaction] that will mint a new [DelegationToken]
176  /// and send it to `recipient`.
177  pub fn delegate(
178    &self,
179    recipient: IotaAddress,
180    permissions: Option<DelegatePermissions>,
181  ) -> Option<TransactionBuilder<DelegateToken>> {
182    if !self.can_delegate {
183      return None;
184    }
185
186    let tx = {
187      let permissions = permissions.unwrap_or_default();
188      DelegateToken::new_with_permissions(self, recipient, permissions)
189    };
190
191    Some(TransactionBuilder::new(tx))
192  }
193}
194
195impl From<ControllerCap> for ControllerToken {
196  fn from(cap: ControllerCap) -> Self {
197    Self::Controller(cap)
198  }
199}
200
201/// A token minted by a controller that allows another entity to act in
202/// its stead - with full or reduced permissions.
203#[derive(Debug, Clone, Serialize, Deserialize)]
204pub struct DelegationToken {
205  #[serde(deserialize_with = "deserialize_from_uid")]
206  id: ObjectID,
207  permissions: DelegatePermissions,
208  controller: ObjectID,
209  controller_of: ObjectID,
210}
211
212impl DelegationToken {
213  /// Returns the ID of this [DelegationToken].
214  pub fn id(&self) -> ObjectID {
215    self.id
216  }
217
218  /// Returns the ID of the [ControllerCap] that minted
219  /// this [DelegationToken].
220  pub fn controller(&self) -> ObjectID {
221    self.controller
222  }
223
224  /// Returns the ID of the object this token controls.
225  pub fn controller_of(&self) -> ObjectID {
226    self.controller_of
227  }
228
229  /// Returns the permissions of this token.
230  pub fn permissions(&self) -> DelegatePermissions {
231    self.permissions
232  }
233}
234
235impl From<DelegationToken> for ControllerToken {
236  fn from(value: DelegationToken) -> Self {
237    Self::Delegate(value)
238  }
239}
240
241impl MoveType for DelegationToken {
242  fn move_type(package: ObjectID) -> TypeTag {
243    format!("{package}::controller::DelegationToken")
244      .parse()
245      .expect("valid Move type")
246  }
247}
248
249/// Permissions of a [DelegationToken].
250///
251/// Permissions can be operated on as if they were bit vectors:
252/// ```
253/// use identity_iota_core::rebased::migration::DelegatePermissions;
254///
255/// let permissions = DelegatePermissions::CREATE_PROPOSAL | DelegatePermissions::APPROVE_PROPOSAL;
256/// assert!(permissions & DelegatePermissions::DELETE_PROPOSAL == DelegatePermissions::NONE);
257/// ```
258#[derive(Debug, Clone, Copy, Serialize, Deserialize, Eq, PartialEq)]
259#[serde(transparent)]
260pub struct DelegatePermissions(u32);
261
262impl Default for DelegatePermissions {
263  fn default() -> Self {
264    Self(u32::MAX)
265  }
266}
267
268impl From<u32> for DelegatePermissions {
269  fn from(value: u32) -> Self {
270    Self(value)
271  }
272}
273
274impl From<DelegatePermissions> for u32 {
275  fn from(value: DelegatePermissions) -> Self {
276    value.0
277  }
278}
279
280impl DelegatePermissions {
281  /// No permissions.
282  pub const NONE: Self = Self(0);
283  /// Permission that enables the creation of new proposals.
284  pub const CREATE_PROPOSAL: Self = Self(1);
285  /// Permission that enables the approval of existing proposals.
286  pub const APPROVE_PROPOSAL: Self = Self(1 << 1);
287  /// Permission that enables the execution of existing proposals.
288  pub const EXECUTE_PROPOSAL: Self = Self(1 << 2);
289  /// Permission that enables the deletion of existing proposals.
290  pub const DELETE_PROPOSAL: Self = Self(1 << 3);
291  /// Permission that enables the remove of one's approval for an existing proposal.
292  pub const REMOVE_APPROVAL: Self = Self(1 << 4);
293  /// All permissions.
294  pub const ALL: Self = Self(u32::MAX);
295
296  /// Returns whether this set of permissions contains `permission`.
297  /// ```
298  /// use identity_iota_core::rebased::migration::DelegatePermissions;
299  ///
300  /// let all_permissions = DelegatePermissions::ALL;
301  /// assert_eq!(
302  ///   all_permissions.has(DelegatePermissions::CREATE_PROPOSAL),
303  ///   true
304  /// );
305  /// ```
306  pub fn has(&self, permission: Self) -> bool {
307    *self | permission != Self::NONE
308  }
309}
310
311impl Not for DelegatePermissions {
312  type Output = Self;
313  fn not(self) -> Self::Output {
314    Self(!self.0)
315  }
316}
317impl BitOr for DelegatePermissions {
318  type Output = Self;
319  fn bitor(self, rhs: Self) -> Self::Output {
320    Self(self.0 | rhs.0)
321  }
322}
323impl BitOrAssign for DelegatePermissions {
324  fn bitor_assign(&mut self, rhs: Self) {
325    self.0 |= rhs.0;
326  }
327}
328impl BitAnd for DelegatePermissions {
329  type Output = Self;
330  fn bitand(self, rhs: Self) -> Self::Output {
331    Self(self.0 & rhs.0)
332  }
333}
334impl BitAndAssign for DelegatePermissions {
335  fn bitand_assign(&mut self, rhs: Self) {
336    self.0 &= rhs.0;
337  }
338}
339impl BitXor for DelegatePermissions {
340  type Output = Self;
341  fn bitxor(self, rhs: Self) -> Self::Output {
342    Self(self.0 ^ rhs.0)
343  }
344}
345impl BitXorAssign for DelegatePermissions {
346  fn bitxor_assign(&mut self, rhs: Self) {
347    self.0 ^= rhs.0;
348  }
349}
350
351/// A [Transaction] that creates a new [DelegationToken]
352/// for a given [ControllerCap].
353#[derive(Debug, Clone)]
354pub struct DelegateToken {
355  cap_id: ObjectID,
356  permissions: DelegatePermissions,
357  recipient: IotaAddress,
358}
359
360impl DelegateToken {
361  /// Creates a new [DelegateToken] transaction that will create a new [DelegationToken] with all permissions
362  /// for `controller_cap` and send it to `recipient`.
363  pub fn new(controller_cap: &ControllerCap, recipient: IotaAddress) -> Self {
364    Self::new_with_permissions(controller_cap, recipient, DelegatePermissions::default())
365  }
366
367  /// Same as [DelegateToken::new] but permissions for the new token can be specified.
368  pub fn new_with_permissions(
369    controller_cap: &ControllerCap,
370    recipient: IotaAddress,
371    permissions: DelegatePermissions,
372  ) -> Self {
373    Self {
374      cap_id: controller_cap.id(),
375      permissions,
376      recipient,
377    }
378  }
379}
380
381#[cfg_attr(feature = "send-sync", async_trait)]
382#[cfg_attr(not(feature = "send-sync"), async_trait(?Send))]
383impl Transaction for DelegateToken {
384  type Output = DelegationToken;
385  type Error = Error;
386  async fn build_programmable_transaction<C>(&self, client: &C) -> Result<ProgrammableTransaction, Self::Error>
387  where
388    C: CoreClientReadOnly + OptionalSync,
389  {
390    let package = identity_package_id(client).await?;
391    let controller_cap_ref = client
392      .get_object_ref_by_id(self.cap_id)
393      .await?
394      .expect("ControllerCap exists on-chain")
395      .reference
396      .to_object_ref();
397
398    let ptb_bcs =
399      move_calls::identity::delegate_controller_cap(controller_cap_ref, self.recipient, self.permissions.0, package)
400        .await?;
401    Ok(bcs::from_bytes(&ptb_bcs)?)
402  }
403
404  async fn apply<C>(self, effects: &mut IotaTransactionBlockEffects, client: &C) -> Result<Self::Output, Self::Error>
405  where
406    C: CoreClientReadOnly + OptionalSync,
407  {
408    if let IotaExecutionStatus::Failure { error } = effects.status() {
409      return Err(Error::TransactionUnexpectedResponse(error.clone()));
410    }
411
412    let created_objects = effects
413      .created()
414      .iter()
415      .enumerate()
416      .filter(|(_, elem)| matches!(elem.owner, Owner::AddressOwner(addr) if addr == self.recipient))
417      .map(|(i, obj)| (i, obj.object_id()));
418
419    let is_target_token = |delegation_token: &DelegationToken| -> bool {
420      delegation_token.controller == self.cap_id && delegation_token.permissions == self.permissions
421    };
422    let mut target_token_pos = None;
423    let mut target_token = None;
424    for (i, obj_id) in created_objects {
425      match client.get_object_by_id(obj_id).await {
426        Ok(token) if is_target_token(&token) => {
427          target_token_pos = Some(i);
428          target_token = Some(token);
429          break;
430        }
431        _ => continue,
432      }
433    }
434
435    let (Some(i), Some(token)) = (target_token_pos, target_token) else {
436      return Err(Error::TransactionUnexpectedResponse(
437        "failed to find the correct identity in this transaction's effects".to_owned(),
438      ));
439    };
440
441    effects.created_mut().swap_remove(i);
442
443    Ok(token)
444  }
445}
446
447/// [Transaction] for revoking / unrevoking a [DelegationToken].
448#[derive(Debug, Clone)]
449pub struct DelegationTokenRevocation {
450  identity_id: ObjectID,
451  controller_cap_id: ObjectID,
452  delegation_token_id: ObjectID,
453  // `true` revokes the token, `false` un-revokes it.
454  revoke: bool,
455}
456
457impl DelegationTokenRevocation {
458  fn revocation_impl(
459    identity: &OnChainIdentity,
460    controller_cap: &ControllerCap,
461    delegation_token: &DelegationToken,
462    is_revocation: bool,
463  ) -> Result<Self, Error> {
464    if delegation_token.controller_of != identity.id() {
465      return Err(Error::Identity(format!(
466        "DelegationToken {} has no control over Identity {}",
467        delegation_token.id,
468        identity.id()
469      )));
470    }
471
472    Ok(Self {
473      identity_id: identity.id(),
474      controller_cap_id: controller_cap.id(),
475      delegation_token_id: delegation_token.id,
476      revoke: is_revocation,
477    })
478  }
479  /// Returns a new [DelegationTokenRevocation] that will revoke [DelegationToken] `delegation_token_id`.
480  pub fn revoke(
481    identity: &OnChainIdentity,
482    controller_cap: &ControllerCap,
483    delegation_token: &DelegationToken,
484  ) -> Result<Self, Error> {
485    Self::revocation_impl(identity, controller_cap, delegation_token, true)
486  }
487
488  /// Returns a new [DelegationTokenRevocation] that will un-revoke [DelegationToken] `delegation_token_id`.
489  pub fn unrevoke(
490    identity: &OnChainIdentity,
491    controller_cap: &ControllerCap,
492    delegation_token: &DelegationToken,
493  ) -> Result<Self, Error> {
494    Self::revocation_impl(identity, controller_cap, delegation_token, false)
495  }
496
497  /// Returns `true` if this transaction is used to revoke a token.
498  pub fn is_revocation(&self) -> bool {
499    self.revoke
500  }
501
502  /// Return the ID of the [DelegationToken] handled by this transaction.
503  pub fn token_id(&self) -> ObjectID {
504    self.delegation_token_id
505  }
506}
507
508#[cfg_attr(feature = "send-sync", async_trait)]
509#[cfg_attr(not(feature = "send-sync"), async_trait(?Send))]
510impl Transaction for DelegationTokenRevocation {
511  type Output = ();
512  type Error = Error;
513
514  async fn build_programmable_transaction<C>(&self, client: &C) -> Result<ProgrammableTransaction, Self::Error>
515  where
516    C: CoreClientReadOnly + OptionalSync,
517  {
518    let package = identity_package_id(client).await?;
519    let identity_ref = client
520      .get_object_ref_by_id(self.identity_id)
521      .await?
522      .expect("identity exists on-chain");
523    let controller_cap_ref = client
524      .get_object_ref_by_id(self.controller_cap_id)
525      .await?
526      .expect("controller_cap exists on-chain")
527      .reference
528      .to_object_ref();
529
530    let tx_bytes = if self.is_revocation() {
531      move_calls::identity::revoke_delegation_token(identity_ref, controller_cap_ref, self.delegation_token_id, package)
532    } else {
533      move_calls::identity::unrevoke_delegation_token(
534        identity_ref,
535        controller_cap_ref,
536        self.delegation_token_id,
537        package,
538      )
539    }?;
540
541    Ok(bcs::from_bytes(&tx_bytes)?)
542  }
543
544  async fn apply<C>(self, effects: &mut IotaTransactionBlockEffects, _client: &C) -> Result<Self::Output, Self::Error>
545  where
546    C: CoreClientReadOnly + OptionalSync,
547  {
548    if let IotaExecutionStatus::Failure { error } = effects.status() {
549      return Err(Error::TransactionUnexpectedResponse(error.clone()));
550    }
551
552    Ok(())
553  }
554}
555
556/// [Transaction] for deleting a given [DelegationToken].
557#[derive(Debug, Clone)]
558pub struct DeleteDelegationToken {
559  identity_id: ObjectID,
560  delegation_token_id: ObjectID,
561}
562
563impl DeleteDelegationToken {
564  /// Returns a new [DeleteDelegationToken] [Transaction], that will delete the given [DelegationToken].
565  pub fn new(identity: &OnChainIdentity, delegation_token: DelegationToken) -> Result<Self, Error> {
566    if identity.id() != delegation_token.controller_of {
567      return Err(Error::Identity(format!(
568        "DelegationToken {} has no control over Identity {}",
569        delegation_token.id,
570        identity.id()
571      )));
572    }
573
574    Ok(Self {
575      identity_id: identity.id(),
576      delegation_token_id: delegation_token.id,
577    })
578  }
579
580  /// Returns the ID of the [DelegationToken] to be deleted.
581  pub fn token_id(&self) -> ObjectID {
582    self.delegation_token_id
583  }
584}
585
586#[cfg_attr(feature = "send-sync", async_trait)]
587#[cfg_attr(not(feature = "send-sync"), async_trait(?Send))]
588impl Transaction for DeleteDelegationToken {
589  type Output = ();
590  type Error = Error;
591
592  async fn build_programmable_transaction<C>(&self, client: &C) -> Result<ProgrammableTransaction, Self::Error>
593  where
594    C: CoreClientReadOnly + OptionalSync,
595  {
596    let package = identity_package_id(client).await?;
597    let identity_ref = client
598      .get_object_ref_by_id(self.identity_id)
599      .await?
600      .ok_or_else(|| Error::ObjectLookup(format!("Identity {} doesn't exist on-chain", self.identity_id)))?;
601    let delegation_token_ref = client
602      .get_object_ref_by_id(self.delegation_token_id)
603      .await?
604      .ok_or_else(|| {
605        Error::ObjectLookup(format!(
606          "DelegationToken {} doesn't exist on-chain",
607          self.delegation_token_id,
608        ))
609      })?
610      .reference
611      .to_object_ref();
612
613    let tx_bytes = move_calls::identity::destroy_delegation_token(identity_ref, delegation_token_ref, package).await?;
614
615    Ok(bcs::from_bytes(&tx_bytes)?)
616  }
617
618  async fn apply<C>(self, effects: &mut IotaTransactionBlockEffects, _client: &C) -> Result<Self::Output, Self::Error>
619  where
620    C: CoreClientReadOnly + OptionalSync,
621  {
622    if let IotaExecutionStatus::Failure { error } = effects.status() {
623      return Err(Error::TransactionUnexpectedResponse(error.clone()));
624    }
625
626    let Some(deleted_token_pos) = effects
627      .deleted()
628      .iter()
629      .find_position(|obj_ref| obj_ref.object_id == self.delegation_token_id)
630      .map(|(pos, _)| pos)
631    else {
632      return Err(Error::TransactionUnexpectedResponse(format!(
633        "DelegationToken {} wasn't deleted in this transaction",
634        self.delegation_token_id,
635      )));
636    };
637
638    effects.deleted_mut().swap_remove(deleted_token_pos);
639
640    Ok(())
641  }
642}