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