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 )
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}