use serde::Serialize;
#[cfg(test)]
use cosmwasm_std::testing::{mock_env, MockApi};
use cosmwasm_std::{
from_slice, to_binary, to_vec, Addr, Api, Attribute, BankMsg, Binary, BlockInfo, Coin,
ContractResult, CosmosMsg, Empty, MessageInfo, Querier, QuerierResult, QuerierWrapper,
QueryRequest, Response, SystemError, SystemResult, WasmMsg,
};
use crate::bank::{Bank, BankCache, BankOps, BankRouter};
use crate::wasm::{Contract, StorageFactory, WasmCache, WasmOps, WasmRouter};
use schemars::JsonSchema;
use std::fmt;
#[derive(Default, Clone, Debug)]
pub struct AppResponse {
pub attributes: Vec<Attribute>,
pub data: Option<Binary>,
}
#[derive(Default, Clone)]
pub struct ActionResponse<C>
where
C: Clone + fmt::Debug + PartialEq + JsonSchema,
{
pub messages: Vec<CosmosMsg<C>>,
pub attributes: Vec<Attribute>,
pub data: Option<Binary>,
}
impl<C> From<Response<C>> for ActionResponse<C>
where
C: Clone + fmt::Debug + PartialEq + JsonSchema,
{
fn from(input: Response<C>) -> Self {
ActionResponse {
messages: input.messages,
attributes: input.attributes,
data: input.data,
}
}
}
impl<C> ActionResponse<C>
where
C: Clone + fmt::Debug + PartialEq + JsonSchema,
{
fn init(input: Response<C>, address: Addr) -> Self {
ActionResponse {
messages: input.messages,
attributes: input.attributes,
data: Some(address.as_ref().as_bytes().into()),
}
}
}
impl<C> Querier for App<C>
where
C: Clone + fmt::Debug + PartialEq + JsonSchema,
{
fn raw_query(&self, bin_request: &[u8]) -> QuerierResult {
let request: QueryRequest<Empty> = match from_slice(bin_request) {
Ok(v) => v,
Err(e) => {
return SystemResult::Err(SystemError::InvalidRequest {
error: format!("Parsing query request: {}", e.to_string()),
request: bin_request.into(),
})
}
};
let contract_result: ContractResult<Binary> = self.query(request).into();
SystemResult::Ok(contract_result)
}
}
pub struct App<C = Empty>
where
C: Clone + fmt::Debug + PartialEq + JsonSchema,
{
wasm: WasmRouter<C>,
bank: BankRouter,
}
impl<C> App<C>
where
C: Clone + fmt::Debug + PartialEq + JsonSchema,
{
pub fn new<B: Bank + 'static>(
api: Box<dyn Api>,
block: BlockInfo,
bank: B,
storage_factory: StorageFactory,
) -> Self {
App {
wasm: WasmRouter::new(api, block, storage_factory),
bank: BankRouter::new(bank, storage_factory()),
}
}
pub fn cache(&'_ self) -> AppCache<'_, C> {
AppCache::new(self)
}
pub fn set_block(&mut self, block: BlockInfo) {
self.wasm.set_block(block);
}
pub fn update_block<F: Fn(&mut BlockInfo)>(&mut self, action: F) {
self.wasm.update_block(action);
}
pub fn block_info(&self) -> BlockInfo {
self.wasm.block_info()
}
pub fn set_bank_balance(&mut self, account: &Addr, amount: Vec<Coin>) -> Result<(), String> {
self.bank.set_balance(account, amount)
}
pub fn store_code(&mut self, code: Box<dyn Contract<C>>) -> u64 {
self.wasm.store_code(code) as u64
}
pub fn wrap(&self) -> QuerierWrapper {
QuerierWrapper::new(self)
}
pub fn query(&self, request: QueryRequest<Empty>) -> Result<Binary, String> {
match request {
QueryRequest::Wasm(req) => self.wasm.query(self, req),
QueryRequest::Bank(req) => self.bank.query(req),
_ => unimplemented!(),
}
}
pub fn instantiate_contract<T: Serialize, U: Into<String>>(
&mut self,
code_id: u64,
sender: Addr,
init_msg: &T,
send_funds: &[Coin],
label: U,
) -> Result<Addr, String> {
let init_msg = to_binary(init_msg).map_err(|e| e.to_string())?;
let msg: CosmosMsg<C> = WasmMsg::Instantiate {
admin: None,
code_id,
msg: init_msg,
send: send_funds.to_vec(),
label: label.into(),
}
.into();
let res = self.execute(sender, msg)?;
parse_contract_addr(&res.data)
}
pub fn execute_contract<T: Serialize>(
&mut self,
sender: Addr,
contract_addr: Addr,
msg: &T,
send_funds: &[Coin],
) -> Result<AppResponse, String> {
let msg = to_binary(msg).map_err(|e| e.to_string())?;
let msg = WasmMsg::Execute {
contract_addr: contract_addr.into(),
msg,
send: send_funds.to_vec(),
}
.into();
self.execute(sender, msg)
}
pub fn execute(&mut self, sender: Addr, msg: CosmosMsg<C>) -> Result<AppResponse, String> {
let mut all = self.execute_multi(sender, vec![msg])?;
let res = all.pop().unwrap();
Ok(res)
}
pub fn execute_multi(
&mut self,
sender: Addr,
msgs: Vec<CosmosMsg<C>>,
) -> Result<Vec<AppResponse>, String> {
let mut cache = self.cache();
let res: Result<Vec<AppResponse>, String> = msgs
.into_iter()
.map(|msg| cache.execute(sender.clone(), msg))
.collect();
if res.is_ok() {
let ops = cache.prepare();
ops.commit(self);
}
res
}
pub fn sudo<T: Serialize, U: Into<Addr>>(
&mut self,
contract_addr: U,
msg: &T,
) -> Result<AppResponse, String> {
let msg = to_vec(msg).map_err(|e| e.to_string())?;
let mut cache = self.cache();
let res = cache.sudo(contract_addr.into(), msg);
if res.is_ok() {
let ops = cache.prepare();
ops.commit(self);
}
res
}
}
pub struct AppCache<'a, C>
where
C: Clone + fmt::Debug + PartialEq + JsonSchema,
{
router: &'a App<C>,
wasm: WasmCache<'a, C>,
bank: BankCache<'a>,
}
pub struct AppOps {
wasm: WasmOps,
bank: BankOps,
}
impl AppOps {
pub fn commit<C>(self, router: &mut App<C>)
where
C: Clone + fmt::Debug + PartialEq + JsonSchema,
{
self.bank.commit(&mut router.bank);
self.wasm.commit(&mut router.wasm);
}
}
impl<'a, C> AppCache<'a, C>
where
C: Clone + fmt::Debug + PartialEq + JsonSchema,
{
fn new(router: &'a App<C>) -> Self {
AppCache {
router,
wasm: router.wasm.cache(),
bank: router.bank.cache(),
}
}
pub fn prepare(self) -> AppOps {
AppOps {
wasm: self.wasm.prepare(),
bank: self.bank.prepare(),
}
}
fn execute(&mut self, sender: Addr, msg: CosmosMsg<C>) -> Result<AppResponse, String> {
match msg {
CosmosMsg::Wasm(msg) => {
let (resender, res) = self.handle_wasm(sender, msg)?;
let mut attributes = res.attributes;
for resend in res.messages {
let subres = self.execute(resender.clone(), resend)?;
attributes.extend_from_slice(&subres.attributes);
}
Ok(AppResponse {
attributes,
data: res.data,
})
}
CosmosMsg::Bank(msg) => {
self.bank.execute(sender, msg)?;
Ok(AppResponse::default())
}
_ => unimplemented!(),
}
}
fn sudo(&mut self, contract_addr: Addr, msg: Vec<u8>) -> Result<AppResponse, String> {
let res = self.wasm.sudo(contract_addr.clone(), self.router, msg)?;
let mut attributes = res.attributes;
for resend in res.messages {
let subres = self.execute(contract_addr.clone(), resend)?;
attributes.extend_from_slice(&subres.attributes);
}
Ok(AppResponse {
attributes,
data: res.data,
})
}
fn handle_wasm(
&mut self,
sender: Addr,
msg: WasmMsg,
) -> Result<(Addr, ActionResponse<C>), String> {
match msg {
WasmMsg::Execute {
contract_addr,
msg,
send,
} => {
let contract_addr = Addr::unchecked(contract_addr);
self.send(sender.clone(), contract_addr.clone().into(), &send)?;
let info = MessageInfo {
sender,
funds: send,
};
let res =
self.wasm
.handle(contract_addr.clone(), self.router, info, msg.to_vec())?;
Ok((contract_addr, res.into()))
}
WasmMsg::Instantiate {
admin: _,
code_id,
msg,
send,
label: _,
} => {
let contract_addr = Addr::unchecked(self.wasm.register_contract(code_id as usize)?);
self.send(sender.clone(), contract_addr.clone().into(), &send)?;
let info = MessageInfo {
sender,
funds: send,
};
let res = self
.wasm
.init(contract_addr.clone(), self.router, info, msg.to_vec())?;
Ok((
contract_addr.clone(),
ActionResponse::init(res, contract_addr),
))
}
WasmMsg::Migrate { .. } => unimplemented!(),
m => panic!("Unsupported wasm message: {:?}", m),
}
}
fn send<T: Into<Addr>>(
&mut self,
sender: T,
recipient: String,
amount: &[Coin],
) -> Result<AppResponse, String> {
if !amount.is_empty() {
let msg = BankMsg::Send {
to_address: recipient,
amount: amount.to_vec(),
};
self.bank.execute(sender.into(), msg)?;
}
Ok(AppResponse::default())
}
}
pub fn parse_contract_addr(data: &Option<Binary>) -> Result<Addr, String> {
let bin = data
.as_ref()
.ok_or_else(|| "No data response".to_string())?
.to_vec();
let str = String::from_utf8(bin).map_err(|e| e.to_string())?;
Ok(Addr::unchecked(str))
}
#[cfg(test)]
mod test {
use super::*;
use crate::test_helpers::{
contract_payout, contract_payout_custom, contract_reflect, CustomMsg, EmptyMsg,
PayoutMessage, ReflectMessage, ReflectResponse, ReflectSudoMsg,
};
use crate::SimpleBank;
use cosmwasm_std::testing::MockStorage;
use cosmwasm_std::{attr, coin, coins};
fn mock_router() -> App {
let env = mock_env();
let api = Box::new(MockApi::default());
let bank = SimpleBank {};
App::new(api, env.block, bank, || Box::new(MockStorage::new()))
}
fn custom_router() -> App<CustomMsg> {
let env = mock_env();
let api = Box::new(MockApi::default());
let bank = SimpleBank {};
App::new(api, env.block, bank, || Box::new(MockStorage::new()))
}
fn get_balance<C>(router: &App<C>, addr: &Addr) -> Vec<Coin>
where
C: Clone + fmt::Debug + PartialEq + JsonSchema,
{
router.wrap().query_all_balances(addr).unwrap()
}
#[test]
fn send_tokens() {
let mut router = mock_router();
let owner = Addr::unchecked("owner");
let rcpt = Addr::unchecked("receiver");
let init_funds = vec![coin(20, "btc"), coin(100, "eth")];
let rcpt_funds = vec![coin(5, "btc")];
router.set_bank_balance(&owner, init_funds).unwrap();
router.set_bank_balance(&rcpt, rcpt_funds).unwrap();
let to_send = vec![coin(30, "eth"), coin(5, "btc")];
let msg: CosmosMsg = BankMsg::Send {
to_address: rcpt.clone().into(),
amount: to_send,
}
.into();
router.execute(owner.clone(), msg.clone()).unwrap();
let rich = get_balance(&router, &owner);
assert_eq!(vec![coin(15, "btc"), coin(70, "eth")], rich);
let poor = get_balance(&router, &rcpt);
assert_eq!(vec![coin(10, "btc"), coin(30, "eth")], poor);
router.execute(rcpt.clone(), msg).unwrap();
let msg = BankMsg::Send {
to_address: rcpt.into(),
amount: coins(20, "btc"),
}
.into();
router.execute(owner.clone(), msg).unwrap_err();
let rich = get_balance(&router, &owner);
assert_eq!(vec![coin(15, "btc"), coin(70, "eth")], rich);
}
#[test]
fn simple_contract() {
let mut router = mock_router();
let owner = Addr::unchecked("owner");
let init_funds = vec![coin(20, "btc"), coin(100, "eth")];
router.set_bank_balance(&owner, init_funds).unwrap();
let code_id = router.store_code(contract_payout());
let msg = PayoutMessage {
payout: coin(5, "eth"),
};
let contract_addr = router
.instantiate_contract(code_id, owner.clone(), &msg, &coins(23, "eth"), "Payout")
.unwrap();
let sender = get_balance(&router, &owner);
assert_eq!(sender, vec![coin(20, "btc"), coin(77, "eth")]);
let funds = get_balance(&router, &contract_addr);
assert_eq!(funds, coins(23, "eth"));
let random = Addr::unchecked("random");
let funds = get_balance(&router, &random);
assert_eq!(funds, vec![]);
let res = router
.execute_contract(random.clone(), contract_addr.clone(), &EmptyMsg {}, &[])
.unwrap();
assert_eq!(1, res.attributes.len());
assert_eq!(&attr("action", "payout"), &res.attributes[0]);
let funds = get_balance(&router, &random);
assert_eq!(funds, coins(5, "eth"));
let funds = get_balance(&router, &contract_addr);
assert_eq!(funds, coins(18, "eth"));
}
#[test]
fn reflect_success() {
let mut router = custom_router();
let owner = Addr::unchecked("owner");
let init_funds = vec![coin(20, "btc"), coin(100, "eth")];
router.set_bank_balance(&owner, init_funds).unwrap();
let payout_id = router.store_code(contract_payout_custom());
let msg = PayoutMessage {
payout: coin(5, "eth"),
};
let payout_addr = router
.instantiate_contract(payout_id, owner.clone(), &msg, &coins(23, "eth"), "Payout")
.unwrap();
let reflect_id = router.store_code(contract_reflect());
let reflect_addr = router
.instantiate_contract(reflect_id, owner, &EmptyMsg {}, &[], "Reflect")
.unwrap();
let funds = get_balance(&router, &reflect_addr);
assert_eq!(funds, vec![]);
let qres: ReflectResponse = router
.wrap()
.query_wasm_smart(&reflect_addr, &EmptyMsg {})
.unwrap();
assert_eq!(1, qres.count);
let msg = WasmMsg::Execute {
contract_addr: payout_addr.into(),
msg: b"{}".into(),
send: vec![],
}
.into();
let msgs = ReflectMessage {
messages: vec![msg],
};
let res = router
.execute_contract(Addr::unchecked("random"), reflect_addr.clone(), &msgs, &[])
.unwrap();
assert_eq!(1, res.attributes.len());
assert_eq!(&attr("action", "payout"), &res.attributes[0]);
let funds = get_balance(&router, &reflect_addr);
assert_eq!(funds, coins(5, "eth"));
let qres: ReflectResponse = router
.wrap()
.query_wasm_smart(&reflect_addr, &EmptyMsg {})
.unwrap();
assert_eq!(2, qres.count);
}
#[test]
fn reflect_error() {
let mut router = custom_router();
let owner = Addr::unchecked("owner");
let init_funds = vec![coin(20, "btc"), coin(100, "eth")];
router.set_bank_balance(&owner, init_funds).unwrap();
let reflect_id = router.store_code(contract_reflect());
let reflect_addr = router
.instantiate_contract(
reflect_id,
owner,
&EmptyMsg {},
&coins(40, "eth"),
"Reflect",
)
.unwrap();
let funds = get_balance(&router, &reflect_addr);
assert_eq!(funds, coins(40, "eth"));
let random = Addr::unchecked("random");
let msg = BankMsg::Send {
to_address: random.clone().into(),
amount: coins(7, "eth"),
}
.into();
let msgs = ReflectMessage {
messages: vec![msg],
};
let res = router
.execute_contract(random.clone(), reflect_addr.clone(), &msgs, &[])
.unwrap();
assert_eq!(0, res.attributes.len());
let funds = get_balance(&router, &random);
assert_eq!(funds, coins(7, "eth"));
let qres: ReflectResponse = router
.wrap()
.query_wasm_smart(&reflect_addr, &EmptyMsg {})
.unwrap();
assert_eq!(2, qres.count);
let msg = BankMsg::Send {
to_address: random.clone().into(),
amount: coins(8, "eth"),
}
.into();
let msg2 = BankMsg::Send {
to_address: random.clone().into(),
amount: coins(3, "btc"),
}
.into();
let msgs = ReflectMessage {
messages: vec![msg, msg2],
};
let err = router
.execute_contract(random.clone(), reflect_addr.clone(), &msgs, &[])
.unwrap_err();
assert_eq!("Overflow: Cannot Sub with 0 and 3", err.as_str());
let funds = get_balance(&router, &random);
assert_eq!(funds, coins(7, "eth"));
let qres: ReflectResponse = router
.wrap()
.query_wasm_smart(&reflect_addr, &EmptyMsg {})
.unwrap();
assert_eq!(2, qres.count);
}
#[test]
fn sudo_works() {
let mut router = custom_router();
let owner = Addr::unchecked("owner");
let reflect_id = router.store_code(contract_reflect());
let reflect_addr = router
.instantiate_contract(reflect_id, owner, &EmptyMsg {}, &[], "Reflect")
.unwrap();
let ReflectResponse { count } = router
.wrap()
.query_wasm_smart(&reflect_addr, &EmptyMsg {})
.unwrap();
assert_eq!(1, count);
let msg = ReflectSudoMsg { set_count: 25 };
router.sudo(reflect_addr.clone(), &msg).unwrap();
let ReflectResponse { count } = router
.wrap()
.query_wasm_smart(&reflect_addr, &EmptyMsg {})
.unwrap();
assert_eq!(25, count);
}
}