secret_toolkit_utils/
feature_toggle.rs

1use cosmwasm_std::{
2    to_binary, to_vec, Addr, Binary, Deps, DepsMut, MessageInfo, Response, StdError, StdResult,
3    Storage,
4};
5use cosmwasm_storage::{Bucket, ReadonlyBucket};
6use schemars::JsonSchema;
7use serde::de::DeserializeOwned;
8use serde::{Deserialize, Serialize};
9
10const PREFIX_FEATURES: &[u8] = b"features";
11const PREFIX_PAUSERS: &[u8] = b"pausers";
12
13pub struct FeatureToggle;
14
15impl FeatureToggleTrait for FeatureToggle {
16    const STORAGE_KEY: &'static [u8] = b"feature_toggle";
17}
18
19pub trait FeatureToggleTrait {
20    const STORAGE_KEY: &'static [u8];
21
22    fn init_features<T: Serialize>(
23        storage: &mut dyn Storage,
24        feature_statuses: Vec<FeatureStatus<T>>,
25        pausers: Vec<Addr>,
26    ) -> StdResult<()> {
27        for fs in feature_statuses {
28            Self::set_feature_status(storage, &fs.feature, fs.status)?;
29        }
30
31        for p in pausers {
32            Self::set_pauser(storage, &p)?;
33        }
34
35        Ok(())
36    }
37
38    fn require_not_paused<T: Serialize>(storage: &dyn Storage, features: Vec<T>) -> StdResult<()> {
39        for feature in features {
40            let status = Self::get_feature_status(storage, &feature)?;
41            match status {
42                None => {
43                    return Err(StdError::generic_err(format!(
44                        "feature toggle: unknown feature '{}'",
45                        String::from_utf8_lossy(&to_vec(&feature)?)
46                    )))
47                }
48                Some(s) => match s {
49                    Status::NotPaused => {}
50                    Status::Paused => {
51                        return Err(StdError::generic_err(format!(
52                            "feature toggle: feature '{}' is paused",
53                            String::from_utf8_lossy(&to_vec(&feature)?)
54                        )));
55                    }
56                },
57            }
58        }
59
60        Ok(())
61    }
62
63    fn pause<T: Serialize>(storage: &mut dyn Storage, features: Vec<T>) -> StdResult<()> {
64        for f in features {
65            Self::set_feature_status(storage, &f, Status::Paused)?;
66        }
67
68        Ok(())
69    }
70
71    fn unpause<T: Serialize>(storage: &mut dyn Storage, features: Vec<T>) -> StdResult<()> {
72        for f in features {
73            Self::set_feature_status(storage, &f, Status::NotPaused)?;
74        }
75
76        Ok(())
77    }
78
79    fn is_pauser(storage: &dyn Storage, key: &Addr) -> StdResult<bool> {
80        let feature_store: ReadonlyBucket<bool> =
81            ReadonlyBucket::multilevel(storage, &[Self::STORAGE_KEY, PREFIX_PAUSERS]);
82        feature_store.may_load(key.as_bytes()).map(|p| p.is_some())
83    }
84
85    fn set_pauser(storage: &mut dyn Storage, key: &Addr) -> StdResult<()> {
86        let mut feature_store = Bucket::multilevel(storage, &[Self::STORAGE_KEY, PREFIX_PAUSERS]);
87        feature_store.save(key.as_bytes(), &true /* value is insignificant */)
88    }
89
90    fn remove_pauser(storage: &mut dyn Storage, key: &Addr) {
91        let mut feature_store: Bucket<bool> =
92            Bucket::multilevel(storage, &[Self::STORAGE_KEY, PREFIX_PAUSERS]);
93        feature_store.remove(key.as_bytes())
94    }
95
96    fn get_feature_status<T: Serialize>(
97        storage: &dyn Storage,
98        key: &T,
99    ) -> StdResult<Option<Status>> {
100        let feature_store =
101            ReadonlyBucket::multilevel(storage, &[Self::STORAGE_KEY, PREFIX_FEATURES]);
102        feature_store.may_load(&cosmwasm_std::to_vec(&key)?)
103    }
104
105    fn set_feature_status<T: Serialize>(
106        storage: &mut dyn Storage,
107        key: &T,
108        item: Status,
109    ) -> StdResult<()> {
110        let mut feature_store = Bucket::multilevel(storage, &[Self::STORAGE_KEY, PREFIX_FEATURES]);
111        feature_store.save(&cosmwasm_std::to_vec(&key)?, &item)
112    }
113
114    fn handle_pause<T: Serialize>(
115        deps: DepsMut,
116        info: &MessageInfo,
117        features: Vec<T>,
118    ) -> StdResult<Response> {
119        if !Self::is_pauser(deps.storage, &info.sender)? {
120            return Err(StdError::generic_err("unauthorized"));
121        }
122
123        Self::pause(deps.storage, features)?;
124
125        Ok(Response::new().set_data(to_binary(&HandleAnswer::Pause {
126            status: ResponseStatus::Success,
127        })?))
128    }
129
130    fn handle_unpause<T: Serialize>(
131        deps: DepsMut,
132        info: &MessageInfo,
133        features: Vec<T>,
134    ) -> StdResult<Response> {
135        if !Self::is_pauser(deps.storage, &info.sender)? {
136            return Err(StdError::generic_err("unauthorized"));
137        }
138
139        Self::unpause(deps.storage, features)?;
140
141        Ok(Response::new().set_data(to_binary(&HandleAnswer::Unpause {
142            status: ResponseStatus::Success,
143        })?))
144    }
145
146    fn handle_set_pauser(deps: DepsMut, address: Addr) -> StdResult<Response> {
147        Self::set_pauser(deps.storage, &address)?;
148
149        Ok(
150            Response::new().set_data(to_binary(&HandleAnswer::SetPauser {
151                status: ResponseStatus::Success,
152            })?),
153        )
154    }
155
156    fn handle_remove_pauser(deps: DepsMut, address: Addr) -> StdResult<Response> {
157        Self::remove_pauser(deps.storage, &address);
158
159        Ok(
160            Response::new().set_data(to_binary(&HandleAnswer::RemovePauser {
161                status: ResponseStatus::Success,
162            })?),
163        )
164    }
165
166    fn query_status<T: Serialize>(deps: Deps, features: Vec<T>) -> StdResult<Binary> {
167        let mut status = Vec::with_capacity(features.len());
168        for feature in features {
169            match Self::get_feature_status(deps.storage, &feature)? {
170                None => {
171                    return Err(StdError::generic_err(format!(
172                        "invalid feature: {} does not exist",
173                        String::from_utf8_lossy(&to_vec(&feature)?)
174                    )))
175                }
176                Some(s) => status.push(FeatureStatus { feature, status: s }),
177            }
178        }
179
180        to_binary(&FeatureToggleQueryAnswer::Status { features: status })
181    }
182
183    fn query_is_pauser(deps: Deps, address: Addr) -> StdResult<Binary> {
184        let is_pauser = Self::is_pauser(deps.storage, &address)?;
185
186        to_binary(&FeatureToggleQueryAnswer::<()>::IsPauser { is_pauser })
187    }
188}
189
190#[derive(Serialize, Debug, Deserialize, Clone, JsonSchema, PartialEq, Eq, Default)]
191pub enum Status {
192    #[default]
193    NotPaused,
194    Paused,
195}
196
197#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
198#[serde(rename_all = "snake_case")]
199pub enum FeatureToggleHandleMsg<T: Serialize + DeserializeOwned> {
200    #[serde(bound = "")]
201    Pause {
202        features: Vec<T>,
203    },
204    #[serde(bound = "")]
205    Unpause {
206        features: Vec<T>,
207    },
208    SetPauser {
209        address: String,
210    },
211    RemovePauser {
212        address: String,
213    },
214}
215
216#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
217#[serde(rename_all = "snake_case")]
218enum ResponseStatus {
219    Success,
220    Failure,
221}
222
223#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
224#[serde(rename_all = "snake_case")]
225enum HandleAnswer {
226    Pause { status: ResponseStatus },
227    Unpause { status: ResponseStatus },
228    SetPauser { status: ResponseStatus },
229    RemovePauser { status: ResponseStatus },
230}
231
232#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
233#[serde(rename_all = "snake_case")]
234pub enum FeatureToggleQueryMsg<T: Serialize + DeserializeOwned> {
235    #[serde(bound = "")]
236    Status {
237        features: Vec<T>,
238    },
239    IsPauser {
240        address: String,
241    },
242}
243
244#[derive(Serialize, Deserialize, JsonSchema, Debug)]
245#[serde(rename_all = "snake_case")]
246enum FeatureToggleQueryAnswer<T: Serialize> {
247    Status { features: Vec<FeatureStatus<T>> },
248    IsPauser { is_pauser: bool },
249}
250
251#[derive(Serialize, Deserialize, JsonSchema, Debug)]
252pub struct FeatureStatus<T: Serialize> {
253    pub feature: T,
254    pub status: Status,
255}
256
257#[cfg(test)]
258mod tests {
259    use crate::feature_toggle::{
260        FeatureStatus, FeatureToggle, FeatureToggleHandleMsg, FeatureToggleQueryMsg,
261        FeatureToggleTrait, HandleAnswer, ResponseStatus, Status,
262    };
263    use cosmwasm_std::testing::{mock_dependencies, mock_info, MockStorage};
264    use cosmwasm_std::{from_binary, Addr, MemoryStorage, StdError, StdResult};
265
266    fn init_features(storage: &mut MemoryStorage) -> StdResult<()> {
267        FeatureToggle::init_features(
268            storage,
269            vec![
270                FeatureStatus {
271                    feature: "Feature1".to_string(),
272                    status: Status::NotPaused,
273                },
274                FeatureStatus {
275                    feature: "Feature2".to_string(),
276                    status: Status::NotPaused,
277                },
278                FeatureStatus {
279                    feature: "Feature3".to_string(),
280                    status: Status::Paused,
281                },
282            ],
283            vec![Addr::unchecked("alice".to_string())],
284        )
285    }
286
287    #[test]
288    fn test_init_works() -> StdResult<()> {
289        let mut storage = MockStorage::new();
290        init_features(&mut storage)?;
291
292        assert_eq!(
293            FeatureToggle::get_feature_status(&storage, &"Feature1".to_string())?,
294            Some(Status::NotPaused)
295        );
296        assert_eq!(
297            FeatureToggle::get_feature_status(&storage, &"Feature2".to_string())?,
298            Some(Status::NotPaused)
299        );
300        assert_eq!(
301            FeatureToggle::get_feature_status(&storage, &"Feature3".to_string())?,
302            Some(Status::Paused)
303        );
304        assert_eq!(
305            FeatureToggle::get_feature_status(&storage, &"Feature4".to_string())?,
306            None
307        );
308
309        assert!(FeatureToggle::is_pauser(
310            &storage,
311            &Addr::unchecked("alice".to_string())
312        )?,);
313        assert!(!FeatureToggle::is_pauser(
314            &storage,
315            &Addr::unchecked("bob".to_string())
316        )?);
317
318        Ok(())
319    }
320
321    #[test]
322    fn test_unpause() -> StdResult<()> {
323        let mut storage = MockStorage::new();
324        init_features(&mut storage)?;
325
326        FeatureToggle::unpause(&mut storage, vec!["Feature3".to_string()])?;
327        assert_eq!(
328            FeatureToggle::get_feature_status(&storage, &"Feature3".to_string())?,
329            Some(Status::NotPaused)
330        );
331
332        Ok(())
333    }
334
335    #[test]
336    fn test_handle_unpause() -> StdResult<()> {
337        let mut deps = mock_dependencies();
338        init_features(&mut deps.storage)?;
339
340        let info = mock_info("non-pauser", &[]);
341        let error =
342            FeatureToggle::handle_unpause(deps.as_mut(), &info, vec!["Feature3".to_string()]);
343        assert_eq!(error, Err(StdError::generic_err("unauthorized")));
344
345        let info = mock_info("alice", &[]);
346        let response =
347            FeatureToggle::handle_unpause(deps.as_mut(), &info, vec!["Feature3".to_string()])?;
348        let answer: HandleAnswer = from_binary(&response.data.unwrap())?;
349
350        assert_eq!(
351            answer,
352            HandleAnswer::Unpause {
353                status: ResponseStatus::Success,
354            }
355        );
356        Ok(())
357    }
358
359    #[test]
360    fn test_pause() -> StdResult<()> {
361        let mut storage = MockStorage::new();
362        init_features(&mut storage)?;
363
364        FeatureToggle::pause(&mut storage, vec!["Feature1".to_string()])?;
365        assert_eq!(
366            FeatureToggle::get_feature_status(&storage, &"Feature1".to_string())?,
367            Some(Status::Paused)
368        );
369
370        Ok(())
371    }
372
373    #[test]
374    fn test_handle_pause() -> StdResult<()> {
375        let mut deps = mock_dependencies();
376        init_features(&mut deps.storage)?;
377
378        let info = mock_info("non-pauser", &[]);
379        let error = FeatureToggle::handle_pause(deps.as_mut(), &info, vec!["Feature2".to_string()]);
380        assert_eq!(error, Err(StdError::generic_err("unauthorized")));
381
382        let info = mock_info("alice", &[]);
383        let response =
384            FeatureToggle::handle_pause(deps.as_mut(), &info, vec!["Feature2".to_string()])?;
385        let answer: HandleAnswer = from_binary(&response.data.unwrap())?;
386
387        assert_eq!(
388            answer,
389            HandleAnswer::Pause {
390                status: ResponseStatus::Success,
391            }
392        );
393        Ok(())
394    }
395
396    #[test]
397    fn test_require_not_paused() -> StdResult<()> {
398        let mut storage = MockStorage::new();
399        init_features(&mut storage)?;
400
401        assert!(
402            FeatureToggle::require_not_paused(&storage, vec!["Feature1".to_string()]).is_ok(),
403            "{:?}",
404            FeatureToggle::require_not_paused(&storage, vec!["Feature1".to_string()])
405        );
406        assert!(
407            FeatureToggle::require_not_paused(&storage, vec!["Feature3".to_string()]).is_err(),
408            "{:?}",
409            FeatureToggle::require_not_paused(&storage, vec!["Feature3".to_string()])
410        );
411
412        Ok(())
413    }
414
415    #[test]
416    fn test_add_remove_pausers() -> StdResult<()> {
417        let mut storage = MockStorage::new();
418        init_features(&mut storage)?;
419
420        let bob = Addr::unchecked("bob".to_string());
421
422        FeatureToggle::set_pauser(&mut storage, &bob)?;
423        assert!(
424            FeatureToggle::is_pauser(&storage, &bob)?,
425            "{:?}",
426            FeatureToggle::is_pauser(&storage, &bob)
427        );
428
429        FeatureToggle::remove_pauser(&mut storage, &bob);
430        assert!(
431            !FeatureToggle::is_pauser(&storage, &bob)?,
432            "{:?}",
433            FeatureToggle::is_pauser(&storage, &bob)
434        );
435
436        Ok(())
437    }
438
439    #[test]
440    fn test_deserialize_messages() {
441        use serde::{Deserialize, Serialize};
442
443        #[derive(Serialize, Deserialize, Debug, PartialEq)]
444        #[serde(rename_all = "snake_case")]
445        enum Features {
446            Var1,
447            Var2,
448        }
449
450        let handle_msg = b"{\"pause\":{\"features\":[\"var1\",\"var2\"]}}";
451        let query_msg = b"{\"status\":{\"features\": [\"var1\"]}}";
452        let query_msg_invalid = b"{\"status\":{\"features\": [\"var3\"]}}";
453
454        let parsed: FeatureToggleHandleMsg<Features> =
455            cosmwasm_std::from_slice(handle_msg).unwrap();
456        assert_eq!(
457            parsed,
458            FeatureToggleHandleMsg::Pause {
459                features: vec![Features::Var1, Features::Var2]
460            }
461        );
462        let parsed: FeatureToggleQueryMsg<Features> = cosmwasm_std::from_slice(query_msg).unwrap();
463        assert_eq!(
464            parsed,
465            FeatureToggleQueryMsg::Status {
466                features: vec![Features::Var1]
467            }
468        );
469        let parsed: StdResult<FeatureToggleQueryMsg<Features>> =
470            cosmwasm_std::from_slice(query_msg_invalid);
471        assert!(parsed.is_err());
472    }
473}