cosmwasm_vm/testing/
mock.rs

1use bech32::primitives::decode::CheckedHrpstring;
2use bech32::{encode, Bech32, Hrp};
3use cosmwasm_std::{
4    Addr, Binary, BlockInfo, Coin, ContractInfo, Env, MessageInfo, Timestamp, TransactionInfo,
5};
6use sha2::{Digest, Sha256};
7
8use super::querier::MockQuerier;
9use super::storage::MockStorage;
10use crate::backend::unwrap_or_return_with_gas;
11use crate::{Backend, BackendApi, BackendError, BackendResult, GasInfo};
12
13pub const MOCK_CONTRACT_ADDR: &str =
14    "cosmwasm1jpev2csrppg792t22rn8z8uew8h3sjcpglcd0qv9g8gj8ky922tscp8avs";
15
16/// Default gas multiplier in wasmd.
17/// See https://github.com/CosmWasm/wasmd/blob/v0.51.0/x/wasm/types/gas_register.go#L34
18const WASMD_GAS_MULTIPLIER: u64 = 140_000;
19/// See https://github.com/CosmWasm/wasmd/blob/v0.51.0/x/wasm/keeper/api.go#L27
20const GAS_COST_HUMANIZE: u64 = 4 * WASMD_GAS_MULTIPLIER;
21/// See https://github.com/CosmWasm/wasmd/blob/v0.51.0/x/wasm/keeper/api.go#L28
22const GAS_COST_CANONICALIZE: u64 = 5 * WASMD_GAS_MULTIPLIER;
23
24/// Default prefix used when creating Bech32 encoded address.
25const BECH32_PREFIX: &str = "cosmwasm";
26
27/// All external requirements that can be injected for unit tests.
28/// It sets the given balance for the contract itself, nothing else
29pub fn mock_backend(contract_balance: &[Coin]) -> Backend<MockApi, MockStorage, MockQuerier> {
30    Backend {
31        api: MockApi::default(),
32        storage: MockStorage::default(),
33        querier: MockQuerier::new(&[(MOCK_CONTRACT_ADDR, contract_balance)]),
34    }
35}
36
37/// Initializes the querier along with the mock_dependencies.
38/// Sets all balances provided (you must explicitly set contract balance if desired)
39pub fn mock_backend_with_balances(
40    balances: &[(&str, &[Coin])],
41) -> Backend<MockApi, MockStorage, MockQuerier> {
42    Backend {
43        api: MockApi::default(),
44        storage: MockStorage::default(),
45        querier: MockQuerier::new(balances),
46    }
47}
48
49/// Zero-pads all human addresses to make them fit the canonical_length and
50/// trims off zeros for the reverse operation.
51/// This is not really smart, but allows us to see a difference (and consistent length for canonical addresses).
52#[derive(Copy, Clone)]
53pub struct MockApi(MockApiImpl);
54
55#[derive(Copy, Clone)]
56enum MockApiImpl {
57    /// With this variant, all calls to the API fail with BackendError::Unknown
58    /// containing the given message
59    Error(&'static str),
60    /// This variant implements Bech32 addresses.
61    Bech32 {
62        /// Prefix used for creating addresses in Bech32 encoding.
63        bech32_prefix: &'static str,
64    },
65}
66
67impl MockApi {
68    pub fn new_failing(backend_error: &'static str) -> Self {
69        Self(MockApiImpl::Error(backend_error))
70    }
71
72    /// Returns [MockApi] with Bech32 prefix set to provided value.
73    ///
74    /// Bech32 prefix must not be empty.
75    ///
76    /// # Example
77    ///
78    /// ```
79    /// # use cosmwasm_std::Addr;
80    /// # use cosmwasm_std::testing::MockApi;
81    /// #
82    /// let mock_api = MockApi::default().with_prefix("juno");
83    /// let addr = mock_api.addr_make("creator");
84    ///
85    /// assert_eq!(addr.as_str(), "juno1h34lmpywh4upnjdg90cjf4j70aee6z8qqfspugamjp42e4q28kqsksmtyp");
86    /// ```
87    pub fn with_prefix(self, prefix: &'static str) -> Self {
88        Self(MockApiImpl::Bech32 {
89            bech32_prefix: prefix,
90        })
91    }
92
93    /// Returns an address built from provided input string.
94    ///
95    /// # Example
96    ///
97    /// ```
98    /// # use cosmwasm_std::Addr;
99    /// # use cosmwasm_std::testing::MockApi;
100    /// #
101    /// let mock_api = MockApi::default();
102    /// let addr = mock_api.addr_make("creator");
103    ///
104    /// assert_eq!(addr.as_str(), "cosmwasm1h34lmpywh4upnjdg90cjf4j70aee6z8qqfspugamjp42e4q28kqs8s7vcp");
105    /// ```
106    ///
107    /// # Panics
108    ///
109    /// This function panics when generating a valid address is not possible,
110    /// especially when Bech32 prefix set in function [with_prefix](Self::with_prefix) is empty.
111    ///
112    pub fn addr_make(&self, input: &str) -> String {
113        // handle error case
114        let bech32_prefix = match self.0 {
115            MockApiImpl::Error(e) => panic!("Generating address failed: {e}"),
116            MockApiImpl::Bech32 { bech32_prefix } => bech32_prefix,
117        };
118
119        let digest = Sha256::digest(input);
120        let bech32_prefix = Hrp::parse(bech32_prefix).expect("Invalid prefix");
121        match encode::<Bech32>(bech32_prefix, &digest) {
122            Ok(address) => address,
123            Err(reason) => panic!("Generating address failed with reason: {reason}"),
124        }
125    }
126}
127
128impl Default for MockApi {
129    fn default() -> Self {
130        Self(MockApiImpl::Bech32 {
131            bech32_prefix: BECH32_PREFIX,
132        })
133    }
134}
135
136impl BackendApi for MockApi {
137    fn addr_validate(&self, input: &str) -> BackendResult<()> {
138        let mut gas_total = GasInfo {
139            cost: 0,
140            externally_used: 0,
141        };
142
143        let (canonicalize_res, gas_info) = self.addr_canonicalize(input);
144        gas_total += gas_info;
145        let canonical = unwrap_or_return_with_gas!(canonicalize_res, gas_total);
146
147        let (humanize_res, gas_info) = self.addr_humanize(&canonical);
148        gas_total += gas_info;
149        let normalized = unwrap_or_return_with_gas!(humanize_res, gas_total);
150        if input != normalized.as_str() {
151            return (
152                Err(BackendError::user_err(
153                    "Invalid input: address not normalized",
154                )),
155                gas_total,
156            );
157        }
158        (Ok(()), gas_total)
159    }
160
161    fn addr_canonicalize(&self, input: &str) -> BackendResult<Vec<u8>> {
162        let gas_total = GasInfo::with_cost(GAS_COST_CANONICALIZE);
163
164        // handle error case
165        let bech32_prefix = match self.0 {
166            MockApiImpl::Error(e) => return (Err(BackendError::unknown(e)), gas_total),
167            MockApiImpl::Bech32 { bech32_prefix } => bech32_prefix,
168        };
169
170        let hrp_str = unwrap_or_return_with_gas!(
171            CheckedHrpstring::new::<Bech32>(input)
172                .map_err(|_| BackendError::user_err("Error decoding bech32")),
173            gas_total
174        );
175
176        if !hrp_str
177            .hrp()
178            .as_bytes()
179            .eq_ignore_ascii_case(bech32_prefix.as_bytes())
180        {
181            return (
182                Err(BackendError::user_err("Wrong bech32 prefix")),
183                gas_total,
184            );
185        }
186
187        let bytes: Vec<u8> = hrp_str.byte_iter().collect();
188        unwrap_or_return_with_gas!(validate_length(&bytes), gas_total);
189        (Ok(bytes), gas_total)
190    }
191
192    fn addr_humanize(&self, canonical: &[u8]) -> BackendResult<String> {
193        let gas_total = GasInfo::with_cost(GAS_COST_HUMANIZE);
194
195        // handle error case
196        let bech32_prefix = match self.0 {
197            MockApiImpl::Error(e) => return (Err(BackendError::unknown(e)), gas_total),
198            MockApiImpl::Bech32 { bech32_prefix } => bech32_prefix,
199        };
200
201        unwrap_or_return_with_gas!(validate_length(canonical), gas_total);
202        let bech32_prefix = unwrap_or_return_with_gas!(
203            Hrp::parse(bech32_prefix).map_err(|_| BackendError::user_err("Invalid bech32 prefix")),
204            gas_total
205        );
206        let result = encode::<Bech32>(bech32_prefix, canonical)
207            .map_err(|_| BackendError::user_err("Invalid data to be encoded to bech32"));
208
209        (result, gas_total)
210    }
211}
212
213/// Does basic validation of the number of bytes in a canonical address
214fn validate_length(bytes: &[u8]) -> Result<(), BackendError> {
215    match bytes.len() {
216        1..=255 => Ok(()),
217        _ => Err(BackendError::user_err("Invalid canonical address length")),
218    }
219}
220
221/// Returns a default environment with height, time, chain_id, and contract address.
222/// You can submit as is to most contracts, or modify height/time if you want to
223/// test for expiration.
224///
225/// This is intended for use in test code only.
226///
227/// The contract address uses the same bech32 prefix as [`MockApi`](crate::testing::MockApi). While
228/// this is good for the majority of users, you might need to create your `Env`s
229/// differently if you need a valid address using a different prefix.
230///
231/// ## Examples
232///
233/// Create an env:
234///
235/// ```
236/// # use cosmwasm_std::{Addr, Binary, BlockInfo, ContractInfo, Env, Timestamp, TransactionInfo};
237/// use cosmwasm_vm::testing::mock_env;
238///
239/// let env = mock_env();
240/// assert_eq!(env, Env {
241///     block: BlockInfo {
242///         height: 12_345,
243///         time: Timestamp::from_nanos(1_571_797_419_879_305_533),
244///         chain_id: "cosmos-testnet-14002".to_string(),
245///     },
246///     transaction: Some(TransactionInfo::new(3, Binary::from_hex("E5469DACEC17CEF8A260FD37675ED87E7FB6A2B5AD95193C51308006C7E494B3").unwrap())),
247///     contract: ContractInfo {
248///         address: Addr::unchecked("cosmwasm1jpev2csrppg792t22rn8z8uew8h3sjcpglcd0qv9g8gj8ky922tscp8avs"),
249///     },
250/// });
251/// ```
252///
253/// Mutate and reuse environment:
254///
255/// ```
256/// # use cosmwasm_std::{Addr, BlockInfo, ContractInfo, Env, Timestamp, TransactionInfo};
257/// use cosmwasm_vm::testing::mock_env;
258///
259/// let env1 = mock_env();
260///
261/// // First test with `env1`
262///
263/// let mut env2 = env1.clone();
264/// env2.block.height += 1;
265/// env2.block.time = env1.block.time.plus_seconds(6);
266///
267/// // `env2` is one block and 6 seconds later
268///
269/// let mut env3 = env2.clone();
270/// env3.block.height += 1;
271/// env3.block.time = env2.block.time.plus_nanos(5_500_000_000);
272///
273/// // `env3` is one block and 5.5 seconds later
274/// ```
275pub fn mock_env() -> Env {
276    let contract_addr = MockApi::default().addr_make("cosmos2contract");
277    Env {
278        block: BlockInfo {
279            height: 12_345,
280            time: Timestamp::from_nanos(1_571_797_419_879_305_533),
281            chain_id: "cosmos-testnet-14002".to_string(),
282        },
283        transaction: Some(TransactionInfo::new(
284            3,
285            Binary::from_hex("E5469DACEC17CEF8A260FD37675ED87E7FB6A2B5AD95193C51308006C7E494B3")
286                .unwrap(),
287        )),
288        contract: ContractInfo {
289            address: Addr::unchecked(contract_addr),
290        },
291    }
292}
293
294/// Just set sender and funds for the message.
295/// This is intended for use in test code only.
296pub fn mock_info(sender: &str, funds: &[Coin]) -> MessageInfo {
297    MessageInfo {
298        sender: Addr::unchecked(sender),
299        funds: funds.to_vec(),
300    }
301}
302
303#[cfg(test)]
304mod tests {
305    use super::*;
306    use cosmwasm_std::coins;
307
308    #[test]
309    fn mock_env_matches_mock_contract_addr() {
310        let contract_address = mock_env().contract.address;
311        assert_eq!(contract_address, Addr::unchecked(MOCK_CONTRACT_ADDR));
312    }
313
314    #[test]
315    fn mock_info_works() {
316        let info = mock_info("my name", &coins(100, "atom"));
317        assert_eq!(
318            info,
319            MessageInfo {
320                sender: Addr::unchecked("my name"),
321                funds: vec![Coin {
322                    amount: 100u128.into(),
323                    denom: "atom".into(),
324                }]
325            }
326        );
327    }
328
329    #[test]
330    fn addr_canonicalize_works() {
331        let api = MockApi::default().with_prefix("osmo");
332
333        api.addr_canonicalize("osmo186kh7c0k0gh4ww0wh4jqc4yhzu7n7dhswe845d")
334            .0
335            .unwrap();
336
337        // is case insensitive
338        let data1 = api
339            .addr_canonicalize("osmo186kh7c0k0gh4ww0wh4jqc4yhzu7n7dhswe845d")
340            .0
341            .unwrap();
342        let data2 = api
343            .addr_canonicalize("OSMO186KH7C0K0GH4WW0WH4JQC4YHZU7N7DHSWE845D")
344            .0
345            .unwrap();
346        assert_eq!(data1, data2);
347    }
348
349    #[test]
350    fn canonicalize_and_humanize_restores_original() {
351        let api = MockApi::default().with_prefix("juno");
352
353        // simple
354        let original = api.addr_make("shorty");
355        let canonical = api.addr_canonicalize(&original).0.unwrap();
356        let (recovered, _gas_cost) = api.addr_humanize(&canonical);
357        assert_eq!(recovered.unwrap(), original);
358
359        // normalizes input
360        let original = "JUNO1MEPRU9FUQ4E65856ARD6068MFSFRWPGEMD0C3R";
361        let canonical = api.addr_canonicalize(original).0.unwrap();
362        let recovered = api.addr_humanize(&canonical).0.unwrap();
363        assert_eq!(recovered, original.to_lowercase());
364
365        // Long input (Juno contract address)
366        let original =
367            String::from("juno1v82su97skv6ucfqvuvswe0t5fph7pfsrtraxf0x33d8ylj5qnrysdvkc95");
368        let canonical = api.addr_canonicalize(&original).0.unwrap();
369        let recovered = api.addr_humanize(&canonical).0.unwrap();
370        assert_eq!(recovered, original);
371    }
372
373    #[test]
374    fn addr_humanize_input_length() {
375        let api = MockApi::default();
376        let input = vec![61; 256]; // too long
377        let (result, _gas_info) = api.addr_humanize(&input);
378        match result.unwrap_err() {
379            BackendError::UserErr { .. } => {}
380            err => panic!("Unexpected error: {err:?}"),
381        }
382    }
383
384    #[test]
385    fn addr_canonicalize_min_input_length() {
386        let api = MockApi::default();
387
388        // empty address should fail
389        let empty = "cosmwasm1pj90vm";
390        assert!(matches!(api
391            .addr_canonicalize(empty)
392            .0
393            .unwrap_err(),
394            BackendError::UserErr { msg } if msg.contains("address length")));
395    }
396
397    #[test]
398    fn addr_canonicalize_max_input_length() {
399        let api = MockApi::default();
400
401        let too_long = "cosmwasm1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqehqqkz";
402
403        assert!(matches!(api
404            .addr_canonicalize(too_long)
405            .0
406            .unwrap_err(),
407            BackendError::UserErr { msg } if msg.contains("address length")));
408    }
409
410    #[test]
411    fn colon_in_prefix_is_valid() {
412        let mock_api = MockApi::default().with_prefix("did:com:");
413        let bytes = mock_api
414            .addr_canonicalize("did:com:1jkf0kmeyefvyzpwf56m7sne2000ay53r6upttu")
415            .0
416            .unwrap();
417        let humanized = mock_api.addr_humanize(&bytes).0.unwrap();
418
419        assert_eq!(
420            humanized.as_str(),
421            "did:com:1jkf0kmeyefvyzpwf56m7sne2000ay53r6upttu"
422        );
423    }
424}