added better sample test assertions + fixed MockApi

This commit is contained in:
Jędrzej Stuczyński
2023-02-16 14:52:27 +00:00
parent 090efa7263
commit 66b2f1f051
9 changed files with 359 additions and 40 deletions
Generated
+2
View File
@@ -909,6 +909,7 @@ dependencies = [
"base64 0.21.0",
"cosmwasm-std",
"cosmwasm-storage",
"cw-storage-plus",
"hex",
"rand_chacha 0.3.1",
"serde",
@@ -2759,6 +2760,7 @@ version = "0.1.0"
dependencies = [
"cosmwasm-contract-testing",
"cosmwasm-std",
"cw-storage-plus",
"mixnet-contract",
"mixnet-contract-common",
"vesting-contract",
@@ -7,6 +7,7 @@ edition = "2021"
[dependencies]
cosmwasm-std = "1.0.0"
cw-storage-plus = "0.13.4"
cosmwasm-contract-testing = { path = "../testing" }
mixnet-contract-common = { path= "../mixnet-contract" }
vesting-contract-common = { path= "../vesting-contract" }
@@ -6,8 +6,11 @@ use cosmwasm_contract_testing::{
};
use cosmwasm_std::testing::mock_info;
use cosmwasm_std::{
coin, BlockInfo, Deps, DepsMut, Env, MessageInfo, QueryResponse, Response, Timestamp,
Addr, BankMsg, BlockInfo, Deps, DepsMut, Env, MessageInfo, QueryResponse, Response, Timestamp,
};
use cw_storage_plus::Map;
use mixnet_contract_common::rewarding::PendingRewardResponse;
use vesting_contract::vesting::Account;
struct VestingContract;
@@ -87,11 +90,15 @@ impl TestableContract for MixnetContract {
}
}
#[test]
fn multi_mock() {
let mixnet_contract_address = "n14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9sjyvg3g";
let vesting_contract_address = "n1nc5tatafv6eyq7llkr2gv50ff9e22mnf70qgjlv737ktmt4eswrq73f2nw";
// this is not directly exported by the vesting contract, but we can easily recreate it
const VESTING_ACCOUNTS: Map<'_, Addr, Account> = Map::new("acc");
const MIXNET_CONTRACT_ADDRESS: &str =
"n14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9sjyvg3g";
const VESTING_CONTRACT_ADDRESS: &str =
"n1nc5tatafv6eyq7llkr2gv50ff9e22mnf70qgjlv737ktmt4eswrq73f2nw";
fn set_mock() -> MultiContractMock {
let current_block = BlockInfo {
height: 1928125,
time: Timestamp::from_seconds(1676482616),
@@ -104,13 +111,13 @@ fn multi_mock() {
Some(custom_env.clone()),
)
.unwrap()
.with_contract_address(mixnet_contract_address);
.with_contract_address(MIXNET_CONTRACT_ADDRESS);
let vesting_mock = ContractState::try_from_state_dump(
"contract-states/15.02.23-173000-qwerty-vesting.json",
Some(custom_env),
)
.unwrap()
.with_contract_address(vesting_contract_address);
.with_contract_address(VESTING_CONTRACT_ADDRESS);
let mut multi_mock = MultiContractMock::new();
@@ -118,26 +125,86 @@ fn multi_mock() {
multi_mock
.add_contract::<VestingContract>(vesting_mock)
.unwrap();
multi_mock
}
#[test]
fn claiming_vesting_delegator_rewards() {
let mut multi_mock = set_mock();
let dummy_account = Addr::unchecked("n1ktpuwtweku40uaxcl4uq7mdkkmjeh698g3l3c8");
// do some queries to verify state is updated correctly for both contracts
let pending_reward: PendingRewardResponse = multi_mock
.query::<MixnetContract, _>(
MIXNET_CONTRACT_ADDRESS,
mixnet_contract_common::QueryMsg::GetPendingDelegatorReward {
address: dummy_account.to_string(),
mix_id: 8,
proxy: Some(VESTING_CONTRACT_ADDRESS.to_string()),
},
)
.unwrap();
let pending_reward_amount = pending_reward.amount_earned.unwrap().amount;
// we can also get whatever we want directly from storage!
let contract_state = multi_mock.contract_state(VESTING_CONTRACT_ADDRESS).unwrap();
let vesting_account = contract_state
.load_map_value(&VESTING_ACCOUNTS, dummy_account.clone())
.unwrap();
let vesting_balance = vesting_account
.load_balance(contract_state.deps().storage)
.unwrap();
let res = multi_mock.execute_full::<VestingContract>(
vesting_contract_address,
mock_info("n1vuz06p7cgagxcaplfezchvpu99u4np7erfxa4c", &[]),
vesting_contract_common::ExecuteMsg::DelegateToMixnode {
mix_id: 7,
amount: coin(1000, "unym"),
on_behalf_of: None,
},
VESTING_CONTRACT_ADDRESS,
mock_info(dummy_account.as_str(), &[]),
vesting_contract_common::ExecuteMsg::ClaimDelegatorReward { mix_id: 8 },
);
match res {
Ok(success) => {
// first we should have emitted a "vesting_delegation" event from the vesting contract
// followed by "v2_pending_delegation" from the mixnet contract
assert_eq!("vesting_delegation", success.steps[0].events[0].ty);
assert_eq!("v2_pending_delegation", success.steps[1].events[0].ty);
println!("{}", success.pretty());
// println!("{}", success.pretty())
// check the output
// unfortunately `ClaimDelegatorReward` doesn't emit any events, but we can see
// it's going to result into a call into the mixnet contract
assert_eq!(
success.steps[0].further_execution[0].contract.as_str(),
MIXNET_CONTRACT_ADDRESS
);
// mixnet contract will emit a `v2_withdraw_delegator_reward` event
// and call the vesting contract again
assert_eq!(
"v2_withdraw_delegator_reward",
success.steps[1].events[0].ty
);
assert_eq!(
success.steps[1].further_execution[0].contract.as_str(),
VESTING_CONTRACT_ADDRESS
);
// and will move our reward amount into the vesting contract...
assert!(matches!(
&success.steps[1].bank_msgs[0],
BankMsg::Send { to_address, amount }
if to_address == VESTING_CONTRACT_ADDRESS && amount[0].amount == pending_reward_amount
));
// and finally the vesting contract will emit the mistyped `track_reaward` event
assert_eq!("track_reaward", success.steps[2].events[0].ty);
}
Err(err) => panic!("{err}"),
}
// state after execution
let updated_state = multi_mock.contract_state(VESTING_CONTRACT_ADDRESS).unwrap();
let vesting_account = updated_state
.load_map_value(&VESTING_ACCOUNTS, dummy_account.clone())
.unwrap();
let new_vesting_balance = vesting_account
.load_balance(updated_state.deps().storage)
.unwrap();
assert_eq!(new_vesting_balance, vesting_balance + pending_reward_amount)
}
@@ -9,8 +9,12 @@ edition = "2021"
base64 = "0.21.0"
cosmwasm-std = { version = "1.0.0" }
cosmwasm-storage = { version = "1.0.0" }
cw-storage-plus = { version = "0.13.4", optional = true }
hex = "0.4.3"
rand_chacha = "0.3"
serde = { version = "1", features=["derive"] }
serde_json = "1"
thiserror = "1.0.38"
[features]
default = ["cw-storage-plus"]
@@ -1,20 +1,27 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::mock_api::CW12MockApi;
use crate::raw_state::{DecodingError, EncodingError, ImportedContractState, KeyValue};
use crate::AVERAGE_BLOCKTIME_SECS;
use cosmwasm_std::testing::{mock_env, MockApi, MockQuerier, MockStorage};
use cosmwasm_std::testing::{mock_env, MockQuerier, MockStorage};
use cosmwasm_std::{
Addr, Coin, Deps, DepsMut, Env, Order, QuerierWrapper, StdResult, Storage, Timestamp,
TransactionInfo,
};
use serde::de::DeserializeOwned;
use serde::Serialize;
use std::collections::HashMap;
use std::path::Path;
#[cfg(feature = "cw-storage-plus")]
use cw_storage_plus::{Item, Map, PrimaryKey};
// extracted into separate struct for easier cloning, access to mock structs, etc.
// we also had to redefine the MockApi
struct MockedDependencies {
storage: MockStorage,
api: MockApi,
api: CW12MockApi,
querier: MockQuerier,
// that's a bit annoying. We have to keep track of all balance changes for when we clone the state
@@ -115,6 +122,65 @@ impl ContractState {
self.deps().querier.query_all_balances(address)
}
#[cfg(feature = "cw-storage-plus")]
pub fn save_map_value<'a, K, T>(&mut self, map: &Map<'a, K, T>, k: K, data: &T) -> StdResult<()>
where
T: Serialize + DeserializeOwned,
K: PrimaryKey<'a>,
{
map.save(&mut self.deps.storage, k, data)
}
#[cfg(feature = "cw-storage-plus")]
pub fn load_map_value<'a, K, T>(&self, map: &Map<'a, K, T>, k: K) -> StdResult<T>
where
T: Serialize + DeserializeOwned,
K: PrimaryKey<'a>,
{
map.load(&self.deps.storage, k)
}
#[cfg(feature = "cw-storage-plus")]
pub fn may_load_map_value<'a, K, T>(&self, map: &Map<'a, K, T>, k: K) -> StdResult<Option<T>>
where
T: Serialize + DeserializeOwned,
K: PrimaryKey<'a>,
{
map.may_load(&self.deps.storage, k)
}
#[cfg(feature = "cw-storage-plus")]
pub fn save_item<T>(&mut self, item: &Item<T>, data: &T) -> StdResult<()>
where
T: Serialize + DeserializeOwned,
{
item.save(&mut self.deps.storage, data)
}
#[cfg(feature = "cw-storage-plus")]
pub fn load_item<T>(&self, item: &Item<T>) -> StdResult<T>
where
T: Serialize + DeserializeOwned,
{
item.load(&self.deps.storage)
}
#[cfg(feature = "cw-storage-plus")]
pub fn may_load_item<T>(&self, item: &Item<T>) -> StdResult<Option<T>>
where
T: Serialize + DeserializeOwned,
{
item.may_load(&self.deps.storage)
}
pub fn read_key(&self, key: &[u8]) -> Option<Vec<u8>> {
self.deps.storage.get(key)
}
pub fn set_key_value(&mut self, key: &[u8], value: &[u8]) {
self.deps.storage.set(key, value)
}
pub fn deps(&self) -> Deps<'_> {
Deps {
storage: &self.deps.storage,
@@ -1,14 +1,19 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::raw_msg_to_string;
use cosmwasm_std::{Addr, BankMsg, Binary, Coin, Event};
fn format_coins(coins: &[Coin]) -> String {
coins
.iter()
.map(|c| c.to_string())
.collect::<Vec<_>>()
.join(",")
if coins.is_empty() {
"<zero>".to_string()
} else {
coins
.iter()
.map(|c| c.to_string())
.collect::<Vec<_>>()
.join(",")
}
}
// specifically for tokens included in `Execute` that go into the contract
@@ -75,12 +80,12 @@ impl FurtherExecution {
}
pub fn pretty(&self) -> String {
let msg_placeholder = "<PLACEHOLDER>";
let msg = raw_msg_to_string(&self.msg);
let total_funds = format_coins(&self.funds);
format!(
"{} will be called with msg {} and {total_funds} funds",
self.contract, msg_placeholder
"{} will be called with msg {msg} and {total_funds} funds",
self.contract
)
}
}
@@ -101,10 +106,12 @@ impl ExecutionStepResult {
let events = format!("EVENTS: {:?}\n", self.events);
out.push_str(&events);
if !self.incoming_tokens.is_empty() {
if self.incoming_tokens.iter().any(|c| !c.amount.is_empty()) {
out.push_str("MOVED TOKENS (CONTRACTS):\n");
for incoming in &self.incoming_tokens {
out.push_str(&format!("{}\n", incoming.pretty()))
if !incoming.amount.is_empty() {
out.push_str(&format!("{}\n", incoming.pretty()))
}
}
}
@@ -4,6 +4,7 @@
mod contract_mock;
mod error;
mod execution;
mod mock_api;
mod multi_contract_mock;
mod raw_state;
mod single_contract_mock;
@@ -155,6 +156,14 @@ fn serialize_msg<M: Serialize>(msg: &M) -> StdResult<Binary> {
cosmwasm_std::to_binary(msg)
}
// used only for purposes of providing more informative error messages
fn raw_msg_to_string(raw: &Binary) -> String {
match serde_json::from_slice::<serde_json::Value>(raw.as_slice()) {
Ok(deserialized) => deserialized.to_string(),
Err(_) => "ERR: COULD NOT RECOVER THE ORIGINAL MESSAGE".to_string(),
}
}
pub(crate) mod sealed {
use crate::{deserialize_msg, TestableContract};
use cosmwasm_std::{Binary, Deps, DepsMut, Env, MessageInfo, QueryResponse, Response};
@@ -0,0 +1,143 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
// unfortunately we have to redefine cosmwasm' MockApi,
// as in cw1.0 they have set `CANONICAL_LENGTH` to 54 which makes
// `addr_validate` of our existing contracts fail (say of 'n1nc5tatafv6eyq7llkr2gv50ff9e22mnf70qgjlv737ktmt4eswrq73f2nw')
// this has changed in 1.2 but we can't use that version yet...
use cosmwasm_std::testing::{digit_sum, riffle_shuffle, MockApi};
use cosmwasm_std::{
Addr, Api, CanonicalAddr, RecoverPubkeyError, StdError, StdResult, VerificationError,
};
const CANONICAL_LENGTH: usize = 90; // n = 45
const SHUFFLES_ENCODE: usize = 10;
const SHUFFLES_DECODE: usize = 2;
#[derive(Copy, Clone)]
pub(crate) struct CW12MockApi {
inner: MockApi,
canonical_length: usize,
}
impl Default for CW12MockApi {
fn default() -> Self {
CW12MockApi {
inner: MockApi::default(),
canonical_length: CANONICAL_LENGTH,
}
}
}
// whatever we can, hand over to 1.0 MockApi
impl Api for CW12MockApi {
fn addr_validate(&self, input: &str) -> StdResult<Addr> {
let canonical = self.addr_canonicalize(input)?;
let normalized = self.addr_humanize(&canonical)?;
if input != normalized {
return Err(StdError::generic_err(
"Invalid input: address not normalized",
));
}
Ok(Addr::unchecked(input))
}
fn addr_canonicalize(&self, input: &str) -> StdResult<CanonicalAddr> {
// Dummy input validation. This is more sophisticated for formats like bech32, where format and checksum are validated.
let min_length = 3;
let max_length = self.canonical_length;
if input.len() < min_length {
return Err(StdError::generic_err(
format!("Invalid input: human address too short for this mock implementation (must be >= {min_length})."),
));
}
if input.len() > max_length {
return Err(StdError::generic_err(
format!("Invalid input: human address too long for this mock implementation (must be <= {max_length})."),
));
}
// mimicks formats like hex or bech32 where different casings are valid for one address
let normalized = input.to_lowercase();
let mut out = Vec::from(normalized);
// pad to canonical length with NULL bytes
out.resize(self.canonical_length, 0x00);
// content-dependent rotate followed by shuffle to destroy
// the most obvious structure (https://github.com/CosmWasm/cosmwasm/issues/552)
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.into())
}
fn addr_humanize(&self, canonical: &CanonicalAddr) -> StdResult<Addr> {
if canonical.len() != self.canonical_length {
return Err(StdError::generic_err(
"Invalid input: canonical address length not correct",
));
}
let mut tmp: Vec<u8> = canonical.clone().into();
// Shuffle two more times which restored the original value (24 elements are back to original after 20 rounds)
for _ in 0..SHUFFLES_DECODE {
tmp = riffle_shuffle(&tmp);
}
// Rotate back
let rotate_by = digit_sum(&tmp) % self.canonical_length;
tmp.rotate_right(rotate_by);
// Remove NULL bytes (i.e. the padding)
let trimmed = tmp.into_iter().filter(|&x| x != 0x00).collect();
// decode UTF-8 bytes into string
let human = String::from_utf8(trimmed)?;
Ok(Addr::unchecked(human))
}
fn secp256k1_verify(
&self,
message_hash: &[u8],
signature: &[u8],
public_key: &[u8],
) -> Result<bool, VerificationError> {
self.inner
.secp256k1_verify(message_hash, signature, public_key)
}
fn secp256k1_recover_pubkey(
&self,
message_hash: &[u8],
signature: &[u8],
recovery_param: u8,
) -> Result<Vec<u8>, RecoverPubkeyError> {
self.inner
.secp256k1_recover_pubkey(message_hash, signature, recovery_param)
}
fn ed25519_verify(
&self,
message: &[u8],
signature: &[u8],
public_key: &[u8],
) -> Result<bool, VerificationError> {
self.inner.ed25519_verify(message, signature, public_key)
}
fn ed25519_batch_verify(
&self,
messages: &[&[u8]],
signatures: &[&[u8]],
public_keys: &[&[u8]],
) -> Result<bool, VerificationError> {
self.inner
.ed25519_batch_verify(messages, signatures, public_keys)
}
fn debug(&self, message: &str) {
self.inner.debug(message)
}
}
@@ -5,7 +5,7 @@ use crate::contract_mock::ContractState;
use crate::execution::{
CrossContractTokenMove, ExecutionResult, ExecutionStepResult, FurtherExecution,
};
use crate::{sealed, serialize_msg, test_rng, MockingError, TestableContract};
use crate::{raw_msg_to_string, sealed, serialize_msg, test_rng, MockingError, TestableContract};
use cosmwasm_std::testing::{mock_env, mock_info};
use cosmwasm_std::{Addr, Binary, CosmosMsg, Env, MessageInfo, ReplyOn, Response, WasmMsg};
use rand_chacha::rand_core::RngCore;
@@ -183,6 +183,34 @@ impl MultiContractMock {
Ok(())
}
pub fn contract_state(
&self,
contract_address: impl Into<String>,
) -> Result<&ContractState, MockingError> {
let addr = Addr::unchecked(contract_address.into());
let contract =
self.contracts
.get(&addr)
.ok_or_else(|| MockingError::NonExistentContract {
address: addr.clone(),
})?;
Ok(&contract.state)
}
pub fn contract_state_mut(
&mut self,
contract_address: impl Into<String>,
) -> Result<&mut ContractState, MockingError> {
let addr = Addr::unchecked(contract_address.into());
let contract =
self.contracts
.get_mut(&addr)
.ok_or_else(|| MockingError::NonExistentContract {
address: addr.clone(),
})?;
Ok(&mut contract.state)
}
// TODO: add support for sub msgs in instantiate response
pub fn instantiate<C>(
&mut self,
@@ -332,14 +360,6 @@ impl MultiContractMock {
}
}
// used only for purposes of providing more informative error messages
fn raw_msg_to_string(raw: &Binary) -> String {
match serde_json::from_slice::<serde_json::Value>(raw.as_slice()) {
Ok(deserialized) => deserialized.to_string(),
Err(_) => "ERR: COULD NOT RECOVER THE ORIGINAL MESSAGE".to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;