identity_iota_core/rebased/proposals/
borrow.rs

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