use bech32::primitives::decode::CheckedHrpstring;
use bech32::{encode, Bech32, Hrp};
use cosmwasm_std::{
Addr, BlockInfo, Coin, ContractInfo, Env, MessageInfo, Timestamp, TransactionInfo,
};
use sha2::{Digest, Sha256};
use super::querier::MockQuerier;
use super::storage::MockStorage;
use crate::backend::unwrap_or_return_with_gas;
use crate::{Backend, BackendApi, BackendError, BackendResult, GasInfo};
pub const MOCK_CONTRACT_ADDR: &str =
"cosmwasm1jpev2csrppg792t22rn8z8uew8h3sjcpglcd0qv9g8gj8ky922tscp8avs";
const WASMD_GAS_MULTIPLIER: u64 = 140_000;
const GAS_COST_HUMANIZE: u64 = 4 * WASMD_GAS_MULTIPLIER;
const GAS_COST_CANONICALIZE: u64 = 5 * WASMD_GAS_MULTIPLIER;
const BECH32_PREFIX: &str = "cosmwasm";
pub fn mock_backend(contract_balance: &[Coin]) -> Backend<MockApi, MockStorage, MockQuerier> {
Backend {
api: MockApi::default(),
storage: MockStorage::default(),
querier: MockQuerier::new(&[(MOCK_CONTRACT_ADDR, contract_balance)]),
}
}
pub fn mock_backend_with_balances(
balances: &[(&str, &[Coin])],
) -> Backend<MockApi, MockStorage, MockQuerier> {
Backend {
api: MockApi::default(),
storage: MockStorage::default(),
querier: MockQuerier::new(balances),
}
}
#[derive(Copy, Clone)]
pub struct MockApi(MockApiImpl);
#[derive(Copy, Clone)]
enum MockApiImpl {
Error(&'static str),
Bech32 {
bech32_prefix: &'static str,
},
}
impl MockApi {
pub fn new_failing(backend_error: &'static str) -> Self {
Self(MockApiImpl::Error(backend_error))
}
pub fn with_prefix(self, prefix: &'static str) -> Self {
Self(MockApiImpl::Bech32 {
bech32_prefix: prefix,
})
}
pub fn addr_make(&self, input: &str) -> String {
let bech32_prefix = match self.0 {
MockApiImpl::Error(e) => panic!("Generating address failed: {e}"),
MockApiImpl::Bech32 { bech32_prefix } => bech32_prefix,
};
let digest = Sha256::digest(input);
let bech32_prefix = Hrp::parse(bech32_prefix).expect("Invalid prefix");
match encode::<Bech32>(bech32_prefix, &digest) {
Ok(address) => address,
Err(reason) => panic!("Generating address failed with reason: {reason}"),
}
}
}
impl Default for MockApi {
fn default() -> Self {
Self(MockApiImpl::Bech32 {
bech32_prefix: BECH32_PREFIX,
})
}
}
impl BackendApi for MockApi {
fn addr_validate(&self, input: &str) -> BackendResult<()> {
let mut gas_total = GasInfo {
cost: 0,
externally_used: 0,
};
let (canonicalize_res, gas_info) = self.addr_canonicalize(input);
gas_total += gas_info;
let canonical = unwrap_or_return_with_gas!(canonicalize_res, gas_total);
let (humanize_res, gas_info) = self.addr_humanize(&canonical);
gas_total += gas_info;
let normalized = unwrap_or_return_with_gas!(humanize_res, gas_total);
if input != normalized.as_str() {
return (
Err(BackendError::user_err(
"Invalid input: address not normalized",
)),
gas_total,
);
}
(Ok(()), gas_total)
}
fn addr_canonicalize(&self, input: &str) -> BackendResult<Vec<u8>> {
let gas_total = GasInfo::with_cost(GAS_COST_CANONICALIZE);
let bech32_prefix = match self.0 {
MockApiImpl::Error(e) => return (Err(BackendError::unknown(e)), gas_total),
MockApiImpl::Bech32 { bech32_prefix } => bech32_prefix,
};
let hrp_str = unwrap_or_return_with_gas!(
CheckedHrpstring::new::<Bech32>(input)
.map_err(|_| BackendError::user_err("Error decoding bech32")),
gas_total
);
if !hrp_str
.hrp()
.as_bytes()
.eq_ignore_ascii_case(bech32_prefix.as_bytes())
{
return (
Err(BackendError::user_err("Wrong bech32 prefix")),
gas_total,
);
}
let bytes: Vec<u8> = hrp_str.byte_iter().collect();
unwrap_or_return_with_gas!(validate_length(&bytes), gas_total);
(Ok(bytes), gas_total)
}
fn addr_humanize(&self, canonical: &[u8]) -> BackendResult<String> {
let gas_total = GasInfo::with_cost(GAS_COST_HUMANIZE);
let bech32_prefix = match self.0 {
MockApiImpl::Error(e) => return (Err(BackendError::unknown(e)), gas_total),
MockApiImpl::Bech32 { bech32_prefix } => bech32_prefix,
};
unwrap_or_return_with_gas!(validate_length(canonical), gas_total);
let bech32_prefix = unwrap_or_return_with_gas!(
Hrp::parse(bech32_prefix).map_err(|_| BackendError::user_err("Invalid bech32 prefix")),
gas_total
);
let result = encode::<Bech32>(bech32_prefix, canonical)
.map_err(|_| BackendError::user_err("Invalid data to be encoded to bech32"));
(result, gas_total)
}
}
fn validate_length(bytes: &[u8]) -> Result<(), BackendError> {
match bytes.len() {
1..=255 => Ok(()),
_ => Err(BackendError::user_err("Invalid canonical address length")),
}
}
pub fn mock_env() -> Env {
let contract_addr = MockApi::default().addr_make("cosmos2contract");
Env {
block: BlockInfo {
height: 12_345,
time: Timestamp::from_nanos(1_571_797_419_879_305_533),
chain_id: "cosmos-testnet-14002".to_string(),
},
transaction: Some(TransactionInfo { index: 3 }),
contract: ContractInfo {
address: Addr::unchecked(contract_addr),
},
}
}
pub fn mock_info(sender: &str, funds: &[Coin]) -> MessageInfo {
MessageInfo {
sender: Addr::unchecked(sender),
funds: funds.to_vec(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use cosmwasm_std::coins;
#[test]
fn mock_env_matches_mock_contract_addr() {
let contract_address = mock_env().contract.address;
assert_eq!(contract_address, Addr::unchecked(MOCK_CONTRACT_ADDR));
}
#[test]
fn mock_info_works() {
let info = mock_info("my name", &coins(100, "atom"));
assert_eq!(
info,
MessageInfo {
sender: Addr::unchecked("my name"),
funds: vec![Coin {
amount: 100u128.into(),
denom: "atom".into(),
}]
}
);
}
#[test]
fn addr_canonicalize_works() {
let api = MockApi::default().with_prefix("osmo");
api.addr_canonicalize("osmo186kh7c0k0gh4ww0wh4jqc4yhzu7n7dhswe845d")
.0
.unwrap();
let data1 = api
.addr_canonicalize("osmo186kh7c0k0gh4ww0wh4jqc4yhzu7n7dhswe845d")
.0
.unwrap();
let data2 = api
.addr_canonicalize("OSMO186KH7C0K0GH4WW0WH4JQC4YHZU7N7DHSWE845D")
.0
.unwrap();
assert_eq!(data1, data2);
}
#[test]
fn canonicalize_and_humanize_restores_original() {
let api = MockApi::default().with_prefix("juno");
let original = api.addr_make("shorty");
let canonical = api.addr_canonicalize(&original).0.unwrap();
let (recovered, _gas_cost) = api.addr_humanize(&canonical);
assert_eq!(recovered.unwrap(), original);
let original = "JUNO1MEPRU9FUQ4E65856ARD6068MFSFRWPGEMD0C3R";
let canonical = api.addr_canonicalize(original).0.unwrap();
let recovered = api.addr_humanize(&canonical).0.unwrap();
assert_eq!(recovered, original.to_lowercase());
let original =
String::from("juno1v82su97skv6ucfqvuvswe0t5fph7pfsrtraxf0x33d8ylj5qnrysdvkc95");
let canonical = api.addr_canonicalize(&original).0.unwrap();
let recovered = api.addr_humanize(&canonical).0.unwrap();
assert_eq!(recovered, original);
}
#[test]
fn addr_humanize_input_length() {
let api = MockApi::default();
let input = vec![61; 256]; let (result, _gas_info) = api.addr_humanize(&input);
match result.unwrap_err() {
BackendError::UserErr { .. } => {}
err => panic!("Unexpected error: {err:?}"),
}
}
#[test]
fn addr_canonicalize_min_input_length() {
let api = MockApi::default();
let empty = "cosmwasm1pj90vm";
assert!(matches!(api
.addr_canonicalize(empty)
.0
.unwrap_err(),
BackendError::UserErr { msg } if msg.contains("address length")));
}
#[test]
fn addr_canonicalize_max_input_length() {
let api = MockApi::default();
let too_long = "cosmwasm1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqehqqkz";
assert!(matches!(api
.addr_canonicalize(too_long)
.0
.unwrap_err(),
BackendError::UserErr { msg } if msg.contains("address length")));
}
#[test]
fn colon_in_prefix_is_valid() {
let mock_api = MockApi::default().with_prefix("did:com:");
let bytes = mock_api
.addr_canonicalize("did:com:1jkf0kmeyefvyzpwf56m7sne2000ay53r6upttu")
.0
.unwrap();
let humanized = mock_api.addr_humanize(&bytes).0.unwrap();
assert_eq!(
humanized.as_str(),
"did:com:1jkf0kmeyefvyzpwf56m7sne2000ay53r6upttu"
);
}
}