Skip to content

Commit

Permalink
refactor(ensemble): separate from main rust package
Browse files Browse the repository at this point in the history
  • Loading branch information
egasimus committed Nov 7, 2023
1 parent 38d787d commit 77536e5
Show file tree
Hide file tree
Showing 20 changed files with 8,273 additions and 0 deletions.
1,502 changes: 1,502 additions & 0 deletions ensemble/Cargo.lock

Large diffs are not rendered by default.

49 changes: 49 additions & 0 deletions ensemble/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
[package]
name = "fadroma-ensemble"
version = "0.1.0"
edition = "2021"
license = "AGPL-3.0"
keywords = ["blockchain", "cosmos", "cosmwasm", "smart-contract"]
description = "Testing framework for Secret Network"
repository = "https://github.com/hackbg/fadroma"
readme = "README.md"
authors = [
"Asparuh Kamenov <[email protected]>",
"Adam A. <[email protected]>",
"denismaxim0v <[email protected]>",
"Chris Ricketts <[email protected]>",
"Tibor Hudik <[email protected]>",
"Wiz1991 <[email protected]>",
"hydropump3 <[email protected]>",
"Itzik <[email protected]>"
]

[lib]
path = "src/lib.rs"

[package.metadata.docs.rs]
rustc-args = ["--cfg", "docsrs"]
all-features = true

[features]
staking = [ "time/formatting", "secret-cosmwasm-std/staking" ]

# Can't be used on the stable channel
#backtraces = [ "secret-cosmwasm-std/backtraces" ]

[dependencies]
fadroma = { path = "..", features = [ "scrt", "scrt-staking" ] }
secret-cosmwasm-std = { version = "1.1.10", default-features = false, optional = true }
oorandom = { version = "11.1.3" }
anyhow = { version = "1.0.65" }
time = { optional = true, version = "0.3.17" }
serde = { version = "1.0.114", default-features = false, features = ["derive"] }

# Enable iterator for testing (not supported in production)
[target.'cfg(not(target_arch="wasm32"))'.dependencies]
secret-cosmwasm-std = { version = "1.1.10", default-features = false, features = ["iterator", "random"], optional = true }

[dev-dependencies]
criterion = "0.4.0"
bincode2 = "2.0.1"
proptest = "1.1.0"
116 changes: 116 additions & 0 deletions ensemble/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# Fadroma Ensemble

