cw_config/
lib.rs

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
10// Re-exports for convenience
11pub use optional_struct;
12
13pub trait Validateable<T> {
14    fn validate(&self, deps: &Deps) -> StdResult<T>;
15}
16
17/// Updates the a config item with new values.
18///
19/// # Generics
20///
21/// * `T` - The type of the validated config.
22/// * `U` - The type of the unvalidated config.
23/// * `E` - The type of the error returned by the access check.
24///
25/// Requires that T implements `Serialize + DeserializeOwned`.
26/// Requires that U implements `From<T> + Validateable<T>`. I.e. that the unvalidated config can be
27/// validated into a validated config and that the unvalidated config can be created from a validated config.
28///
29/// # Arguments
30///
31/// * `deps` - The dependencies for querying the chain.
32/// * `info` - The message info of the transaction.
33/// * `config_item` - The item to load and save the config.
34/// * `updates` - The updates to apply to the config.
35/// * `access_allowed` - A function that checks if the sender is allowed to update the config.
36///                If `None`, the sender is always allowed to update the config.
37///                The function takes the storage and the sender address and returns an error if the sender is not allowed.
38pub 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    // Validate that the sender is the owner
46    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    // Load the old config, turn it into the unchecked version, apply the updates,
55    // validate the new config and save it back to the item
56    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        // Instantiate owner
127        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        // Call from other sender, should fail
141        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        // Call form owner, should succeed
153        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}