tg_bindings_test/
multitest.rs

1use anyhow::{bail, Result as AnyResult};
2use schemars::JsonSchema;
3use serde::de::DeserializeOwned;
4use serde::{Deserialize, Serialize};
5use std::cmp::max;
6use std::fmt::Debug;
7use std::ops::{Deref, DerefMut};
8use thiserror::Error;
9
10use cosmwasm_std::testing::{MockApi, MockQuerier, MockStorage};
11use cosmwasm_std::OwnedDeps;
12use std::marker::PhantomData;
13
14use cosmwasm_std::Order::Ascending;
15use cosmwasm_std::{
16    from_slice, to_binary, Addr, Api, Binary, BlockInfo, Coin, CustomQuery, Empty, Order, Querier,
17    QuerierResult, StdError, StdResult, Storage, Timestamp,
18};
19use cw_multi_test::{
20    App, AppResponse, BankKeeper, BankSudo, BasicAppBuilder, CosmosRouter, Executor, Module,
21    WasmKeeper, WasmSudo,
22};
23use cw_storage_plus::{Item, Map};
24
25use tg_bindings::{
26    Evidence, GovProposal, ListPrivilegedResponse, Privilege, PrivilegeChangeMsg, PrivilegeMsg,
27    TgradeMsg, TgradeQuery, TgradeSudoMsg, ValidatorDiff, ValidatorVote, ValidatorVoteResponse,
28};
29
30pub struct TgradeModule {}
31
32pub type Privileges = Vec<Privilege>;
33
34/// How many seconds per block
35/// (when we increment block.height, use this multiplier for block.time)
36pub const BLOCK_TIME: u64 = 5;
37
38const PRIVILEGES: Map<&Addr, Privileges> = Map::new("privileges");
39const VOTES: Item<ValidatorVoteResponse> = Item::new("votes");
40const PINNED: Item<Vec<u64>> = Item::new("pinned");
41const PLANNED_UPGRADE: Item<UpgradePlan> = Item::new("planned_upgrade");
42const PARAMS: Map<String, String> = Map::new("params");
43
44const ADMIN_PRIVILEGES: &[Privilege] = &[
45    Privilege::GovProposalExecutor,
46    Privilege::Sudoer,
47    Privilege::TokenMinter,
48    Privilege::ConsensusParamChanger,
49];
50
51pub type TgradeDeps = OwnedDeps<MockStorage, MockApi, MockQuerier, TgradeQuery>;
52
53pub fn mock_deps_tgrade() -> TgradeDeps {
54    OwnedDeps {
55        storage: MockStorage::default(),
56        api: MockApi::default(),
57        querier: MockQuerier::default(),
58        custom_query_type: PhantomData,
59    }
60}
61
62impl TgradeModule {
63    /// Intended for init_modules to set someone who can grant privileges or call arbitrary
64    /// TgradeMsg externally
65    pub fn set_owner(&self, storage: &mut dyn Storage, owner: &Addr) -> StdResult<()> {
66        PRIVILEGES.save(storage, owner, &ADMIN_PRIVILEGES.to_vec())?;
67        Ok(())
68    }
69
70    /// Used to mock out the response for TgradeQuery::ValidatorVotes
71    pub fn set_votes(&self, storage: &mut dyn Storage, votes: Vec<ValidatorVote>) -> StdResult<()> {
72        VOTES.save(storage, &ValidatorVoteResponse { votes })
73    }
74
75    pub fn is_pinned(&self, storage: &dyn Storage, code: u64) -> StdResult<bool> {
76        let pinned = PINNED.may_load(storage)?;
77        match pinned {
78            Some(pinned) => Ok(pinned.contains(&code)),
79            None => Ok(false),
80        }
81    }
82
83    pub fn upgrade_is_planned(&self, storage: &dyn Storage) -> StdResult<Option<UpgradePlan>> {
84        PLANNED_UPGRADE.may_load(storage)
85    }
86
87    pub fn get_params(&self, storage: &dyn Storage) -> StdResult<Vec<(String, String)>> {
88        PARAMS.range(storage, None, None, Ascending).collect()
89    }
90
91    fn require_privilege(
92        &self,
93        storage: &dyn Storage,
94        addr: &Addr,
95        required: Privilege,
96    ) -> AnyResult<()> {
97        let allowed = PRIVILEGES
98            .may_load(storage, addr)?
99            .unwrap_or_default()
100            .into_iter()
101            .any(|p| p == required);
102        if !allowed {
103            return Err(TgradeError::Unauthorized("Admin privileges required".to_owned()).into());
104        }
105        Ok(())
106    }
107}
108
109impl Module for TgradeModule {
110    type ExecT = TgradeMsg;
111    type QueryT = TgradeQuery;
112    type SudoT = Empty;
113
114    fn execute<ExecC, QueryC>(
115        &self,
116        api: &dyn Api,
117        storage: &mut dyn Storage,
118        router: &dyn CosmosRouter<ExecC = ExecC, QueryC = QueryC>,
119        block: &BlockInfo,
120        sender: Addr,
121        msg: TgradeMsg,
122    ) -> AnyResult<AppResponse>
123    where
124        ExecC: Debug + Clone + PartialEq + JsonSchema + DeserializeOwned + 'static,
125        QueryC: CustomQuery + DeserializeOwned + 'static,
126    {
127        match msg {
128            TgradeMsg::Privilege(PrivilegeMsg::Request(add)) => {
129                if add == Privilege::ValidatorSetUpdater {
130                    // there can be only one with ValidatorSetUpdater privilege
131                    let validator_registered =
132                        PRIVILEGES
133                            .range(storage, None, None, Order::Ascending)
134                            .fold(Ok(false), |val, item| match (val, item) {
135                                (Err(e), _) => Err(e),
136                                (_, Err(e)) => Err(e),
137                                (Ok(found), Ok((_, privs))) => Ok(found
138                                    || privs.iter().any(|p| *p == Privilege::ValidatorSetUpdater)),
139                            })?;
140                    if validator_registered {
141                        bail!(
142                            "One ValidatorSetUpdater already registered, cannot register a second"
143                        );
144                    }
145                }
146
147                // if we are privileged (even an empty array), we can auto-add more
148                let mut powers = PRIVILEGES.may_load(storage, &sender)?.ok_or_else(|| {
149                    TgradeError::Unauthorized("Admin privileges required".to_owned())
150                })?;
151                powers.push(add);
152                PRIVILEGES.save(storage, &sender, &powers)?;
153                Ok(AppResponse::default())
154            }
155            TgradeMsg::Privilege(PrivilegeMsg::Release(remove)) => {
156                let powers = PRIVILEGES.may_load(storage, &sender)?;
157                if let Some(powers) = powers {
158                    let updated = powers.into_iter().filter(|p| *p != remove).collect();
159                    PRIVILEGES.save(storage, &sender, &updated)?;
160                }
161                Ok(AppResponse::default())
162            }
163            TgradeMsg::WasmSudo { contract_addr, msg } => {
164                self.require_privilege(storage, &sender, Privilege::Sudoer)?;
165                let contract_addr = api.addr_validate(&contract_addr)?;
166                let sudo = WasmSudo { contract_addr, msg };
167                router.sudo(api, storage, block, sudo.into())
168            }
169            TgradeMsg::ConsensusParams(_) => {
170                // We don't do anything here
171                self.require_privilege(storage, &sender, Privilege::ConsensusParamChanger)?;
172                Ok(AppResponse::default())
173            }
174            TgradeMsg::ExecuteGovProposal {
175                title: _,
176                description: _,
177                proposal,
178            } => {
179                self.require_privilege(storage, &sender, Privilege::GovProposalExecutor)?;
180                match proposal {
181                    GovProposal::PromoteToPrivilegedContract { contract } => {
182                        // update contract state
183                        let contract_addr = api.addr_validate(&contract)?;
184                        PRIVILEGES.update(storage, &contract_addr, |current| -> StdResult<_> {
185                            // if nothing is set, make it an empty array
186                            Ok(current.unwrap_or_default())
187                        })?;
188
189                        // call into contract
190                        let msg = to_binary(&TgradeSudoMsg::<Empty>::PrivilegeChange(
191                            PrivilegeChangeMsg::Promoted {},
192                        ))?;
193                        let sudo = WasmSudo { contract_addr, msg };
194                        router.sudo(api, storage, block, sudo.into())
195                    }
196                    GovProposal::DemotePrivilegedContract { contract } => {
197                        // remove contract privileges
198                        let contract_addr = api.addr_validate(&contract)?;
199                        PRIVILEGES.remove(storage, &contract_addr);
200
201                        // call into contract
202                        let msg = to_binary(&TgradeSudoMsg::<Empty>::PrivilegeChange(
203                            PrivilegeChangeMsg::Demoted {},
204                        ))?;
205                        let sudo = WasmSudo { contract_addr, msg };
206                        router.sudo(api, storage, block, sudo.into())
207                    }
208                    GovProposal::PinCodes { code_ids } => {
209                        let mut pinned = PINNED.may_load(storage)?.unwrap_or_default();
210                        pinned.extend(code_ids);
211                        pinned.sort_unstable();
212                        pinned.dedup();
213                        PINNED.save(storage, &pinned)?;
214
215                        Ok(AppResponse::default())
216                    }
217                    GovProposal::UnpinCodes { code_ids } => {
218                        let pinned = PINNED
219                            .may_load(storage)?
220                            .unwrap_or_default()
221                            .into_iter()
222                            .filter(|id| !code_ids.contains(id))
223                            .collect();
224                        PINNED.save(storage, &pinned)?;
225
226                        Ok(AppResponse::default())
227                    }
228                    GovProposal::RegisterUpgrade { name, height, info } => {
229                        match PLANNED_UPGRADE.may_load(storage)? {
230                            Some(_) => Err(anyhow::anyhow!("an upgrade plan already exists")),
231                            None => {
232                                PLANNED_UPGRADE
233                                    .save(storage, &UpgradePlan::new(name, height, info))?;
234                                Ok(AppResponse::default())
235                            }
236                        }
237                    }
238                    GovProposal::CancelUpgrade {} => match PLANNED_UPGRADE.may_load(storage)? {
239                        None => Err(anyhow::anyhow!("an upgrade plan doesn't exist")),
240                        Some(_) => {
241                            PLANNED_UPGRADE.remove(storage);
242                            Ok(AppResponse::default())
243                        }
244                    },
245                    // these are not yet implemented, but should be
246                    GovProposal::InstantiateContract { .. } => {
247                        bail!("GovProposal::InstantiateContract not implemented")
248                    }
249                    // these cannot be implemented, should fail
250                    GovProposal::MigrateContract { .. } => {
251                        bail!("GovProposal::MigrateContract not implemented")
252                    }
253                    GovProposal::ChangeParams(params) => {
254                        let mut sorted_params = params.clone();
255                        sorted_params.sort_unstable();
256                        sorted_params.dedup_by(|a, b| a.subspace == b.subspace && a.key == b.key);
257                        if sorted_params.len() < params.len() {
258                            return Err(anyhow::anyhow!(
259                                "duplicate subspace + keys in params vector"
260                            ));
261                        }
262                        for p in params {
263                            if p.subspace.is_empty() {
264                                return Err(anyhow::anyhow!("empty subspace key"));
265                            }
266                            if p.key.is_empty() {
267                                return Err(anyhow::anyhow!("empty key key"));
268                            }
269                            PARAMS.save(storage, format!("{}/{}", p.subspace, p.key), &p.value)?;
270                        }
271                        Ok(AppResponse::default())
272                    }
273                    // most are ignored
274                    _ => Ok(AppResponse::default()),
275                }
276            }
277            TgradeMsg::MintTokens {
278                denom,
279                amount,
280                recipient,
281            } => {
282                self.require_privilege(storage, &sender, Privilege::TokenMinter)?;
283                let mint = BankSudo::Mint {
284                    to_address: recipient,
285                    amount: vec![Coin { denom, amount }],
286                };
287                router.sudo(api, storage, block, mint.into())
288            }
289            TgradeMsg::Delegate {
290                funds: _funds,
291                staker: _staker,
292            } => {
293                self.require_privilege(storage, &sender, Privilege::Delegator)?;
294                // FIXME? We don't do anything here
295                Ok(AppResponse::default())
296            }
297            TgradeMsg::Undelegate {
298                funds: _funds,
299                recipient: _recipient,
300            } => {
301                self.require_privilege(storage, &sender, Privilege::Delegator)?;
302                // FIXME? We don't do anything here
303                Ok(AppResponse::default())
304            }
305        }
306    }
307
308    fn sudo<ExecC, QueryC>(
309        &self,
310        _api: &dyn Api,
311        _storage: &mut dyn Storage,
312        _router: &dyn CosmosRouter<ExecC = ExecC, QueryC = QueryC>,
313        _block: &BlockInfo,
314        _msg: Self::SudoT,
315    ) -> AnyResult<AppResponse>
316    where
317        ExecC: Debug + Clone + PartialEq + JsonSchema + DeserializeOwned + 'static,
318        QueryC: CustomQuery + DeserializeOwned + 'static,
319    {
320        bail!("sudo not implemented for TgradeModule")
321    }
322
323    fn query(
324        &self,
325        _api: &dyn Api,
326        storage: &dyn Storage,
327        _querier: &dyn Querier,
328        _block: &BlockInfo,
329        request: TgradeQuery,
330    ) -> anyhow::Result<Binary> {
331        match request {
332            TgradeQuery::ListPrivileged(check) => {
333                // FIXME: secondary index to make this more efficient
334                let privileged = PRIVILEGES
335                    .range(storage, None, None, Order::Ascending)
336                    .filter_map(|r| {
337                        r.map(|(addr, privs)| match privs.iter().any(|p| *p == check) {
338                            true => Some(addr),
339                            false => None,
340                        })
341                        .transpose()
342                    })
343                    .collect::<StdResult<Vec<_>>>()?;
344                Ok(to_binary(&ListPrivilegedResponse { privileged })?)
345            }
346            TgradeQuery::ValidatorVotes {} => {
347                let res = VOTES.may_load(storage)?.unwrap_or_default();
348                Ok(to_binary(&res)?)
349            }
350        }
351    }
352}
353
354#[derive(Error, Debug, PartialEq)]
355pub enum TgradeError {
356    #[error("{0}")]
357    Std(#[from] StdError),
358
359    #[error("Unauthorized: {0}")]
360    Unauthorized(String),
361}
362
363pub type TgradeAppWrapped =
364    App<BankKeeper, MockApi, MockStorage, TgradeModule, WasmKeeper<TgradeMsg, TgradeQuery>>;
365
366pub struct TgradeApp(TgradeAppWrapped);
367
368impl Deref for TgradeApp {
369    type Target = TgradeAppWrapped;
370
371    fn deref(&self) -> &Self::Target {
372        &self.0
373    }
374}
375
376impl DerefMut for TgradeApp {
377    fn deref_mut(&mut self) -> &mut Self::Target {
378        &mut self.0
379    }
380}
381
382impl Querier for TgradeApp {
383    fn raw_query(&self, bin_request: &[u8]) -> QuerierResult {
384        self.0.raw_query(bin_request)
385    }
386}
387
388impl TgradeApp {
389    pub fn new(owner: &str) -> Self {
390        let owner = Addr::unchecked(owner);
391        Self(
392            BasicAppBuilder::<TgradeMsg, TgradeQuery>::new_custom()
393                .with_custom(TgradeModule {})
394                .build(|router, _, storage| {
395                    router.custom.set_owner(storage, &owner).unwrap();
396                }),
397        )
398    }
399
400    pub fn new_genesis(owner: &str) -> Self {
401        let owner = Addr::unchecked(owner);
402        let block_info = BlockInfo {
403            height: 0,
404            time: Timestamp::from_nanos(1_571_797_419_879_305_533),
405            chain_id: "tgrade-testnet-14002".to_owned(),
406        };
407
408        Self(
409            BasicAppBuilder::<TgradeMsg, TgradeQuery>::new_custom()
410                .with_custom(TgradeModule {})
411                .with_block(block_info)
412                .build(|router, _, storage| {
413                    router.custom.set_owner(storage, &owner).unwrap();
414                }),
415        )
416    }
417
418    pub fn block_info(&self) -> BlockInfo {
419        self.0.block_info()
420    }
421
422    pub fn promote(&mut self, owner: &str, contract: &str) -> AnyResult<AppResponse> {
423        let msg = TgradeMsg::ExecuteGovProposal {
424            title: "Promote Contract".to_string(),
425            description: "Promote Contract".to_string(),
426            proposal: GovProposal::PromoteToPrivilegedContract {
427                contract: contract.to_string(),
428            },
429        };
430        self.execute(Addr::unchecked(owner), msg.into())
431    }
432
433    /// This reverses to genesis (based on current time/height)
434    pub fn back_to_genesis(&mut self) {
435        self.update_block(|block| {
436            block.time = block.time.minus_seconds(BLOCK_TIME * block.height);
437            block.height = 0;
438        });
439    }
440
441    /// This advances BlockInfo by given number of blocks.
442    /// It does not do any callbacks, but keeps the ratio of seconds/blokc
443    pub fn advance_blocks(&mut self, blocks: u64) {
444        self.update_block(|block| {
445            block.time = block.time.plus_seconds(BLOCK_TIME * blocks);
446            block.height += blocks;
447        });
448    }
449
450    /// This advances BlockInfo by given number of seconds.
451    /// It does not do any callbacks, but keeps the ratio of seconds/blokc
452    pub fn advance_seconds(&mut self, seconds: u64) {
453        self.update_block(|block| {
454            block.time = block.time.plus_seconds(seconds);
455            block.height += max(1, seconds / BLOCK_TIME);
456        });
457    }
458
459    /// next_block will call the end_blocker, increment block info 1 height and 5 seconds,
460    /// and then call the begin_blocker (with no evidence) in the next block.
461    /// It returns the validator diff if any.
462    ///
463    /// Simple iterator when you don't care too much about the details and just want to
464    /// simulate forward motion.
465    pub fn next_block(&mut self) -> AnyResult<Option<ValidatorDiff>> {
466        let (_, diff) = self.end_block()?;
467        self.update_block(|block| {
468            block.time = block.time.plus_seconds(BLOCK_TIME);
469            block.height += 1;
470        });
471        self.begin_block(vec![])?;
472        Ok(diff)
473    }
474
475    /// Returns a list of all contracts that have the requested privilege
476    pub fn with_privilege(&self, requested: Privilege) -> AnyResult<Vec<Addr>> {
477        let ListPrivilegedResponse { privileged } = self
478            .wrap()
479            .query(&TgradeQuery::ListPrivileged(requested).into())?;
480        Ok(privileged)
481    }
482
483    fn valset_updater(&self) -> AnyResult<Option<Addr>> {
484        let mut updaters = self.with_privilege(Privilege::ValidatorSetUpdater)?;
485        if updaters.len() > 1 {
486            bail!("Multiple ValidatorSetUpdater registered")
487        } else {
488            Ok(updaters.pop())
489        }
490    }
491
492    /// Make the BeginBlock sudo callback on all contracts that have registered
493    /// with the BeginBlocker Privilege
494    pub fn begin_block(&mut self, evidence: Vec<Evidence>) -> AnyResult<Vec<AppResponse>> {
495        let to_call = self.with_privilege(Privilege::BeginBlocker)?;
496        let msg = TgradeSudoMsg::<Empty>::BeginBlock { evidence };
497        let res = to_call
498            .into_iter()
499            .map(|contract| self.wasm_sudo(contract, &msg))
500            .collect::<AnyResult<_>>()?;
501        Ok(res)
502    }
503
504    /// Make the EndBlock sudo callback on all contracts that have registered
505    /// with the EndBlocker Privilege. Then makes the EndWithValidatorUpdate callback
506    /// on any registered valset_updater.
507    pub fn end_block(&mut self) -> AnyResult<(Vec<AppResponse>, Option<ValidatorDiff>)> {
508        let to_call = self.with_privilege(Privilege::EndBlocker)?;
509        let msg = TgradeSudoMsg::<Empty>::EndBlock {};
510
511        let mut res: Vec<AppResponse> = to_call
512            .into_iter()
513            .map(|contract| self.wasm_sudo(contract, &msg))
514            .collect::<AnyResult<_>>()?;
515
516        let diff = match self.valset_updater()? {
517            Some(contract) => {
518                let mut r =
519                    self.wasm_sudo(contract, &TgradeSudoMsg::<Empty>::EndWithValidatorUpdate {})?;
520                let data = r.data.take();
521                res.push(r);
522                match data {
523                    Some(b) if !b.is_empty() => Some(from_slice(&b)?),
524                    _ => None,
525                }
526            }
527            None => None,
528        };
529        Ok((res, diff))
530    }
531}
532
533#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
534pub struct UpgradePlan {
535    name: String,
536    height: u64,
537    info: String,
538}
539
540impl UpgradePlan {
541    pub fn new(name: impl ToString, height: u64, info: impl ToString) -> Self {
542        Self {
543            name: name.to_string(),
544            height,
545            info: info.to_string(),
546        }
547    }
548}
549
550#[cfg(test)]
551mod tests {
552    use super::*;
553    use cosmwasm_std::coin;
554    use cw_multi_test::Executor;
555
556    #[test]
557    fn init_and_owner_mints_tokens() {
558        let owner = Addr::unchecked("govner");
559        let rcpt = Addr::unchecked("townies");
560
561        let mut app = TgradeApp::new(owner.as_str());
562
563        // no tokens
564        let start = app.wrap().query_all_balances(rcpt.as_str()).unwrap();
565        assert_eq!(start, vec![]);
566
567        // prepare to mint
568        let mintable = coin(123456, "shilling");
569        let msg = TgradeMsg::MintTokens {
570            denom: mintable.denom.clone(),
571            amount: mintable.amount,
572            recipient: rcpt.to_string(),
573        };
574
575        // townies cannot
576        let _ = app.execute(rcpt.clone(), msg.clone().into()).unwrap_err();
577
578        // Gov'ner can
579        app.execute(owner, msg.into()).unwrap();
580
581        // we got tokens!
582        let end = app
583            .wrap()
584            .query_balance(rcpt.as_str(), &mintable.denom)
585            .unwrap();
586        assert_eq!(end, mintable);
587    }
588
589    // TODO: Delegate / Undelegate tests
590}