1use cosmwasm_std::{
2 Addr, Deps, DepsMut, Event, MessageInfo, Response, StdError, StdResult, Storage,
3};
4use cw_storage_plus::Item;
5pub use optional_struct::Applyable;
6use serde::{de::DeserializeOwned, Serialize};
7use std::fmt::Debug;
8use thiserror::Error;
9
10pub use optional_struct;
12
13pub trait Validateable<T> {
14 fn validate(&self, deps: &Deps) -> StdResult<T>;
15}
16
17pub fn update_config<T: Serialize + DeserializeOwned, U: From<T> + Validateable<T>, E>(
39 deps: DepsMut,
40 info: &MessageInfo,
41 config_item: Item<T>,
42 updates: impl Applyable<U> + Debug,
43 access_allowed: Option<impl FnOnce(&dyn Storage, &Addr) -> Result<(), E>>,
44) -> Result<Response, ConfigError> {
45 access_allowed
47 .map(|check| check(deps.storage, &info.sender))
48 .transpose()
49 .map_err(|_| ConfigError::Unauthorized {})?;
50
51 let event = Event::new("apollodao/cw-config/update-config")
52 .add_attribute("updates", format!("{:?}", updates));
53
54 let config = config_item.load(deps.storage)?;
57 let mut config_unchecked: U = config.into();
58 updates.apply_to(&mut config_unchecked);
59 let config = config_unchecked.validate(&deps.as_ref())?;
60 config_item.save(deps.storage, &config)?;
61
62 Ok(Response::new().add_event(event))
63}
64
65#[derive(Error, Debug, PartialEq)]
66pub enum ConfigError {
67 #[error("{0}")]
68 StdError(#[from] StdError),
69
70 #[error("{0}")]
71 OwnershipError(#[from] cw_ownable::OwnershipError),
72
73 #[error("Invalid config: {reason}")]
74 InvalidConfig { reason: String },
75
76 #[error("Unauthorized")]
77 Unauthorized {},
78}
79
80#[cfg(test)]
81mod tests {
82 use std::borrow::BorrowMut;
83
84 use crate::{update_config, ConfigError, Validateable};
85 use cosmwasm_schema::schemars::JsonSchema;
86 use cosmwasm_schema::serde::{Deserialize, Serialize};
87 use cosmwasm_std::{
88 testing::{mock_dependencies, mock_info},
89 Addr, StdError,
90 };
91 use cw_address_like::AddressLike;
92 use cw_storage_plus::Item;
93 use optional_struct::{optional_struct, Applyable};
94
95 #[optional_struct(ConfigUpdates)]
96 #[derive(Serialize, Deserialize, JsonSchema, Clone, Debug, PartialEq)]
97 pub struct ConfigBase<T: AddressLike> {
98 pub example_addr: T,
99 }
100
101 pub type Config = ConfigBase<Addr>;
102 pub type ConfigUnchecked = ConfigBase<String>;
103
104 impl From<Config> for ConfigUnchecked {
105 fn from(config: Config) -> Self {
106 ConfigUnchecked {
107 example_addr: config.example_addr.to_string(),
108 }
109 }
110 }
111
112 impl Validateable<Config> for ConfigUnchecked {
113 fn validate(&self, deps: &cosmwasm_std::Deps) -> Result<Config, StdError> {
114 Ok(Config {
115 example_addr: deps.api.addr_validate(&self.example_addr)?,
116 })
117 }
118 }
119
120 const CONFIG: Item<Config> = Item::new("config");
121
122 #[test]
123 fn test_access_control() {
124 let mut deps = mock_dependencies();
125
126 let owner = Addr::unchecked("owner");
128 cw_ownable::initialize_owner(deps.storage.borrow_mut(), &deps.api, Some(owner.as_str()))
129 .unwrap();
130
131 let config = Config {
132 example_addr: Addr::unchecked("example"),
133 };
134 CONFIG.save(deps.as_mut().storage, &config).unwrap();
135
136 let updates = ConfigUpdates {
137 example_addr: Some("example2".to_string()),
138 };
139
140 let info = mock_info("sender", &[]);
142 let err = update_config::<Config, ConfigUnchecked, _>(
143 deps.as_mut(),
144 &info,
145 CONFIG,
146 updates.clone(),
147 Some(cw_ownable::assert_owner),
148 )
149 .unwrap_err();
150 assert!(matches!(err, ConfigError::Unauthorized {}));
151
152 let info = mock_info(owner.as_str(), &[]);
154 update_config::<Config, ConfigUnchecked, _>(
155 deps.as_mut(),
156 &info,
157 CONFIG,
158 updates,
159 Some(cw_ownable::assert_owner),
160 )
161 .unwrap();
162 }
163}