![](https://img.shields.io/badge/version-0.1.0-blueviolet)

**How to write multi-contract CosmWasm integration tests in Rust using `fadroma-ensemble`**

## Introduction
Fadroma Ensemble provides a way to test multi-contract interactions without having to deploy contracts on-chain.

## Getting started
To start testing with ensemble `ContractHarness` has to be implemented for each contract and registered by the `ContractEnsemble`. This approach allows a lot of flexibility for testing contracts. Mock implementations can be created, contract methods can be overridden, `Bank` interactions are also possible.

### ContractHarness
`ContractHarness` defines entrypoints to any contract: `init`, `handle`, `query`. In order to implement contract we can use `DefaultImpl` from existing contract code, or override contract methods. You can also use the `impl_contract_harness!` macro.
```rust
// Here we create a ContractHarness implementation for an Oracle contract
use path::to::contracts::oracle;

pub struct Oracle;
impl ContractHarness for Oracle {
// Use the method from the default implementation
fn instantiate(&self, deps: DepsMut, env: Env, info: MessageInfo, msg: Binary) -> AnyResult<Response> {
oracle::init(
deps,
env,
info,
from_binary(&msg)?,
oracle::DefaultImpl,
).map_err(|x| anyhow::anyhow!(x))
}

fn execute(&self, deps: DepsMut, env: Env, info: MessageInfo, msg: Binary) -> AnyResult<Response> {
oracle::handle(
deps,
env,
info,
from_binary(&msg)?,
oracle::DefaultImpl,
).map_err(|x| anyhow::anyhow!(x))
}

fn reply(&self, deps: DepsMut, env: Env, reply: Reply) -> AnyResult<Response> {
oracle::reply(
deps,
env,
reply,
oracle::DefaultImpl,
).map_err(|x| anyhow::anyhow!(x))
}

// Override with some hardcoded value for the ease of testing
fn query(&self, deps: Deps, _env: Env, msg: Binary) -> AnyResult<Binary> {
let msg = from_binary(&msg).unwrap();

let result = match msg {
oracle::QueryMsg::GetPrice { base_symbol: _, .. } => to_binary(&Uint128(1_000_000_000)),
// don't override the rest
_ => oracle::query(deps, from_binary(&msg)?, oracle::DefaultImpl)
}?;

result
}
}
```
### ContractEnsemble
`ContractEnsemble` is the centerpiece that takes care of managing contract storage and bank state and executing messages between contracts. Currently, supported messages are `CosmosMsg::Wasm` and `CosmosMsg::Bank`. It exposes methods like `register` for registering contract harnesses and `instantiate`, `execute`, `reply`, `query` for interacting with contracts and methods to inspect/alter the raw storage if needed. Just like on the blockchain, if any contract returns an error during exection, all state is reverted.

```rust
#[test]
fn test_query_price() {
let mut ensemble = ContractEnsemble::new();

// register contract
let oracle = ensemble.register(Box::new(Oracle));

// instantiate
let oracle = ensemble.instantiate(
oracle.id,
&{},
MockEnv::new(
"Admin",
"oracle" // This will be the contract address
)
).unwrap().instance;

// query
let oracle::QueryMsg::GetPrice { price } = ensemble.query(
oracle.address,
&oracle::QueryMsg::GetPrice { base_symbol: "SCRT".into },
).unwrap();

assert_eq!(price, Uint128(1_000_000_000));
}
```

### Simulating blocks
Since the ensemble is designed to simulate a blockchain environment it maintains an idea of block height and time. Block height increases automatically with each successful call to execute and instantiate messages (**sub-messages don't trigger this behaviour**). It is possible to configure as needed: blocks can be incremented by a fixed amount or by a random value within a provided range. In addition, the current block can be frozen so subsequent calls will not modify it if desired.

Set the block height manually:

```rust
let mut ensemble = ContractEnsemble::new();

ensemble.block_mut().height = 10;
ensemble.block_mut().time = 10000;
```

Use auto-increments (after each **successful** call) for block height and time when initializing the ensemble:

```rust
// For exact increments
ensemble.block_mut().exact_increments(10, 7);

// For random increments within specified ranges
ensemble.block_mut().random_increments(1..11, 1..9);
```
141 changes: 141 additions & 0 deletions ensemble/src/bank.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
use std::collections::HashMap;

use fadroma::cosmwasm_std::{Uint128, Coin, coin};
use super::{
EnsembleResult, EnsembleError
};

pub type Balances = HashMap<String, Uint128>;

#[derive(Clone, Default, Debug)]
pub(crate) struct Bank(pub HashMap<String, Balances>);

impl Bank {
pub fn add_funds(&mut self, address: &str, coin: Coin) {
self.assert_account_exists(address);

let account = self.0.get_mut(address).unwrap();
add_balance(account, coin);
}

pub fn remove_funds(
&mut self,
address: &str,
coin: Coin
) -> EnsembleResult<()> {
if !self.0.contains_key(address) {
return Err(EnsembleError::Bank(
format!("Account {} does not exist for remove balance", address)
))
}

let account = self.0.get_mut(address).unwrap();
let balance = account.get_mut(&coin.denom);

match balance {
Some(amount) => {
if *amount >= coin.amount {
*amount -= coin.amount;
} else {
return Err(EnsembleError::Bank(format!(
"Insufficient balance: account: {}, denom: {}, balance: {}, required: {}",
address,
coin.denom,
amount,
coin.amount
)))
}
},
None => {
return Err(EnsembleError::Bank(format!(
"Insufficient balance: account: {}, denom: {}, balance: {}, required: {}",
address,
coin.denom,
Uint128::zero(),
coin.amount
)))
}
}

Ok(())
}

pub fn transfer(
&mut self,
from: &str,
to: &str,
coin: Coin,
) -> EnsembleResult<()> {
self.assert_account_exists(from);
self.assert_account_exists(to);

let amount = self
.0
.get_mut(from)
.unwrap()
.get_mut(&coin.denom)
.ok_or_else(|| {
EnsembleError::Bank(format!(
"Insufficient balance: sender: {}, denom: {}, balance: {}, required: {}",
from,
coin.denom,
Uint128::zero(),
coin.amount
))
})?;

*amount = amount.checked_sub(coin.amount).map_err(|_| {
EnsembleError::Bank(format!(
"Insufficient balance: sender: {}, denom: {}, balance: {}, required: {}",
from, coin.denom, amount, coin.amount
))
})?;

add_balance(self.0.get_mut(to).unwrap(), coin);

Ok(())
}

pub fn query_balances(&self, address: &str, denom: Option<String>) -> Vec<Coin> {
let account = self.0.get(address);

match account {
Some(account) => match denom {
Some(denom) => {
let amount = account.get(&denom);

vec![coin(amount.cloned().unwrap_or_default().u128(), &denom)]
}
None => {
let mut result = Vec::new();

for (k, v) in account.iter() {
result.push(coin(v.u128(), k));
}

result
}
},
None => match denom {
Some(denom) => vec![coin(0, &denom)],
None => vec![],
},
}
}

fn assert_account_exists(&mut self, address: &str) {
if !self.0.contains_key(address) {
self.0.insert(address.to_string(), Default::default());
}
}
}

fn add_balance(balances: &mut Balances, coin: Coin) {
let balance = balances.get_mut(&coin.denom);

if let Some(amount) = balance {
*amount += coin.amount;
} else {
balances.insert(coin.denom, coin.amount);
}
}
Loading

0 comments on commit 77536e5

Please sign in to comment.