identity_iota_core/rebased/proposals/
send.rs

1// Copyright 2020-2024 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4use std::marker::PhantomData;
5
6use crate::rebased::iota::package::identity_package_id;
7use async_trait::async_trait;
8use iota_interaction::rpc_types::IotaTransactionBlockEffects;
9use iota_interaction::types::base_types::IotaAddress;
10use iota_interaction::types::base_types::ObjectID;
11use iota_interaction::types::TypeTag;
12use iota_interaction::MoveType;
13use iota_interaction::OptionalSync;
14use product_common::core_client::CoreClientReadOnly;
15use product_common::transaction::transaction_builder::TransactionBuilder;
16use serde::Deserialize;
17use serde::Serialize;
18
19use crate::rebased::iota::move_calls;
20use crate::rebased::migration::ControllerToken;
21use crate::rebased::migration::OnChainIdentity;
22use crate::rebased::Error;
23
24use super::CreateProposal;
25use super::ExecuteProposal;
26use super::Proposal;
27use super::ProposalBuilder;
28use super::ProposalT;
29
30/// An action used to transfer [`crate::migration::OnChainIdentity`]-owned assets to other addresses.
31#[derive(Debug, Clone, Deserialize, Default, Serialize)]
32#[serde(from = "IotaSendAction", into = "IotaSendAction")]
33pub struct SendAction(Vec<(ObjectID, IotaAddress)>);
34
35impl MoveType for SendAction {
36  fn move_type(package: ObjectID) -> TypeTag {
37    use std::str::FromStr;
38
39    TypeTag::from_str(&format!("{package}::transfer_proposal::Send")).expect("valid move type")
40  }
41}
42
43impl SendAction {
44  /// Adds to the list of object to send the object with ID `object_id` and send it to address `recipient`.
45  pub fn send_object(&mut self, object_id: ObjectID, recipient: IotaAddress) {
46    self.0.push((object_id, recipient));
47  }
48
49  /// Adds multiple objects to the list of objects to send.
50  pub fn send_objects<I>(&mut self, objects: I)
51  where
52    I: IntoIterator<Item = (ObjectID, IotaAddress)>,
53  {
54    objects
55      .into_iter()
56      .for_each(|(obj_id, recp)| self.send_object(obj_id, recp));
57  }
58}
59
60impl AsRef<[(ObjectID, IotaAddress)]> for SendAction {
61  fn as_ref(&self) -> &[(ObjectID, IotaAddress)] {
62    &self.0
63  }
64}
65
66impl ProposalBuilder<'_, '_, SendAction> {
67  /// Adds one object to the list of objects to send.
68  pub fn object(mut self, object_id: ObjectID, recipient: IotaAddress) -> Self {
69    self.send_object(object_id, recipient);
70    self
71  }
72
73  /// Adds multiple objects to the list of objects to send.
74  pub fn objects<I>(mut self, objects: I) -> Self
75  where
76    I: IntoIterator<Item = (ObjectID, IotaAddress)>,
77  {
78    self.send_objects(objects);
79    self
80  }
81}
82
83#[cfg_attr(not(feature = "send-sync"), async_trait(?Send))]
84#[cfg_attr(feature = "send-sync", async_trait)]
85impl ProposalT for Proposal<SendAction> {
86  type Action = SendAction;
87  type Output = ();
88
89  async fn create<'i, C>(
90    action: Self::Action,
91    expiration: Option<u64>,
92    identity: &'i mut OnChainIdentity,
93    controller_token: &ControllerToken,
94    client: &C,
95  ) -> Result<TransactionBuilder<CreateProposal<'i, Self::Action>>, Error>
96  where
97    C: CoreClientReadOnly + OptionalSync,
98  {
99    if identity.id() != controller_token.controller_of() {
100      return Err(Error::Identity(format!(
101        "token {} doesn't grant access to identity {}",
102        controller_token.id(),
103        identity.id()
104      )));
105    }
106    let package = identity_package_id(client).await?;
107    let identity_ref = client
108      .get_object_ref_by_id(identity.id())
109      .await?
110      .expect("identity exists on-chain");
111    let controller_cap_ref = controller_token.controller_ref(client).await?;
112    let can_execute = identity
113      .controller_voting_power(controller_token.controller_id())
114      .expect("controller_cap is for this identity")
115      >= identity.threshold();
116    let tx = if can_execute {
117      // Construct a list of `(ObjectRef, TypeTag)` from the list of objects to send.
118      let object_type_list = {
119        let ids = action.0.iter().map(|(obj_id, _rcp)| obj_id);
120        let mut object_and_type_list = vec![];
121        for obj_id in ids {
122          let ref_and_type = super::obj_ref_and_type_for_id(client, *obj_id)
123            .await
124            .map_err(|e| Error::ObjectLookup(e.to_string()))?;
125          object_and_type_list.push(ref_and_type);
126        }
127        object_and_type_list
128      };
129      move_calls::identity::create_and_execute_send(
130        identity_ref,
131        controller_cap_ref,
132        action.0,
133        expiration,
134        object_type_list,
135        package,
136      )
137    } else {
138      move_calls::identity::propose_send(identity_ref, controller_cap_ref, action.0, expiration, package)
139    }
140    .map_err(|e| Error::TransactionBuildingFailed(e.to_string()))?;
141    Ok(TransactionBuilder::new(CreateProposal {
142      identity,
143      ptb: bcs::from_bytes(&tx)?,
144      chained_execution: can_execute,
145      _action: PhantomData,
146    }))
147  }
148
149  async fn into_tx<'i, C>(
150    self,
151    identity: &'i mut OnChainIdentity,
152    controller_token: &ControllerToken,
153    client: &C,
154  ) -> Result<TransactionBuilder<ExecuteProposal<'i, Self::Action>>, Error>
155  where
156    C: CoreClientReadOnly + OptionalSync,
157  {
158    if identity.id() != controller_token.controller_of() {
159      return Err(Error::Identity(format!(
160        "token {} doesn't grant access to identity {}",
161        controller_token.id(),
162        identity.id()
163      )));
164    }
165    let proposal_id = self.id();
166    let identity_ref = client
167      .get_object_ref_by_id(identity.id())
168      .await?
169      .expect("identity exists on-chain");
170    let controller_cap_ref = controller_token.controller_ref(client).await?;
171
172    // Construct a list of `(ObjectRef, TypeTag)` from the list of objects to send.
173    let object_type_list = {
174      let ids = self.into_action().0.into_iter().map(|(obj_id, _rcp)| obj_id);
175      let mut object_and_type_list = vec![];
176      for obj_id in ids {
177        let ref_and_type = super::obj_ref_and_type_for_id(client, obj_id)
178          .await
179          .map_err(|e| Error::ObjectLookup(e.to_string()))?;
180        object_and_type_list.push(ref_and_type);
181      }
182      object_and_type_list
183    };
184    let package = identity_package_id(client).await?;
185
186    let tx =
187      move_calls::identity::execute_send(identity_ref, controller_cap_ref, proposal_id, object_type_list, package)
188        .map_err(|e| Error::TransactionBuildingFailed(e.to_string()))?;
189
190    Ok(TransactionBuilder::new(ExecuteProposal {
191      identity,
192      ptb: bcs::from_bytes(&tx)?,
193      _action: PhantomData,
194    }))
195  }
196
197  fn parse_tx_effects(_effects: &IotaTransactionBlockEffects) -> Result<Self::Output, Error> {
198    Ok(())
199  }
200}
201
202#[derive(Debug, Deserialize, Serialize)]
203struct IotaSendAction {
204  objects: Vec<ObjectID>,
205  recipients: Vec<IotaAddress>,
206}
207
208impl From<IotaSendAction> for SendAction {
209  fn from(value: IotaSendAction) -> Self {
210    let IotaSendAction { objects, recipients } = value;
211    let transfer_map = objects.into_iter().zip(recipients).collect();
212    SendAction(transfer_map)
213  }
214}
215
216impl From<SendAction> for IotaSendAction {
217  fn from(action: SendAction) -> Self {
218    let (objects, recipients) = action.0.into_iter().unzip();
219    Self { objects, recipients }
220  }
221}