use cosmwasm_std::testing::{digit_sum, riffle_shuffle};
use cosmwasm_std::{
Addr, BlockInfo, Coin, ContractInfo, Env, MessageInfo, Timestamp, TransactionInfo,
};
use super::querier::MockQuerier;
use super::storage::MockStorage;
use crate::{Backend, BackendApi, BackendError, BackendResult, GasInfo};
pub const MOCK_CONTRACT_ADDR: &str = "cosmos2contract";
const GAS_COST_HUMANIZE: u64 = 44;
const GAS_COST_CANONICALIZE: u64 = 55;
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),
}
}
const CANONICAL_LENGTH: usize = 64; const SHUFFLES_ENCODE: usize = 10;
const SHUFFLES_DECODE: usize = 2;
#[derive(Copy, Clone)]
pub struct MockApi {
canonical_length: usize,
backend_error: Option<&'static str>,
}
impl MockApi {
pub fn canonical_length(&self) -> usize {
self.canonical_length
}
pub fn new_failing(backend_error: &'static str) -> Self {
MockApi {
backend_error: Some(backend_error),
..MockApi::default()
}
}
}
impl Default for MockApi {
fn default() -> Self {
MockApi {
canonical_length: CANONICAL_LENGTH,
backend_error: None,
}
}
}
impl BackendApi for MockApi {
fn canonical_address(&self, input: &str) -> BackendResult<Vec<u8>> {
let normalized = input.to_lowercase();
let gas_info = GasInfo::with_cost(GAS_COST_CANONICALIZE);
if let Some(backend_error) = self.backend_error {
return (Err(BackendError::unknown(backend_error)), gas_info);
}
let min_length = 3;
let max_length = self.canonical_length;
if normalized.len() < min_length {
return (
Err(BackendError::user_err(
format!("Invalid input: human address too short for this mock implementation (must be >= {min_length})."),
)),
gas_info,
);
}
if normalized.len() > max_length {
return (
Err(BackendError::user_err(
format!("Invalid input: human address too long for this mock implementation (must be <= {max_length})."),
)),
gas_info,
);
}
let mut out = Vec::from(normalized);
out.resize(self.canonical_length, 0x00);
let rotate_by = digit_sum(&out) % self.canonical_length;
out.rotate_left(rotate_by);
for _ in 0..SHUFFLES_ENCODE {
out = riffle_shuffle(&out);
}
(Ok(out), gas_info)
}
fn human_address(&self, canonical: &[u8]) -> BackendResult<String> {
let gas_info = GasInfo::with_cost(GAS_COST_HUMANIZE);
if let Some(backend_error) = self.backend_error {
return (Err(BackendError::unknown(backend_error)), gas_info);
}
if canonical.len() != self.canonical_length {
return (
Err(BackendError::user_err(
"Invalid input: canonical address length not correct",
)),
gas_info,
);
}
let mut tmp: Vec<u8> = canonical.into();
for _ in 0..SHUFFLES_DECODE {
tmp = riffle_shuffle(&tmp);
}
let rotate_by = digit_sum(&tmp) % self.canonical_length;
tmp.rotate_right(rotate_by);
let trimmed = tmp.into_iter().filter(|&x| x != 0x00).collect();
let result = match String::from_utf8(trimmed) {
Ok(human) => Ok(human),
Err(err) => Err(err.into()),
};
(result, gas_info)
}
}
pub fn mock_env() -> Env {
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(MOCK_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 crate::BackendError;
use cosmwasm_std::coins;
#[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 canonical_address_works() {
let api = MockApi::default();
api.canonical_address("foobar123").0.unwrap();
let data1 = api.canonical_address("foo123").0.unwrap();
let data2 = api.canonical_address("FOO123").0.unwrap();
assert_eq!(data1, data2);
}
#[test]
fn canonicalize_and_humanize_restores_original() {
let api = MockApi::default();
let original = "shorty";
let canonical = api.canonical_address(original).0.unwrap();
let (recovered, _gas_cost) = api.human_address(&canonical);
assert_eq!(recovered.unwrap(), original);
let original = String::from("CosmWasmChef");
let canonical = api.canonical_address(&original).0.unwrap();
let recovered = api.human_address(&canonical).0.unwrap();
assert_eq!(recovered, "cosmwasmchef");
let original =
String::from("juno1v82su97skv6ucfqvuvswe0t5fph7pfsrtraxf0x33d8ylj5qnrysdvkc95");
let canonical = api.canonical_address(&original).0.unwrap();
let recovered = api.human_address(&canonical).0.unwrap();
assert_eq!(recovered, original);
}
#[test]
fn human_address_input_length() {
let api = MockApi::default();
let input = vec![61; 11];
let (result, _gas_info) = api.human_address(&input);
match result.unwrap_err() {
BackendError::UserErr { .. } => {}
err => panic!("Unexpected error: {:?}", err),
}
}
#[test]
fn canonical_address_min_input_length() {
let api = MockApi::default();
let human = "1";
match api.canonical_address(human).0.unwrap_err() {
BackendError::UserErr { msg } => assert!(msg.contains("too short")),
err => panic!("Unexpected error: {:?}", err),
}
}
#[test]
fn canonical_address_max_input_length() {
let api = MockApi::default();
let human = "longer-than-the-address-length-supported-by-this-api-longer-than-54";
match api.canonical_address(human).0.unwrap_err() {
BackendError::UserErr { msg } => assert!(msg.contains("too long")),
err => panic!("Unexpected error: {:?}", err),
}
}
}