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
16const WASMD_GAS_MULTIPLIER: u64 = 140_000;
19const GAS_COST_HUMANIZE: u64 = 4 * WASMD_GAS_MULTIPLIER;
21const GAS_COST_CANONICALIZE: u64 = 5 * WASMD_GAS_MULTIPLIER;
23
24const BECH32_PREFIX: &str = "cosmwasm";
26
27pub 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
37pub 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#[derive(Copy, Clone)]
53pub struct MockApi(MockApiImpl);
54
55#[derive(Copy, Clone)]
56enum MockApiImpl {
57 Error(&'static str),
60 Bech32 {
62 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 pub fn with_prefix(self, prefix: &'static str) -> Self {
88 Self(MockApiImpl::Bech32 {
89 bech32_prefix: prefix,
90 })
91 }
92
93 pub fn addr_make(&self, input: &str) -> String {
113 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 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 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
213fn 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
221pub 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
294pub 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 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 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 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 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]; 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 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}