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