diff --git a/identity_iota_core/packages/iota_identity/sources/controller.move b/identity_iota_core/packages/iota_identity/sources/controller.move index ae39130d5..4d26a742f 100644 --- a/identity_iota_core/packages/iota_identity/sources/controller.move +++ b/identity_iota_core/packages/iota_identity/sources/controller.move @@ -5,6 +5,7 @@ module iota_identity::controller { public use fun delete_controller_cap as ControllerCap.delete; public use fun delete_delegation_token as DelegationToken.delete; + public use fun delegation_token_id as DelegationToken.id; /// This `ControllerCap` cannot delegate access. const ECannotDelegate: u64 = 0; @@ -61,6 +62,11 @@ module iota_identity::controller { controller: ID, } + /// Returns the ID of this `DelegationToken`. + public fun delegation_token_id(self: &DelegationToken): ID { + self.id.to_inner() + } + /// Returns the controller's ID of this `DelegationToken`. public fun controller(self: &DelegationToken): ID { self.controller diff --git a/identity_iota_core/packages/iota_identity/sources/identity.move b/identity_iota_core/packages/iota_identity/sources/identity.move index 0cbacf0e2..e57dd213b 100644 --- a/identity_iota_core/packages/iota_identity/sources/identity.move +++ b/identity_iota_core/packages/iota_identity/sources/identity.move @@ -15,6 +15,7 @@ module iota_identity::identity { transfer_proposal::{Self, Send}, borrow_proposal::{Self, Borrow}, did_deactivation_proposal::{Self, DidDeactivation}, + controller_proposal::{Self, ControllerExecution}, upgrade_proposal::{Self, Upgrade}, }; @@ -190,7 +191,7 @@ module iota_identity::identity { self.execute_deactivation(cap, proposal_id, clock, ctx); option::none() } else { - emit_proposal_event(self.id().to_inner(), cap.id().to_inner(), proposal_id, false); + emit_proposal_event(self.id().to_inner(), cap.id(), proposal_id, false); option::some(proposal_id) } } @@ -211,13 +212,43 @@ module iota_identity::identity { self.did_doc.set_controlled_value(vector[]); self.updated = clock.timestamp_ms(); - emit_proposal_event(self.id().to_inner(), cap.id().to_inner(), proposal_id, true); + emit_proposal_event(self.id().to_inner(), cap.id(), proposal_id, true); + } + + /// Creates a new `ControllerExecution` proposal. + public fun propose_controller_execution( + self: &mut Identity, + cap: &DelegationToken, + controller_cap_id: ID, + expiration: Option, + ctx: &mut TxContext, + ): ID { + let identity_address = self.id().to_address(); + let proposal_id = self.did_doc.create_proposal( + cap, + controller_proposal::new(controller_cap_id, identity_address), + expiration, + ctx, + ); + + emit_proposal_event(self.id().to_inner(), cap.id(), proposal_id, false); + proposal_id + } + + /// Borrow the identity-owned controller cap specified in `ControllerExecution`. + /// The borrowed cap must be put back by calling `controller_proposal::put_back`. + public fun borrow_controller_cap( + self: &mut Identity, + action: &mut Action, + receiving: Receiving, + ): ControllerCap { + controller_proposal::receive(action, &mut self.id, receiving) } /// Proposes to upgrade this `Identity` to this package's version. public fun propose_upgrade( self: &mut Identity, - cap: &ControllerCap, + cap: &DelegationToken, expiration: Option, ctx: &mut TxContext, ): Option { @@ -235,7 +266,7 @@ module iota_identity::identity { self.execute_upgrade(cap, proposal_id, ctx); option::none() } else { - emit_proposal_event(self.id().to_inner(), cap.id().to_inner(), proposal_id, false); + emit_proposal_event(self.id().to_inner(), cap.id(), proposal_id, false); option::some(proposal_id) } } @@ -244,13 +275,13 @@ module iota_identity::identity { /// package's version. public fun execute_upgrade( self: &mut Identity, - cap: &ControllerCap, + cap: &DelegationToken, proposal_id: ID, ctx: &mut TxContext, ) { self.execute_proposal(cap, proposal_id, ctx).unwrap(); self.migrate(); - emit_proposal_event(self.id().to_inner(), cap.id().to_inner(), proposal_id, true); + emit_proposal_event(self.id().to_inner(), cap.id(), proposal_id, true); } /// Migrates this `Identity` to this package's version. @@ -286,7 +317,7 @@ module iota_identity::identity { self.execute_update(cap, proposal_id, clock, ctx); option::none() } else { - emit_proposal_event(self.id().to_inner(), cap.id().to_inner(), proposal_id, false); + emit_proposal_event(self.id().to_inner(), cap.id(), proposal_id, false); option::some(proposal_id) } } @@ -307,7 +338,7 @@ module iota_identity::identity { ); self.updated = clock.timestamp_ms(); - emit_proposal_event(self.id().to_inner(), cap.id().to_inner(), proposal_id, true); + emit_proposal_event(self.id().to_inner(), cap.id(), proposal_id, true); } /// Proposes to update this `Identity`'s AC. @@ -341,7 +372,7 @@ module iota_identity::identity { self.execute_config_change(cap, proposal_id, ctx); option::none() } else { - emit_proposal_event(self.id().to_inner(), cap.id().to_inner(), proposal_id, false); + emit_proposal_event(self.id().to_inner(), cap.id(), proposal_id, false); option::some(proposal_id) } } @@ -359,7 +390,7 @@ module iota_identity::identity { proposal_id, ctx, ); - emit_proposal_event(self.id().to_inner(), cap.id().to_inner(), proposal_id, true); + emit_proposal_event(self.id().to_inner(), cap.id(), proposal_id, true); } /// Proposes the transfer of a set of objects owned by this `Identity`. @@ -370,7 +401,7 @@ module iota_identity::identity { objects: vector, recipients: vector
, ctx: &mut TxContext, - ) { + ): ID { let proposal_id = transfer_proposal::propose_send( &mut self.did_doc, cap, @@ -379,7 +410,8 @@ module iota_identity::identity { recipients, ctx ); - emit_proposal_event(self.id().to_inner(), cap.id().to_inner(), proposal_id, false); + emit_proposal_event(self.id().to_inner(), cap.id(), proposal_id, false); + proposal_id } /// Sends one object among the one specified in a `Send` proposal. @@ -399,7 +431,7 @@ module iota_identity::identity { expiration: Option, objects: vector, ctx: &mut TxContext, - ) { + ): ID { let identity_address = self.id().to_address(); let proposal_id = borrow_proposal::propose_borrow( &mut self.did_doc, @@ -409,7 +441,8 @@ module iota_identity::identity { identity_address, ctx, ); - emit_proposal_event(self.id().to_inner(), cap.id().to_inner(), proposal_id, false); + emit_proposal_event(self.id().to_inner(), cap.id(), proposal_id, false); + proposal_id } /// Takes one of the borrowed assets. @@ -444,7 +477,7 @@ module iota_identity::identity { proposal_id: ID, ctx: &mut TxContext, ): Action { - emit_proposal_event(self.id().to_inner(), cap.id().to_inner(), proposal_id, true); + emit_proposal_event(self.id().to_inner(), cap.id(), proposal_id, true); self.did_doc.execute_proposal(cap, proposal_id, ctx) } diff --git a/identity_iota_core/packages/iota_identity/sources/multicontroller.move b/identity_iota_core/packages/iota_identity/sources/multicontroller.move index 3a49b5687..da88fd270 100644 --- a/identity_iota_core/packages/iota_identity/sources/multicontroller.move +++ b/identity_iota_core/packages/iota_identity/sources/multicontroller.move @@ -293,7 +293,7 @@ module iota_identity::multicontroller { /// Destroys a `ControllerCap`. Can only be used after a controller has been removed from /// the controller committee. public fun destroy_controller_cap(self: &Multicontroller, cap: ControllerCap) { - assert!(self.controllers.contains(&cap.id().to_inner()), EInvalidController); + assert!(!self.controllers.contains(&cap.id().to_inner()), EInvalidController); cap.delete(); } diff --git a/identity_iota_core/packages/iota_identity/sources/proposals/borrow.move b/identity_iota_core/packages/iota_identity/sources/proposals/borrow.move index f2204c741..ac82cc9a7 100644 --- a/identity_iota_core/packages/iota_identity/sources/proposals/borrow.move +++ b/identity_iota_core/packages/iota_identity/sources/proposals/borrow.move @@ -28,7 +28,7 @@ module iota_identity::borrow_proposal { ): ID { let action = Borrow { objects, objects_to_return: vector::empty(), owner }; - multi.create_proposal(cap, action,expiration, ctx) + multi.create_proposal(cap, action, expiration, ctx) } /// Borrows an asset from this action. This function will fail if: diff --git a/identity_iota_core/packages/iota_identity/sources/proposals/controller.move b/identity_iota_core/packages/iota_identity/sources/proposals/controller.move new file mode 100644 index 000000000..f83b52d3f --- /dev/null +++ b/identity_iota_core/packages/iota_identity/sources/proposals/controller.move @@ -0,0 +1,56 @@ +module iota_identity::controller_proposal { + use iota::transfer::Receiving; + use iota_identity::controller::{Self, ControllerCap}; + use iota_identity::multicontroller::Action; + + /// The received `ControllerCap` does not match the one + /// specified in the `ControllerExecution` action. + const EControllerCapMismatch: u64 = 0; + /// The provided `UID` is not the `UID` of the `Identity` + /// specified in the action. + const EInvalidIdentityUID: u64 = 1; + + /// Borrow a given `ControllerCap` from an `Identity` for + /// a single transaction. + public struct ControllerExecution has store { + /// ID of the `ControllerCap` to borrow. + controller_cap: ID, + /// The address of the `Identity` that owns + /// the `ControllerCap` we are borrowing. + identity: address, + } + + /// Returns a new `ControllerExecution` that - in a Proposal - allows whoever + /// executes it to receive `identity`'s `ControllerCap` (the one that has ID `controller_cap`) + /// for the duration of a single transaction. + public fun new(controller_cap: ID, identity: address): ControllerExecution { + ControllerExecution { + controller_cap, + identity, + } + } + + /// Returns the `ControllerCap` specified in this action. + public fun receive( + self: &mut Action, + identity: &mut UID, + cap: Receiving + ): ControllerCap { + assert!(identity.to_address() == self.borrow().identity, EInvalidIdentityUID); + assert!(cap.receiving_object_id() == self.borrow().controller_cap, EControllerCapMismatch); + + controller::receive(identity, cap) + } + + /// Consumes a `ControllerExecution` action by returning the borrowed `ControllerCap` + /// to the corresponding `Identity`. + public fun put_back( + action: Action, + cap: ControllerCap, + ) { + let ControllerExecution { identity, controller_cap } = action.unwrap(); + assert!(object::id(&cap) == controller_cap, EControllerCapMismatch); + + cap.transfer(identity); + } +} \ No newline at end of file diff --git a/identity_iota_core/src/rebased/client/read_only.rs b/identity_iota_core/src/rebased/client/read_only.rs index 96cb6e800..cf155d7dd 100644 --- a/identity_iota_core/src/rebased/client/read_only.rs +++ b/identity_iota_core/src/rebased/client/read_only.rs @@ -118,8 +118,8 @@ impl IdentityClientReadOnly { .map_err(|e| Error::ObjectLookup(e.to_string())) } - #[allow(dead_code)] - pub(crate) async fn get_object_ref_by_id(&self, obj: ObjectID) -> Result, Error> { + /// Returns an object's [`OwnedObjectRef`], if any. + pub async fn get_object_ref_by_id(&self, obj: ObjectID) -> Result, Error> { self .read_api() .get_object_with_options(obj, IotaObjectDataOptions::default().with_owner()) diff --git a/identity_iota_core/src/rebased/migration/identity.rs b/identity_iota_core/src/rebased/migration/identity.rs index 3b7069e61..3d6d963e0 100644 --- a/identity_iota_core/src/rebased/migration/identity.rs +++ b/identity_iota_core/src/rebased/migration/identity.rs @@ -41,6 +41,7 @@ use crate::rebased::client::IdentityClientReadOnly; use crate::rebased::client::IotaKeySignature; use crate::rebased::proposals::BorrowAction; use crate::rebased::proposals::ConfigChange; +use crate::rebased::proposals::ControllerExecution; use crate::rebased::proposals::DeactivateDid; use crate::rebased::proposals::ProposalBuilder; use crate::rebased::proposals::SendAction; @@ -182,13 +183,18 @@ impl OnChainIdentity { } /// Borrows assets owned by this [`OnChainIdentity`] to use them in a custom transaction. - /// # Notes - /// Make sure to call [`super::Proposal::with_intent`] before executing the proposal. - /// Failing to do so will make [`crate::proposals::ProposalT::execute`] return an error. pub fn borrow_assets(&mut self) -> ProposalBuilder<'_, BorrowAction> { ProposalBuilder::new(self, BorrowAction::default()) } + /// Borrows a `ControllerCap` with ID `controller_cap` owned by this identity in a transaction. + /// This proposal is used to perform operation on a sub-identity controlled + /// by this one. + pub fn controller_execution(&mut self, controller_cap: ObjectID) -> ProposalBuilder<'_, ControllerExecution> { + let action = ControllerExecution::new(controller_cap, self); + ProposalBuilder::new(self, action) + } + /// Returns historical data for this [`OnChainIdentity`]. pub async fn get_history( &self, @@ -409,7 +415,7 @@ pub(crate) fn unpack_identity_data( did_doc: multi_controller, created, updated, - version + version, } = serde_json::from_value::(value.fields.to_json_value()) .map_err(|err| Error::ObjectLookup(format!("could not parse identity document with DID {did}; {err}")))?; diff --git a/identity_iota_core/src/rebased/proposals/borrow.rs b/identity_iota_core/src/rebased/proposals/borrow.rs index d14292e2b..471e65a39 100644 --- a/identity_iota_core/src/rebased/proposals/borrow.rs +++ b/identity_iota_core/src/rebased/proposals/borrow.rs @@ -29,6 +29,7 @@ use super::ExecuteProposalTx; use super::OnChainIdentity; use super::ProposalBuilder; use super::ProposalT; +use super::UserDrivenTx; pub(crate) type IntentFn = Box) + Send>; @@ -128,17 +129,17 @@ impl ProposalT for Proposal { self, identity: &'i mut OnChainIdentity, _: &IdentityClient, - ) -> Result, Error> + ) -> Result, Error> where S: Signer + Sync, { let proposal_id = self.id(); let borrow_action = self.into_action(); - Ok(ExecuteBorrowTx { + Ok(UserDrivenTx { identity, proposal_id, - borrow_action, + action: borrow_action, }) } @@ -147,37 +148,31 @@ impl ProposalT for Proposal { } } -pub struct ExecuteBorrowTx<'i, B> { - identity: &'i mut OnChainIdentity, - borrow_action: B, - proposal_id: ObjectID, -} - -impl<'i> ExecuteBorrowTx<'i, BorrowAction> { +impl<'i> UserDrivenTx<'i, BorrowAction> { /// Defines how the borrowed assets should be used. - pub fn with_intent(self, intent_fn: F) -> ExecuteBorrowTx<'i, BorrowActionWithIntent> + pub fn with_intent(self, intent_fn: F) -> UserDrivenTx<'i, BorrowActionWithIntent> where F: FnOnce(&mut Ptb, &HashMap), { - let ExecuteBorrowTx { + let UserDrivenTx { identity, - borrow_action, + action, proposal_id, } = self; - ExecuteBorrowTx { + UserDrivenTx { identity, proposal_id, - borrow_action: BorrowActionWithIntent { - action: borrow_action, + action: BorrowActionWithIntent { + action, intent_fn, }, } } } -impl<'i> ProtoTransaction for ExecuteBorrowTx<'i, BorrowAction> { +impl<'i> ProtoTransaction for UserDrivenTx<'i, BorrowAction> { type Input = IntentFn; - type Tx = ExecuteBorrowTx<'i, BorrowActionWithIntent>; + type Tx = UserDrivenTx<'i, BorrowActionWithIntent>; fn with(self, input: Self::Input) -> Self::Tx { self.with_intent(input) @@ -185,7 +180,7 @@ impl<'i> ProtoTransaction for ExecuteBorrowTx<'i, BorrowAction> { } #[async_trait] -impl<'i, F> Transaction for ExecuteBorrowTx<'i, BorrowActionWithIntent> +impl<'i, F> Transaction for UserDrivenTx<'i, BorrowActionWithIntent> where F: FnOnce(&mut Ptb, &HashMap) + Send, { @@ -200,7 +195,7 @@ where { let Self { identity, - borrow_action, + action: borrow_action, proposal_id, } = self; let identity_ref = client diff --git a/identity_iota_core/src/rebased/proposals/controller.rs b/identity_iota_core/src/rebased/proposals/controller.rs new file mode 100644 index 000000000..ec71fc0ec --- /dev/null +++ b/identity_iota_core/src/rebased/proposals/controller.rs @@ -0,0 +1,219 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::marker::PhantomData; + +use crate::rebased::client::IdentityClient; +use crate::rebased::client::IotaKeySignature; +use crate::rebased::migration::Proposal; +use crate::rebased::sui::move_calls; +use crate::rebased::transaction::ProtoTransaction; +use crate::rebased::transaction::Transaction; +use crate::rebased::transaction::TransactionOutput; +use crate::rebased::utils::MoveType; +use crate::rebased::Error; +use async_trait::async_trait; +use iota_sdk::rpc_types::IotaObjectRef; +use iota_sdk::rpc_types::IotaTransactionBlockResponse; +use iota_sdk::rpc_types::OwnedObjectRef; +use iota_sdk::types::base_types::IotaAddress; +use iota_sdk::types::base_types::ObjectID; +use iota_sdk::types::programmable_transaction_builder::ProgrammableTransactionBuilder as Ptb; +use iota_sdk::types::transaction::Argument; +use iota_sdk::types::TypeTag; +use secret_storage::Signer; +use serde::Deserialize; +use serde::Serialize; + +use super::CreateProposalTx; +use super::ExecuteProposalTx; +use super::OnChainIdentity; +use super::ProposalT; +use super::UserDrivenTx; + +pub(crate) type IntentFn = Box; + +/// Borrow an [`OnChainIdentity`]'s controller capability to exert control on +/// a sub-owned identity. +#[derive(Debug, Deserialize, Serialize)] +pub struct ControllerExecution { + controller_cap: ObjectID, + identity: IotaAddress, +} + +/// A [`ControllerExecution`] action coupled with a user-provided function to describe how +/// the borrowed identity's controller capability will be used. +pub struct ControllerExecutionWithIntent +where + F: FnOnce(&mut Ptb, &Argument), +{ + action: ControllerExecution, + intent_fn: F, +} + +impl ControllerExecution { + /// Creates a new [`ControllerExecution`] action, allowing a controller of `identity` to + /// borrow `identity`'s controller cap for a transaction. + pub fn new(controller_cap: ObjectID, identity: &OnChainIdentity) -> Self { + Self { + controller_cap, + identity: identity.id().into(), + } + } +} + +impl MoveType for ControllerExecution { + fn move_type(package: ObjectID) -> TypeTag { + use std::str::FromStr; + + TypeTag::from_str(&format!("{package}::controller_proposal::ControllerExecution")).expect("valid move type") + } +} + +#[async_trait] +impl ProposalT for Proposal { + type Action = ControllerExecution; + type Output = (); + + async fn create<'i, S>( + action: Self::Action, + expiration: Option, + identity: &'i mut OnChainIdentity, + client: &IdentityClient, + ) -> Result, Error> + where + S: Signer + Sync, + { + let identity_ref = client + .get_object_ref_by_id(identity.id()) + .await? + .expect("identity exists on-chain"); + let controller_cap_ref = identity.get_controller_cap(client).await?; + + let tx = move_calls::identity::propose_controller_execution( + identity_ref, + controller_cap_ref, + action.controller_cap, + expiration, + client.package_id(), + ) + .map_err(|e| Error::TransactionBuildingFailed(e.to_string()))?; + + Ok(CreateProposalTx { + identity, + tx, + // Borrow proposals cannot be chain-executed as they have to be driven. + chained_execution: false, + _action: PhantomData, + }) + } + + async fn into_tx<'i, S>( + self, + identity: &'i mut OnChainIdentity, + _: &IdentityClient, + ) -> Result, Error> + where + S: Signer + Sync, + { + let proposal_id = self.id(); + let controller_execution_action = self.into_action(); + + Ok(UserDrivenTx { + identity, + proposal_id, + action: controller_execution_action, + }) + } + + fn parse_tx_effects(_tx_response: &IotaTransactionBlockResponse) -> Result { + Ok(()) + } +} + +impl<'i> UserDrivenTx<'i, ControllerExecution> { + /// Defines how the borrowed assets should be used. + pub fn with_intent(self, intent_fn: F) -> UserDrivenTx<'i, ControllerExecutionWithIntent> + where + F: FnOnce(&mut Ptb, &Argument), + { + let UserDrivenTx { + identity, + action, + proposal_id, + } = self; + UserDrivenTx { + identity, + proposal_id, + action: ControllerExecutionWithIntent { action, intent_fn }, + } + } +} + +impl<'i> ProtoTransaction for UserDrivenTx<'i, ControllerExecution> { + type Input = IntentFn; + type Tx = UserDrivenTx<'i, ControllerExecutionWithIntent>; + + fn with(self, input: Self::Input) -> Self::Tx { + self.with_intent(input) + } +} + +#[async_trait] +impl<'i, F> Transaction for UserDrivenTx<'i, ControllerExecutionWithIntent> +where + F: FnOnce(&mut Ptb, &Argument) + Send, +{ + type Output = (); + async fn execute_with_opt_gas( + self, + gas_budget: Option, + client: &IdentityClient, + ) -> Result, Error> + where + S: Signer + Sync, + { + let Self { + identity, + action, + proposal_id, + } = self; + let identity_ref = client + .get_object_ref_by_id(identity.id()) + .await? + .expect("identity exists on-chain"); + let controller_cap_ref = identity.get_controller_cap(client).await?; + + let borrowing_cap_id = action.action.controller_cap; + let borrowing_controller_cap_ref = client + .get_object_ref_by_id(borrowing_cap_id) + .await? + .map(|OwnedObjectRef { reference, .. }| { + let IotaObjectRef { + object_id, + version, + digest, + } = reference; + (object_id, version, digest) + }) + .ok_or_else(|| Error::ObjectLookup(format!("object {borrowing_cap_id} doesn't exist")))?; + + let tx = move_calls::identity::execute_controller_execution( + identity_ref, + controller_cap_ref, + proposal_id, + borrowing_controller_cap_ref, + action.intent_fn, + client.package_id(), + ) + .map_err(|e| Error::TransactionBuildingFailed(e.to_string()))?; + + ExecuteProposalTx { + identity, + tx, + _action: PhantomData::, + } + .execute_with_opt_gas(gas_budget, client) + .await + } +} diff --git a/identity_iota_core/src/rebased/proposals/mod.rs b/identity_iota_core/src/rebased/proposals/mod.rs index 52058ac0f..d83724a91 100644 --- a/identity_iota_core/src/rebased/proposals/mod.rs +++ b/identity_iota_core/src/rebased/proposals/mod.rs @@ -6,6 +6,7 @@ mod config_change; mod deactivate_did; mod send; mod update_did_doc; +mod controller; mod upgrade; use std::marker::PhantomData; @@ -22,6 +23,7 @@ pub use borrow::*; pub use config_change::*; pub use upgrade::*; pub use deactivate_did::*; +pub use controller::*; use iota_sdk::rpc_types::IotaExecutionStatus; use iota_sdk::rpc_types::IotaObjectData; use iota_sdk::rpc_types::IotaObjectDataOptions; @@ -333,3 +335,10 @@ async fn obj_ref_and_type_for_id( Ok((obj_ref, obj_type)) } + +/// A transaction that requires user input in order to be executed. +pub struct UserDrivenTx<'i, A> { + identity: &'i mut OnChainIdentity, + action: A, + proposal_id: ObjectID, +} \ No newline at end of file diff --git a/identity_iota_core/src/rebased/sui/move_calls/identity/controller_execution.rs b/identity_iota_core/src/rebased/sui/move_calls/identity/controller_execution.rs new file mode 100644 index 000000000..86ee5e3d5 --- /dev/null +++ b/identity_iota_core/src/rebased/sui/move_calls/identity/controller_execution.rs @@ -0,0 +1,95 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use iota_sdk::rpc_types::OwnedObjectRef; +use iota_sdk::types::base_types::ObjectID; +use iota_sdk::types::base_types::ObjectRef; +use iota_sdk::types::programmable_transaction_builder::ProgrammableTransactionBuilder; +use iota_sdk::types::transaction::Argument; +use iota_sdk::types::transaction::ObjectArg; +use iota_sdk::types::transaction::ProgrammableTransaction; +use move_core_types::ident_str; + +use crate::rebased::proposals::ControllerExecution; +use crate::rebased::sui::move_calls::utils; +use crate::rebased::utils::MoveType; + +pub(crate) fn propose_controller_execution( + identity: OwnedObjectRef, + capability: ObjectRef, + controller_cap_id: ObjectID, + expiration: Option, + package_id: ObjectID, +) -> Result { + let mut ptb = ProgrammableTransactionBuilder::new(); + let cap_arg = ptb.obj(ObjectArg::ImmOrOwnedObject(capability))?; + let (delegation_token, borrow) = utils::get_controller_delegation(&mut ptb, cap_arg, package_id); + let identity_arg = utils::owned_ref_to_shared_object_arg(identity, &mut ptb, true)?; + let controller_cap_id = ptb.pure(controller_cap_id)?; + let exp_arg = utils::option_to_move(expiration, &mut ptb, package_id)?; + + let _proposal_id = ptb.programmable_move_call( + package_id, + ident_str!("identity").into(), + ident_str!("propose_controller_execution").into(), + vec![], + vec![identity_arg, delegation_token, controller_cap_id, exp_arg], + ); + + utils::put_back_delegation_token(&mut ptb, cap_arg, delegation_token, borrow, package_id); + + Ok(ptb.finish()) +} + +pub(crate) fn execute_controller_execution( + identity: OwnedObjectRef, + capability: ObjectRef, + proposal_id: ObjectID, + borrowing_controller_cap_ref: ObjectRef, + intent_fn: F, + package: ObjectID, +) -> Result +where + F: FnOnce(&mut ProgrammableTransactionBuilder, &Argument), +{ + let mut ptb = ProgrammableTransactionBuilder::new(); + let identity = utils::owned_ref_to_shared_object_arg(identity, &mut ptb, true)?; + let controller_cap = ptb.obj(ObjectArg::ImmOrOwnedObject(capability))?; + let (delegation_token, borrow) = utils::get_controller_delegation(&mut ptb, controller_cap, package); + let proposal_id = ptb.pure(proposal_id)?; + + // Get the proposal's action as argument. + let controller_execution_action = ptb.programmable_move_call( + package, + ident_str!("identity").into(), + ident_str!("execute_proposal").into(), + vec![ControllerExecution::move_type(package)], + vec![identity, delegation_token, proposal_id], + ); + + utils::put_back_delegation_token(&mut ptb, controller_cap, delegation_token, borrow, package); + + // Borrow the controller cap into this transaction. + let receiving = ptb.obj(ObjectArg::Receiving(borrowing_controller_cap_ref))?; + let borrowed_controller_cap = ptb.programmable_move_call( + package, + ident_str!("identity").into(), + ident_str!("borrow_controller_cap").into(), + vec![], + vec![identity, controller_execution_action, receiving], + ); + + // Apply the user-defined operation. + intent_fn(&mut ptb, &borrowed_controller_cap); + + // Put back the borrowed controller cap. + ptb.programmable_move_call( + package, + ident_str!("controller_proposal").into(), + ident_str!("put_back").into(), + vec![], + vec![controller_execution_action, borrowed_controller_cap], + ); + + Ok(ptb.finish()) +} diff --git a/identity_iota_core/src/rebased/sui/move_calls/identity/mod.rs b/identity_iota_core/src/rebased/sui/move_calls/identity/mod.rs index cb57e14a4..525d8778c 100644 --- a/identity_iota_core/src/rebased/sui/move_calls/identity/mod.rs +++ b/identity_iota_core/src/rebased/sui/move_calls/identity/mod.rs @@ -8,6 +8,7 @@ mod deactivate; pub(crate) mod proposal; mod send_asset; mod update; +mod controller_execution; mod upgrade; pub(crate) use borrow_asset::*; @@ -16,4 +17,5 @@ pub(crate) use create::*; pub(crate) use deactivate::*; pub(crate) use send_asset::*; pub(crate) use update::*; +pub(crate) use controller_execution::*; pub(crate) use upgrade::*; diff --git a/identity_iota_core/tests/e2e/identity.rs b/identity_iota_core/tests/e2e/identity.rs index 7dde485e1..06c9a675b 100644 --- a/identity_iota_core/tests/e2e/identity.rs +++ b/identity_iota_core/tests/e2e/identity.rs @@ -20,7 +20,10 @@ use identity_iota_core::IotaDocument; use identity_verification::MethodScope; use identity_verification::VerificationMethod; use iota_sdk::rpc_types::IotaObjectData; +use iota_sdk::types::base_types::ObjectID; use iota_sdk::types::base_types::SequenceNumber; +use iota_sdk::types::object::Owner; +use iota_sdk::types::transaction::ObjectArg; use iota_sdk::types::TypeTag; use iota_sdk::types::IOTA_FRAMEWORK_PACKAGE_ID; use move_core_types::ident_str; @@ -361,3 +364,82 @@ async fn borrow_proposal_works() -> anyhow::Result<()> { Ok(()) } + +#[tokio::test] +async fn controller_execution_works() -> anyhow::Result<()> { + let test_client = get_test_client().await?; + let identity_client = test_client.new_user_client().await?; + + let mut identity = identity_client + .create_identity(TEST_DOC) + .finish() + .execute(&identity_client) + .await? + .output; + let identity_address = identity.id().into(); + + // Create a second identity owned by the first. + let identity2 = identity_client + .create_identity(TEST_DOC) + .controller(identity_address, 1) + .threshold(1) + .finish() + .execute(&identity_client) + .await? + .output; + + // Let's find identity's controller cap for identity2. + let controller_cap = identity_client + .find_owned_ref_for_address( + identity_address, + format!("{}::controller::ControllerCap", identity_client.package_id()).parse()?, + |_| true, + ) + .await? + .expect("identity is a controller of identity2"); + + // Perform an action on `identity2` as a controller of `identity`. + let ProposalResult::Pending(controller_execution) = identity + .controller_execution(controller_cap.0) + .finish(&identity_client) + .await? + .execute(&identity_client) + .await? + .output + else { + panic!("controller execution proposals cannot be executed without being driven by the user") + }; + let identity2_ref = identity_client.get_object_ref_by_id(identity2.id()).await?.unwrap(); + let Owner::Shared { initial_shared_version } = identity2_ref.owner else { + panic!("identity2 is shared") + }; + let tx_output = controller_execution + .into_tx(&mut identity, &identity_client) + .await? + // specify the operation to perform with the borrowed identity's controller_cap + .with_intent(|ptb, controller_cap| { + let identity2 = ptb + .obj(ObjectArg::SharedObject { + id: identity2_ref.object_id(), + initial_shared_version, + mutable: true, + }) + .unwrap(); + + let token_to_revoke = ptb.pure(ObjectID::ZERO).unwrap(); + + ptb.programmable_move_call( + identity_client.package_id(), + ident_str!("identity").into(), + ident_str!("revoke_token").into(), + vec![], + vec![identity2, *controller_cap, token_to_revoke], + ); + }) + .execute(&identity_client) + .await?; + + assert!(tx_output.response.status_ok().unwrap()); + + Ok(()) +}