1mod 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#[cfg_attr(not(feature = "send-sync"), async_trait(?Send))]
58#[cfg_attr(feature = "send-sync", async_trait)]
59pub trait ProposalT: Sized {
60 type Action;
62 type Output;
64
65 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 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 fn parse_tx_effects(effects: &IotaTransactionBlockEffects) -> Result<Self::Output, Error>;
88}
89
90impl<A> Proposal<A>
91where
92 A: MoveType + OptionalSend + OptionalSync,
93{
94 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#[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 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 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#[derive(Debug)]
169pub enum ProposedTxResult<P, T> {
170 Pending(P),
172 Executed(T),
174}
175
176#[allow(type_alias_bounds)]
180pub type ProposalResult<P: ProposalT> = ProposedTxResult<P, P::Output>;
181
182#[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 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 = 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 Proposal::<A>::parse_tx_effects(effects).map(ProposalResult::Executed)
238 } else {
239 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#[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 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#[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 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
433pub 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}