hermit_toolkit_utils/
feature_toggle.rs

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