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