1use std::convert::Infallible;
5use std::future::Future;
6use std::marker::PhantomData;
7
8use async_trait::async_trait;
9use iota_interaction::rpc_types::IotaEvent;
10use iota_interaction::rpc_types::IotaExecutionStatus;
11use iota_interaction::rpc_types::IotaTransactionBlockEffects;
12use iota_interaction::rpc_types::IotaTransactionBlockEffectsAPI as _;
13use iota_interaction::rpc_types::IotaTransactionBlockEvents;
14use iota_interaction::rpc_types::IotaTransactionBlockResponseOptions;
15use iota_interaction::types::base_types::ObjectID;
16use iota_interaction::types::transaction::ProgrammableTransaction;
17use iota_interaction::types::TypeTag;
18use iota_interaction::MoveType;
19use iota_interaction::OptionalSend;
20use iota_interaction::OptionalSync;
21use product_common::core_client::CoreClientReadOnly;
22use product_common::transaction::transaction_builder::Transaction;
23use product_common::transaction::transaction_builder::TransactionBuilder;
24use product_common::transaction::IntoTransaction;
25use serde::Deserialize;
26use serde::Serialize;
27
28use crate::rebased::iota::move_calls;
29use crate::rebased::iota::package::identity_package_id;
30use crate::rebased::migration::ControllerToken;
31use crate::rebased::migration::InvalidControllerTokenForIdentity;
32use crate::rebased::migration::OnChainIdentity;
33use crate::rebased::migration::Proposal;
34
35use super::ProposalEvent;
36use super::ProposedTxResult;
37
38type BoxedStdError = Box<dyn std::error::Error + Send + Sync>;
39
40pub trait SubAccessFnT<'a>: FnOnce(&'a mut OnChainIdentity, ControllerToken) -> Self::Future {
42 type Future: Future<Output = Result<Self::IntoTx, Self::Error>> + OptionalSend + 'a;
44 type IntoTx: IntoTransaction<Tx = Self::Tx>;
46 type Tx: Transaction + 'a;
48 type Error: Into<BoxedStdError>;
50}
51
52impl<'a, F, Fut, IntoTx, Tx, E> SubAccessFnT<'a> for F
53where
54 F: FnOnce(&'a mut OnChainIdentity, ControllerToken) -> Fut,
55 Fut: Future<Output = Result<IntoTx, E>> + OptionalSend + 'a,
56 IntoTx: IntoTransaction<Tx = Tx>,
57 Tx: Transaction + 'a,
58 E: Into<BoxedStdError>,
59{
60 type Future = Fut;
61 type IntoTx = IntoTx;
62 type Tx = Tx;
63 type Error = E;
64}
65
66#[derive(Debug)]
70pub struct EmptyTx;
71
72#[cfg_attr(feature = "send-sync", async_trait)]
73#[cfg_attr(not(feature = "send-sync"), async_trait(?Send))]
74impl Transaction for EmptyTx {
75 type Output = ();
76 type Error = Infallible;
77
78 async fn build_programmable_transaction<C>(&self, _client: &C) -> Result<ProgrammableTransaction, Self::Error>
79 where
80 C: CoreClientReadOnly + OptionalSync,
81 {
82 Ok(ProgrammableTransaction {
83 inputs: vec![],
84 commands: vec![],
85 })
86 }
87
88 async fn apply<C>(self, _effects: &mut IotaTransactionBlockEffects, _client: &C) -> Result<Self::Output, Self::Error>
89 where
90 C: CoreClientReadOnly + OptionalSync,
91 {
92 Ok(())
93 }
94}
95
96#[derive(Debug, Clone, Serialize, Deserialize)]
99#[non_exhaustive]
100pub struct AccessSubIdentity {
101 #[serde(rename = "entity")]
103 pub identity: ObjectID,
104 #[serde(rename = "sub_entity")]
105 pub sub_identity: ObjectID,
107}
108
109#[derive(Debug)]
111pub struct AccessSubIdentityBuilder<'i, 'sub, F = ()> {
112 identity: &'i mut OnChainIdentity,
113 identity_token: ControllerToken,
114 sub_identity: &'sub mut OnChainIdentity,
115 expiration: Option<u64>,
116 sub_action: Option<F>,
117}
118
119impl<'i, 'sub, F> AccessSubIdentityBuilder<'i, 'sub, F> {
120 pub fn new(
123 identity: &'i mut OnChainIdentity,
124 sub_identity: &'sub mut OnChainIdentity,
125 identity_token: &ControllerToken,
126 ) -> Self {
127 Self {
128 identity,
129 sub_identity,
130 identity_token: identity_token.clone(),
131 expiration: None,
132 sub_action: None,
133 }
134 }
135
136 pub fn with_expiration(mut self, epoch_id: u64) -> Self {
140 self.expiration = Some(epoch_id);
141 self
142 }
143
144 pub fn to_perform<F1>(self, f: F1) -> AccessSubIdentityBuilder<'i, 'sub, F1>
155 where
156 F1: SubAccessFnT<'sub>,
157 {
158 AccessSubIdentityBuilder {
159 identity: self.identity,
160 identity_token: self.identity_token,
161 sub_identity: self.sub_identity,
162 expiration: self.expiration,
163 sub_action: Some(f),
164 }
165 }
166
167 async fn get_identity_token<C>(&self, client: &C) -> Result<ControllerToken, AccessSubIdentityBuilderErrorKind>
168 where
169 C: CoreClientReadOnly + OptionalSync,
170 {
171 if self.identity.id() != self.identity_token.controller_of() {
173 return Err(AccessSubIdentityBuilderErrorKind::Unauthorized(
174 InvalidControllerTokenForIdentity {
175 identity: self.identity.id(),
176 controller_token: self.identity_token.clone(),
177 },
178 ));
179 }
180
181 self
183 .sub_identity
184 .get_controller_token_for_address(self.identity.id().into(), client)
185 .await
186 .map_err(|e| AccessSubIdentityBuilderErrorKind::RpcError(e.into()))?
187 .ok_or(AccessSubIdentityBuilderErrorKind::UnrelatedIdentities(
189 UnrelatedIdentities {
190 identity: self.identity.id(),
191 sub_identity: self.sub_identity.id(),
192 },
193 ))
194 }
195}
196
197impl<'i, 'sub> AccessSubIdentityBuilder<'i, 'sub, ()> {
198 pub async fn finish<C>(
201 self,
202 client: &C,
203 ) -> Result<TransactionBuilder<AccessSubIdentityTx<'i, 'sub, EmptyTx>>, AccessSubIdentityBuilderError>
204 where
205 C: CoreClientReadOnly + OptionalSync,
206 {
207 let _ = self.get_identity_token(client).await?;
208 let tx_kind = TxKind::Create {
209 expiration: self.expiration,
210 };
211
212 Ok(TransactionBuilder::new(AccessSubIdentityTx {
213 identity: self.identity,
214 identity_token: self.identity_token,
215 sub_identity: self.sub_identity.id(),
216 tx_kind,
217 _sub: PhantomData,
218 }))
219 }
220}
221
222impl<'i, 'sub, F> AccessSubIdentityBuilder<'i, 'sub, F>
223where
224 F: SubAccessFnT<'sub>,
225 F::Tx: Transaction + OptionalSend + OptionalSync,
226{
227 pub async fn finish<C>(
230 self,
231 client: &C,
232 ) -> Result<TransactionBuilder<AccessSubIdentityTx<'i, 'sub, F::Tx>>, AccessSubIdentityBuilderError>
233 where
234 C: CoreClientReadOnly + OptionalSync,
235 {
236 let sub_identity_token = self.get_identity_token(client).await?;
237
238 let can_execute = self
240 .identity
241 .controller_voting_power(self.identity_token.controller_id())
242 .expect("valid controller token")
243 >= self.identity.threshold();
244
245 let sub_identity_id = self.sub_identity.id();
248 let maybe_sub_tx = if let Some(fetch_sub_tx) = self.sub_action.filter(|_| can_execute) {
249 fetch_sub_tx(self.sub_identity, sub_identity_token.clone())
250 .await
251 .map(|into_tx| Some(into_tx.into_transaction()))
252 .map_err(|e| AccessSubIdentityBuilderErrorKind::SubIdentityOperation {
253 sub_identity: sub_identity_id,
254 source: e.into(),
255 })?
256 } else {
257 None
258 };
259
260 let tx_kind = maybe_sub_tx
261 .map(move |sub_tx| TxKind::CreateAndExecute {
262 sub_tx,
263 sub_identity_token,
264 })
265 .unwrap_or(TxKind::Create {
266 expiration: self.expiration,
267 });
268 let tx = AccessSubIdentityTx {
269 identity: self.identity,
270 identity_token: self.identity_token,
271 sub_identity: sub_identity_id,
272 tx_kind,
273 _sub: PhantomData,
274 };
275
276 Ok(TransactionBuilder::new(tx))
277 }
278}
279
280impl Proposal<AccessSubIdentity> {
281 pub async fn into_tx<'i, 'sub, F, C>(
283 self,
284 identity: &'i mut OnChainIdentity,
285 sub_identity: &'sub mut OnChainIdentity,
286 identity_token: &ControllerToken,
287 sub_action: F,
288 client: &C,
289 ) -> Result<TransactionBuilder<AccessSubIdentityTx<'i, 'sub, F::Tx>>, AccessSubIdentityBuilderError>
290 where
291 F: SubAccessFnT<'sub>,
292 F::Tx: Transaction + OptionalSync + OptionalSend,
293 C: CoreClientReadOnly + OptionalSync,
294 {
295 let mut tx = identity
297 .access_sub_identity(sub_identity, identity_token)
298 .to_perform(sub_action)
299 .finish(client)
300 .await?
301 .into_inner();
302 let TxKind::CreateAndExecute {
304 sub_tx,
305 sub_identity_token,
306 } = tx.tx_kind
307 else {
308 unreachable!("a sub_action was passed");
309 };
310 tx.tx_kind = TxKind::Execute {
311 proposal_id: self.id(),
312 sub_tx,
313 sub_identity_token,
314 };
315
316 Ok(TransactionBuilder::new(tx))
317 }
318}
319
320#[derive(Debug, thiserror::Error)]
323#[non_exhaustive]
324#[error("Identity `{identity}` has no control over Identity `{sub_identity}`")]
325pub struct UnrelatedIdentities {
326 pub identity: ObjectID,
328 pub sub_identity: ObjectID,
330}
331
332#[derive(Debug, thiserror::Error)]
334#[non_exhaustive]
335pub enum AccessSubIdentityBuilderErrorKind {
336 #[error(transparent)]
338 RpcError(BoxedStdError),
339 #[error(transparent)]
341 UnrelatedIdentities(#[from] UnrelatedIdentities),
342 #[error(transparent)]
344 Unauthorized(#[from] InvalidControllerTokenForIdentity),
345 #[non_exhaustive]
347 #[error("user-defined operation on sub-Identity `{sub_identity}` failed")]
348 SubIdentityOperation {
349 sub_identity: ObjectID,
351 source: BoxedStdError,
353 },
354}
355
356#[derive(Debug, thiserror::Error)]
358#[non_exhaustive]
359#[error("failed to build a valid sub-Identity access operation")]
360pub struct AccessSubIdentityBuilderError {
361 #[from]
363 #[source]
364 pub kind: AccessSubIdentityBuilderErrorKind,
365}
366
367#[derive(Debug)]
368enum TxKind<Tx> {
369 Create {
370 expiration: Option<u64>,
371 },
372 Execute {
373 proposal_id: ObjectID,
374 sub_tx: Tx,
375 sub_identity_token: ControllerToken,
376 },
377 CreateAndExecute {
378 sub_tx: Tx,
379 sub_identity_token: ControllerToken,
380 },
381}
382
383#[derive(Debug)]
386pub struct AccessSubIdentityTx<'i, 'sub, Tx = EmptyTx> {
387 identity: &'i mut OnChainIdentity,
388 identity_token: ControllerToken,
389 sub_identity: ObjectID,
390 tx_kind: TxKind<Tx>,
391 _sub: PhantomData<&'sub ()>,
393}
394
395impl<'i, 'sub, Tx> AccessSubIdentityTx<'i, 'sub, Tx>
396where
397 Tx: Transaction,
398{
399 async fn build_pt_impl<C>(&self, client: &C) -> Result<ProgrammableTransaction, AccessSubIdentityErrorKind>
400 where
401 C: CoreClientReadOnly + OptionalSync,
402 {
403 let identity = client
405 .get_object_ref_by_id(self.identity.id())
406 .await
407 .map_err(|e| AccessSubIdentityErrorKind::RpcError(e.into()))?
408 .expect("exists on-chain");
409 let sub_identity = client
410 .get_object_ref_by_id(self.sub_identity)
411 .await
412 .map_err(|e| AccessSubIdentityErrorKind::RpcError(e.into()))?
413 .expect("exists on-chain");
414 let identity_token = self
415 .identity_token
416 .controller_ref(client)
417 .await
418 .map_err(|e| AccessSubIdentityErrorKind::RpcError(e.into()))?;
419 let package_id = identity_package_id(client)
420 .await
421 .map_err(|e| AccessSubIdentityErrorKind::RpcError(e.into()))?;
422
423 match &self.tx_kind {
424 TxKind::Create { expiration } => move_calls::identity::sub_identity::propose_identity_sub_access(
425 identity,
426 sub_identity,
427 identity_token,
428 *expiration,
429 package_id,
430 ),
431 TxKind::CreateAndExecute {
432 sub_tx,
433 sub_identity_token,
434 } => {
435 let sub_identity_token = sub_identity_token
436 .controller_ref(client)
437 .await
438 .map_err(|e| AccessSubIdentityErrorKind::RpcError(e.into()))?;
439 let sub_pt = sub_tx
440 .build_programmable_transaction(client)
441 .await
442 .map_err(|e| AccessSubIdentityErrorKind::InnerTransactionBuilding(e.into()))?;
443
444 move_calls::identity::sub_identity::propose_and_execute_sub_identity_access(
445 identity,
446 sub_identity,
447 identity_token,
448 sub_identity_token,
449 sub_pt,
450 None, package_id,
452 )
453 }
454 TxKind::Execute {
455 proposal_id,
456 sub_tx,
457 sub_identity_token,
458 } => {
459 let sub_identity_token = sub_identity_token
460 .controller_ref(client)
461 .await
462 .map_err(|e| AccessSubIdentityErrorKind::RpcError(e.into()))?;
463 let sub_pt = sub_tx
464 .build_programmable_transaction(client)
465 .await
466 .map_err(|e| AccessSubIdentityErrorKind::InnerTransactionBuilding(e.into()))?;
467
468 move_calls::identity::sub_identity::execute_sub_identity_access(
469 identity,
470 identity_token,
471 *proposal_id,
472 sub_identity_token,
473 sub_pt,
474 package_id,
475 )
476 }
477 }
478 .map_err(|e| AccessSubIdentityErrorKind::TransactionBuilding(e.into()))
479 }
480}
481
482#[cfg_attr(feature = "send-sync", async_trait)]
483#[cfg_attr(not(feature = "send-sync"), async_trait(?Send))]
484impl<'i, 'sub, Tx> Transaction for AccessSubIdentityTx<'i, 'sub, Tx>
485where
486 Tx: Transaction + OptionalSync + OptionalSend,
487{
488 type Error = AccessSubIdentityError;
489 type Output = ProposedTxResult<Proposal<AccessSubIdentity>, Tx::Output>;
490 async fn build_programmable_transaction<C>(&self, client: &C) -> Result<ProgrammableTransaction, Self::Error>
491 where
492 C: CoreClientReadOnly + OptionalSync,
493 {
494 self.build_pt_impl(client).await.map_err(|kind| AccessSubIdentityError {
495 identity: self.identity.id(),
496 sub_identity: self.sub_identity,
497 kind,
498 })
499 }
500
501 async fn apply<C>(self, effects: &mut IotaTransactionBlockEffects, client: &C) -> Result<Self::Output, Self::Error>
502 where
503 C: CoreClientReadOnly + OptionalSync,
504 {
505 use iota_interaction::IotaClientTrait as _;
506
507 let tx_digest = effects.transaction_digest();
508 let tx_block = client
509 .client_adapter()
510 .read_api()
511 .get_transaction_with_options(*tx_digest, IotaTransactionBlockResponseOptions::default().with_events())
512 .await
513 .map_err(|e| AccessSubIdentityError {
514 identity: self.identity.id(),
515 sub_identity: self.sub_identity,
516 kind: AccessSubIdentityErrorKind::RpcError(e.into()),
517 })?;
518 let mut tx_events = tx_block.events().cloned().unwrap_or_default();
519
520 self.apply_with_events(effects, &mut tx_events, client).await
521 }
522
523 async fn apply_with_events<C>(
524 self,
525 effects: &mut IotaTransactionBlockEffects,
526 events: &mut IotaTransactionBlockEvents,
527 client: &C,
528 ) -> Result<Self::Output, Self::Error>
529 where
530 C: CoreClientReadOnly + OptionalSync,
531 {
532 let extract_proposal_id = |event: &IotaEvent| -> Option<ProposalEvent> {
534 if event.type_.module.as_str() == "identity" && event.type_.name.as_str() == "ProposalEvent" {
535 serde_json::from_value::<ProposalEvent>(event.parsed_json.clone())
536 .ok()
537 .filter(|event| event.identity == self.identity.id() && event.controller == self.identity_token.id())
538 } else {
539 None
540 }
541 };
542
543 if let IotaExecutionStatus::Failure { error } = effects.status() {
544 return Err(AccessSubIdentityError {
545 identity: self.identity.id(),
546 sub_identity: self.sub_identity,
547 kind: AccessSubIdentityErrorKind::TransactionExecution(error.as_str().into()),
548 });
549 }
550
551 let maybe_proposal_id = {
552 let maybe_proposal_event = events
553 .data
554 .iter()
555 .enumerate()
556 .find_map(|(i, event)| extract_proposal_id(event).map(|event| (i, event)));
557
558 if let Some((i, event)) = maybe_proposal_event {
559 events.data.swap_remove(i);
561 Some(event.proposal)
562 } else {
563 None
564 }
565 };
566
567 match self.tx_kind {
568 TxKind::Create { .. } => client
569 .get_object_by_id(maybe_proposal_id.expect("tx was successful"))
570 .await
571 .map(ProposedTxResult::Pending)
572 .map_err(|e| AccessSubIdentityErrorKind::RpcError(e.into())),
573 TxKind::CreateAndExecute { sub_tx, .. } | TxKind::Execute { sub_tx, .. } => sub_tx
574 .apply_with_events(effects, events, client)
575 .await
576 .map(ProposedTxResult::Executed)
577 .map_err(|e| AccessSubIdentityErrorKind::EffectsApplication(e.into())),
578 }
579 .map_err(|kind| AccessSubIdentityError {
580 kind,
581 sub_identity: self.sub_identity,
582 identity: self.identity.id(),
583 })
584 }
585}
586
587impl MoveType for AccessSubIdentity {
588 fn move_type(package: ObjectID) -> TypeTag {
589 use std::str::FromStr;
590
591 TypeTag::from_str(&format!("{package}::access_sub_entity_proposal::AccessSubEntity")).expect("valid move type")
592 }
593}
594
595#[derive(Debug, thiserror::Error)]
598#[non_exhaustive]
599enum AccessSubIdentityErrorKind {
600 #[error("RPC request failed")]
602 RpcError(#[source] Box<dyn std::error::Error + Send + Sync>),
603 #[error("failed to build user-provided Transaction")]
605 InnerTransactionBuilding(#[source] Box<dyn std::error::Error + Send + Sync>),
606 #[error("failed to build transaction")]
608 TransactionBuilding(#[source] Box<dyn std::error::Error + Send + Sync>),
609 #[error("transaction execution failed")]
611 TransactionExecution(#[source] Box<dyn std::error::Error + Send + Sync>),
612 #[error("transaction was successful but its effect couldn't be applied off-chain")]
614 EffectsApplication(#[source] Box<dyn std::error::Error + Send + Sync>),
615}
616
617#[derive(Debug, thiserror::Error)]
619#[error("transaction to access Identity `{sub_identity}` through Identity `{identity}` failed")]
620#[non_exhaustive]
621pub struct AccessSubIdentityError {
622 pub identity: ObjectID,
624 pub sub_identity: ObjectID,
626 #[source]
628 kind: AccessSubIdentityErrorKind,
629}