bvs_library/testing/
contract.rs

1use cosmwasm_std::{to_json_binary, Addr, Coin, Empty, Env, StdResult, Storage, Uint128, WasmMsg};
2use cw20::{Cw20Coin, MinterResponse};
3use cw20_base;
4use cw_multi_test::error::AnyResult;
5use cw_multi_test::{App, AppResponse, Contract, ContractWrapper, Executor};
6use serde::de::DeserializeOwned;
7use serde::{Deserialize, Serialize};
8use std::fmt::Debug;
9
10/// TestingContract is a trait that provides a common interface for setting up testing contracts.
11pub trait TestingContract<IM, EM, QM, MM = Empty>
12where
13    IM: serde::Serialize,
14    EM: serde::Serialize + Debug,
15    QM: serde::Serialize,
16    MM: serde::Serialize,
17{
18    fn wrapper() -> Box<dyn Contract<Empty>>;
19
20    fn default_init(app: &mut App, env: &Env) -> IM;
21
22    fn new(app: &mut App, env: &Env, msg: Option<IM>) -> Self;
23
24    fn store_code(app: &mut App) -> u64 {
25        app.store_code(Self::wrapper())
26    }
27
28    fn instantiate(app: &mut App, code_id: u64, label: &str, msg: &IM) -> Addr {
29        let admin = app.api().addr_make("admin");
30        let addr = app
31            .instantiate_contract(
32                code_id,
33                app.api().addr_make("sender"),
34                msg,
35                &[],
36                label,
37                Some(admin.to_string()),
38            )
39            .unwrap();
40        Self::set_contract_addr(app, label, &addr);
41        addr
42    }
43
44    /// Set the contract address in the storage for the given label.
45    /// Using the storage system for easy orchestration of contract addresses for testing.
46    fn set_contract_addr(app: &mut App, label: &str, addr: &Addr) {
47        let key = format!("CONTRACT:{label}");
48        let value = String::from_utf8(addr.as_bytes().to_vec()).unwrap();
49        app.storage_mut().set(key.as_bytes(), value.as_bytes());
50    }
51
52    /// Get the contract address in the storage for the given label.
53    fn get_contract_addr(app: &App, label: &str) -> Addr {
54        let key = format!("CONTRACT:{label}");
55        match app.storage().get(key.as_bytes()) {
56            Some(value) => Addr::unchecked(String::from_utf8(value).unwrap()),
57            None => app.api().addr_make(key.as_str()), // fallback to dummy address
58        }
59    }
60
61    fn addr(&self) -> &Addr;
62
63    fn execute(&self, app: &mut App, sender: &Addr, msg: &EM) -> AnyResult<AppResponse> {
64        self.execute_with_funds(app, sender, msg, vec![])
65    }
66
67    fn execute_with_funds(
68        &self,
69        app: &mut App,
70        sender: &Addr,
71        msg: &EM,
72        funds: Vec<Coin>,
73    ) -> AnyResult<AppResponse> {
74        app.execute_contract(sender.clone(), self.addr().clone(), msg, &funds)
75    }
76
77    fn query<T: DeserializeOwned>(&self, app: &App, msg: &QM) -> StdResult<T> {
78        app.wrap().query_wasm_smart(self.addr(), &msg)
79    }
80
81    fn migrate(&self, app: &mut App, sender: &Addr, msg: &MM) -> AnyResult<AppResponse> {
82        let msg_bin = to_json_binary(&msg).expect("cannot serialize MigrateMsg");
83        let code_id = Self::store_code(app);
84        let migrate_msg = WasmMsg::Migrate {
85            contract_addr: self.addr().to_string(),
86            new_code_id: code_id,
87            msg: msg_bin,
88        };
89
90        app.execute(sender.clone(), migrate_msg.into())
91    }
92}
93
94#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
95pub struct Cw20TokenContract {
96    pub addr: Addr,
97    pub init: cw20_base::msg::InstantiateMsg,
98}
99
100impl
101    TestingContract<
102        cw20_base::msg::InstantiateMsg,
103        cw20_base::msg::ExecuteMsg,
104        cw20_base::msg::QueryMsg,
105    > for Cw20TokenContract
106{
107    fn wrapper() -> Box<dyn Contract<Empty>> {
108        Box::new(ContractWrapper::new(
109            cw20_base::contract::execute,
110            cw20_base::contract::instantiate,
111            cw20_base::contract::query,
112        ))
113    }
114
115    fn default_init(app: &mut App, _env: &Env) -> cw20_base::msg::InstantiateMsg {
116        cw20_base::msg::InstantiateMsg {
117            symbol: "SATL".to_string(),
118            name: "Satlayer Test Token".to_string(),
119            decimals: 18,
120            initial_balances: vec![Cw20Coin {
121                address: app.api().addr_make("owner").to_string(),
122                amount: Uint128::new(1_000_000e18 as u128),
123            }],
124            mint: Some(MinterResponse {
125                minter: app.api().addr_make("owner").to_string(),
126                cap: Some(Uint128::new(1_000_000_000e18 as u128)), // 1000e18 = 1e21
127            }),
128            marketing: None,
129        }
130    }
131
132    fn new(app: &mut App, env: &Env, msg: Option<cw20_base::msg::InstantiateMsg>) -> Self {
133        let init = msg.unwrap_or(Self::default_init(app, env));
134        let code_id = Self::store_code(app);
135        let addr = Self::instantiate(app, code_id, "cw20", &init);
136        Self { addr, init }
137    }
138
139    fn addr(&self) -> &Addr {
140        &self.addr
141    }
142}
143
144impl Cw20TokenContract {
145    /// For testing with pre-approved spending for x address.
146    pub fn increase_allowance(&self, app: &mut App, sender: &Addr, spender: &Addr, amount: u128) {
147        let msg = &cw20_base::msg::ExecuteMsg::IncreaseAllowance {
148            spender: spender.to_string(),
149            amount: Uint128::new(amount),
150            expires: None,
151        };
152        self.execute(app, sender, msg).unwrap();
153    }
154
155    /// Fund a recipient with some tokens
156    pub fn fund(&self, app: &mut App, recipient: &Addr, amount: u128) {
157        let owner = Addr::unchecked(&self.init.initial_balances[0].address);
158        let msg = &cw20_base::msg::ExecuteMsg::Transfer {
159            recipient: recipient.to_string(),
160            amount: Uint128::new(amount),
161        };
162        self.execute(app, &owner, msg).unwrap();
163    }
164
165    pub fn transfer(&self, app: &mut App, sender: &Addr, recipient: &Addr, amount: u128) {
166        let msg = &cw20_base::msg::ExecuteMsg::Transfer {
167            recipient: recipient.to_string(),
168            amount: Uint128::new(amount),
169        };
170        self.execute(app, sender, msg).unwrap();
171    }
172
173    pub fn balance(&self, app: &App, address: &Addr) -> u128 {
174        let query = cw20_base::msg::QueryMsg::Balance {
175            address: address.to_string(),
176        };
177        let res: cw20::BalanceResponse = self.query(app, &query).unwrap();
178        res.balance.into()
179    }
180}