identity_iota_core/rebased/proposals/
access_sub_identity.rs

1// Copyright 2020-2025 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4use std::convert::Infallible;
5use std::future::Future;
6use std::marker::PhantomData;
7
8use async_trait::async_trait;
9use iota_interaction::rpc_types::IotaEvent;
10use iota_interaction::rpc_types::IotaExecutionStatus;
11use iota_interaction::rpc_types::IotaTransactionBlockEffects;
12use iota_interaction::rpc_types::IotaTransactionBlockEffectsAPI as _;
13use iota_interaction::rpc_types::IotaTransactionBlockEvents;
14use iota_interaction::rpc_types::IotaTransactionBlockResponseOptions;
15use iota_interaction::types::base_types::ObjectID;
16use iota_interaction::types::transaction::ProgrammableTransaction;
17use iota_interaction::types::TypeTag;
18use iota_interaction::MoveType;
19use iota_interaction::OptionalSend;
20use iota_interaction::OptionalSync;
21use product_common::core_client::CoreClientReadOnly;
22use product_common::transaction::transaction_builder::Transaction;
23use product_common::transaction::transaction_builder::TransactionBuilder;
24use product_common::transaction::IntoTransaction;
25use serde::Deserialize;
26use serde::Serialize;
27
28use crate::rebased::iota::move_calls;
29use crate::rebased::iota::package::identity_package_id;
30use crate::rebased::migration::ControllerToken;
31use crate::rebased::migration::InvalidControllerTokenForIdentity;
32use crate::rebased::migration::OnChainIdentity;
33use crate::rebased::migration::Proposal;
34
35use super::ProposalEvent;
36use super::ProposedTxResult;
37
38type BoxedStdError = Box<dyn std::error::Error + Send + Sync>;
39
40/// Trait describing the function used to define what operation to perform on a sub-identity.
41pub trait SubAccessFnT<'a>: FnOnce(&'a mut OnChainIdentity, ControllerToken) -> Self::Future {
42  /// The [Future] type returned by the closure.
43  type Future: Future<Output = Result<Self::IntoTx, Self::Error>> + OptionalSend + 'a;
44  /// An [IntoTransaction] type.
45  type IntoTx: IntoTransaction<Tx = Self::Tx>;
46  /// The [Transaction] that encodes the operation to be performed on the sub-identity.
47  type Tx: Transaction + 'a;
48  /// The error returned by this function
49  type Error: Into<BoxedStdError>;
50}
51
52impl<'a, F, Fut, IntoTx, Tx, E> SubAccessFnT<'a> for F
53where
54  F: FnOnce(&'a mut OnChainIdentity, ControllerToken) -> Fut,
55  Fut: Future<Output = Result<IntoTx, E>> + OptionalSend + 'a,
56  IntoTx: IntoTransaction<Tx = Tx>,
57  Tx: Transaction + 'a,
58  E: Into<BoxedStdError>,
59{
60  type Future = Fut;
61  type IntoTx = IntoTx;
62  type Tx = Tx;
63  type Error = E;
64}
65
66/// A type implenting [Transansaction] that doesn't return anything meaningful.
67///
68/// Used to encode `sub_tx` in [AccessSubIdentityTx] when no sub_tx is present (create proposal).
69#[derive(Debug)]
70pub struct EmptyTx;
71
72#[cfg_attr(feature = "send-sync", async_trait)]
73#[cfg_attr(not(feature = "send-sync"), async_trait(?Send))]
74impl Transaction for EmptyTx {
75  type Output = ();
76  type Error = Infallible;
77
78  async fn build_programmable_transaction<C>(&self, _client: &C) -> Result<ProgrammableTransaction, Self::Error>
79  where
80    C: CoreClientReadOnly + OptionalSync,
81  {
82    Ok(ProgrammableTransaction {
83      inputs: vec![],
84      commands: vec![],
85    })
86  }
87
88  async fn apply<C>(self, _effects: &mut IotaTransactionBlockEffects, _client: &C) -> Result<Self::Output, Self::Error>
89  where
90    C: CoreClientReadOnly + OptionalSync,
91  {
92    Ok(())
93  }
94}
95
96/// An action for accessing an [OnChainIdentity] that is owned by another
97/// [OnChainIdentity].
98#[derive(Debug, Clone, Serialize, Deserialize)]
99#[non_exhaustive]
100pub struct AccessSubIdentity {
101  /// ID of the Identity whose token will be used to access the sub-Identity.
102  #[serde(rename = "entity")]
103  pub identity: ObjectID,
104  #[serde(rename = "sub_entity")]
105  /// ID of the sub-Identity that will be accessed through this action.
106  pub sub_identity: ObjectID,
107}
108
109/// A builder structure that eases the creation of an [AccessSubIdentityTx].
110#[derive(Debug)]
111pub struct AccessSubIdentityBuilder<'i, 'sub, F = ()> {
112  identity: &'i mut OnChainIdentity,
113  identity_token: ControllerToken,
114  sub_identity: &'sub mut OnChainIdentity,
115  expiration: Option<u64>,
116  sub_action: Option<F>,
117}
118
119impl<'i, 'sub, F> AccessSubIdentityBuilder<'i, 'sub, F> {
120  /// Returns a new [AccessSubIdentityBuilder] that when built will return a [AccessSubIdentityTx]
121  /// to access `sub_identity` through `identity`'s token.
122  pub fn new(
123    identity: &'i mut OnChainIdentity,
124    sub_identity: &'sub mut OnChainIdentity,
125    identity_token: &ControllerToken,
126  ) -> Self {
127    Self {
128      identity,
129      sub_identity,
130      identity_token: identity_token.clone(),
131      expiration: None,
132      sub_action: None,
133    }
134  }
135
136  /// Sets an epoch before which this proposal must be executed by any member of the controllers committee.
137  ///
138  /// If this action can be carried out in the same transaction as its proposal, this option is ignored.
139  pub fn with_expiration(mut self, epoch_id: u64) -> Self {
140    self.expiration = Some(epoch_id);
141    self
142  }
143
144  /// Sets the operation to be performed on the sub-Identity.
145  ///
146  /// # Example
147  /// ```ignore
148  /// identity
149  ///   .access_sub_identity(&mut sub_identity, &identity_token)
150  ///   .to_perform(|sub_identity, sub_identity_token| async move {
151  ///     sub_identity.deactivate_did(&sub_identity_token).finish().await
152  ///   })
153  /// ```
154  pub fn to_perform<F1>(self, f: F1) -> AccessSubIdentityBuilder<'i, 'sub, F1>
155  where
156    F1: SubAccessFnT<'sub>,
157  {
158    AccessSubIdentityBuilder {
159      identity: self.identity,
160      identity_token: self.identity_token,
161      sub_identity: self.sub_identity,
162      expiration: self.expiration,
163      sub_action: Some(f),
164    }
165  }
166
167  async fn get_identity_token<C>(&self, client: &C) -> Result<ControllerToken, AccessSubIdentityBuilderErrorKind>
168  where
169    C: CoreClientReadOnly + OptionalSync,
170  {
171    // Make sure `identity_token` grants access to `identity`.
172    if self.identity.id() != self.identity_token.controller_of() {
173      return Err(AccessSubIdentityBuilderErrorKind::Unauthorized(
174        InvalidControllerTokenForIdentity {
175          identity: self.identity.id(),
176          controller_token: self.identity_token.clone(),
177        },
178      ));
179    }
180
181    // Retrieve from `identity` owned asset any token granting access to `sub_identity`.
182    self
183      .sub_identity
184      .get_controller_token_for_address(self.identity.id().into(), client)
185      .await
186      .map_err(|e| AccessSubIdentityBuilderErrorKind::RpcError(e.into()))?
187      // If no token was found, the two identities are unrelated, AKA `identity` is not a controller of `sub_identity`.
188      .ok_or(AccessSubIdentityBuilderErrorKind::UnrelatedIdentities(
189        UnrelatedIdentities {
190          identity: self.identity.id(),
191          sub_identity: self.sub_identity.id(),
192        },
193      ))
194  }
195}
196
197impl<'i, 'sub> AccessSubIdentityBuilder<'i, 'sub, ()> {
198  /// Consumes this builder returning a [TransactionBuilder] wrapping a [AccessSubIdentityTx] created
199  /// with the supplied data.
200  pub async fn finish<C>(
201    self,
202    client: &C,
203  ) -> Result<TransactionBuilder<AccessSubIdentityTx<'i, 'sub, EmptyTx>>, AccessSubIdentityBuilderError>
204  where
205    C: CoreClientReadOnly + OptionalSync,
206  {
207    let _ = self.get_identity_token(client).await?;
208    let tx_kind = TxKind::Create {
209      expiration: self.expiration,
210    };
211
212    Ok(TransactionBuilder::new(AccessSubIdentityTx {
213      identity: self.identity,
214      identity_token: self.identity_token,
215      sub_identity: self.sub_identity.id(),
216      tx_kind,
217      _sub: PhantomData,
218    }))
219  }
220}
221
222impl<'i, 'sub, F> AccessSubIdentityBuilder<'i, 'sub, F>
223where
224  F: SubAccessFnT<'sub>,
225  F::Tx: Transaction + OptionalSend + OptionalSync,
226{
227  /// Consumes this builder returning a [TransactionBuilder] wrapping a [AccessSubIdentityTx] created
228  /// with the supplied data.
229  pub async fn finish<C>(
230    self,
231    client: &C,
232  ) -> Result<TransactionBuilder<AccessSubIdentityTx<'i, 'sub, F::Tx>>, AccessSubIdentityBuilderError>
233  where
234    C: CoreClientReadOnly + OptionalSync,
235  {
236    let sub_identity_token = self.get_identity_token(client).await?;
237
238    // `true` if this operation can also be executed in the same transaction.
239    let can_execute = self
240      .identity
241      .controller_voting_power(self.identity_token.controller_id())
242      .expect("valid controller token")
243      >= self.identity.threshold();
244
245    // Invoke the user-passed function, if any, to compute the transaction to perform on `sub_identity`.
246    // If `can_execute` is `false`, don't bother checking the user sub-action.
247    let sub_identity_id = self.sub_identity.id();
248    let maybe_sub_tx = if let Some(fetch_sub_tx) = self.sub_action.filter(|_| can_execute) {
249      fetch_sub_tx(self.sub_identity, sub_identity_token.clone())
250        .await
251        .map(|into_tx| Some(into_tx.into_transaction()))
252        .map_err(|e| AccessSubIdentityBuilderErrorKind::SubIdentityOperation {
253          sub_identity: sub_identity_id,
254          source: e.into(),
255        })?
256    } else {
257      None
258    };
259
260    let tx_kind = maybe_sub_tx
261      .map(move |sub_tx| TxKind::CreateAndExecute {
262        sub_tx,
263        sub_identity_token,
264      })
265      .unwrap_or(TxKind::Create {
266        expiration: self.expiration,
267      });
268    let tx = AccessSubIdentityTx {
269      identity: self.identity,
270      identity_token: self.identity_token,
271      sub_identity: sub_identity_id,
272      tx_kind,
273      _sub: PhantomData,
274    };
275
276    Ok(TransactionBuilder::new(tx))
277  }
278}
279
280impl Proposal<AccessSubIdentity> {
281  /// Executes this proposal by returning the corresponding transaction to be executed.
282  pub async fn into_tx<'i, 'sub, F, C>(
283    self,
284    identity: &'i mut OnChainIdentity,
285    sub_identity: &'sub mut OnChainIdentity,
286    identity_token: &ControllerToken,
287    sub_action: F,
288    client: &C,
289  ) -> Result<TransactionBuilder<AccessSubIdentityTx<'i, 'sub, F::Tx>>, AccessSubIdentityBuilderError>
290  where
291    F: SubAccessFnT<'sub>,
292    F::Tx: Transaction + OptionalSync + OptionalSend,
293    C: CoreClientReadOnly + OptionalSync,
294  {
295    // Re-use builder's error-handling.
296    let mut tx = identity
297      .access_sub_identity(sub_identity, identity_token)
298      .to_perform(sub_action)
299      .finish(client)
300      .await?
301      .into_inner();
302    // Change tx_kind to `Execute`.
303    let TxKind::CreateAndExecute {
304      sub_tx,
305      sub_identity_token,
306    } = tx.tx_kind
307    else {
308      unreachable!("a sub_action was passed");
309    };
310    tx.tx_kind = TxKind::Execute {
311      proposal_id: self.id(),
312      sub_tx,
313      sub_identity_token,
314    };
315
316    Ok(TransactionBuilder::new(tx))
317  }
318}
319
320/// Error type that is returned when attempting to access an Identity `sub_identity`
321/// that is **not** controlled by `identity`.
322#[derive(Debug, thiserror::Error)]
323#[non_exhaustive]
324#[error("Identity `{identity}` has no control over Identity `{sub_identity}`")]
325pub struct UnrelatedIdentities {
326  /// ID of the base-Identity.
327  pub identity: ObjectID,
328  /// ID of the sub-Identity to be accessed.
329  pub sub_identity: ObjectID,
330}
331
332/// Kind of failure that might happen when consuming an [AccessSubIdentityBuilder].
333#[derive(Debug, thiserror::Error)]
334#[non_exhaustive]
335pub enum AccessSubIdentityBuilderErrorKind {
336  /// An RPC request to an IOTA Node failed.
337  #[error(transparent)]
338  RpcError(BoxedStdError),
339  /// See [UnrelatedIdentities].
340  #[error(transparent)]
341  UnrelatedIdentities(#[from] UnrelatedIdentities),
342  /// See [InvalidControllerTokenForIdentity].
343  #[error(transparent)]
344  Unauthorized(#[from] InvalidControllerTokenForIdentity),
345  /// The user-defined operation passed to the builder through [AccessSubIdentityBuilder::to_perform] failed.
346  #[non_exhaustive]
347  #[error("user-defined operation on sub-Identity `{sub_identity}` failed")]
348  SubIdentityOperation {
349    /// ID of the sub-Identity.
350    sub_identity: ObjectID,
351    /// Error returned by the user closure.
352    source: BoxedStdError,
353  },
354}
355
356/// Error type returned by [AccessSubIdentityBuilder::finish].
357#[derive(Debug, thiserror::Error)]
358#[non_exhaustive]
359#[error("failed to build a valid sub-Identity access operation")]
360pub struct AccessSubIdentityBuilderError {
361  /// Type of failure.
362  #[from]
363  #[source]
364  pub kind: AccessSubIdentityBuilderErrorKind,
365}
366
367#[derive(Debug)]
368enum TxKind<Tx> {
369  Create {
370    expiration: Option<u64>,
371  },
372  Execute {
373    proposal_id: ObjectID,
374    sub_tx: Tx,
375    sub_identity_token: ControllerToken,
376  },
377  CreateAndExecute {
378    sub_tx: Tx,
379    sub_identity_token: ControllerToken,
380  },
381}
382
383/// [Transaction] that allows a controller of `identity` to access `sub_identity`
384/// by borrowing one of `identity`'s token over it.
385#[derive(Debug)]
386pub struct AccessSubIdentityTx<'i, 'sub, Tx = EmptyTx> {
387  identity: &'i mut OnChainIdentity,
388  identity_token: ControllerToken,
389  sub_identity: ObjectID,
390  tx_kind: TxKind<Tx>,
391  // The lifetime of sub-identity, borrowed within type parameter Tx.
392  _sub: PhantomData<&'sub ()>,
393}
394
395impl<'i, 'sub, Tx> AccessSubIdentityTx<'i, 'sub, Tx>
396where
397  Tx: Transaction,
398{
399  async fn build_pt_impl<C>(&self, client: &C) -> Result<ProgrammableTransaction, AccessSubIdentityErrorKind>
400  where
401    C: CoreClientReadOnly + OptionalSync,
402  {
403    // Query the indexer for inputs' refs.
404    let identity = client
405      .get_object_ref_by_id(self.identity.id())
406      .await
407      .map_err(|e| AccessSubIdentityErrorKind::RpcError(e.into()))?
408      .expect("exists on-chain");
409    let sub_identity = client
410      .get_object_ref_by_id(self.sub_identity)
411      .await
412      .map_err(|e| AccessSubIdentityErrorKind::RpcError(e.into()))?
413      .expect("exists on-chain");
414    let identity_token = self
415      .identity_token
416      .controller_ref(client)
417      .await
418      .map_err(|e| AccessSubIdentityErrorKind::RpcError(e.into()))?;
419    let package_id = identity_package_id(client)
420      .await
421      .map_err(|e| AccessSubIdentityErrorKind::RpcError(e.into()))?;
422
423    match &self.tx_kind {
424      TxKind::Create { expiration } => move_calls::identity::sub_identity::propose_identity_sub_access(
425        identity,
426        sub_identity,
427        identity_token,
428        *expiration,
429        package_id,
430      ),
431      TxKind::CreateAndExecute {
432        sub_tx,
433        sub_identity_token,
434      } => {
435        let sub_identity_token = sub_identity_token
436          .controller_ref(client)
437          .await
438          .map_err(|e| AccessSubIdentityErrorKind::RpcError(e.into()))?;
439        let sub_pt = sub_tx
440          .build_programmable_transaction(client)
441          .await
442          .map_err(|e| AccessSubIdentityErrorKind::InnerTransactionBuilding(e.into()))?;
443
444        move_calls::identity::sub_identity::propose_and_execute_sub_identity_access(
445          identity,
446          sub_identity,
447          identity_token,
448          sub_identity_token,
449          sub_pt,
450          None, // We are gonna execute it right away no need for expiration.
451          package_id,
452        )
453      }
454      TxKind::Execute {
455        proposal_id,
456        sub_tx,
457        sub_identity_token,
458      } => {
459        let sub_identity_token = sub_identity_token
460          .controller_ref(client)
461          .await
462          .map_err(|e| AccessSubIdentityErrorKind::RpcError(e.into()))?;
463        let sub_pt = sub_tx
464          .build_programmable_transaction(client)
465          .await
466          .map_err(|e| AccessSubIdentityErrorKind::InnerTransactionBuilding(e.into()))?;
467
468        move_calls::identity::sub_identity::execute_sub_identity_access(
469          identity,
470          identity_token,
471          *proposal_id,
472          sub_identity_token,
473          sub_pt,
474          package_id,
475        )
476      }
477    }
478    .map_err(|e| AccessSubIdentityErrorKind::TransactionBuilding(e.into()))
479  }
480}
481
482#[cfg_attr(feature = "send-sync", async_trait)]
483#[cfg_attr(not(feature = "send-sync"), async_trait(?Send))]
484impl<'i, 'sub, Tx> Transaction for AccessSubIdentityTx<'i, 'sub, Tx>
485where
486  Tx: Transaction + OptionalSync + OptionalSend,
487{
488  type Error = AccessSubIdentityError;
489  type Output = ProposedTxResult<Proposal<AccessSubIdentity>, Tx::Output>;
490  async fn build_programmable_transaction<C>(&self, client: &C) -> Result<ProgrammableTransaction, Self::Error>
491  where
492    C: CoreClientReadOnly + OptionalSync,
493  {
494    self.build_pt_impl(client).await.map_err(|kind| AccessSubIdentityError {
495      identity: self.identity.id(),
496      sub_identity: self.sub_identity,
497      kind,
498    })
499  }
500
501  async fn apply<C>(self, effects: &mut IotaTransactionBlockEffects, client: &C) -> Result<Self::Output, Self::Error>
502  where
503    C: CoreClientReadOnly + OptionalSync,
504  {
505    use iota_interaction::IotaClientTrait as _;
506
507    let tx_digest = effects.transaction_digest();
508    let tx_block = client
509      .client_adapter()
510      .read_api()
511      .get_transaction_with_options(*tx_digest, IotaTransactionBlockResponseOptions::default().with_events())
512      .await
513      .map_err(|e| AccessSubIdentityError {
514        identity: self.identity.id(),
515        sub_identity: self.sub_identity,
516        kind: AccessSubIdentityErrorKind::RpcError(e.into()),
517      })?;
518    let mut tx_events = tx_block.events().cloned().unwrap_or_default();
519
520    self.apply_with_events(effects, &mut tx_events, client).await
521  }
522
523  async fn apply_with_events<C>(
524    self,
525    effects: &mut IotaTransactionBlockEffects,
526    events: &mut IotaTransactionBlockEvents,
527    client: &C,
528  ) -> Result<Self::Output, Self::Error>
529  where
530    C: CoreClientReadOnly + OptionalSync,
531  {
532    // Extract the event for the proposal we are expecting.
533    let extract_proposal_id = |event: &IotaEvent| -> Option<ProposalEvent> {
534      if event.type_.module.as_str() == "identity" && event.type_.name.as_str() == "ProposalEvent" {
535        serde_json::from_value::<ProposalEvent>(event.parsed_json.clone())
536          .ok()
537          .filter(|event| event.identity == self.identity.id() && event.controller == self.identity_token.id())
538      } else {
539        None
540      }
541    };
542
543    if let IotaExecutionStatus::Failure { error } = effects.status() {
544      return Err(AccessSubIdentityError {
545        identity: self.identity.id(),
546        sub_identity: self.sub_identity,
547        kind: AccessSubIdentityErrorKind::TransactionExecution(error.as_str().into()),
548      });
549    }
550
551    let maybe_proposal_id = {
552      let maybe_proposal_event = events
553        .data
554        .iter()
555        .enumerate()
556        .find_map(|(i, event)| extract_proposal_id(event).map(|event| (i, event)));
557
558      if let Some((i, event)) = maybe_proposal_event {
559        // We handled this event, therefore we remove it so that other TXs can avoid going through it.
560        events.data.swap_remove(i);
561        Some(event.proposal)
562      } else {
563        None
564      }
565    };
566
567    match self.tx_kind {
568      TxKind::Create { .. } => client
569        .get_object_by_id(maybe_proposal_id.expect("tx was successful"))
570        .await
571        .map(ProposedTxResult::Pending)
572        .map_err(|e| AccessSubIdentityErrorKind::RpcError(e.into())),
573      TxKind::CreateAndExecute { sub_tx, .. } | TxKind::Execute { sub_tx, .. } => sub_tx
574        .apply_with_events(effects, events, client)
575        .await
576        .map(ProposedTxResult::Executed)
577        .map_err(|e| AccessSubIdentityErrorKind::EffectsApplication(e.into())),
578    }
579    .map_err(|kind| AccessSubIdentityError {
580      kind,
581      sub_identity: self.sub_identity,
582      identity: self.identity.id(),
583    })
584  }
585}
586
587impl MoveType for AccessSubIdentity {
588  fn move_type(package: ObjectID) -> TypeTag {
589    use std::str::FromStr;
590
591    TypeTag::from_str(&format!("{package}::access_sub_entity_proposal::AccessSubEntity")).expect("valid move type")
592  }
593}
594
595/// Type of failures that can be encountered when executing a [AccessSubIdentityTx].
596// TODO: Expose this type after transation building/execution has been reworked throughout the library.
597#[derive(Debug, thiserror::Error)]
598#[non_exhaustive]
599enum AccessSubIdentityErrorKind {
600  /// An RPC request to an IOTA Node failed.
601  #[error("RPC request failed")]
602  RpcError(#[source] Box<dyn std::error::Error + Send + Sync>),
603  /// Building the user-provided transaction failed.
604  #[error("failed to build user-provided Transaction")]
605  InnerTransactionBuilding(#[source] Box<dyn std::error::Error + Send + Sync>),
606  /// Building the whole transaction failed.
607  #[error("failed to build transaction")]
608  TransactionBuilding(#[source] Box<dyn std::error::Error + Send + Sync>),
609  /// Executing the transaction failed.
610  #[error("transaction execution failed")]
611  TransactionExecution(#[source] Box<dyn std::error::Error + Send + Sync>),
612  /// Failed to apply the transaction's effects off-chain.
613  #[error("transaction was successful but its effect couldn't be applied off-chain")]
614  EffectsApplication(#[source] Box<dyn std::error::Error + Send + Sync>),
615}
616
617/// Error type returned by executing an [AccessSubIdentityTx].
618#[derive(Debug, thiserror::Error)]
619#[error("transaction to access Identity `{sub_identity}` through Identity `{identity}` failed")]
620#[non_exhaustive]
621pub struct AccessSubIdentityError {
622  /// ID of the base-Identity.
623  pub identity: ObjectID,
624  /// Id of the sub-Identity.
625  pub sub_identity: ObjectID,
626  /// Type of failure.
627  #[source]
628  kind: AccessSubIdentityErrorKind,
629}