identity_iota_core/rebased/proposals/
controller.rs

1// Copyright 2020-2024 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4use std::marker::PhantomData;
5
6use crate::rebased::iota::move_calls;
7use crate::rebased::iota::package::identity_package_id;
8use crate::rebased::migration::ControllerToken;
9use crate::rebased::migration::Proposal;
10
11use crate::rebased::Error;
12use async_trait::async_trait;
13use iota_interaction::rpc_types::IotaExecutionStatus;
14use iota_interaction::rpc_types::IotaObjectRef;
15use iota_interaction::rpc_types::IotaTransactionBlockEffects;
16use iota_interaction::rpc_types::IotaTransactionBlockEffectsAPI;
17use iota_interaction::rpc_types::OwnedObjectRef;
18use iota_interaction::types::base_types::IotaAddress;
19use iota_interaction::types::base_types::ObjectID;
20use iota_interaction::types::transaction::Argument;
21use iota_interaction::types::transaction::ProgrammableTransaction;
22use iota_interaction::types::TypeTag;
23use iota_interaction::MoveType;
24use iota_interaction::OptionalSync;
25use product_common::core_client::CoreClientReadOnly;
26use product_common::transaction::transaction_builder::Transaction;
27use product_common::transaction::transaction_builder::TransactionBuilder;
28use product_common::transaction::ProtoTransaction;
29use serde::Deserialize;
30use serde::Serialize;
31use tokio::sync::Mutex;
32
33use super::CreateProposal;
34use super::OnChainIdentity;
35use super::ProposalBuilder;
36use super::ProposalT;
37use super::UserDrivenTx;
38
39cfg_if::cfg_if! {
40    if #[cfg(target_arch = "wasm32")] {
41      use iota_interaction::types::programmable_transaction_builder::ProgrammableTransactionBuilder as Ptb;
42      /// Instances of ControllerIntentFnT can be used as user-provided function to describe how
43      /// a borrowed identity's controller capability will be used.
44      pub trait ControllerIntentFnT: FnOnce(&mut Ptb, &Argument) {}
45      impl<T> ControllerIntentFnT for T where T: FnOnce(&mut Ptb, &Argument) {}
46      #[allow(unreachable_pub)]
47      /// Boxed dynamic trait object of {@link ControllerIntentFnT}
48      pub type ControllerIntentFn = Box<dyn ControllerIntentFnT + Send>;
49    } else {
50      use iota_interaction::types::programmable_transaction_builder::ProgrammableTransactionBuilder as Ptb;
51      /// Instances of ControllerIntentFnT can be used as user-provided function to describe how
52      /// a borrowed identity's controller capability will be used.
53      pub trait ControllerIntentFnT: FnOnce(&mut Ptb, &Argument) {}
54      impl<T> ControllerIntentFnT for T where T: FnOnce(&mut Ptb, &Argument) {}
55      #[allow(unreachable_pub)]
56      /// Boxed dynamic trait object of {@link ControllerIntentFnT}
57      pub type ControllerIntentFn = Box<dyn ControllerIntentFnT + Send>;
58    }
59}
60
61/// Borrow an [`OnChainIdentity`]'s controller capability to exert control on
62/// a sub-owned identity.
63#[derive(Debug, Deserialize, Serialize)]
64pub struct ControllerExecution<F = ControllerIntentFn> {
65  controller_cap: ObjectID,
66  identity: IotaAddress,
67  #[serde(skip, default = "Mutex::default")]
68  intent_fn: Mutex<Option<F>>,
69}
70
71/// A [`ControllerExecution`] action coupled with a user-provided function to describe how
72/// the borrowed identity's controller capability will be used.
73pub struct ControllerExecutionWithIntent<F>(ControllerExecution<F>)
74where
75  F: FnOnce(&mut Ptb, &Argument);
76
77impl<F> ControllerExecutionWithIntent<F>
78where
79  F: ControllerIntentFnT,
80{
81  fn new(mut action: ControllerExecution<F>) -> Self {
82    debug_assert!(action.intent_fn.get_mut().is_some());
83    Self(action)
84  }
85}
86
87impl<F> ControllerExecution<F> {
88  /// Creates a new [`ControllerExecution`] action, allowing a controller of `identity` to
89  /// borrow `identity`'s controller cap for a transaction.
90  pub fn new(controller_cap: ObjectID, identity: &OnChainIdentity) -> Self {
91    Self {
92      controller_cap,
93      identity: identity.id().into(),
94      intent_fn: Mutex::default(),
95    }
96  }
97
98  /// Specifies how the borrowed `ControllerCap` should be used in the transaction.
99  /// This is only useful if the controller creating this proposal has enough voting
100  /// power to carry out it out immediately.
101  pub fn with_intent<F1>(self, intent_fn: F1) -> ControllerExecution<F1>
102  where
103    F1: FnOnce(&mut Ptb, &Argument),
104  {
105    let Self {
106      controller_cap,
107      identity,
108      ..
109    } = self;
110    ControllerExecution {
111      controller_cap,
112      identity,
113      intent_fn: Mutex::new(Some(intent_fn)),
114    }
115  }
116}
117
118impl<'i, 'c, F> ProposalBuilder<'i, 'c, ControllerExecution<F>> {
119  /// Specifies how the borrowed `ControllerCap` should be used in the transaction.
120  /// This is only useful if the controller creating this proposal has enough voting
121  /// power to carry out it out immediately.
122  pub fn with_intent<F1>(self, intent_fn: F1) -> ProposalBuilder<'i, 'c, ControllerExecution<F1>>
123  where
124    F1: FnOnce(&mut Ptb, &Argument),
125  {
126    let ProposalBuilder {
127      identity,
128      controller_token,
129      expiration,
130      action,
131    } = self;
132    ProposalBuilder {
133      identity,
134      controller_token,
135      expiration,
136      action: action.with_intent(intent_fn),
137    }
138  }
139}
140
141impl MoveType for ControllerExecution {
142  fn move_type(package: ObjectID) -> TypeTag {
143    use std::str::FromStr;
144
145    TypeTag::from_str(&format!("{package}::controller_proposal::ControllerExecution")).expect("valid move type")
146  }
147}
148
149#[cfg_attr(not(feature = "send-sync"), async_trait(?Send))]
150#[cfg_attr(feature = "send-sync", async_trait)]
151impl<F> ProposalT for Proposal<ControllerExecution<F>>
152where
153  F: ControllerIntentFnT + Send,
154{
155  type Action = ControllerExecution<F>;
156  type Output = ();
157
158  async fn create<'i, C>(
159    action: Self::Action,
160    expiration: Option<u64>,
161    identity: &'i mut OnChainIdentity,
162    controller_token: &ControllerToken,
163    client: &C,
164  ) -> Result<TransactionBuilder<CreateProposal<'i, Self::Action>>, Error>
165  where
166    C: CoreClientReadOnly + OptionalSync,
167  {
168    if identity.id() != controller_token.controller_of() {
169      return Err(Error::Identity(format!(
170        "token {} doesn't grant access to identity {}",
171        controller_token.id(),
172        identity.id()
173      )));
174    }
175    let identity_ref = client
176      .get_object_ref_by_id(identity.id())
177      .await?
178      .expect("identity exists on-chain");
179    let controller_cap_ref = controller_token.controller_ref(client).await?;
180    let maybe_intent_fn = action.intent_fn.into_inner();
181    let chained_execution = maybe_intent_fn.is_some()
182      && identity
183        .controller_voting_power(controller_token.controller_id())
184        .expect("is an identity's controller")
185        >= identity.threshold();
186
187    let package = identity_package_id(client).await?;
188    let ptb = if chained_execution {
189      let borrowing_controller_cap_ref = client
190        .get_object_ref_by_id(action.controller_cap)
191        .await?
192        .map(|OwnedObjectRef { reference, .. }| {
193          let IotaObjectRef {
194            object_id,
195            version,
196            digest,
197          } = reference;
198          (object_id, version, digest)
199        })
200        .ok_or_else(|| Error::ObjectLookup(format!("object {} doesn't exist", action.controller_cap)))?;
201
202      move_calls::identity::create_and_execute_controller_execution(
203        identity_ref,
204        controller_cap_ref,
205        expiration,
206        borrowing_controller_cap_ref,
207        maybe_intent_fn.unwrap(),
208        package,
209      )
210    } else {
211      move_calls::identity::propose_controller_execution(
212        identity_ref,
213        controller_cap_ref,
214        action.controller_cap,
215        expiration,
216        package,
217      )
218    }
219    .map_err(|e| Error::TransactionBuildingFailed(e.to_string()))?;
220
221    Ok(TransactionBuilder::new(CreateProposal {
222      identity,
223      ptb: bcs::from_bytes(&ptb)?,
224      chained_execution,
225      _action: PhantomData,
226    }))
227  }
228
229  async fn into_tx<'i, C>(
230    self,
231    identity: &'i mut OnChainIdentity,
232    controller_token: &ControllerToken,
233    _client: &C,
234  ) -> Result<UserDrivenTx<'i, Self::Action>, Error> {
235    if identity.id() != controller_token.controller_of() {
236      return Err(Error::Identity(format!(
237        "token {} doesn't grant access to identity {}",
238        controller_token.id(),
239        identity.id()
240      )));
241    }
242
243    let proposal_id = self.id();
244    let controller_execution_action = self.into_action();
245
246    Ok(UserDrivenTx::new(
247      identity,
248      controller_token.id(),
249      controller_execution_action,
250      proposal_id,
251    ))
252  }
253
254  fn parse_tx_effects(effects: &IotaTransactionBlockEffects) -> Result<Self::Output, Error> {
255    if let IotaExecutionStatus::Failure { error } = effects.status() {
256      return Err(Error::TransactionUnexpectedResponse(error.clone()));
257    }
258
259    Ok(())
260  }
261}
262
263impl<'i, F> UserDrivenTx<'i, ControllerExecution<F>> {
264  /// Defines how the borrowed assets should be used.
265  pub fn with_intent<F1>(self, intent_fn: F1) -> UserDrivenTx<'i, ControllerExecutionWithIntent<F1>>
266  where
267    F1: ControllerIntentFnT,
268  {
269    let UserDrivenTx {
270      identity,
271      action,
272      controller_token,
273      proposal_id,
274      ..
275    } = self;
276
277    UserDrivenTx::new(
278      identity,
279      controller_token,
280      ControllerExecutionWithIntent::new(action.with_intent(intent_fn)),
281      proposal_id,
282    )
283  }
284}
285
286impl<'i, F> ProtoTransaction for UserDrivenTx<'i, ControllerExecution<F>> {
287  type Input = ControllerIntentFn;
288  type Tx = TransactionBuilder<UserDrivenTx<'i, ControllerExecutionWithIntent<ControllerIntentFn>>>;
289
290  fn with(self, input: Self::Input) -> Self::Tx {
291    TransactionBuilder::new(self.with_intent(input))
292  }
293}
294
295impl<F> UserDrivenTx<'_, ControllerExecutionWithIntent<F>>
296where
297  F: ControllerIntentFnT + Send,
298{
299  async fn make_ptb<C>(&self, client: &C) -> Result<ProgrammableTransaction, Error>
300  where
301    C: CoreClientReadOnly + OptionalSync,
302  {
303    let Self {
304      identity,
305      action,
306      controller_token,
307      proposal_id,
308      ..
309    } = self;
310    let identity_ref = client
311      .get_object_ref_by_id(identity.id())
312      .await?
313      .expect("identity exists on-chain");
314    let controller_token = client.get_object_by_id::<ControllerToken>(*controller_token).await?;
315    let controller_cap_ref = controller_token.controller_ref(client).await?;
316
317    let borrowing_cap_id = action.0.controller_cap;
318    let borrowing_controller_cap_ref = client
319      .get_object_ref_by_id(borrowing_cap_id)
320      .await?
321      .map(|object_ref| object_ref.reference.to_object_ref())
322      .ok_or_else(|| Error::ObjectLookup(format!("object {borrowing_cap_id} doesn't exist")))?;
323    let package = identity_package_id(client).await?;
324
325    let tx = move_calls::identity::execute_controller_execution(
326      identity_ref,
327      controller_cap_ref,
328      *proposal_id,
329      borrowing_controller_cap_ref,
330      action
331        .0
332        .intent_fn
333        .lock()
334        .await
335        .take()
336        .expect("BorrowActionWithIntent makes sure intent_fn is present"),
337      package,
338    )
339    .map_err(|e| Error::TransactionBuildingFailed(e.to_string()))?;
340
341    Ok(bcs::from_bytes(&tx)?)
342  }
343}
344
345#[cfg_attr(not(feature = "send-sync"), async_trait(?Send))]
346#[cfg_attr(feature = "send-sync", async_trait)]
347impl<F> Transaction for UserDrivenTx<'_, ControllerExecutionWithIntent<F>>
348where
349  F: ControllerIntentFnT + Send,
350{
351  type Output = ();
352  type Error = Error;
353  async fn build_programmable_transaction<C>(&self, client: &C) -> Result<ProgrammableTransaction, Self::Error>
354  where
355    C: CoreClientReadOnly + OptionalSync,
356  {
357    self.cached_ptb.get_or_try_init(|| self.make_ptb(client)).await.cloned()
358  }
359
360  async fn apply<C>(self, effects: &mut IotaTransactionBlockEffects, _client: &C) -> Result<(), Self::Error>
361  where
362    C: CoreClientReadOnly + OptionalSync,
363  {
364    if let IotaExecutionStatus::Failure { error } = effects.status() {
365      return Err(Error::TransactionUnexpectedResponse(error.clone()));
366    }
367
368    Ok(())
369  }
370}