-
Notifications
You must be signed in to change notification settings - Fork 15
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor(ensemble): separate from main rust package
- Loading branch information
Showing
20 changed files
with
8,273 additions
and
0 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
Oops, something went wrong.