identity_iota_core/rebased/proposals/
mod.rs

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