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