From 11b487ead8536c7896b634c55acb7463d1362b66 Mon Sep 17 00:00:00 2001 From: umr1352 Date: Mon, 9 Dec 2024 15:52:28 +0100 Subject: [PATCH 1/4] send proposals are chain-executed if controller has enough voting power --- .../iota/move_calls/identity/send_asset.rs | 106 +++++++++++++++--- .../src/rebased/proposals/send.rs | 50 ++++++--- 2 files changed, 123 insertions(+), 33 deletions(-) diff --git a/identity_iota_core/src/rebased/iota/move_calls/identity/send_asset.rs b/identity_iota_core/src/rebased/iota/move_calls/identity/send_asset.rs index e51c9d1bc..7178b4bb0 100644 --- a/identity_iota_core/src/rebased/iota/move_calls/identity/send_asset.rs +++ b/identity_iota_core/src/rebased/iota/move_calls/identity/send_asset.rs @@ -6,6 +6,7 @@ use iota_sdk::types::base_types::IotaAddress; 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 iota_sdk::types::TypeTag; @@ -17,13 +18,22 @@ use crate::rebased::utils::MoveType; use self::move_calls::utils; -pub(crate) fn propose_send( +struct SendProposalContext { + ptb: ProgrammableTransactionBuilder, + controller_cap: Argument, + delegation_token: Argument, + borrow: Argument, + identity: Argument, + proposal_id: Argument, +} + +fn send_proposal_impl( identity: OwnedObjectRef, capability: ObjectRef, transfer_map: Vec<(ObjectID, IotaAddress)>, expiration: Option, package_id: ObjectID, -) -> Result { +) -> anyhow::Result { let mut ptb = ProgrammableTransactionBuilder::new(); let cap_arg = ptb.obj(ObjectArg::ImmOrOwnedObject(capability))?; let (delegation_token, borrow) = move_calls::utils::get_controller_delegation(&mut ptb, cap_arg, package_id); @@ -37,7 +47,7 @@ pub(crate) fn propose_send( (objects, recipients) }; - let _proposal_id = ptb.programmable_move_call( + let proposal_id = ptb.programmable_move_call( package_id, ident_str!("identity").into(), ident_str!("propose_send").into(), @@ -45,24 +55,44 @@ pub(crate) fn propose_send( vec![identity_arg, delegation_token, exp_arg, objects, recipients], ); - utils::put_back_delegation_token(&mut ptb, cap_arg, delegation_token, borrow, package_id); - - Ok(ptb.finish()) + Ok(SendProposalContext { + ptb, + identity: identity_arg, + controller_cap: cap_arg, + delegation_token, + borrow, + proposal_id, + }) } -pub(crate) fn execute_send( +pub(crate) fn propose_send( identity: OwnedObjectRef, capability: ObjectRef, - proposal_id: ObjectID, - objects: Vec<(ObjectRef, TypeTag)>, - package: ObjectID, + transfer_map: Vec<(ObjectID, IotaAddress)>, + expiration: Option, + package_id: ObjectID, ) -> Result { - let mut ptb = ProgrammableTransactionBuilder::new(); - let identity = move_calls::utils::owned_ref_to_shared_object_arg(identity, &mut ptb, true)?; - let controller_cap = ptb.obj(ObjectArg::ImmOrOwnedObject(capability))?; - let (delegation_token, borrow) = move_calls::utils::get_controller_delegation(&mut ptb, controller_cap, package); - let proposal_id = ptb.pure(proposal_id)?; + let SendProposalContext { + mut ptb, + controller_cap, + delegation_token, + borrow, + .. + } = send_proposal_impl(identity, capability, transfer_map, expiration, package_id)?; + + utils::put_back_delegation_token(&mut ptb, controller_cap, delegation_token, borrow, package_id); + + Ok(ptb.finish()) +} +fn execute_send_impl( + ptb: &mut ProgrammableTransactionBuilder, + identity: Argument, + delegation_token: Argument, + proposal_id: Argument, + objects: Vec<(ObjectRef, TypeTag)>, + package: ObjectID, +) -> anyhow::Result<()> { // Get the proposal's action as argument. let send_action = ptb.programmable_move_call( package, @@ -72,8 +102,6 @@ pub(crate) fn execute_send( vec![identity, delegation_token, proposal_id], ); - utils::put_back_delegation_token(&mut ptb, controller_cap, delegation_token, borrow, package); - // Send each object in this send action. // Traversing the map in reverse reduces the number of operations on the move side. for (obj, obj_type) in objects.into_iter().rev() { @@ -97,5 +125,49 @@ pub(crate) fn execute_send( vec![send_action], ); + Ok(()) +} + +pub(crate) fn execute_send( + identity: OwnedObjectRef, + capability: ObjectRef, + proposal_id: ObjectID, + objects: Vec<(ObjectRef, TypeTag)>, + package: ObjectID, +) -> Result { + let mut ptb = ProgrammableTransactionBuilder::new(); + let identity = move_calls::utils::owned_ref_to_shared_object_arg(identity, &mut ptb, true)?; + let controller_cap = ptb.obj(ObjectArg::ImmOrOwnedObject(capability))?; + let (delegation_token, borrow) = move_calls::utils::get_controller_delegation(&mut ptb, controller_cap, package); + let proposal_id = ptb.pure(proposal_id)?; + + execute_send_impl(&mut ptb, identity, delegation_token, proposal_id, objects, package)?; + + utils::put_back_delegation_token(&mut ptb, controller_cap, delegation_token, borrow, package); + + Ok(ptb.finish()) +} + +pub(crate) fn create_and_execute_send( + identity: OwnedObjectRef, + capability: ObjectRef, + transfer_map: Vec<(ObjectID, IotaAddress)>, + expiration: Option, + objects: Vec<(ObjectRef, TypeTag)>, + package: ObjectID, +) -> anyhow::Result { + let SendProposalContext { + mut ptb, + identity, + controller_cap, + delegation_token, + borrow, + proposal_id, + } = send_proposal_impl(identity, capability, transfer_map, expiration, package)?; + + execute_send_impl(&mut ptb, identity, delegation_token, proposal_id, objects, package)?; + + utils::put_back_delegation_token(&mut ptb, controller_cap, delegation_token, borrow, package); + Ok(ptb.finish()) } diff --git a/identity_iota_core/src/rebased/proposals/send.rs b/identity_iota_core/src/rebased/proposals/send.rs index fa6dbf2db..b70bcdcf6 100644 --- a/identity_iota_core/src/rebased/proposals/send.rs +++ b/identity_iota_core/src/rebased/proposals/send.rs @@ -2,7 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 use std::marker::PhantomData; -use std::ops::Deref; use async_trait::async_trait; use iota_sdk::rpc_types::IotaTransactionBlockResponse; @@ -39,13 +38,6 @@ impl MoveType for SendAction { } } -impl Deref for SendAction { - type Target = [(ObjectID, IotaAddress)]; - fn deref(&self) -> &Self::Target { - &self.0 - } -} - impl SendAction { /// Adds to the list of object to send the object with ID `object_id` and send it to address `recipient`. pub fn send_object(&mut self, object_id: ObjectID, recipient: IotaAddress) { @@ -99,15 +91,41 @@ impl ProposalT for Proposal { .await? .expect("identity exists on-chain"); let controller_cap_ref = identity.get_controller_cap(client).await?; - let tx = move_calls::identity::propose_send( - identity_ref, - controller_cap_ref, - action.0, - expiration, - client.package_id(), - ) + let can_execute = identity + .controller_voting_power(controller_cap_ref.0) + .expect("controller_cap is for this identity") + > identity.threshold(); + let tx = if can_execute { + // Construct a list of `(ObjectRef, TypeTag)` from the list of objects to send. + let object_type_list = { + let ids = action.0.iter().map(|(obj_id, _rcp)| obj_id); + let mut object_and_type_list = vec![]; + for obj_id in ids { + let ref_and_type = super::obj_ref_and_type_for_id(client, *obj_id) + .await + .map_err(|e| Error::ObjectLookup(e.to_string()))?; + object_and_type_list.push(ref_and_type); + } + object_and_type_list + }; + move_calls::identity::create_and_execute_send( + identity_ref, + controller_cap_ref, + action.0, + expiration, + object_type_list, + client.package_id(), + ) + } else { + move_calls::identity::propose_send( + identity_ref, + controller_cap_ref, + action.0, + expiration, + client.package_id(), + ) + } .map_err(|e| Error::TransactionBuildingFailed(e.to_string()))?; - Ok(CreateProposalTx { identity, tx, From 34d68c07d195719cbe23d46b4a4340bed3699b40 Mon Sep 17 00:00:00 2001 From: umr1352 Date: Tue, 10 Dec 2024 12:14:43 +0100 Subject: [PATCH 2/4] fix tests --- .../iota/move_calls/identity/borrow_asset.rs | 130 +++++++++++++++--- .../rebased/iota/move_calls/identity/mod.rs | 10 ++ .../iota/move_calls/identity/send_asset.rs | 18 +-- .../src/rebased/proposals/borrow.rs | 120 ++++++++++++---- .../src/rebased/proposals/mod.rs | 11 +- .../src/rebased/proposals/send.rs | 5 +- identity_iota_core/tests/e2e/identity.rs | 35 ++--- 7 files changed, 238 insertions(+), 91 deletions(-) diff --git a/identity_iota_core/src/rebased/iota/move_calls/identity/borrow_asset.rs b/identity_iota_core/src/rebased/iota/move_calls/identity/borrow_asset.rs index 1cd3830a1..667e8de61 100644 --- a/identity_iota_core/src/rebased/iota/move_calls/identity/borrow_asset.rs +++ b/identity_iota_core/src/rebased/iota/move_calls/identity/borrow_asset.rs @@ -12,19 +12,22 @@ use iota_sdk::types::programmable_transaction_builder::ProgrammableTransactionBu use iota_sdk::types::transaction::Argument; use iota_sdk::types::transaction::ObjectArg; use iota_sdk::types::transaction::ProgrammableTransaction; +use itertools::Itertools; use move_core_types::ident_str; use crate::rebased::iota::move_calls::utils; use crate::rebased::proposals::BorrowAction; use crate::rebased::utils::MoveType; -pub(crate) fn propose_borrow( +use super::ProposalContext; + +fn borrow_proposal_impl( identity: OwnedObjectRef, capability: ObjectRef, objects: Vec, expiration: Option, package_id: ObjectID, -) -> Result { +) -> anyhow::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); @@ -32,7 +35,7 @@ pub(crate) fn propose_borrow( let exp_arg = utils::option_to_move(expiration, &mut ptb, package_id)?; let objects_arg = ptb.pure(objects)?; - let _proposal_id = ptb.programmable_move_call( + let proposal_id = ptb.programmable_move_call( package_id, ident_str!("identity").into(), ident_str!("propose_borrow").into(), @@ -40,28 +43,48 @@ pub(crate) fn propose_borrow( vec![identity_arg, delegation_token, exp_arg, objects_arg], ); - utils::put_back_delegation_token(&mut ptb, cap_arg, delegation_token, borrow, package_id); - - Ok(ptb.finish()) + Ok(ProposalContext { + ptb, + identity: identity_arg, + controller_cap: cap_arg, + delegation_token, + borrow, + proposal_id, + }) } -pub(crate) fn execute_borrow( +pub(crate) fn propose_borrow( identity: OwnedObjectRef, capability: ObjectRef, - proposal_id: ObjectID, + objects: Vec, + expiration: Option, + package_id: ObjectID, +) -> Result { + let ProposalContext { + mut ptb, + controller_cap, + delegation_token, + borrow, + .. + } = borrow_proposal_impl(identity, capability, objects, expiration, package_id)?; + + utils::put_back_delegation_token(&mut ptb, controller_cap, delegation_token, borrow, package_id); + + Ok(ptb.finish()) +} + +fn execute_borrow_impl( + ptb: &mut ProgrammableTransactionBuilder, + identity: Argument, + delegation_token: Argument, + proposal_id: Argument, objects: Vec, intent_fn: F, package: ObjectID, -) -> Result +) -> anyhow::Result<()> where F: FnOnce(&mut ProgrammableTransactionBuilder, &HashMap), { - 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 borrow_action = ptb.programmable_move_call( package, @@ -71,8 +94,6 @@ where vec![identity, delegation_token, proposal_id], ); - utils::put_back_delegation_token(&mut ptb, controller_cap, delegation_token, borrow, package); - // Borrow all the objects specified in the action. let obj_arg_map = objects .into_iter() @@ -96,7 +117,7 @@ where .collect::>()?; // Apply the user-defined operation. - intent_fn(&mut ptb, &obj_arg_map); + intent_fn(ptb, &obj_arg_map); // Put back all the objects. obj_arg_map.into_values().for_each(|(obj_arg, obj_data)| { @@ -121,5 +142,78 @@ where vec![borrow_action], ); + Ok(()) +} + +pub(crate) fn execute_borrow( + identity: OwnedObjectRef, + capability: ObjectRef, + proposal_id: ObjectID, + objects: Vec, + intent_fn: F, + package: ObjectID, +) -> Result +where + F: FnOnce(&mut ProgrammableTransactionBuilder, &HashMap), +{ + 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)?; + + execute_borrow_impl( + &mut ptb, + identity, + delegation_token, + proposal_id, + objects, + intent_fn, + package, + )?; + + utils::put_back_delegation_token(&mut ptb, controller_cap, delegation_token, borrow, package); + + Ok(ptb.finish()) +} + +pub(crate) fn create_and_execute_borrow( + identity: OwnedObjectRef, + capability: ObjectRef, + objects: Vec, + intent_fn: F, + expiration: Option, + package_id: ObjectID, +) -> anyhow::Result +where + F: FnOnce(&mut ProgrammableTransactionBuilder, &HashMap), +{ + let ProposalContext { + mut ptb, + controller_cap, + delegation_token, + borrow, + identity, + proposal_id, + } = borrow_proposal_impl( + identity, + capability, + objects.iter().map(|obj_data| obj_data.object_id).collect_vec(), + expiration, + package_id, + )?; + + execute_borrow_impl( + &mut ptb, + identity, + delegation_token, + proposal_id, + objects, + intent_fn, + package_id, + )?; + + utils::put_back_delegation_token(&mut ptb, controller_cap, delegation_token, borrow, package_id); + Ok(ptb.finish()) } diff --git a/identity_iota_core/src/rebased/iota/move_calls/identity/mod.rs b/identity_iota_core/src/rebased/iota/move_calls/identity/mod.rs index 3dac0cea1..f71b319e8 100644 --- a/identity_iota_core/src/rebased/iota/move_calls/identity/mod.rs +++ b/identity_iota_core/src/rebased/iota/move_calls/identity/mod.rs @@ -16,6 +16,16 @@ pub(crate) use config::*; pub(crate) use controller_execution::*; pub(crate) use create::*; pub(crate) use deactivate::*; +use iota_sdk::types::{programmable_transaction_builder::ProgrammableTransactionBuilder as Ptb, transaction::Argument}; pub(crate) use send_asset::*; pub(crate) use update::*; pub(crate) use upgrade::*; + +struct ProposalContext { + ptb: Ptb, + controller_cap: Argument, + delegation_token: Argument, + borrow: Argument, + identity: Argument, + proposal_id: Argument, +} diff --git a/identity_iota_core/src/rebased/iota/move_calls/identity/send_asset.rs b/identity_iota_core/src/rebased/iota/move_calls/identity/send_asset.rs index 7178b4bb0..e7b531168 100644 --- a/identity_iota_core/src/rebased/iota/move_calls/identity/send_asset.rs +++ b/identity_iota_core/src/rebased/iota/move_calls/identity/send_asset.rs @@ -17,15 +17,7 @@ use crate::rebased::proposals::SendAction; use crate::rebased::utils::MoveType; use self::move_calls::utils; - -struct SendProposalContext { - ptb: ProgrammableTransactionBuilder, - controller_cap: Argument, - delegation_token: Argument, - borrow: Argument, - identity: Argument, - proposal_id: Argument, -} +use super::ProposalContext; fn send_proposal_impl( identity: OwnedObjectRef, @@ -33,7 +25,7 @@ fn send_proposal_impl( transfer_map: Vec<(ObjectID, IotaAddress)>, expiration: Option, package_id: ObjectID, -) -> anyhow::Result { +) -> anyhow::Result { let mut ptb = ProgrammableTransactionBuilder::new(); let cap_arg = ptb.obj(ObjectArg::ImmOrOwnedObject(capability))?; let (delegation_token, borrow) = move_calls::utils::get_controller_delegation(&mut ptb, cap_arg, package_id); @@ -55,7 +47,7 @@ fn send_proposal_impl( vec![identity_arg, delegation_token, exp_arg, objects, recipients], ); - Ok(SendProposalContext { + Ok(ProposalContext { ptb, identity: identity_arg, controller_cap: cap_arg, @@ -72,7 +64,7 @@ pub(crate) fn propose_send( expiration: Option, package_id: ObjectID, ) -> Result { - let SendProposalContext { + let ProposalContext { mut ptb, controller_cap, delegation_token, @@ -156,7 +148,7 @@ pub(crate) fn create_and_execute_send( objects: Vec<(ObjectRef, TypeTag)>, package: ObjectID, ) -> anyhow::Result { - let SendProposalContext { + let ProposalContext { mut ptb, identity, controller_cap, diff --git a/identity_iota_core/src/rebased/proposals/borrow.rs b/identity_iota_core/src/rebased/proposals/borrow.rs index a4348b4f5..38c96d516 100644 --- a/identity_iota_core/src/rebased/proposals/borrow.rs +++ b/identity_iota_core/src/rebased/proposals/borrow.rs @@ -34,20 +34,28 @@ use super::UserDrivenTx; pub(crate) type IntentFn = Box) + Send>; /// Action used to borrow in transaction [OnChainIdentity]'s assets. -#[derive(Default, Deserialize, Serialize)] -pub struct BorrowAction { +#[derive(Deserialize, Serialize)] +#[serde(bound(deserialize = ""))] +pub struct BorrowAction { objects: Vec, + #[serde(skip)] + intent_fn: Option, +} + +impl Default for BorrowAction { + fn default() -> Self { + Self { + objects: vec![], + intent_fn: None, + } + } } /// A [`BorrowAction`] coupled with a user-provided function to describe how /// the borrowed assets shall be used. -pub struct BorrowActionWithIntent +pub struct BorrowActionWithIntent(BorrowAction) where - F: FnOnce(&mut Ptb, &HashMap), -{ - action: BorrowAction, - intent_fn: F, -} + F: FnOnce(&mut Ptb, &HashMap); impl MoveType for BorrowAction { fn move_type(package: ObjectID) -> TypeTag { @@ -57,7 +65,7 @@ impl MoveType for BorrowAction { } } -impl BorrowAction { +impl BorrowAction { /// Adds an object to the lists of objects that will be borrowed when executing /// this action in a proposal. pub fn borrow_object(&mut self, object_id: ObjectID) { @@ -73,10 +81,10 @@ impl BorrowAction { } } -impl ProposalBuilder<'_, BorrowAction> { +impl<'i, F> ProposalBuilder<'i, BorrowAction> { /// Adds an object to the list of objects that will be borrowed when executing this action. pub fn borrow(mut self, object_id: ObjectID) -> Self { - self.borrow_object(object_id); + self.action.borrow_object(object_id); self } /// Adds many objects. See [`BorrowAction::borrow_object`] for more details. @@ -86,11 +94,33 @@ impl ProposalBuilder<'_, BorrowAction> { { objects.into_iter().fold(self, |builder, obj| builder.borrow(obj)) } + + /// Specifies how to use the borrowed assets. This is only useful if the sender of this + /// transaction has enough voting power to execute this proposal right-away. + pub fn with_intent(self, intent_fn: F1) -> ProposalBuilder<'i, BorrowAction> + where + F1: FnOnce(&mut Ptb, &HashMap), + { + let ProposalBuilder { + identity, + expiration, + action: BorrowAction { objects, .. }, + } = self; + let intent_fn = Some(intent_fn); + ProposalBuilder { + identity, + expiration, + action: BorrowAction { objects, intent_fn }, + } + } } #[async_trait] -impl ProposalT for Proposal { - type Action = BorrowAction; +impl ProposalT for Proposal> +where + F: FnOnce(&mut Ptb, &HashMap) + Send, +{ + type Action = BorrowAction; type Output = (); async fn create<'i, S>( @@ -107,20 +137,46 @@ impl ProposalT for Proposal { .await? .expect("identity exists on-chain"); let controller_cap_ref = identity.get_controller_cap(client).await?; - let tx = move_calls::identity::propose_borrow( - identity_ref, - controller_cap_ref, - action.objects, - expiration, - client.package_id(), - ) + let can_execute = identity + .controller_voting_power(controller_cap_ref.0) + .expect("is a controller of identity") + >= identity.threshold(); + let chained_execution = can_execute && action.intent_fn.is_some(); + let tx = if chained_execution { + // Construct a list of `(ObjectRef, TypeTag)` from the list of objects to send. + let object_data_list = { + let mut object_data_list = vec![]; + for obj_id in action.objects { + let object_data = super::obj_data_for_id(client, obj_id) + .await + .map_err(|e| Error::TransactionBuildingFailed(e.to_string()))?; + object_data_list.push(object_data); + } + object_data_list + }; + move_calls::identity::create_and_execute_borrow( + identity_ref, + controller_cap_ref, + object_data_list, + action.intent_fn.unwrap(), + expiration, + client.package_id(), + ) + } else { + move_calls::identity::propose_borrow( + identity_ref, + controller_cap_ref, + action.objects, + 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, + chained_execution, _action: PhantomData, }) } @@ -148,26 +204,27 @@ impl ProposalT for Proposal { } } -impl<'i> UserDrivenTx<'i, BorrowAction> { +impl<'i, F> UserDrivenTx<'i, BorrowAction> { /// Defines how the borrowed assets should be used. - pub fn with_intent(self, intent_fn: F) -> UserDrivenTx<'i, BorrowActionWithIntent> + pub fn with_intent(self, intent_fn: F1) -> UserDrivenTx<'i, BorrowActionWithIntent> where - F: FnOnce(&mut Ptb, &HashMap), + F1: FnOnce(&mut Ptb, &HashMap), { let UserDrivenTx { identity, - action, + action: BorrowAction { objects, .. }, proposal_id, } = self; + let intent_fn = Some(intent_fn); UserDrivenTx { identity, proposal_id, - action: BorrowActionWithIntent { action, intent_fn }, + action: BorrowActionWithIntent(BorrowAction { objects, intent_fn }), } } } -impl<'i> ProtoTransaction for UserDrivenTx<'i, BorrowAction> { +impl<'i, F> ProtoTransaction for UserDrivenTx<'i, BorrowAction> { type Input = IntentFn; type Tx = UserDrivenTx<'i, BorrowActionWithIntent>; @@ -204,7 +261,7 @@ where // Construct a list of `(ObjectRef, TypeTag)` from the list of objects to send. let object_data_list = { let mut object_data_list = vec![]; - for obj_id in borrow_action.action.objects { + for obj_id in borrow_action.0.objects { let object_data = super::obj_data_for_id(client, obj_id) .await .map_err(|e| Error::TransactionBuildingFailed(e.to_string()))?; @@ -218,7 +275,10 @@ where controller_cap_ref, proposal_id, object_data_list, - borrow_action.intent_fn, + borrow_action + .0 + .intent_fn + .expect("BorrowActionWithIntent makes sure intent_fn is there"), client.package_id(), ) .map_err(|e| Error::TransactionBuildingFailed(e.to_string()))?; diff --git a/identity_iota_core/src/rebased/proposals/mod.rs b/identity_iota_core/src/rebased/proposals/mod.rs index 8b1503876..f0bef4835 100644 --- a/identity_iota_core/src/rebased/proposals/mod.rs +++ b/identity_iota_core/src/rebased/proposals/mod.rs @@ -49,7 +49,7 @@ use crate::rebased::Error; /// Interface that allows the creation and execution of an [`OnChainIdentity`]'s [`Proposal`]s. #[async_trait] -pub trait ProposalT { +pub trait ProposalT: Sized { /// The [`Proposal`] action's type. type Action; /// The output of the [`Proposal`] @@ -61,7 +61,7 @@ pub trait ProposalT { expiration: Option, identity: &'i mut OnChainIdentity, client: &IdentityClient, - ) -> Result, Error> + ) -> Result>, Error> where S: Signer + Sync; @@ -126,10 +126,15 @@ impl<'i, A> ProposalBuilder<'i, A> { /// Creates a [`Proposal`] with the provided arguments. If `forbid_chained_execution` is set to `true`, /// the [`Proposal`] won't be executed even if creator alone has enough voting power. - pub async fn finish(self, client: &IdentityClient) -> Result, Error> + pub async fn finish<'c, S>( + self, + client: &'c IdentityClient, + ) -> Result>> + use<'i, 'c, S, A>, Error> where Proposal: ProposalT, S: Signer + Sync, + A: 'c, + 'i: 'c, { let Self { action, diff --git a/identity_iota_core/src/rebased/proposals/send.rs b/identity_iota_core/src/rebased/proposals/send.rs index b70bcdcf6..00bffcacf 100644 --- a/identity_iota_core/src/rebased/proposals/send.rs +++ b/identity_iota_core/src/rebased/proposals/send.rs @@ -94,7 +94,7 @@ impl ProposalT for Proposal { let can_execute = identity .controller_voting_power(controller_cap_ref.0) .expect("controller_cap is for this identity") - > identity.threshold(); + >= identity.threshold(); let tx = if can_execute { // Construct a list of `(ObjectRef, TypeTag)` from the list of objects to send. let object_type_list = { @@ -129,8 +129,7 @@ impl ProposalT for Proposal { Ok(CreateProposalTx { identity, tx, - // Send proposals cannot be chain-executed as they have to be driven. - chained_execution: false, + chained_execution: can_execute, _action: PhantomData, }) } diff --git a/identity_iota_core/tests/e2e/identity.rs b/identity_iota_core/tests/e2e/identity.rs index 94a2d3478..24579bbd0 100644 --- a/identity_iota_core/tests/e2e/identity.rs +++ b/identity_iota_core/tests/e2e/identity.rs @@ -280,8 +280,8 @@ async fn send_proposal_works() -> anyhow::Result<()> { let coin1 = common::get_test_coin(identity_address, &identity_client).await?; let coin2 = common::get_test_coin(identity_address, &identity_client).await?; - // Let's propose the send of those two caps to the identity_client's address. - let ProposalResult::Pending(send_proposal) = identity + // Let's propose the send of those two coins to the identity_client's address. + let ProposalResult::Executed(_) = identity .send_assets() .object(coin1, identity_client.sender_address()) .object(coin2, identity_client.sender_address()) @@ -291,15 +291,9 @@ async fn send_proposal_works() -> anyhow::Result<()> { .await? .output else { - panic!("send proposal cannot be chain-executed!"); + panic!("the controller has enough voting power and the proposal should have been executed"); }; - send_proposal - .into_tx(&mut identity, &identity_client) - .await? - .execute(&identity_client) - .await?; - // Assert that identity_client's address now owns those coins. identity_client .find_owned_ref(TEST_COIN_TYPE.clone(), |obj| obj.object_id == coin1) @@ -331,23 +325,10 @@ async fn borrow_proposal_works() -> anyhow::Result<()> { let coin2 = common::get_test_coin(identity_address, &identity_client).await?; // Let's propose the borrow of those two coins to the identity_client's address. - let ProposalResult::Pending(borrow_proposal) = identity + let ProposalResult::Executed(_) = identity .borrow_assets() .borrow(coin1) .borrow(coin2) - .finish(&identity_client) - .await? - .execute(&identity_client) - .await? - .output - else { - panic!("borrow proposal cannot be chain-executed!"); - }; - - borrow_proposal - .into_tx(&mut identity, &identity_client) - .await? - // this doesn't really do anything but if it doesn't fail it means coin1 was properly borrowed. .with_intent(move |ptb, objs| { ptb.programmable_move_call( IOTA_FRAMEWORK_PACKAGE_ID, @@ -357,8 +338,14 @@ async fn borrow_proposal_works() -> anyhow::Result<()> { vec![objs.get(&coin1).expect("coin1 data is borrowed").0], ); }) + .finish(&identity_client) + .await? .execute(&identity_client) - .await?; + .await? + .output + else { + panic!("controller has enough voting power and proposal should have been executed"); + }; Ok(()) } From db0e8c1584db65e4a3ad9b133b3d510b447604c8 Mon Sep 17 00:00:00 2001 From: umr1352 Date: Tue, 10 Dec 2024 14:25:32 +0100 Subject: [PATCH 3/4] controller execution proposal can be executed right away --- .../identity/controller_execution.rs | 128 ++++++++++++++--- .../src/rebased/proposals/borrow.rs | 3 +- .../src/rebased/proposals/controller.rs | 131 +++++++++++++++--- identity_iota_core/tests/e2e/identity.rs | 24 +--- 4 files changed, 226 insertions(+), 60 deletions(-) diff --git a/identity_iota_core/src/rebased/iota/move_calls/identity/controller_execution.rs b/identity_iota_core/src/rebased/iota/move_calls/identity/controller_execution.rs index 6063dc80d..d31dad53e 100644 --- a/identity_iota_core/src/rebased/iota/move_calls/identity/controller_execution.rs +++ b/identity_iota_core/src/rebased/iota/move_calls/identity/controller_execution.rs @@ -14,13 +14,15 @@ use crate::rebased::iota::move_calls::utils; use crate::rebased::proposals::ControllerExecution; use crate::rebased::utils::MoveType; -pub(crate) fn propose_controller_execution( +use super::ProposalContext; + +fn controller_execution_impl( identity: OwnedObjectRef, capability: ObjectRef, controller_cap_id: ObjectID, expiration: Option, package_id: ObjectID, -) -> Result { +) -> anyhow::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); @@ -28,7 +30,7 @@ pub(crate) fn propose_controller_execution( 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( + let proposal_id = ptb.programmable_move_call( package_id, ident_str!("identity").into(), ident_str!("propose_controller_execution").into(), @@ -36,28 +38,47 @@ pub(crate) fn propose_controller_execution( 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()) + Ok(ProposalContext { + ptb, + controller_cap: cap_arg, + delegation_token, + borrow, + identity: identity_arg, + proposal_id, + }) } -pub(crate) fn execute_controller_execution( +pub(crate) fn propose_controller_execution( identity: OwnedObjectRef, capability: ObjectRef, - proposal_id: ObjectID, + controller_cap_id: ObjectID, + expiration: Option, + package_id: ObjectID, +) -> Result { + let ProposalContext { + mut ptb, + controller_cap, + delegation_token, + borrow, + .. + } = controller_execution_impl(identity, capability, controller_cap_id, expiration, package_id)?; + utils::put_back_delegation_token(&mut ptb, controller_cap, delegation_token, borrow, package_id); + + Ok(ptb.finish()) +} + +fn execute_controller_execution_impl( + ptb: &mut ProgrammableTransactionBuilder, + identity: Argument, + proposal_id: Argument, + delegation_token: Argument, borrowing_controller_cap_ref: ObjectRef, intent_fn: F, package: ObjectID, -) -> Result +) -> anyhow::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, @@ -67,8 +88,6 @@ where 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( @@ -80,7 +99,7 @@ where ); // Apply the user-defined operation. - intent_fn(&mut ptb, &borrowed_controller_cap); + intent_fn(ptb, &borrowed_controller_cap); // Put back the borrowed controller cap. ptb.programmable_move_call( @@ -91,5 +110,78 @@ where vec![controller_execution_action, borrowed_controller_cap], ); + Ok(()) +} + +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)?; + + execute_controller_execution_impl( + &mut ptb, + identity, + proposal_id, + delegation_token, + borrowing_controller_cap_ref, + intent_fn, + package, + )?; + + utils::put_back_delegation_token(&mut ptb, controller_cap, delegation_token, borrow, package); + + Ok(ptb.finish()) +} + +pub(crate) fn create_and_execute_controller_execution( + identity: OwnedObjectRef, + capability: ObjectRef, + expiration: Option, + borrowing_controller_cap_ref: ObjectRef, + intent_fn: F, + package_id: ObjectID, +) -> anyhow::Result +where + F: FnOnce(&mut ProgrammableTransactionBuilder, &Argument), +{ + let ProposalContext { + mut ptb, + controller_cap, + delegation_token, + borrow, + proposal_id, + identity, + } = controller_execution_impl( + identity, + capability, + borrowing_controller_cap_ref.0, + expiration, + package_id, + )?; + + execute_controller_execution_impl( + &mut ptb, + identity, + proposal_id, + delegation_token, + borrowing_controller_cap_ref, + intent_fn, + package_id, + )?; + + utils::put_back_delegation_token(&mut ptb, controller_cap, delegation_token, borrow, package_id); + Ok(ptb.finish()) } diff --git a/identity_iota_core/src/rebased/proposals/borrow.rs b/identity_iota_core/src/rebased/proposals/borrow.rs index 38c96d516..9f0bc7cc9 100644 --- a/identity_iota_core/src/rebased/proposals/borrow.rs +++ b/identity_iota_core/src/rebased/proposals/borrow.rs @@ -35,10 +35,9 @@ pub(crate) type IntentFn = Box { objects: Vec, - #[serde(skip)] + #[serde(skip, default = "Option::default")] intent_fn: Option, } diff --git a/identity_iota_core/src/rebased/proposals/controller.rs b/identity_iota_core/src/rebased/proposals/controller.rs index 767a9067a..920bc6da9 100644 --- a/identity_iota_core/src/rebased/proposals/controller.rs +++ b/identity_iota_core/src/rebased/proposals/controller.rs @@ -28,6 +28,7 @@ use serde::Serialize; use super::CreateProposalTx; use super::ExecuteProposalTx; use super::OnChainIdentity; +use super::ProposalBuilder; use super::ProposalT; use super::UserDrivenTx; @@ -36,28 +37,77 @@ 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 { +pub struct ControllerExecution { controller_cap: ObjectID, identity: IotaAddress, + #[serde(skip, default = "Option::default")] + intent_fn: Option, } /// A [`ControllerExecution`] action coupled with a user-provided function to describe how /// the borrowed identity's controller capability will be used. -pub struct ControllerExecutionWithIntent +pub struct ControllerExecutionWithIntent(ControllerExecution) +where + F: FnOnce(&mut Ptb, &Argument); + +impl ControllerExecutionWithIntent where F: FnOnce(&mut Ptb, &Argument), { - action: ControllerExecution, - intent_fn: F, + fn new(action: ControllerExecution) -> Self { + debug_assert!(action.intent_fn.is_some()); + Self(action) + } } -impl ControllerExecution { +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(), + intent_fn: None, + } + } + + /// Specifies how the borrowed `ControllerCap` should be used in the transaction. + /// This is only useful if the controller creating this proposal has enough voting + /// power to carry out it out immediately. + pub fn with_intent(self, intent_fn: F1) -> ControllerExecution + where + F1: FnOnce(&mut Ptb, &Argument), + { + let Self { + controller_cap, + identity, + .. + } = self; + ControllerExecution { + controller_cap, + identity, + intent_fn: Some(intent_fn), + } + } +} + +impl<'i, F> ProposalBuilder<'i, ControllerExecution> { + /// Specifies how the borrowed `ControllerCap` should be used in the transaction. + /// This is only useful if the controller creating this proposal has enough voting + /// power to carry out it out immediately. + pub fn with_intent(self, intent_fn: F1) -> ProposalBuilder<'i, ControllerExecution> + where + F1: FnOnce(&mut Ptb, &Argument), + { + let ProposalBuilder { + identity, + expiration, + action, + } = self; + ProposalBuilder { + identity, + expiration, + action: action.with_intent(intent_fn), } } } @@ -71,8 +121,11 @@ impl MoveType for ControllerExecution { } #[async_trait] -impl ProposalT for Proposal { - type Action = ControllerExecution; +impl ProposalT for Proposal> +where + F: FnOnce(&mut Ptb, &Argument) + Send, +{ + type Action = ControllerExecution; type Output = (); async fn create<'i, S>( @@ -89,21 +142,49 @@ impl ProposalT for Proposal { .await? .expect("identity exists on-chain"); let controller_cap_ref = identity.get_controller_cap(client).await?; + let chained_execution = action.intent_fn.is_some() + && identity + .controller_voting_power(controller_cap_ref.0) + .expect("is an identity's controller") + >= identity.threshold(); - let tx = move_calls::identity::propose_controller_execution( - identity_ref, - controller_cap_ref, - action.controller_cap, - expiration, - client.package_id(), - ) + let tx = if chained_execution { + let borrowing_controller_cap_ref = client + .get_object_ref_by_id(action.controller_cap) + .await? + .map(|OwnedObjectRef { reference, .. }| { + let IotaObjectRef { + object_id, + version, + digest, + } = reference; + (object_id, version, digest) + }) + .ok_or_else(|| Error::ObjectLookup(format!("object {} doesn't exist", action.controller_cap)))?; + + move_calls::identity::create_and_execute_controller_execution( + identity_ref, + controller_cap_ref, + expiration, + borrowing_controller_cap_ref, + action.intent_fn.unwrap(), + client.package_id(), + ) + } else { + 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, + chained_execution, _action: PhantomData, }) } @@ -131,26 +212,27 @@ impl ProposalT for Proposal { } } -impl<'i> UserDrivenTx<'i, ControllerExecution> { +impl<'i, F> UserDrivenTx<'i, ControllerExecution> { /// Defines how the borrowed assets should be used. - pub fn with_intent(self, intent_fn: F) -> UserDrivenTx<'i, ControllerExecutionWithIntent> + pub fn with_intent(self, intent_fn: F1) -> UserDrivenTx<'i, ControllerExecutionWithIntent> where - F: FnOnce(&mut Ptb, &Argument), + F1: FnOnce(&mut Ptb, &Argument), { let UserDrivenTx { identity, action, proposal_id, } = self; + UserDrivenTx { identity, proposal_id, - action: ControllerExecutionWithIntent { action, intent_fn }, + action: ControllerExecutionWithIntent::new(action.with_intent(intent_fn)), } } } -impl<'i> ProtoTransaction for UserDrivenTx<'i, ControllerExecution> { +impl<'i, F> ProtoTransaction for UserDrivenTx<'i, ControllerExecution> { type Input = IntentFn; type Tx = UserDrivenTx<'i, ControllerExecutionWithIntent>; @@ -184,7 +266,7 @@ where .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_cap_id = action.0.controller_cap; let borrowing_controller_cap_ref = client .get_object_ref_by_id(borrowing_cap_id) .await? @@ -203,7 +285,10 @@ where controller_cap_ref, proposal_id, borrowing_controller_cap_ref, - action.intent_fn, + action + .0 + .intent_fn + .expect("BorrowActionWithIntent makes sure intent_fn is present"), client.package_id(), ) .map_err(|e| Error::TransactionBuildingFailed(e.to_string()))?; diff --git a/identity_iota_core/tests/e2e/identity.rs b/identity_iota_core/tests/e2e/identity.rs index 24579bbd0..7ad719dd8 100644 --- a/identity_iota_core/tests/e2e/identity.rs +++ b/identity_iota_core/tests/e2e/identity.rs @@ -12,7 +12,6 @@ use identity_iota_core::rebased::client::get_object_id_from_did; use identity_iota_core::rebased::migration::has_previous_version; use identity_iota_core::rebased::migration::Identity; use identity_iota_core::rebased::proposals::ProposalResult; -use identity_iota_core::rebased::proposals::ProposalT as _; use identity_iota_core::rebased::transaction::Transaction; use identity_iota_core::IotaDID; use identity_iota_core::IotaDocument; @@ -383,25 +382,13 @@ async fn controller_execution_works() -> anyhow::Result<()> { .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 + // Perform an action on `identity2` as a controller of `identity`. + let result = identity + .controller_execution(controller_cap.0) .with_intent(|ptb, controller_cap| { let identity2 = ptb .obj(ObjectArg::SharedObject { @@ -421,10 +408,13 @@ async fn controller_execution_works() -> anyhow::Result<()> { vec![identity2, *controller_cap, token_to_revoke], ); }) + .finish(&identity_client) + .await? .execute(&identity_client) .await?; - assert!(tx_output.response.status_ok().unwrap()); + assert!(result.response.status_ok().unwrap()); + assert!(matches!(result.output, ProposalResult::Executed(_))); Ok(()) } From 31a8fad367835074952d74b707f7fa39cffea4e5 Mon Sep 17 00:00:00 2001 From: umr1352 Date: Wed, 11 Dec 2024 12:58:10 +0100 Subject: [PATCH 4/4] cargo fmt --- identity_iota_core/src/rebased/iota/move_calls/identity/mod.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/identity_iota_core/src/rebased/iota/move_calls/identity/mod.rs b/identity_iota_core/src/rebased/iota/move_calls/identity/mod.rs index f71b319e8..fa85ae61d 100644 --- a/identity_iota_core/src/rebased/iota/move_calls/identity/mod.rs +++ b/identity_iota_core/src/rebased/iota/move_calls/identity/mod.rs @@ -16,7 +16,8 @@ pub(crate) use config::*; pub(crate) use controller_execution::*; pub(crate) use create::*; pub(crate) use deactivate::*; -use iota_sdk::types::{programmable_transaction_builder::ProgrammableTransactionBuilder as Ptb, transaction::Argument}; +use iota_sdk::types::programmable_transaction_builder::ProgrammableTransactionBuilder as Ptb; +use iota_sdk::types::transaction::Argument; pub(crate) use send_asset::*; pub(crate) use update::*; pub(crate) use upgrade::*;