identity_iota_core/rebased/proposals/
controller.rs1use std::marker::PhantomData;
5
6use crate::rebased::iota::move_calls;
7use crate::rebased::iota::package::identity_package_id;
8use crate::rebased::migration::ControllerToken;
9use crate::rebased::migration::Proposal;
10
11use crate::rebased::Error;
12use async_trait::async_trait;
13use iota_interaction::rpc_types::IotaExecutionStatus;
14use iota_interaction::rpc_types::IotaObjectRef;
15use iota_interaction::rpc_types::IotaTransactionBlockEffects;
16use iota_interaction::rpc_types::IotaTransactionBlockEffectsAPI;
17use iota_interaction::rpc_types::OwnedObjectRef;
18use iota_interaction::types::base_types::IotaAddress;
19use iota_interaction::types::base_types::ObjectID;
20use iota_interaction::types::transaction::Argument;
21use iota_interaction::types::transaction::ProgrammableTransaction;
22use iota_interaction::types::TypeTag;
23use iota_interaction::MoveType;
24use iota_interaction::OptionalSend;
25use iota_interaction::OptionalSync;
26use product_common::core_client::CoreClientReadOnly;
27use product_common::transaction::transaction_builder::Transaction;
28use product_common::transaction::transaction_builder::TransactionBuilder;
29use product_common::transaction::ProtoTransaction;
30use serde::Deserialize;
31use serde::Serialize;
32use tokio::sync::Mutex;
33
34use super::CreateProposal;
35use super::OnChainIdentity;
36use super::ProposalBuilder;
37use super::ProposalT;
38use super::UserDrivenTx;
39
40cfg_if::cfg_if! {
41 if #[cfg(target_arch = "wasm32")] {
42 use iota_interaction::types::programmable_transaction_builder::ProgrammableTransactionBuilder as Ptb;
43 pub trait ControllerIntentFnT: FnOnce(&mut Ptb, &Argument) {}
46 impl<T> ControllerIntentFnT for T where T: FnOnce(&mut Ptb, &Argument) {}
47 #[allow(unreachable_pub)]
48 pub type ControllerIntentFn = Box<dyn ControllerIntentFnT>;
50 } else {
51 use iota_interaction::types::programmable_transaction_builder::ProgrammableTransactionBuilder as Ptb;
52 pub trait ControllerIntentFnT: FnOnce(&mut Ptb, &Argument) {}
55 impl<T> ControllerIntentFnT for T where T: FnOnce(&mut Ptb, &Argument) {}
56 #[allow(unreachable_pub)]
57 pub type ControllerIntentFn = Box<dyn ControllerIntentFnT + Send>;
59 }
60}
61
62#[derive(Debug, Deserialize, Serialize)]
65pub struct ControllerExecution<F = ControllerIntentFn> {
66 controller_cap: ObjectID,
67 identity: IotaAddress,
68 #[serde(skip, default = "Mutex::default")]
69 intent_fn: Mutex<Option<F>>,
70}
71
72pub struct ControllerExecutionWithIntent<F>(ControllerExecution<F>)
75where
76 F: FnOnce(&mut Ptb, &Argument);
77
78impl<F> ControllerExecutionWithIntent<F>
79where
80 F: ControllerIntentFnT,
81{
82 fn new(action: ControllerExecution<F>) -> Self {
83 Self(action)
84 }
85}
86
87impl<F> ControllerExecution<F> {
88 pub fn new(controller_cap: ObjectID, identity: &OnChainIdentity) -> Self {
91 Self {
92 controller_cap,
93 identity: identity.id().into(),
94 intent_fn: Mutex::default(),
95 }
96 }
97
98 pub fn new_from_identity_address(controller_cap: ObjectID, identity_address: IotaAddress) -> Self {
100 Self {
101 controller_cap,
102 identity: identity_address,
103 intent_fn: Mutex::default(),
104 }
105 }
106
107 pub fn controller_cap(&self) -> ObjectID {
109 self.controller_cap
110 }
111
112 pub fn identity_address(&self) -> IotaAddress {
114 self.identity
115 }
116
117 pub fn with_intent<F1>(self, intent_fn: F1) -> ControllerExecution<F1>
121 where
122 F1: FnOnce(&mut Ptb, &Argument),
123 {
124 let Self {
125 controller_cap,
126 identity,
127 ..
128 } = self;
129 ControllerExecution {
130 controller_cap,
131 identity,
132 intent_fn: Mutex::new(Some(intent_fn)),
133 }
134 }
135}
136
137impl<'i, 'c, F> ProposalBuilder<'i, 'c, ControllerExecution<F>> {
138 pub fn with_intent<F1>(self, intent_fn: F1) -> ProposalBuilder<'i, 'c, ControllerExecution<F1>>
142 where
143 F1: FnOnce(&mut Ptb, &Argument),
144 {
145 let ProposalBuilder {
146 identity,
147 controller_token,
148 expiration,
149 action,
150 } = self;
151 ProposalBuilder {
152 identity,
153 controller_token,
154 expiration,
155 action: action.with_intent(intent_fn),
156 }
157 }
158}
159
160impl MoveType for ControllerExecution {
161 fn move_type(package: ObjectID) -> TypeTag {
162 use std::str::FromStr;
163
164 TypeTag::from_str(&format!("{package}::controller_proposal::ControllerExecution")).expect("valid move type")
165 }
166}
167
168#[cfg_attr(not(feature = "send-sync"), async_trait(?Send))]
169#[cfg_attr(feature = "send-sync", async_trait)]
170impl<F> ProposalT for Proposal<ControllerExecution<F>>
171where
172 F: ControllerIntentFnT + OptionalSend,
173{
174 type Action = ControllerExecution<F>;
175 type Output = ();
176
177 async fn create<'i, C>(
178 action: Self::Action,
179 expiration: Option<u64>,
180 identity: &'i mut OnChainIdentity,
181 controller_token: &ControllerToken,
182 client: &C,
183 ) -> Result<TransactionBuilder<CreateProposal<'i, Self::Action>>, Error>
184 where
185 C: CoreClientReadOnly + OptionalSync,
186 {
187 if identity.id() != controller_token.controller_of() {
188 return Err(Error::Identity(format!(
189 "token {} doesn't grant access to identity {}",
190 controller_token.id(),
191 identity.id()
192 )));
193 }
194 let identity_ref = client
195 .get_object_ref_by_id(identity.id())
196 .await?
197 .expect("identity exists on-chain");
198 let controller_cap_ref = controller_token.controller_ref(client).await?;
199 let maybe_intent_fn = action.intent_fn.into_inner();
200 let chained_execution = maybe_intent_fn.is_some()
201 && identity
202 .controller_voting_power(controller_token.controller_id())
203 .expect("is an identity's controller")
204 >= identity.threshold();
205
206 let package = identity_package_id(client).await?;
207 let ptb = if chained_execution {
208 let borrowing_controller_cap_ref = client
209 .get_object_ref_by_id(action.controller_cap)
210 .await?
211 .map(|OwnedObjectRef { reference, .. }| {
212 let IotaObjectRef {
213 object_id,
214 version,
215 digest,
216 } = reference;
217 (object_id, version, digest)
218 })
219 .ok_or_else(|| Error::ObjectLookup(format!("object {} doesn't exist", action.controller_cap)))?;
220
221 move_calls::identity::create_and_execute_controller_execution(
222 identity_ref,
223 controller_cap_ref,
224 expiration,
225 borrowing_controller_cap_ref,
226 maybe_intent_fn.unwrap(),
227 package,
228 )
229 } else {
230 move_calls::identity::propose_controller_execution(
231 identity_ref,
232 controller_cap_ref,
233 action.controller_cap,
234 expiration,
235 package,
236 )
237 }
238 .map_err(|e| Error::TransactionBuildingFailed(e.to_string()))?;
239
240 Ok(TransactionBuilder::new(CreateProposal {
241 identity,
242 ptb: bcs::from_bytes(&ptb)?,
243 chained_execution,
244 _action: PhantomData,
245 }))
246 }
247
248 async fn into_tx<'i, C>(
249 self,
250 identity: &'i mut OnChainIdentity,
251 controller_token: &ControllerToken,
252 _client: &C,
253 ) -> Result<UserDrivenTx<'i, Self::Action>, Error> {
254 if identity.id() != controller_token.controller_of() {
255 return Err(Error::Identity(format!(
256 "token {} doesn't grant access to identity {}",
257 controller_token.id(),
258 identity.id()
259 )));
260 }
261
262 let proposal_id = self.id();
263 let controller_execution_action = self.into_action();
264
265 Ok(UserDrivenTx::new(
266 identity,
267 controller_token.id(),
268 controller_execution_action,
269 proposal_id,
270 ))
271 }
272
273 fn parse_tx_effects(effects: &IotaTransactionBlockEffects) -> Result<Self::Output, Error> {
274 if let IotaExecutionStatus::Failure { error } = effects.status() {
275 return Err(Error::TransactionUnexpectedResponse(error.clone()));
276 }
277
278 Ok(())
279 }
280}
281
282impl<'i, F> UserDrivenTx<'i, ControllerExecution<F>> {
283 pub fn with_intent<F1>(self, intent_fn: F1) -> UserDrivenTx<'i, ControllerExecutionWithIntent<F1>>
285 where
286 F1: ControllerIntentFnT,
287 {
288 let UserDrivenTx {
289 identity,
290 action,
291 controller_token,
292 proposal_id,
293 ..
294 } = self;
295
296 UserDrivenTx::new(
297 identity,
298 controller_token,
299 ControllerExecutionWithIntent::new(action.with_intent(intent_fn)),
300 proposal_id,
301 )
302 }
303}
304
305impl<'i, F> ProtoTransaction for UserDrivenTx<'i, ControllerExecution<F>> {
306 type Input = ControllerIntentFn;
307 type Tx = TransactionBuilder<UserDrivenTx<'i, ControllerExecutionWithIntent<ControllerIntentFn>>>;
308
309 fn with(self, input: Self::Input) -> Self::Tx {
310 TransactionBuilder::new(self.with_intent(input))
311 }
312}
313
314impl<F> UserDrivenTx<'_, ControllerExecutionWithIntent<F>>
315where
316 F: ControllerIntentFnT + OptionalSend,
317{
318 async fn make_ptb<C>(&self, client: &C) -> Result<ProgrammableTransaction, Error>
319 where
320 C: CoreClientReadOnly + OptionalSync,
321 {
322 let Self {
323 identity,
324 action,
325 controller_token,
326 proposal_id,
327 ..
328 } = self;
329 let identity_ref = client
330 .get_object_ref_by_id(identity.id())
331 .await?
332 .expect("identity exists on-chain");
333 let controller_token = client.get_object_by_id::<ControllerToken>(*controller_token).await?;
334 let controller_cap_ref = controller_token.controller_ref(client).await?;
335
336 let borrowing_cap_id = action.0.controller_cap;
337 let borrowing_controller_cap_ref = client
338 .get_object_ref_by_id(borrowing_cap_id)
339 .await?
340 .map(|object_ref| object_ref.reference.to_object_ref())
341 .ok_or_else(|| Error::ObjectLookup(format!("object {borrowing_cap_id} doesn't exist")))?;
342 let package = identity_package_id(client).await?;
343
344 let tx = move_calls::identity::execute_controller_execution(
345 identity_ref,
346 controller_cap_ref,
347 *proposal_id,
348 borrowing_controller_cap_ref,
349 action
350 .0
351 .intent_fn
352 .lock()
353 .await
354 .take()
355 .expect("BorrowActionWithIntent makes sure intent_fn is present"),
356 package,
357 )
358 .map_err(|e| Error::TransactionBuildingFailed(e.to_string()))?;
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<F> Transaction for UserDrivenTx<'_, ControllerExecutionWithIntent<F>>
367where
368 F: ControllerIntentFnT + OptionalSend,
369{
370 type Output = ();
371 type Error = Error;
372 async fn build_programmable_transaction<C>(&self, client: &C) -> Result<ProgrammableTransaction, Self::Error>
373 where
374 C: CoreClientReadOnly + OptionalSync,
375 {
376 self.cached_ptb.get_or_try_init(|| self.make_ptb(client)).await.cloned()
377 }
378
379 async fn apply<C>(self, effects: &mut IotaTransactionBlockEffects, _client: &C) -> Result<(), 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 Ok(())
388 }
389}