identity_iota_core/rebased/proposals/
mod.rs

1// Copyright 2020-2024 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4mod borrow;
5mod config_change;
6mod controller;
7mod send;
8mod update_did_doc;
9mod upgrade;
10
11use std::marker::PhantomData;
12use std::ops::Deref;
13use std::ops::DerefMut;
14
15use crate::rebased::iota::move_calls;
16use crate::rebased::migration::get_identity;
17use async_trait::async_trait;
18pub use borrow::*;
19pub use config_change::*;
20pub use controller::*;
21use iota_interaction::rpc_types::IotaExecutionStatus;
22use iota_interaction::rpc_types::IotaObjectData;
23use iota_interaction::rpc_types::IotaObjectDataOptions;
24use iota_interaction::rpc_types::IotaTransactionBlockEffects;
25use iota_interaction::rpc_types::IotaTransactionBlockEffectsAPI as _;
26use iota_interaction::types::base_types::ObjectID;
27use iota_interaction::types::base_types::ObjectRef;
28use iota_interaction::types::base_types::ObjectType;
29use iota_interaction::types::transaction::ProgrammableTransaction;
30use iota_interaction::types::TypeTag;
31use iota_interaction::IotaClientTrait;
32use iota_interaction::OptionalSend;
33use iota_interaction::OptionalSync;
34use product_common::core_client::CoreClientReadOnly;
35use product_common::transaction::transaction_builder::Transaction;
36use product_common::transaction::transaction_builder::TransactionBuilder;
37use product_common::transaction::ProtoTransaction;
38use tokio::sync::OnceCell;
39
40pub use send::*;
41use serde::de::DeserializeOwned;
42pub use update_did_doc::*;
43pub use upgrade::*;
44
45use super::iota::package::identity_package_id;
46use crate::rebased::migration::OnChainIdentity;
47use crate::rebased::migration::Proposal;
48use crate::rebased::Error;
49use iota_interaction::MoveType;
50
51use super::migration::ControllerToken;
52
53/// Interface that allows the creation and execution of an [`OnChainIdentity`]'s [`Proposal`]s.
54#[cfg_attr(not(feature = "send-sync"), async_trait(?Send))]
55#[cfg_attr(feature = "send-sync", async_trait)]
56pub trait ProposalT: Sized {
57  /// The [`Proposal`] action's type.
58  type Action;
59  /// The output of the [`Proposal`]
60  type Output;
61
62  /// Creates a new [`Proposal`] with the provided action and expiration.
63  async fn create<'i, C>(
64    action: Self::Action,
65    expiration: Option<u64>,
66    identity: &'i mut OnChainIdentity,
67    controller_token: &ControllerToken,
68    client: &C,
69  ) -> Result<TransactionBuilder<CreateProposal<'i, Self::Action>>, Error>
70  where
71    C: CoreClientReadOnly + OptionalSync;
72
73  /// Converts the [`Proposal`] into a transaction that can be executed.
74  async fn into_tx<'i, C>(
75    self,
76    identity: &'i mut OnChainIdentity,
77    controller_token: &ControllerToken,
78    client: &C,
79  ) -> Result<impl ProtoTransaction, Error>
80  where
81    C: CoreClientReadOnly + OptionalSync;
82
83  /// Parses the transaction's effects and returns the output of the [`Proposal`].
84  fn parse_tx_effects(effects: &IotaTransactionBlockEffects) -> Result<Self::Output, Error>;
85}
86
87impl<A> Proposal<A>
88where
89  Proposal<A>: ProposalT<Action = A>,
90  A: MoveType + OptionalSend + OptionalSync,
91{
92  /// Creates a new [ApproveProposal] for the provided [`Proposal`].
93  pub fn approve<'i>(
94    &mut self,
95    identity: &'i OnChainIdentity,
96    controller_token: &ControllerToken,
97  ) -> Result<TransactionBuilder<ApproveProposal<'_, 'i, A>>, Error> {
98    ApproveProposal::new(self, identity, controller_token).map(TransactionBuilder::new)
99  }
100}
101
102/// A builder for creating a [`Proposal`].
103#[derive(Debug)]
104pub struct ProposalBuilder<'i, 'c, A> {
105  identity: &'i mut OnChainIdentity,
106  controller_token: &'c ControllerToken,
107  expiration: Option<u64>,
108  action: A,
109}
110
111impl<A> Deref for ProposalBuilder<'_, '_, A> {
112  type Target = A;
113  fn deref(&self) -> &Self::Target {
114    &self.action
115  }
116}
117
118impl<A> DerefMut for ProposalBuilder<'_, '_, A> {
119  fn deref_mut(&mut self) -> &mut Self::Target {
120    &mut self.action
121  }
122}
123
124impl<'i, 'c, A> ProposalBuilder<'i, 'c, A> {
125  pub(crate) fn new(identity: &'i mut OnChainIdentity, controller_token: &'c ControllerToken, action: A) -> Self {
126    Self {
127      identity,
128      controller_token,
129      expiration: None,
130      action,
131    }
132  }
133
134  /// Sets the expiration epoch for the [`Proposal`].
135  pub fn expiration_epoch(mut self, exp: u64) -> Self {
136    self.expiration = Some(exp);
137    self
138  }
139
140  /// Creates a [`Proposal`] with the provided arguments. If `forbid_chained_execution` is set to `true`,
141  /// the [`Proposal`] won't be executed even if creator alone has enough voting power.
142  pub async fn finish<C>(self, client: &C) -> Result<TransactionBuilder<CreateProposal<'i, A>>, Error>
143  where
144    Proposal<A>: ProposalT<Action = A>,
145    C: CoreClientReadOnly + OptionalSync,
146  {
147    let Self {
148      action,
149      expiration,
150      controller_token,
151      identity,
152    } = self;
153
154    Proposal::<A>::create(action, expiration, identity, controller_token, client).await
155  }
156}
157
158#[derive(Debug)]
159/// The result of creating a [`Proposal`]. When a [`Proposal`] is executed
160/// in the same transaction as its creation, a [`ProposalResult::Executed`] is
161/// returned. [`ProposalResult::Pending`] otherwise.
162pub enum ProposalResult<P: ProposalT> {
163  /// A [`Proposal`] that has yet to be executed.
164  Pending(P),
165  /// A [`Proposal`]'s execution output.
166  Executed(P::Output),
167}
168
169/// A transaction to create a [`Proposal`].
170#[derive(Debug)]
171pub struct CreateProposal<'i, A> {
172  identity: &'i mut OnChainIdentity,
173  chained_execution: bool,
174  ptb: ProgrammableTransaction,
175  _action: PhantomData<A>,
176}
177
178impl<A> CreateProposal<'_, A> {
179  /// Returns this [Transaction]'s [ProgrammableTransaction].
180  pub fn ptb(&self) -> &ProgrammableTransaction {
181    &self.ptb
182  }
183}
184
185#[cfg_attr(not(feature = "send-sync"), async_trait(?Send))]
186#[cfg_attr(feature = "send-sync", async_trait)]
187impl<A> Transaction for CreateProposal<'_, A>
188where
189  Proposal<A>: ProposalT<Action = A> + DeserializeOwned,
190  A: OptionalSend + OptionalSync,
191{
192  type Output = ProposalResult<Proposal<A>>;
193  type Error = Error;
194
195  async fn build_programmable_transaction<C>(&self, _client: &C) -> Result<ProgrammableTransaction, Error>
196  where
197    C: CoreClientReadOnly + OptionalSync,
198  {
199    Ok(self.ptb.clone())
200  }
201
202  async fn apply<C>(self, effects: &mut IotaTransactionBlockEffects, client: &C) -> Result<Self::Output, Error>
203  where
204    C: CoreClientReadOnly + OptionalSync,
205  {
206    let Self {
207      identity,
208      chained_execution,
209      ..
210    } = self;
211
212    if let IotaExecutionStatus::Failure { error } = effects.status() {
213      return Err(Error::TransactionUnexpectedResponse(error.clone()));
214    }
215
216    // Identity has been changed regardless of whether the proposal has been executed
217    // or simply created. Refetch it, to sync it with its on-chain state.
218    *identity = get_identity(client, identity.id())
219      .await?
220      .ok_or_else(|| Error::Identity(format!("identity {} cannot be found", identity.id())))?;
221
222    if chained_execution {
223      // The proposal has been created and executed right-away. Parse its effects.
224      Proposal::<A>::parse_tx_effects(effects).map(ProposalResult::Executed)
225    } else {
226      // 2 objects are created, one is the Bag's Field and the other is our Proposal. Proposal is not owned by the bag,
227      // but the field is.
228      let proposals_bag_id = identity.multicontroller().proposals_bag_id();
229      let proposal_id = effects
230        .created()
231        .iter()
232        .find(|obj_ref| obj_ref.owner != proposals_bag_id)
233        .expect("tx was successful")
234        .object_id();
235
236      client
237        .get_object_by_id(proposal_id)
238        .await
239        .map_err(Error::from)
240        .map(ProposalResult::Pending)
241    }
242  }
243}
244
245/// A transaction to execute a [`Proposal`].
246#[derive(Debug)]
247pub struct ExecuteProposal<'i, A> {
248  ptb: ProgrammableTransaction,
249  identity: &'i mut OnChainIdentity,
250  _action: PhantomData<A>,
251}
252
253impl<A> ExecuteProposal<'_, A> {
254  /// Returns this [Transaction]'s [ProgrammableTransaction].
255  pub fn ptb(&self) -> &ProgrammableTransaction {
256    &self.ptb
257  }
258}
259
260#[cfg_attr(not(feature = "send-sync"), async_trait(?Send))]
261#[cfg_attr(feature = "send-sync", async_trait)]
262impl<A> Transaction for ExecuteProposal<'_, A>
263where
264  Proposal<A>: ProposalT<Action = A>,
265  A: OptionalSend + OptionalSync,
266{
267  type Output = <Proposal<A> as ProposalT>::Output;
268  type Error = Error;
269
270  async fn build_programmable_transaction<C>(&self, _client: &C) -> Result<ProgrammableTransaction, Error>
271  where
272    C: CoreClientReadOnly + OptionalSync,
273  {
274    Ok(self.ptb.clone())
275  }
276
277  async fn apply<C>(self, effects: &mut IotaTransactionBlockEffects, client: &C) -> Result<Self::Output, Error>
278  where
279    C: CoreClientReadOnly + OptionalSync,
280  {
281    let Self { identity, .. } = self;
282
283    if let IotaExecutionStatus::Failure { error } = effects.status() {
284      return Err(Error::TransactionUnexpectedResponse(error.clone()));
285    }
286
287    *identity = get_identity(client, identity.id())
288      .await?
289      .ok_or_else(|| Error::Identity(format!("identity {} cannot be found", identity.id())))?;
290
291    Proposal::<A>::parse_tx_effects(effects)
292  }
293}
294
295/// A transaction to approve a [`Proposal`].
296#[derive(Debug)]
297pub struct ApproveProposal<'p, 'i, A> {
298  proposal: &'p mut Proposal<A>,
299  identity: &'i OnChainIdentity,
300  controller_token: ControllerToken,
301  cached_ptb: OnceCell<ProgrammableTransaction>,
302}
303
304impl<'p, 'i, A> ApproveProposal<'p, 'i, A> {
305  /// Creates a new [Transaction] to approve `identity`'s `proposal`.
306  pub fn new(
307    proposal: &'p mut Proposal<A>,
308    identity: &'i OnChainIdentity,
309    controller_token: &ControllerToken,
310  ) -> Result<Self, Error> {
311    if identity.id() != controller_token.controller_of() {
312      return Err(Error::Identity(format!(
313        "token {} doesn't grant access to identity {}",
314        controller_token.id(),
315        identity.id()
316      )));
317    }
318
319    Ok(Self {
320      proposal,
321      identity,
322      controller_token: controller_token.clone(),
323      cached_ptb: OnceCell::new(),
324    })
325  }
326}
327impl<A: MoveType> ApproveProposal<'_, '_, A> {
328  async fn make_ptb<C>(&self, client: &C) -> Result<ProgrammableTransaction, Error>
329  where
330    C: CoreClientReadOnly + OptionalSync,
331  {
332    let Self {
333      proposal,
334      identity,
335      controller_token,
336      ..
337    } = self;
338    let identity_ref = client
339      .get_object_ref_by_id(identity.id())
340      .await?
341      .ok_or_else(|| Error::Identity(format!("identity {} doesn't exist", identity.id())))?;
342    let controller_cap = controller_token.controller_ref(client).await?;
343    let package = identity_package_id(client).await?;
344
345    let tx = move_calls::identity::approve_proposal::<A>(identity_ref.clone(), controller_cap, proposal.id(), package)?;
346
347    Ok(bcs::from_bytes(&tx)?)
348  }
349}
350
351#[cfg_attr(not(feature = "send-sync"), async_trait(?Send))]
352#[cfg_attr(feature = "send-sync", async_trait)]
353impl<A> Transaction for ApproveProposal<'_, '_, A>
354where
355  Proposal<A>: ProposalT<Action = A>,
356  A: MoveType + OptionalSend + OptionalSync,
357{
358  type Output = ();
359  type Error = Error;
360
361  async fn build_programmable_transaction<C>(&self, client: &C) -> Result<ProgrammableTransaction, Self::Error>
362  where
363    C: CoreClientReadOnly + OptionalSync,
364  {
365    self.cached_ptb.get_or_try_init(|| self.make_ptb(client)).await.cloned()
366  }
367  async fn apply<C>(self, effects: &mut IotaTransactionBlockEffects, _client: &C) -> Result<Self::Output, Self::Error>
368  where
369    C: CoreClientReadOnly + OptionalSync,
370  {
371    if let IotaExecutionStatus::Failure { error } = effects.status() {
372      return Err(Error::TransactionUnexpectedResponse(error.clone()));
373    }
374
375    let proposal_was_updated = effects
376      .mutated()
377      .iter()
378      .any(|obj| obj.object_id() == self.proposal.id());
379    if proposal_was_updated {
380      let vp = self
381        .identity
382        .controller_voting_power(self.controller_token.controller_id())
383        .expect("is identity's controller");
384      *self.proposal.votes_mut() = self.proposal.votes() + vp;
385      Ok(())
386    } else {
387      Err(Error::TransactionUnexpectedResponse(format!(
388        "proposal {} wasn't updated in this transaction",
389        self.proposal.id()
390      )))
391    }
392  }
393}
394
395async fn obj_data_for_id(client: &impl CoreClientReadOnly, obj_id: ObjectID) -> anyhow::Result<IotaObjectData> {
396  use anyhow::Context;
397
398  client
399    .client_adapter()
400    .read_api()
401    .get_object_with_options(obj_id, IotaObjectDataOptions::default().with_type().with_owner())
402    .await?
403    .into_object()
404    .context("no iota object in response")
405}
406
407async fn obj_ref_and_type_for_id(
408  client: &impl CoreClientReadOnly,
409  obj_id: ObjectID,
410) -> anyhow::Result<(ObjectRef, TypeTag)> {
411  let res = obj_data_for_id(client, obj_id).await?;
412  let obj_ref = res.object_ref();
413  let obj_type = match res.object_type().expect("object type is requested") {
414    ObjectType::Package => anyhow::bail!("a move package cannot be sent"),
415    ObjectType::Struct(type_) => type_.into(),
416  };
417
418  Ok((obj_ref, obj_type))
419}
420
421/// A transaction that requires user input in order to be executed.
422pub struct UserDrivenTx<'i, A> {
423  identity: &'i mut OnChainIdentity,
424  controller_token: ObjectID,
425  action: A,
426  proposal_id: ObjectID,
427  cached_ptb: OnceCell<ProgrammableTransaction>,
428}
429
430impl<'i, A> UserDrivenTx<'i, A> {
431  fn new(identity: &'i mut OnChainIdentity, controller_token: ObjectID, action: A, proposal_id: ObjectID) -> Self {
432    Self {
433      identity,
434      controller_token,
435      action,
436      proposal_id,
437      cached_ptb: OnceCell::new(),
438    }
439  }
440}