1mod 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#[cfg_attr(not(feature = "send-sync"), async_trait(?Send))]
55#[cfg_attr(feature = "send-sync", async_trait)]
56pub trait ProposalT: Sized {
57 type Action;
59 type Output;
61
62 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 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 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 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#[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 pub fn expiration_epoch(mut self, exp: u64) -> Self {
136 self.expiration = Some(exp);
137 self
138 }
139
140 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)]
159pub enum ProposalResult<P: ProposalT> {
163 Pending(P),
165 Executed(P::Output),
167}
168
169#[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 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 = 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 Proposal::<A>::parse_tx_effects(effects).map(ProposalResult::Executed)
225 } else {
226 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#[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 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#[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 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
421pub 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}