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