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