identity_iota_core/rebased/proposals/
config_change.rs1use 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#[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 pub fn threshold(mut self, threshold: u64) -> Self {
62 self.set_threshold(threshold);
63 self
64 }
65
66 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 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 pub fn remove_controller(mut self, controller_id: ObjectID) -> Self {
83 self.deref_mut().remove_controller(controller_id);
84 self
85 }
86
87 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 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 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 pub fn new() -> Self {
119 Self::default()
120 }
121
122 pub fn set_threshold(&mut self, new_threshold: u64) {
124 self.threshold = Some(new_threshold);
125 }
126
127 pub fn threshold(&self) -> Option<u64> {
129 self.threshold
130 }
131
132 pub fn controllers_to_add(&self) -> &HashMap<IotaAddress, u64> {
134 &self.controllers_to_add
135 }
136
137 pub fn controllers_to_remove(&self) -> &HashSet<ObjectID> {
139 &self.controllers_to_remove
140 }
141
142 pub fn controllers_to_update(&self) -> &HashMap<ObjectID, u64> {
144 &self.controllers_voting_power
145 }
146
147 pub fn add_controller(&mut self, address: IotaAddress, voting_power: u64) {
149 self.controllers_to_add.insert(address, voting_power);
150 }
151
152 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 pub fn remove_controller(&mut self, controller_id: ObjectID) {
164 self.controllers_to_remove.insert(controller_id);
165 }
166
167 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 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 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 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 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 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}