identity_iota_core/rebased/proposals/
config_change.rs

1// Copyright 2020-2024 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::rebased::iota::package::identity_package_id;
5
6use std::collections::HashMap;
7use std::collections::HashSet;
8use std::marker::PhantomData;
9use std::ops::DerefMut as _;
10use std::str::FromStr as _;
11
12use crate::rebased::iota::move_calls;
13use crate::rebased::migration::ControllerToken;
14use product_common::core_client::CoreClientReadOnly;
15use product_common::transaction::transaction_builder::TransactionBuilder;
16
17use crate::rebased::migration::Proposal;
18use async_trait::async_trait;
19use iota_interaction::rpc_types::IotaTransactionBlockEffects;
20use iota_interaction::types::base_types::IotaAddress;
21use iota_interaction::types::base_types::ObjectID;
22use iota_interaction::types::collection_types::Entry;
23use iota_interaction::types::collection_types::VecMap;
24use iota_interaction::types::TypeTag;
25use serde::Deserialize;
26use serde::Serialize;
27
28use crate::rebased::iota::types::Number;
29use crate::rebased::migration::OnChainIdentity;
30use crate::rebased::Error;
31use iota_interaction::MoveType;
32use iota_interaction::OptionalSync;
33
34use super::CreateProposal;
35use super::ExecuteProposal;
36use super::ProposalBuilder;
37use super::ProposalT;
38
39/// [`Proposal`] action that modifies an [`OnChainIdentity`]'s configuration - e.g:
40/// - remove controllers
41/// - add controllers
42/// - update controllers voting powers
43/// - update threshold
44#[derive(Debug, Clone, Default, Serialize, Deserialize)]
45#[serde(try_from = "Modify")]
46pub struct ConfigChange {
47  threshold: Option<u64>,
48  controllers_to_add: HashMap<IotaAddress, u64>,
49  controllers_to_remove: HashSet<ObjectID>,
50  controllers_voting_power: HashMap<ObjectID, u64>,
51}
52
53impl MoveType for ConfigChange {
54  fn move_type(package: ObjectID) -> TypeTag {
55    TypeTag::from_str(&format!("{package}::config_proposal::Modify")).expect("valid type tag")
56  }
57}
58
59impl ProposalBuilder<'_, '_, ConfigChange> {
60  /// Sets a new value for the identity's threshold.
61  pub fn threshold(mut self, threshold: u64) -> Self {
62    self.set_threshold(threshold);
63    self
64  }
65
66  /// Makes address `address` a new controller with voting power `voting_power`.
67  pub fn add_controller(mut self, address: IotaAddress, voting_power: u64) -> Self {
68    self.deref_mut().add_controller(address, voting_power);
69    self
70  }
71
72  /// Adds multiple controllers. See [`ProposalBuilder::add_controller`].
73  pub fn add_multiple_controllers<I>(mut self, controllers: I) -> Self
74  where
75    I: IntoIterator<Item = (IotaAddress, u64)>,
76  {
77    self.deref_mut().add_multiple_controllers(controllers);
78    self
79  }
80
81  /// Removes an existing controller.
82  pub fn remove_controller(mut self, controller_id: ObjectID) -> Self {
83    self.deref_mut().remove_controller(controller_id);
84    self
85  }
86
87  /// Removes many controllers.
88  pub fn remove_multiple_controllers<I>(mut self, controllers: I) -> Self
89  where
90    I: IntoIterator<Item = ObjectID>,
91  {
92    self.deref_mut().remove_multiple_controllers(controllers);
93    self
94  }
95
96  /// Sets a new voting power for a controller.
97  pub fn update_controller(mut self, controller_id: ObjectID, voting_power: u64) -> Self {
98    self.action.controllers_voting_power.insert(controller_id, voting_power);
99    self
100  }
101
102  /// Updates many controllers' voting power.
103  pub fn update_multiple_controllers<I>(mut self, controllers: I) -> Self
104  where
105    I: IntoIterator<Item = (ObjectID, u64)>,
106  {
107    let controllers_to_update = &mut self.action.controllers_voting_power;
108    for (id, vp) in controllers {
109      controllers_to_update.insert(id, vp);
110    }
111
112    self
113  }
114}
115
116impl ConfigChange {
117  /// Creates a new [`ConfigChange`] proposal action.
118  pub fn new() -> Self {
119    Self::default()
120  }
121
122  /// Sets the new threshold.
123  pub fn set_threshold(&mut self, new_threshold: u64) {
124    self.threshold = Some(new_threshold);
125  }
126
127  /// Returns the value for the new threshold.
128  pub fn threshold(&self) -> Option<u64> {
129    self.threshold
130  }
131
132  /// Returns the controllers that will be added, as the map [IotaAddress] -> [u64].
133  pub fn controllers_to_add(&self) -> &HashMap<IotaAddress, u64> {
134    &self.controllers_to_add
135  }
136
137  /// Returns the set of controllers that will be removed.
138  pub fn controllers_to_remove(&self) -> &HashSet<ObjectID> {
139    &self.controllers_to_remove
140  }
141
142  /// Returns the controllers that will be updated as the map [IotaAddress] -> [u64].
143  pub fn controllers_to_update(&self) -> &HashMap<ObjectID, u64> {
144    &self.controllers_voting_power
145  }
146
147  /// Adds a controller.
148  pub fn add_controller(&mut self, address: IotaAddress, voting_power: u64) {
149    self.controllers_to_add.insert(address, voting_power);
150  }
151
152  /// Adds many controllers.
153  pub fn add_multiple_controllers<I>(&mut self, controllers: I)
154  where
155    I: IntoIterator<Item = (IotaAddress, u64)>,
156  {
157    for (addr, vp) in controllers {
158      self.add_controller(addr, vp)
159    }
160  }
161
162  /// Removes an existing controller.
163  pub fn remove_controller(&mut self, controller_id: ObjectID) {
164    self.controllers_to_remove.insert(controller_id);
165  }
166
167  /// Removes many controllers.
168  pub fn remove_multiple_controllers<I>(&mut self, controllers: I)
169  where
170    I: IntoIterator<Item = ObjectID>,
171  {
172    for controller in controllers {
173      self.remove_controller(controller)
174    }
175  }
176
177  fn validate(&self, identity: &OnChainIdentity) -> Result<(), Error> {
178    let new_threshold = self.threshold.unwrap_or(identity.threshold());
179    let mut controllers = identity.controllers().clone();
180    // check if update voting powers is valid
181    for (controller, new_vp) in &self.controllers_voting_power {
182      match controllers.get_mut(controller) {
183        Some(vp) => *vp = *new_vp,
184        None => {
185          return Err(Error::InvalidConfig(format!(
186            "object \"{controller}\" is not among identity \"{}\"'s controllers",
187            identity.id()
188          )))
189        }
190      }
191    }
192    // check if deleting controllers is valid
193    for controller in &self.controllers_to_remove {
194      if controllers.remove(controller).is_none() {
195        return Err(Error::InvalidConfig(format!(
196          "object \"{controller}\" is not among identity \"{}\"'s controllers",
197          identity.id()
198        )));
199      }
200    }
201    // check if adding controllers is valid
202    for (controller, vp) in &self.controllers_to_add {
203      if controllers.insert((*controller).into(), *vp).is_some() {
204        return Err(Error::InvalidConfig(format!(
205          "object \"{controller}\" is already among identity \"{}\"'s controllers",
206          identity.id()
207        )));
208      }
209    }
210    // check whether the new threshold allows to interact with the identity
211    if new_threshold > controllers.values().sum::<u64>() {
212      return Err(Error::InvalidConfig(
213        "the resulting configuration will result in an unaccessible identity".to_string(),
214      ));
215    }
216    Ok(())
217  }
218}
219
220#[cfg_attr(not(feature = "send-sync"), async_trait(?Send))]
221#[cfg_attr(feature = "send-sync", async_trait)]
222impl ProposalT for Proposal<ConfigChange> {
223  type Action = ConfigChange;
224  type Output = ();
225
226  async fn create<'i, C>(
227    action: Self::Action,
228    expiration: Option<u64>,
229    identity: &'i mut OnChainIdentity,
230    controller_token: &ControllerToken,
231    client: &C,
232  ) -> Result<TransactionBuilder<CreateProposal<'i, Self::Action>>, Error>
233  where
234    C: CoreClientReadOnly + OptionalSync,
235  {
236    // Check the validity of the proposed changes.
237    action.validate(identity)?;
238
239    if identity.id() != controller_token.controller_of() {
240      return Err(Error::Identity(format!(
241        "token {} doesn't grant access to identity {}",
242        controller_token.id(),
243        identity.id()
244      )));
245    }
246
247    let package = identity_package_id(client).await?;
248    let identity_ref = client
249      .get_object_ref_by_id(identity.id())
250      .await?
251      .expect("identity exists on-chain");
252    let controller_cap_ref = controller_token.controller_ref(client).await?;
253    let sender_vp = identity
254      .controller_voting_power(controller_token.controller_id())
255      .expect("controller exists");
256    let chained_execution = sender_vp >= identity.threshold();
257    let tx = move_calls::identity::propose_config_change(
258      identity_ref,
259      controller_cap_ref,
260      expiration,
261      action.threshold,
262      action.controllers_to_add,
263      action.controllers_to_remove,
264      action.controllers_voting_power,
265      package,
266    )
267    .map_err(|e| Error::TransactionBuildingFailed(e.to_string()))?;
268
269    Ok(TransactionBuilder::new(CreateProposal {
270      identity,
271      ptb: bcs::from_bytes(&tx)?,
272      chained_execution,
273      _action: PhantomData,
274    }))
275  }
276
277  async fn into_tx<'i, C>(
278    self,
279    identity: &'i mut OnChainIdentity,
280    controller_token: &ControllerToken,
281    client: &C,
282  ) -> Result<TransactionBuilder<ExecuteProposal<'i, Self::Action>>, Error>
283  where
284    C: CoreClientReadOnly + OptionalSync,
285  {
286    if identity.id() != controller_token.controller_of() {
287      return Err(Error::Identity(format!(
288        "token {} doesn't grant access to identity {}",
289        controller_token.id(),
290        identity.id()
291      )));
292    }
293
294    let proposal_id = self.id();
295    let identity_ref = client
296      .get_object_ref_by_id(identity.id())
297      .await?
298      .expect("identity exists on-chain");
299    let controller_cap_ref = controller_token.controller_ref(client).await?;
300    let package = identity_package_id(client).await?;
301    let tx = move_calls::identity::execute_config_change(identity_ref, controller_cap_ref, proposal_id, package)
302      .map_err(|e| Error::TransactionBuildingFailed(e.to_string()))?;
303
304    Ok(TransactionBuilder::new(ExecuteProposal {
305      identity,
306      ptb: bcs::from_bytes(&tx)?,
307      _action: PhantomData,
308    }))
309  }
310
311  fn parse_tx_effects(_effects: &IotaTransactionBlockEffects) -> Result<Self::Output, Error> {
312    Ok(())
313  }
314}
315
316#[derive(Debug, Deserialize)]
317struct Modify {
318  threshold: Option<Number<u64>>,
319  controllers_to_add: VecMap<IotaAddress, Number<u64>>,
320  controllers_to_remove: HashSet<ObjectID>,
321  controllers_to_update: VecMap<ObjectID, Number<u64>>,
322}
323
324impl TryFrom<Modify> for ConfigChange {
325  type Error = <u64 as TryFrom<Number<u64>>>::Error;
326  fn try_from(value: Modify) -> Result<Self, Self::Error> {
327    let Modify {
328      threshold,
329      controllers_to_add,
330      controllers_to_remove,
331      controllers_to_update,
332    } = value;
333    let threshold = threshold.map(|num| num.try_into()).transpose()?;
334    let controllers_to_add = controllers_to_add
335      .contents
336      .into_iter()
337      .map(|Entry { key, value }| value.try_into().map(|n| (key, n)))
338      .collect::<Result<_, _>>()?;
339    let controllers_to_update = controllers_to_update
340      .contents
341      .into_iter()
342      .map(|Entry { key, value }| value.try_into().map(|n| (key, n)))
343      .collect::<Result<_, _>>()?;
344    Ok(Self {
345      threshold,
346      controllers_to_add,
347      controllers_to_remove,
348      controllers_voting_power: controllers_to_update,
349    })
350  }
351}