diff --git a/.circleci/config.yml b/.circleci/config.yml index 0c5231200..bde54ddb3 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -664,6 +664,9 @@ jobs: - run: name: Run unit tests command: cargo test --locked + - run: + name: Run unit tests (with iterator) + command: cargo test --locked --features iterator - save_cache: paths: - /usr/local/cargo/registry diff --git a/Cargo.lock b/Cargo.lock index f9832a41f..a055833f1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -77,6 +77,7 @@ name = "cw-multi-test" version = "0.3.2" dependencies = [ "cosmwasm-std", + "cw0", "schemars", "serde", ] diff --git a/packages/multi-test/Cargo.toml b/packages/multi-test/Cargo.toml index c33a8016f..aecf02b14 100644 --- a/packages/multi-test/Cargo.toml +++ b/packages/multi-test/Cargo.toml @@ -10,8 +10,12 @@ homepage = "https://cosmwasm.com" documentation = "https://docs.cosmwasm.com" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[features] +default = ["iterator"] +iterator = ["cosmwasm-std/iterator"] [dependencies] +cw0 = { path = "../../packages/cw0", version = "0.3.2" } cosmwasm-std = { version = "0.12.0-alpha2" } schemars = "0.7" serde = { version = "1.0.103", default-features = false, features = ["derive"] } diff --git a/packages/multi-test/src/balance.rs b/packages/multi-test/src/balance.rs deleted file mode 100644 index ac36baca0..000000000 --- a/packages/multi-test/src/balance.rs +++ /dev/null @@ -1,301 +0,0 @@ -//*** TODO: remove this and import cw0::balance when we are both on 0.12 ***/ -#![allow(dead_code)] - -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use std::ops; - -use cosmwasm_std::{Coin, StdError, StdResult, Uint128}; - -// Balance wraps Vec and provides some nice helpers. It mutates the Vec and can be -// unwrapped when done. -#[derive(Serialize, Deserialize, Clone, Default, Debug, PartialEq, JsonSchema)] -pub struct NativeBalance(pub Vec); - -impl NativeBalance { - pub fn into_vec(self) -> Vec { - self.0 - } - - /// returns true if the list of coins has at least the required amount - pub fn has(&self, required: &Coin) -> bool { - self.0 - .iter() - .find(|c| c.denom == required.denom) - .map(|m| m.amount >= required.amount) - .unwrap_or(false) - } - - /// normalize Wallet (sorted by denom, no 0 elements, no duplicate denoms) - pub fn normalize(&mut self) { - // drop 0's - self.0.retain(|c| c.amount.u128() != 0); - // sort - self.0.sort_unstable_by(|a, b| a.denom.cmp(&b.denom)); - - // find all i where (self[i-1].denom == self[i].denom). - let mut dups: Vec = self - .0 - .iter() - .enumerate() - .filter_map(|(i, c)| { - if i != 0 && c.denom == self.0[i - 1].denom { - Some(i) - } else { - None - } - }) - .collect(); - dups.reverse(); - - // we go through the dups in reverse order (to avoid shifting indexes of other ones) - for dup in dups { - let add = self.0[dup].amount; - self.0[dup - 1].amount += add; - self.0.remove(dup); - } - } - - fn find(&self, denom: &str) -> Option<(usize, &Coin)> { - self.0.iter().enumerate().find(|(_i, c)| c.denom == denom) - } - - /// insert_pos should only be called when denom is not in the Wallet. - /// it returns the position where denom should be inserted at (via splice). - /// It returns None if this should be appended - fn insert_pos(&self, denom: &str) -> Option { - self.0.iter().position(|c| c.denom.as_str() >= denom) - } - - pub fn is_empty(&self) -> bool { - !self.0.iter().any(|x| x.amount != Uint128(0)) - } - - /// similar to `Balance.sub`, but doesn't fail when minuend less than subtrahend - pub fn sub_saturating(mut self, other: Coin) -> StdResult { - match self.find(&other.denom) { - Some((i, c)) => { - if c.amount <= other.amount { - self.0.remove(i); - } else { - self.0[i].amount = (self.0[i].amount - other.amount)?; - } - } - // error if no tokens - None => return Err(StdError::underflow(0, other.amount.u128())), - }; - Ok(self) - } -} - -impl ops::AddAssign for NativeBalance { - fn add_assign(&mut self, other: Coin) { - match self.find(&other.denom) { - Some((i, c)) => { - self.0[i].amount = c.amount + other.amount; - } - // place this in proper sorted order - None => match self.insert_pos(&other.denom) { - Some(idx) => self.0.insert(idx, other), - None => self.0.push(other), - }, - }; - } -} - -impl ops::Add for NativeBalance { - type Output = Self; - - fn add(mut self, other: Coin) -> Self { - self += other; - self - } -} - -impl ops::AddAssign for NativeBalance { - fn add_assign(&mut self, other: NativeBalance) { - for coin in other.0.into_iter() { - self.add_assign(coin); - } - } -} - -impl ops::Add for NativeBalance { - type Output = Self; - - fn add(mut self, other: NativeBalance) -> Self { - self += other; - self - } -} - -impl ops::Sub for NativeBalance { - type Output = StdResult; - - fn sub(mut self, other: Coin) -> StdResult { - match self.find(&other.denom) { - Some((i, c)) => { - let remainder = (c.amount - other.amount)?; - if remainder.u128() == 0 { - self.0.remove(i); - } else { - self.0[i].amount = remainder; - } - } - // error if no tokens - None => return Err(StdError::underflow(0, other.amount.u128())), - }; - Ok(self) - } -} - -impl ops::Sub> for NativeBalance { - type Output = StdResult; - - fn sub(self, amount: Vec) -> StdResult { - let mut res = self; - for coin in amount { - res = res.sub(coin.clone())?; - } - Ok(res) - } -} - -#[cfg(test)] -mod test { - use super::*; - use cosmwasm_std::coin; - - #[test] - fn balance_has_works() { - let balance = NativeBalance(vec![coin(555, "BTC"), coin(12345, "ETH")]); - - // less than same type - assert!(balance.has(&coin(777, "ETH"))); - // equal to same type - assert!(balance.has(&coin(555, "BTC"))); - - // too high - assert!(!balance.has(&coin(12346, "ETH"))); - // wrong type - assert!(!balance.has(&coin(456, "ETC"))); - } - - #[test] - fn balance_add_works() { - let balance = NativeBalance(vec![coin(555, "BTC"), coin(12345, "ETH")]); - - // add an existing coin - let more_eth = balance.clone() + coin(54321, "ETH"); - assert_eq!( - more_eth, - NativeBalance(vec![coin(555, "BTC"), coin(66666, "ETH")]) - ); - - // add an new coin - let add_atom = balance.clone() + coin(777, "ATOM"); - assert_eq!( - add_atom, - NativeBalance(vec![ - coin(777, "ATOM"), - coin(555, "BTC"), - coin(12345, "ETH"), - ]) - ); - } - - #[test] - fn balance_in_place_addition() { - let mut balance = NativeBalance(vec![coin(555, "BTC")]); - balance += coin(777, "ATOM"); - assert_eq!( - &balance, - &NativeBalance(vec![coin(777, "ATOM"), coin(555, "BTC")]) - ); - - balance += NativeBalance(vec![coin(666, "ETH"), coin(123, "ATOM")]); - assert_eq!( - &balance, - &NativeBalance(vec![coin(900, "ATOM"), coin(555, "BTC"), coin(666, "ETH")]) - ); - - let foo = balance + NativeBalance(vec![coin(234, "BTC")]); - assert_eq!( - &foo, - &NativeBalance(vec![coin(900, "ATOM"), coin(789, "BTC"), coin(666, "ETH")]) - ); - } - - #[test] - fn balance_subtract_works() { - let balance = NativeBalance(vec![coin(555, "BTC"), coin(12345, "ETH")]); - - // subtract less than we have - let less_eth = (balance.clone() - coin(2345, "ETH")).unwrap(); - assert_eq!( - less_eth, - NativeBalance(vec![coin(555, "BTC"), coin(10000, "ETH")]) - ); - - // subtract all of one coin (and remove with 0 amount) - let no_btc = (balance.clone() - coin(555, "BTC")).unwrap(); - assert_eq!(no_btc, NativeBalance(vec![coin(12345, "ETH")])); - - // subtract more than we have - let underflow = balance.clone() - coin(666, "BTC"); - assert!(underflow.is_err()); - - // subtract non-existent denom - let missing = balance.clone() - coin(1, "ATOM"); - assert!(missing.is_err()); - } - - #[test] - fn balance_subtract_saturating_works() { - let balance = NativeBalance(vec![coin(555, "BTC"), coin(12345, "ETH")]); - - // subtract less than we have - let less_eth = balance.clone().sub_saturating(coin(2345, "ETH")).unwrap(); - assert_eq!( - less_eth, - NativeBalance(vec![coin(555, "BTC"), coin(10000, "ETH")]) - ); - - // subtract all of one coin (and remove with 0 amount) - let no_btc = balance.clone().sub_saturating(coin(555, "BTC")).unwrap(); - assert_eq!(no_btc, NativeBalance(vec![coin(12345, "ETH")])); - - // subtract more than we have - let saturating = balance.clone().sub_saturating(coin(666, "BTC")); - assert!(saturating.is_ok()); - assert_eq!(saturating.unwrap(), NativeBalance(vec![coin(12345, "ETH")])); - - // subtract non-existent denom - let missing = balance.clone() - coin(1, "ATOM"); - assert!(missing.is_err()); - } - - #[test] - fn normalize_balance() { - // remove 0 value items and sort - let mut balance = NativeBalance(vec![coin(123, "ETH"), coin(0, "BTC"), coin(8990, "ATOM")]); - balance.normalize(); - assert_eq!( - balance, - NativeBalance(vec![coin(8990, "ATOM"), coin(123, "ETH")]) - ); - - // merge duplicate entries of same denom - let mut balance = NativeBalance(vec![ - coin(123, "ETH"), - coin(789, "BTC"), - coin(321, "ETH"), - coin(11, "BTC"), - ]); - balance.normalize(); - assert_eq!( - balance, - NativeBalance(vec![coin(800, "BTC"), coin(444, "ETH")]) - ); - } -} diff --git a/packages/multi-test/src/bank.rs b/packages/multi-test/src/bank.rs index 6d08f04e4..1a84430bf 100644 --- a/packages/multi-test/src/bank.rs +++ b/packages/multi-test/src/bank.rs @@ -3,8 +3,8 @@ use cosmwasm_std::{ Binary, Coin, HumanAddr, Storage, }; -//*** TODO: remove this and import cw0::balance when we are both on 0.12 ***/ -use crate::balance::NativeBalance; +use crate::transactions::{RepLog, StorageTransaction}; +use cw0::NativeBalance; /// Bank is a minimal contract-like interface that implements a bank module /// It is initialized outside of the trait @@ -25,6 +25,70 @@ pub trait Bank { account: HumanAddr, amount: Vec, ) -> Result<(), String>; + + fn clone(&self) -> Box; +} + +pub struct BankRouter { + bank: Box, + storage: Box, +} + +impl BankRouter { + pub fn new(bank: B, storage: Box) -> Self { + BankRouter { + bank: Box::new(bank), + storage, + } + } + + // this is an "admin" function to let us adjust bank accounts + pub fn set_balance(&mut self, account: HumanAddr, amount: Vec) -> Result<(), String> { + self.bank + .set_balance(self.storage.as_mut(), account, amount) + } + + pub fn cache(&'_ self) -> BankCache<'_> { + BankCache::new(self) + } + + pub fn query(&self, request: BankQuery) -> Result { + self.bank.query(self.storage.as_ref(), request) + } +} + +pub struct BankCache<'a> { + // and this into one with reference + router: &'a BankRouter, + state: StorageTransaction<'a>, +} + +pub struct BankOps(RepLog); + +impl BankOps { + pub fn commit(self, router: &mut BankRouter) { + self.0.commit(router.storage.as_mut()) + } +} + +impl<'a> BankCache<'a> { + fn new(router: &'a BankRouter) -> Self { + BankCache { + router, + state: StorageTransaction::new(router.storage.as_ref()), + } + } + + /// When we want to commit the BankCache, we need a 2 step process to satisfy Rust reference counting: + /// 1. prepare() consumes BankCache, releasing &BankRouter, and creating a self-owned update info. + /// 2. BankOps::commit() can now take &mut BankRouter and updates the underlying state + pub fn prepare(self) -> BankOps { + BankOps(self.state.prepare()) + } + + pub fn execute(&mut self, sender: HumanAddr, msg: BankMsg) -> Result<(), String> { + self.router.bank.handle(&mut self.state, sender, msg) + } } #[derive(Default)] @@ -122,6 +186,10 @@ impl Bank for SimpleBank { storage.set(key, &value); Ok(()) } + + fn clone(&self) -> Box { + Box::new(SimpleBank {}) + } } #[cfg(test)] diff --git a/packages/multi-test/src/handlers.rs b/packages/multi-test/src/handlers.rs index c42d6e170..98a820965 100644 --- a/packages/multi-test/src/handlers.rs +++ b/packages/multi-test/src/handlers.rs @@ -1,18 +1,15 @@ -use std::cell::RefCell; -use std::ops::{Deref, DerefMut}; +use serde::Serialize; #[cfg(test)] use cosmwasm_std::testing::{mock_env, MockApi}; use cosmwasm_std::{ - from_slice, to_binary, Api, Attribute, BankMsg, BankQuery, Binary, BlockInfo, Coin, - ContractResult, CosmosMsg, Empty, HandleResponse, HumanAddr, InitResponse, MessageInfo, - Querier, QuerierResult, QueryRequest, Storage, SystemError, SystemResult, WasmMsg, WasmQuery, + from_slice, to_binary, Api, Attribute, BankMsg, Binary, BlockInfo, Coin, ContractResult, + CosmosMsg, Empty, HandleResponse, HumanAddr, InitResponse, MessageInfo, Querier, QuerierResult, + QuerierWrapper, QueryRequest, SystemError, SystemResult, WasmMsg, }; -use crate::bank::Bank; -use crate::wasm::WasmRouter; -use crate::Contract; -use serde::Serialize; +use crate::bank::{Bank, BankCache, BankOps, BankRouter}; +use crate::wasm::{Contract, StorageFactory, WasmCache, WasmOps, WasmRouter}; #[derive(Default, Clone, Debug)] pub struct RouterResponse { @@ -49,20 +46,15 @@ impl ActionResponse { } } -pub struct Router -where - S: Storage + Default, -{ - wasm: WasmRouter, - bank: Box, - bank_store: RefCell, - // LATER: staking router +/// Router is a persisted state. You can query this. +/// Execution generally happens on the RouterCache, which then can be atomically committed or rolled back. +/// We offer .execute() as a wrapper around cache, execute, commit/rollback process +pub struct Router { + wasm: WasmRouter, + bank: BankRouter, } -impl Querier for Router -where - S: Storage + Default, -{ +impl Querier for Router { fn raw_query(&self, bin_request: &[u8]) -> QuerierResult { let request: QueryRequest = match from_slice(bin_request) { Ok(v) => v, @@ -78,42 +70,66 @@ where } } -impl Router -where - S: Storage + Default, -{ - // TODO: store BlockInfo in Router not WasmRouter to change easier? - pub fn new(api: Box, block: BlockInfo, bank: B) -> Self { +impl Router { + pub fn new( + api: Box, + block: BlockInfo, + bank: B, + storage_factory: StorageFactory, + ) -> Self { Router { - wasm: WasmRouter::new(api, block), - bank: Box::new(bank), - bank_store: RefCell::new(S::default()), + wasm: WasmRouter::new(api, block, storage_factory), + bank: BankRouter::new(bank, storage_factory()), } } + pub fn cache(&'_ self) -> RouterCache<'_> { + RouterCache::new(self) + } + + /// This can set the block info to any value. Must be done before taking a cache pub fn set_block(&mut self, block: BlockInfo) { self.wasm.set_block(block); } - // this let's use use "next block" steps that add eg. one height and 5 seconds + /// This let's use use "next block" steps that add eg. one height and 5 seconds pub fn update_block(&mut self, action: F) { self.wasm.update_block(action); } - // this is an "admin" function to let us adjust bank accounts - pub fn set_bank_balance(&self, account: HumanAddr, amount: Vec) -> Result<(), String> { - let mut store = self - .bank_store - .try_borrow_mut() - .map_err(|e| format!("Double-borrowing mutable storage - re-entrancy?: {}", e))?; - self.bank.set_balance(store.deref_mut(), account, amount) + /// This is an "admin" function to let us adjust bank accounts + pub fn set_bank_balance( + &mut self, + account: HumanAddr, + amount: Vec, + ) -> Result<(), String> { + self.bank.set_balance(account, amount) } + /// This registers contract code (like uploading wasm bytecode on a chain), + /// so it can later be used to instantiate a contract. pub fn store_code(&mut self, code: Box) -> u64 { self.wasm.store_code(code) as u64 } - // create a contract and get the new address + /// Simple helper so we get access to all the QuerierWrapper helpers, + /// eg. query_wasm_smart, query_all_balances, ... + pub fn wrap(&self) -> QuerierWrapper { + QuerierWrapper::new(self) + } + + /// Handles arbitrary QueryRequest, this is wrapped by the Querier interface, but this + /// is nicer to use. + pub fn query(&self, request: QueryRequest) -> Result { + match request { + QueryRequest::Wasm(req) => self.wasm.query(self, req), + QueryRequest::Bank(req) => self.bank.query(req), + _ => unimplemented!(), + } + } + + /// Create a contract and get the new address. + /// This is just a helper around execute() pub fn instantiate_contract, V: Into>( &mut self, code_id: u64, @@ -135,6 +151,8 @@ where parse_contract_addr(&res.data) } + /// Execute a contract and process all returned messages. + /// This is just a helper around execute() pub fn execute_contract>( &mut self, contract_addr: U, @@ -152,20 +170,94 @@ where self.execute(sender.into(), msg) } + /// Runs arbitrary CosmosMsg. + /// This will create a cache before the execution, so no state changes are persisted if this + /// returns an error, but all are persisted on success. pub fn execute( &mut self, sender: HumanAddr, msg: CosmosMsg, ) -> Result { - // TODO: we need to do some caching of storage here, once in the entry point - // meaning, wrap current state.. all writes go to a cache... only when execute + let mut all = self.execute_multi(sender, vec![msg])?; + let res = all.pop().unwrap(); + Ok(res) + } + + /// Runs multiple CosmosMsg in one atomic operation. + /// This will create a cache before the execution, so no state changes are persisted if any of them + /// return an error. But all writes are persisted on success. + pub fn execute_multi( + &mut self, + sender: HumanAddr, + msgs: Vec>, + ) -> Result, String> { + // we need to do some caching of storage here, once in the entry point: + // meaning, wrap current state, all writes go to a cache, only when execute // returns a success do we flush it (otherwise drop it) - self._execute(&sender, msg) + + let mut cache = self.cache(); + + // run all messages, stops at first error + let res: Result, String> = msgs + .into_iter() + .map(|msg| cache.execute(sender.clone(), msg)) + .collect(); + + // this only happens if all messages run successfully + if res.is_ok() { + let ops = cache.prepare(); + ops.commit(self); + } + res + } +} + +pub struct RouterCache<'a> { + router: &'a Router, + wasm: WasmCache<'a>, + bank: BankCache<'a>, +} + +pub struct RouterOps { + wasm: WasmOps, + bank: BankOps, +} + +impl RouterOps { + pub fn commit(self, router: &mut Router) { + self.bank.commit(&mut router.bank); + self.wasm.commit(&mut router.wasm); + } +} + +impl<'a> RouterCache<'a> { + fn new(router: &'a Router) -> Self { + RouterCache { + router, + wasm: router.wasm.cache(), + bank: router.bank.cache(), + } + } + + /// When we want to commit the RouterCache, we need a 2 step process to satisfy Rust reference counting: + /// 1. prepare() consumes RouterCache, releasing &Router, and creating a self-owned update info. + /// 2. RouterOps::commit() can now take &mut Router and updates the underlying state + pub fn prepare(self) -> RouterOps { + RouterOps { + wasm: self.wasm.prepare(), + bank: self.bank.prepare(), + } } - fn _execute( + /// This will execute the given messages, making all changes to the local cache. + /// This *will* write some data to the cache if the message fails half-way through. + /// All sequential calls to RouterCache will be one atomic unit (all commit or all fail). + /// + /// For normal use cases, you can use Router::execute() or Router::execute_multi(). + /// This is designed to be handled internally as part of larger process flows. + fn execute( &mut self, - sender: &HumanAddr, + sender: HumanAddr, msg: CosmosMsg, ) -> Result { match msg { @@ -174,7 +266,7 @@ where let mut attributes = res.attributes; // recurse in all messages for resend in res.messages { - let subres = self._execute(&resender, resend)?; + let subres = self.execute(resender.clone(), resend)?; // ignore the data now, just like in wasmd // append the events attributes.extend_from_slice(&subres.attributes); @@ -184,7 +276,10 @@ where data: res.data, }) } - CosmosMsg::Bank(msg) => self.handle_bank(sender, msg), + CosmosMsg::Bank(msg) => { + self.bank.execute(sender, msg)?; + Ok(RouterResponse::default()) + } _ => unimplemented!(), } } @@ -192,7 +287,7 @@ where // this returns the contract address as well, so we can properly resend the data fn handle_wasm( &mut self, - sender: &HumanAddr, + sender: HumanAddr, msg: WasmMsg, ) -> Result<(HumanAddr, ActionResponse), String> { match msg { @@ -202,15 +297,15 @@ where send, } => { // first move the cash - self.send(sender, &contract_addr, &send)?; + self.send(&sender, &contract_addr, &send)?; // then call the contract let info = MessageInfo { - sender: sender.clone(), + sender, sent_funds: send, }; - let res = self - .wasm - .handle(contract_addr.clone(), self, info, msg.to_vec())?; + let res = + self.wasm + .handle(contract_addr.clone(), self.router, info, msg.to_vec())?; Ok((contract_addr, res.into())) } WasmMsg::Instantiate { @@ -219,18 +314,17 @@ where send, label: _, } => { - // register the contract let contract_addr = self.wasm.register_contract(code_id as usize)?; // move the cash - self.send(sender, &contract_addr, &send)?; + self.send(&sender, &contract_addr, &send)?; // then call the contract let info = MessageInfo { - sender: sender.clone(), + sender, sent_funds: send, }; let res = self .wasm - .init(contract_addr.clone(), self, info, msg.to_vec())?; + .init(contract_addr.clone(), self.router, info, msg.to_vec())?; Ok(( contract_addr.clone(), ActionResponse::init(res, contract_addr), @@ -239,60 +333,23 @@ where } } - // Returns empty router response, just here for the same function signatures - fn handle_bank(&self, sender: &HumanAddr, msg: BankMsg) -> Result { - let mut store = self - .bank_store - .try_borrow_mut() - .map_err(|e| format!("Double-borrowing mutable storage - re-entrancy?: {}", e))?; - self.bank.handle(store.deref_mut(), sender.into(), msg)?; - Ok(RouterResponse::default()) - } - fn send, U: Into>( - &self, + &mut self, sender: T, recipient: U, amount: &[Coin], ) -> Result { if !amount.is_empty() { let sender: HumanAddr = sender.into(); - self.handle_bank( - &sender, - BankMsg::Send { - from_address: sender.clone(), - to_address: recipient.into(), - amount: amount.to_vec(), - }, - )?; + let msg = BankMsg::Send { + from_address: sender.clone(), + to_address: recipient.into(), + amount: amount.to_vec(), + }; + self.bank.execute(sender, msg)?; } Ok(RouterResponse::default()) } - - pub fn query(&self, request: QueryRequest) -> Result { - match request { - QueryRequest::Wasm(req) => self.query_wasm(req), - QueryRequest::Bank(req) => self.query_bank(req), - _ => unimplemented!(), - } - } - - fn query_wasm(&self, request: WasmQuery) -> Result { - match request { - WasmQuery::Smart { contract_addr, msg } => { - self.wasm.query(contract_addr, self, msg.into()) - } - WasmQuery::Raw { contract_addr, key } => self.wasm.query_raw(contract_addr, &key), - } - } - - fn query_bank(&self, request: BankQuery) -> Result { - let store = self - .bank_store - .try_borrow() - .map_err(|e| format!("Immutable storage borrow failed - re-entrancy?: {}", e))?; - self.bank.query(store.deref(), request) - } } // this parses the result from a wasm contract init @@ -309,24 +366,22 @@ pub fn parse_contract_addr(data: &Option) -> Result { mod test { use super::*; use crate::test_helpers::{ - contract_payout, contract_reflect, EmptyMsg, PayoutMessage, ReflectMessage, + contract_payout, contract_reflect, EmptyMsg, PayoutMessage, ReflectMessage, ReflectResponse, }; use crate::SimpleBank; use cosmwasm_std::testing::MockStorage; - use cosmwasm_std::{attr, coin, coins, QuerierWrapper}; + use cosmwasm_std::{attr, coin, coins}; - fn mock_router() -> Router { + fn mock_router() -> Router { let env = mock_env(); let api = Box::new(MockApi::default()); let bank = SimpleBank {}; - Router::new(api, env.block, bank) + Router::new(api, env.block, bank, || Box::new(MockStorage::new())) } - fn get_balance(router: &Router, addr: &HumanAddr) -> Vec { - QuerierWrapper::new(router) - .query_all_balances(addr) - .unwrap() + fn get_balance(router: &Router, addr: &HumanAddr) -> Vec { + router.wrap().query_all_balances(addr).unwrap() } #[test] @@ -452,6 +507,12 @@ mod test { // reflect account is empty let funds = get_balance(&router, &reflect_addr); assert_eq!(funds, vec![]); + // reflect count is 1 + let qres: ReflectResponse = router + .wrap() + .query_wasm_smart(&reflect_addr, &EmptyMsg {}) + .unwrap(); + assert_eq!(1, qres.count); // reflecting payout message pays reflect contract let msg = WasmMsg::Execute { @@ -474,6 +535,13 @@ mod test { // ensure transfer was executed with reflect as sender let funds = get_balance(&router, &reflect_addr); assert_eq!(funds, coins(5, "eth")); + + // reflect count updated + let qres: ReflectResponse = router + .wrap() + .query_wasm_smart(&reflect_addr, &EmptyMsg {}) + .unwrap(); + assert_eq!(2, qres.count); } #[test] @@ -522,6 +590,13 @@ mod test { let funds = get_balance(&router, &random); assert_eq!(funds, coins(7, "eth")); + // reflect count should be updated to 2 + let qres: ReflectResponse = router + .wrap() + .query_wasm_smart(&reflect_addr, &EmptyMsg {}) + .unwrap(); + assert_eq!(2, qres.count); + // sending 8 eth, then 3 btc should fail both let msg = BankMsg::Send { from_address: reflect_addr.clone(), @@ -543,9 +618,15 @@ mod test { .unwrap_err(); assert_eq!("Cannot subtract 3 from 0", err.as_str()); - // TODO: fix this - // // first one should have been rolled-back on error (no second payment) - // let funds = get_balance(&router, &random); - // assert_eq!(funds, coins(7, "eth")); + // first one should have been rolled-back on error (no second payment) + let funds = get_balance(&router, &random); + assert_eq!(funds, coins(7, "eth")); + + // failure should not update reflect count + let qres: ReflectResponse = router + .wrap() + .query_wasm_smart(&reflect_addr, &EmptyMsg {}) + .unwrap(); + assert_eq!(2, qres.count); } } diff --git a/packages/multi-test/src/lib.rs b/packages/multi-test/src/lib.rs index 42d9f97c1..f5a61e620 100644 --- a/packages/multi-test/src/lib.rs +++ b/packages/multi-test/src/lib.rs @@ -1,9 +1,9 @@ -mod balance; mod bank; mod handlers; mod test_helpers; +mod transactions; mod wasm; -pub use crate::bank::{Bank, SimpleBank}; -pub use crate::handlers::{parse_contract_addr, Router}; -pub use crate::wasm::{next_block, Contract, ContractWrapper, WasmRouter}; +pub use crate::bank::{Bank, BankCache, BankOps, SimpleBank}; +pub use crate::handlers::{parse_contract_addr, Router, RouterCache, RouterOps}; +pub use crate::wasm::{next_block, Contract, ContractWrapper, WasmCache, WasmOps, WasmRouter}; diff --git a/packages/multi-test/src/test_helpers.rs b/packages/multi-test/src/test_helpers.rs index 1b8968ad8..cba509729 100644 --- a/packages/multi-test/src/test_helpers.rs +++ b/packages/multi-test/src/test_helpers.rs @@ -3,8 +3,8 @@ use serde::{Deserialize, Serialize}; use crate::wasm::{Contract, ContractWrapper}; use cosmwasm_std::{ - attr, from_slice, to_vec, BankMsg, Binary, Coin, CosmosMsg, Deps, DepsMut, Empty, Env, - HandleResponse, InitResponse, MessageInfo, StdError, + attr, from_slice, to_binary, to_vec, BankMsg, Binary, Coin, CosmosMsg, Deps, DepsMut, Empty, + Env, HandleResponse, InitResponse, MessageInfo, StdError, }; #[derive(Debug, Clone, Serialize, Deserialize, Default)] @@ -93,21 +93,35 @@ pub struct ReflectMessage { pub messages: Vec>, } +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct ReflectResponse { + pub count: u8, +} + +const REFLECT_KEY: &[u8] = b"reflect"; + fn init_reflect( - _deps: DepsMut, + deps: DepsMut, _env: Env, _info: MessageInfo, _msg: EmptyMsg, ) -> Result { + deps.storage.set(REFLECT_KEY, &[1]); Ok(InitResponse::default()) } fn handle_reflect( - _deps: DepsMut, + deps: DepsMut, _env: Env, _info: MessageInfo, msg: ReflectMessage, ) -> Result { + let old = match deps.storage.get(REFLECT_KEY) { + Some(bz) => bz[0], + None => 0, + }; + deps.storage.set(REFLECT_KEY, &[old + 1]); + let res = HandleResponse { messages: msg.messages, attributes: vec![], @@ -116,8 +130,13 @@ fn handle_reflect( Ok(res) } -fn query_reflect(_deps: Deps, _env: Env, _msg: EmptyMsg) -> Result { - Err(StdError::generic_err("Query not implemented")) +fn query_reflect(deps: Deps, _env: Env, _msg: EmptyMsg) -> Result { + let count = match deps.storage.get(REFLECT_KEY) { + Some(bz) => bz[0], + None => 0, + }; + let res = ReflectResponse { count }; + to_binary(&res) } pub fn contract_reflect() -> Box { diff --git a/packages/multi-test/src/transactions.rs b/packages/multi-test/src/transactions.rs new file mode 100644 index 000000000..5a6b282f2 --- /dev/null +++ b/packages/multi-test/src/transactions.rs @@ -0,0 +1,594 @@ +#[cfg(feature = "iterator")] +use std::cmp::Ordering; +use std::collections::BTreeMap; +#[cfg(feature = "iterator")] +use std::iter; +#[cfg(feature = "iterator")] +use std::iter::Peekable; +#[cfg(feature = "iterator")] +use std::ops::{Bound, RangeBounds}; + +use cosmwasm_std::Storage; +#[cfg(feature = "iterator")] +use cosmwasm_std::{Order, KV}; + +#[cfg(feature = "iterator")] +/// The BTreeMap specific key-value pair reference type, as returned by BTreeMap, T>::range. +/// This is internal as it can change any time if the map implementation is swapped out. +type BTreeMapPairRef<'a, T = Vec> = (&'a Vec, &'a T); + +pub struct StorageTransaction<'a> { + /// read-only access to backing storage + storage: &'a dyn Storage, + /// these are local changes not flushed to backing storage + local_state: BTreeMap, Delta>, + /// a log of local changes not yet flushed to backing storage + rep_log: RepLog, +} + +impl<'a> StorageTransaction<'a> { + pub fn new(storage: &'a dyn Storage) -> Self { + StorageTransaction { + storage, + local_state: BTreeMap::new(), + rep_log: RepLog::new(), + } + } + + /// prepares this transaction to be committed to storage + pub fn prepare(self) -> RepLog { + self.rep_log + } + + /// rollback will consume the checkpoint and drop all changes (not really needed, going out of scope does the same, but nice for clarity) + #[allow(dead_code)] + pub fn rollback(self) {} +} + +impl<'a> Storage for StorageTransaction<'a> { + fn get(&self, key: &[u8]) -> Option> { + match self.local_state.get(key) { + Some(val) => match val { + Delta::Set { value } => Some(value.clone()), + Delta::Delete {} => None, + }, + None => self.storage.get(key), + } + } + + fn set(&mut self, key: &[u8], value: &[u8]) { + let op = Op::Set { + key: key.to_vec(), + value: value.to_vec(), + }; + self.local_state.insert(key.to_vec(), op.to_delta()); + self.rep_log.append(op); + } + + fn remove(&mut self, key: &[u8]) { + let op = Op::Delete { key: key.to_vec() }; + self.local_state.insert(key.to_vec(), op.to_delta()); + self.rep_log.append(op); + } + + #[cfg(feature = "iterator")] + /// range allows iteration over a set of keys, either forwards or backwards + /// uses standard rust range notation, and eg db.range(b"foo"..b"bar") also works reverse + fn range<'b>( + &'b self, + start: Option<&[u8]>, + end: Option<&[u8]>, + order: Order, + ) -> Box + 'b> { + let bounds = range_bounds(start, end); + + // BTreeMap.range panics if range is start > end. + // However, this cases represent just empty range and we treat it as such. + let local: Box>> = + match (bounds.start_bound(), bounds.end_bound()) { + (Bound::Included(start), Bound::Excluded(end)) if start > end => { + Box::new(iter::empty()) + } + _ => { + let local_raw = self.local_state.range(bounds); + match order { + Order::Ascending => Box::new(local_raw), + Order::Descending => Box::new(local_raw.rev()), + } + } + }; + + let base = self.storage.range(start, end, order); + let merged = MergeOverlay::new(local, base, order); + Box::new(merged) + } +} + +pub struct RepLog { + /// this is a list of changes to be written to backing storage upon commit + ops_log: Vec, +} + +impl RepLog { + fn new() -> Self { + RepLog { ops_log: vec![] } + } + + /// appends an op to the list of changes to be applied upon commit + fn append(&mut self, op: Op) { + self.ops_log.push(op); + } + + /// applies the stored list of `Op`s to the provided `Storage` + pub fn commit(self, storage: &mut S) { + for op in self.ops_log { + op.apply(storage); + } + } +} + +/// Op is the user operation, which can be stored in the RepLog. +/// Currently Set or Delete. +enum Op { + /// represents the `Set` operation for setting a key-value pair in storage + Set { + key: Vec, + value: Vec, + }, + Delete { + key: Vec, + }, +} + +impl Op { + /// applies this `Op` to the provided storage + pub fn apply(&self, storage: &mut S) { + match self { + Op::Set { key, value } => storage.set(&key, &value), + Op::Delete { key } => storage.remove(&key), + } + } + + /// converts the Op to a delta, which can be stored in a local cache + pub fn to_delta(&self) -> Delta { + match self { + Op::Set { value, .. } => Delta::Set { + value: value.clone(), + }, + Op::Delete { .. } => Delta::Delete {}, + } + } +} + +/// Delta is the changes, stored in the local transaction cache. +/// This is either Set{value} or Delete{}. Note that this is the "value" +/// part of a BTree, so the Key (from the Op) is stored separately. +enum Delta { + Set { value: Vec }, + Delete {}, +} + +#[cfg(feature = "iterator")] +struct MergeOverlay<'a, L, R> +where + L: Iterator>, + R: Iterator, +{ + left: Peekable, + right: Peekable, + order: Order, +} + +#[cfg(feature = "iterator")] +impl<'a, L, R> MergeOverlay<'a, L, R> +where + L: Iterator>, + R: Iterator, +{ + fn new(left: L, right: R, order: Order) -> Self { + MergeOverlay { + left: left.peekable(), + right: right.peekable(), + order, + } + } + + fn pick_match(&mut self, lkey: Vec, rkey: Vec) -> Option { + // compare keys - result is such that Ordering::Less => return left side + let order = match self.order { + Order::Ascending => lkey.cmp(&rkey), + Order::Descending => rkey.cmp(&lkey), + }; + + // left must be translated and filtered before return, not so with right + match order { + Ordering::Less => self.take_left(), + Ordering::Equal => { + // + let _ = self.right.next(); + self.take_left() + } + Ordering::Greater => self.right.next(), + } + } + + /// take_left must only be called when we know self.left.next() will return Some + fn take_left(&mut self) -> Option { + let (lkey, lval) = self.left.next().unwrap(); + match lval { + Delta::Set { value } => Some((lkey.clone(), value.clone())), + Delta::Delete {} => self.next(), + } + } +} + +#[cfg(feature = "iterator")] +impl<'a, L, R> Iterator for MergeOverlay<'a, L, R> +where + L: Iterator>, + R: Iterator, +{ + type Item = KV; + + fn next(&mut self) -> Option { + let (left, right) = (self.left.peek(), self.right.peek()); + match (left, right) { + (Some(litem), Some(ritem)) => { + let (lkey, _) = litem; + let (rkey, _) = ritem; + + // we just use cloned keys to avoid double mutable references + // (we must release the return value from peek, before beginning to call next or other mut methods + let (l, r) = (lkey.to_vec(), rkey.to_vec()); + self.pick_match(l, r) + } + (Some(_), None) => self.take_left(), + (None, Some(_)) => self.right.next(), + (None, None) => None, + } + } +} + +#[cfg(feature = "iterator")] +fn range_bounds(start: Option<&[u8]>, end: Option<&[u8]>) -> impl RangeBounds> { + ( + start.map_or(Bound::Unbounded, |x| Bound::Included(x.to_vec())), + end.map_or(Bound::Unbounded, |x| Bound::Excluded(x.to_vec())), + ) +} + +#[cfg(test)] +mod test { + use super::*; + use std::cell::RefCell; + use std::ops::{Deref, DerefMut}; + + use cosmwasm_std::MemoryStorage; + + #[test] + fn wrap_storage() { + let mut store = MemoryStorage::new(); + let mut wrap = StorageTransaction::new(&store); + wrap.set(b"foo", b"bar"); + + assert_eq!(None, store.get(b"foo")); + wrap.prepare().commit(&mut store); + assert_eq!(Some(b"bar".to_vec()), store.get(b"foo")); + } + + #[test] + fn wrap_ref_cell() { + let store = RefCell::new(MemoryStorage::new()); + let ops = { + let refer = store.borrow(); + let mut wrap = StorageTransaction::new(refer.deref()); + wrap.set(b"foo", b"bar"); + assert_eq!(None, store.borrow().get(b"foo")); + wrap.prepare() + }; + ops.commit(store.borrow_mut().deref_mut()); + assert_eq!(Some(b"bar".to_vec()), store.borrow().get(b"foo")); + } + + #[test] + fn wrap_box_storage() { + let mut store: Box = Box::new(MemoryStorage::new()); + let mut wrap = StorageTransaction::new(store.as_ref()); + wrap.set(b"foo", b"bar"); + + assert_eq!(None, store.get(b"foo")); + wrap.prepare().commit(store.as_mut()); + assert_eq!(Some(b"bar".to_vec()), store.get(b"foo")); + } + + #[test] + fn wrap_box_dyn_storage() { + let mut store: Box = Box::new(MemoryStorage::new()); + let mut wrap = StorageTransaction::new(store.as_ref()); + wrap.set(b"foo", b"bar"); + + assert_eq!(None, store.get(b"foo")); + wrap.prepare().commit(store.as_mut()); + assert_eq!(Some(b"bar".to_vec()), store.get(b"foo")); + } + + #[test] + fn wrap_ref_cell_dyn_storage() { + let inner: Box = Box::new(MemoryStorage::new()); + let store = RefCell::new(inner); + // Tricky but working + // 1. we cannot inline StorageTransaction::new(store.borrow().as_ref()) as Ref must outlive StorageTransaction + // 2. we cannot call ops.commit() until refer is out of scope - borrow_mut() and borrow() on the same object + // This can work with some careful scoping, this provides a good reference + let ops = { + let refer = store.borrow(); + let mut wrap = StorageTransaction::new(refer.as_ref()); + wrap.set(b"foo", b"bar"); + + assert_eq!(None, store.borrow().get(b"foo")); + wrap.prepare() + }; + ops.commit(store.borrow_mut().as_mut()); + assert_eq!(Some(b"bar".to_vec()), store.borrow().get(b"foo")); + } + + #[cfg(feature = "iterator")] + // iterator_test_suite takes a storage, adds data and runs iterator tests + // the storage must previously have exactly one key: "foo" = "bar" + // (this allows us to test StorageTransaction and other wrapped storage better) + fn iterator_test_suite(store: &mut S) { + // ensure we had previously set "foo" = "bar" + assert_eq!(store.get(b"foo"), Some(b"bar".to_vec())); + assert_eq!(store.range(None, None, Order::Ascending).count(), 1); + + // setup - add some data, and delete part of it as well + store.set(b"ant", b"hill"); + store.set(b"ze", b"bra"); + + // noise that should be ignored + store.set(b"bye", b"bye"); + store.remove(b"bye"); + + // unbounded + { + let iter = store.range(None, None, Order::Ascending); + let elements: Vec = iter.collect(); + assert_eq!( + elements, + vec![ + (b"ant".to_vec(), b"hill".to_vec()), + (b"foo".to_vec(), b"bar".to_vec()), + (b"ze".to_vec(), b"bra".to_vec()), + ] + ); + } + + // unbounded (descending) + { + let iter = store.range(None, None, Order::Descending); + let elements: Vec = iter.collect(); + assert_eq!( + elements, + vec![ + (b"ze".to_vec(), b"bra".to_vec()), + (b"foo".to_vec(), b"bar".to_vec()), + (b"ant".to_vec(), b"hill".to_vec()), + ] + ); + } + + // bounded + { + let iter = store.range(Some(b"f"), Some(b"n"), Order::Ascending); + let elements: Vec = iter.collect(); + assert_eq!(elements, vec![(b"foo".to_vec(), b"bar".to_vec())]); + } + + // bounded (descending) + { + let iter = store.range(Some(b"air"), Some(b"loop"), Order::Descending); + let elements: Vec = iter.collect(); + assert_eq!( + elements, + vec![ + (b"foo".to_vec(), b"bar".to_vec()), + (b"ant".to_vec(), b"hill".to_vec()), + ] + ); + } + + // bounded empty [a, a) + { + let iter = store.range(Some(b"foo"), Some(b"foo"), Order::Ascending); + let elements: Vec = iter.collect(); + assert_eq!(elements, vec![]); + } + + // bounded empty [a, a) (descending) + { + let iter = store.range(Some(b"foo"), Some(b"foo"), Order::Descending); + let elements: Vec = iter.collect(); + assert_eq!(elements, vec![]); + } + + // bounded empty [a, b) with b < a + { + let iter = store.range(Some(b"z"), Some(b"a"), Order::Ascending); + let elements: Vec = iter.collect(); + assert_eq!(elements, vec![]); + } + + // bounded empty [a, b) with b < a (descending) + { + let iter = store.range(Some(b"z"), Some(b"a"), Order::Descending); + let elements: Vec = iter.collect(); + assert_eq!(elements, vec![]); + } + + // right unbounded + { + let iter = store.range(Some(b"f"), None, Order::Ascending); + let elements: Vec = iter.collect(); + assert_eq!( + elements, + vec![ + (b"foo".to_vec(), b"bar".to_vec()), + (b"ze".to_vec(), b"bra".to_vec()), + ] + ); + } + + // right unbounded (descending) + { + let iter = store.range(Some(b"f"), None, Order::Descending); + let elements: Vec = iter.collect(); + assert_eq!( + elements, + vec![ + (b"ze".to_vec(), b"bra".to_vec()), + (b"foo".to_vec(), b"bar".to_vec()), + ] + ); + } + + // left unbounded + { + let iter = store.range(None, Some(b"f"), Order::Ascending); + let elements: Vec = iter.collect(); + assert_eq!(elements, vec![(b"ant".to_vec(), b"hill".to_vec()),]); + } + + // left unbounded (descending) + { + let iter = store.range(None, Some(b"no"), Order::Descending); + let elements: Vec = iter.collect(); + assert_eq!( + elements, + vec![ + (b"foo".to_vec(), b"bar".to_vec()), + (b"ant".to_vec(), b"hill".to_vec()), + ] + ); + } + } + + #[test] + fn delete_local() { + let mut base = Box::new(MemoryStorage::new()); + let mut check = StorageTransaction::new(base.as_ref()); + check.set(b"foo", b"bar"); + check.set(b"food", b"bank"); + check.remove(b"foo"); + + assert_eq!(check.get(b"foo"), None); + assert_eq!(check.get(b"food"), Some(b"bank".to_vec())); + + // now commit to base and query there + check.prepare().commit(base.as_mut()); + assert_eq!(base.get(b"foo"), None); + assert_eq!(base.get(b"food"), Some(b"bank".to_vec())); + } + + #[test] + fn delete_from_base() { + let mut base = Box::new(MemoryStorage::new()); + base.set(b"foo", b"bar"); + let mut check = StorageTransaction::new(base.as_ref()); + check.set(b"food", b"bank"); + check.remove(b"foo"); + + assert_eq!(check.get(b"foo"), None); + assert_eq!(check.get(b"food"), Some(b"bank".to_vec())); + + // now commit to base and query there + check.prepare().commit(base.as_mut()); + assert_eq!(base.get(b"foo"), None); + assert_eq!(base.get(b"food"), Some(b"bank".to_vec())); + } + + #[test] + #[cfg(feature = "iterator")] + fn storage_transaction_iterator_empty_base() { + let base = MemoryStorage::new(); + let mut check = StorageTransaction::new(&base); + check.set(b"foo", b"bar"); + iterator_test_suite(&mut check); + } + + #[test] + #[cfg(feature = "iterator")] + fn storage_transaction_iterator_with_base_data() { + let mut base = MemoryStorage::new(); + base.set(b"foo", b"bar"); + let mut check = StorageTransaction::new(&base); + iterator_test_suite(&mut check); + } + + #[test] + #[cfg(feature = "iterator")] + fn storage_transaction_iterator_removed_items_from_base() { + let mut base = Box::new(MemoryStorage::new()); + base.set(b"foo", b"bar"); + base.set(b"food", b"bank"); + let mut check = StorageTransaction::new(base.as_ref()); + check.remove(b"food"); + iterator_test_suite(&mut check); + } + + #[test] + fn commit_writes_through() { + let mut base = Box::new(MemoryStorage::new()); + base.set(b"foo", b"bar"); + + let mut check = StorageTransaction::new(base.as_ref()); + assert_eq!(check.get(b"foo"), Some(b"bar".to_vec())); + check.set(b"subtx", b"works"); + check.prepare().commit(base.as_mut()); + + assert_eq!(base.get(b"subtx"), Some(b"works".to_vec())); + } + + #[test] + fn storage_remains_readable() { + let mut base = MemoryStorage::new(); + base.set(b"foo", b"bar"); + + let mut stxn1 = StorageTransaction::new(&base); + + assert_eq!(stxn1.get(b"foo"), Some(b"bar".to_vec())); + + stxn1.set(b"subtx", b"works"); + assert_eq!(stxn1.get(b"subtx"), Some(b"works".to_vec())); + + // Can still read from base, txn is not yet committed + assert_eq!(base.get(b"subtx"), None); + + stxn1.prepare().commit(&mut base); + assert_eq!(base.get(b"subtx"), Some(b"works".to_vec())); + } + + #[test] + fn rollback_has_no_effect() { + let mut base = MemoryStorage::new(); + base.set(b"foo", b"bar"); + + let mut check = StorageTransaction::new(&base); + assert_eq!(check.get(b"foo"), Some(b"bar".to_vec())); + check.set(b"subtx", b"works"); + check.rollback(); + + assert_eq!(base.get(b"subtx"), None); + } + + #[test] + fn ignore_same_as_rollback() { + let mut base = MemoryStorage::new(); + base.set(b"foo", b"bar"); + + let mut check = StorageTransaction::new(&base); + assert_eq!(check.get(b"foo"), Some(b"bar".to_vec())); + check.set(b"subtx", b"works"); + + assert_eq!(base.get(b"subtx"), None); + } +} diff --git a/packages/multi-test/src/wasm.rs b/packages/multi-test/src/wasm.rs index 372a4e7b6..b5eaefbdf 100644 --- a/packages/multi-test/src/wasm.rs +++ b/packages/multi-test/src/wasm.rs @@ -1,11 +1,11 @@ use serde::de::DeserializeOwned; -use std::cell::RefCell; use std::collections::HashMap; -use std::ops::{Deref, DerefMut}; +use std::ops::Deref; +use crate::transactions::{RepLog, StorageTransaction}; use cosmwasm_std::{ from_slice, Api, Binary, BlockInfo, ContractInfo, Deps, DepsMut, Env, HandleResponse, - HumanAddr, InitResponse, MessageInfo, Querier, QuerierWrapper, Storage, + HumanAddr, InitResponse, MessageInfo, Querier, QuerierWrapper, Storage, WasmQuery, }; /// Interface to call into a Contract @@ -106,17 +106,14 @@ where } } -struct ContractData { +struct ContractData { code_id: usize, - storage: RefCell, + storage: Box, } -impl ContractData { - fn new(code_id: usize) -> Self { - ContractData { - code_id, - storage: RefCell::new(S::default()), - } +impl ContractData { + fn new(code_id: usize, storage: Box) -> Self { + ContractData { code_id, storage } } } @@ -125,26 +122,26 @@ pub fn next_block(block: &mut BlockInfo) { block.height += 1; } -pub struct WasmRouter -where - S: Storage + Default, -{ +pub type StorageFactory = fn() -> Box; + +pub struct WasmRouter { + // WasmState - cache this, pass in separate? handlers: HashMap>, - contracts: HashMap>, + contracts: HashMap, + // WasmConst block: BlockInfo, api: Box, + storage_factory: StorageFactory, } -impl WasmRouter -where - S: Storage + Default, -{ - pub fn new(api: Box, block: BlockInfo) -> Self { +impl WasmRouter { + pub fn new(api: Box, block: BlockInfo, storage_factory: StorageFactory) -> Self { WasmRouter { handlers: HashMap::new(), contracts: HashMap::new(), block, api, + storage_factory, } } @@ -163,52 +160,27 @@ where idx } - /// This just creates an address and empty storage instance, returning the new address - /// You must call init after this to set up the contract properly. - /// These are separated into two steps to have cleaner return values. - pub fn register_contract(&mut self, code_id: usize) -> Result { - if !self.handlers.contains_key(&code_id) { - return Err("Cannot init contract with unregistered code id".to_string()); - } - // TODO: better addr generation - let addr = HumanAddr::from(self.contracts.len().to_string()); - let info = ContractData::new(code_id); - self.contracts.insert(addr.clone(), info); - Ok(addr) - } - - pub fn handle( - &self, - address: HumanAddr, - querier: &dyn Querier, - info: MessageInfo, - msg: Vec, - ) -> Result { - self.with_storage(querier, address, |handler, deps, env| { - handler.handle(deps, env, info, msg) - }) + pub fn cache(&'_ self) -> WasmCache<'_> { + WasmCache::new(self) } - pub fn init( - &self, - address: HumanAddr, - querier: &dyn Querier, - info: MessageInfo, - msg: Vec, - ) -> Result { - self.with_storage(querier, address, |handler, deps, env| { - handler.init(deps, env, info, msg) - }) + pub fn query(&self, querier: &dyn Querier, request: WasmQuery) -> Result { + match request { + WasmQuery::Smart { contract_addr, msg } => { + self.query_smart(contract_addr, querier, msg.into()) + } + WasmQuery::Raw { contract_addr, key } => self.query_raw(contract_addr, &key), + } } - pub fn query( + pub fn query_smart( &self, address: HumanAddr, querier: &dyn Querier, msg: Vec, ) -> Result { self.with_storage(querier, address, |handler, deps, env| { - handler.query(deps.as_ref(), env, msg) + handler.query(deps, env, msg) }) } @@ -217,11 +189,7 @@ where .contracts .get(&address) .ok_or_else(|| "Unregistered contract address".to_string())?; - let storage = contract - .storage - .try_borrow() - .map_err(|e| format!("Immutable borrowing failed - re-entrancy?: {}", e))?; - let data = storage.get(&key).unwrap_or_default(); + let data = contract.storage.get(&key).unwrap_or_default(); Ok(data.into()) } @@ -241,7 +209,7 @@ where action: F, ) -> Result where - F: FnOnce(&Box, DepsMut, Env) -> Result, + F: FnOnce(&Box, Deps, Env) -> Result, { let contract = self .contracts @@ -253,12 +221,8 @@ where .ok_or_else(|| "Unregistered code id".to_string())?; let env = self.get_env(address); - let mut storage = contract - .storage - .try_borrow_mut() - .map_err(|e| format!("Double-borrowing mutable storage - re-entrancy?: {}", e))?; - let deps = DepsMut { - storage: storage.deref_mut(), + let deps = Deps { + storage: contract.storage.as_ref(), api: self.api.deref(), querier: QuerierWrapper::new(querier), }; @@ -266,6 +230,207 @@ where } } +/// A writable transactional cache over the wasm state. +/// +/// Reads hit local hashmap or then hit router +/// Getting storage wraps the internal contract storage +/// - adding handler +/// - adding contract +/// - writing existing contract +/// Return op-log to flush, like transactional: +/// - consume this struct (release router) and return list of ops to perform +/// - pass ops &mut WasmRouter to update them +/// +/// In Router, we use this exclusively in all the calls in execute (not self.wasm) +/// In Querier, we use self.wasm +pub struct WasmCache<'a> { + // and this into one with reference + router: &'a WasmRouter, + state: WasmCacheState<'a>, +} + +/// This is the mutable state of the cached. +/// Separated out so we can grab a mutable reference to both these HashMaps, +/// while still getting an immutable reference to router. +/// (We cannot take &mut WasmCache) +pub struct WasmCacheState<'a> { + contracts: HashMap, + contract_diffs: HashMap>, +} + +/// This is a set of data from the WasmCache with no external reference, +/// which can be used to commit to the underlying WasmRouter. +pub struct WasmOps { + new_contracts: HashMap, + contract_diffs: Vec<(HumanAddr, RepLog)>, +} + +impl WasmOps { + pub fn commit(self, router: &mut WasmRouter) { + self.new_contracts.into_iter().for_each(|(k, v)| { + router.contracts.insert(k, v); + }); + self.contract_diffs.into_iter().for_each(|(k, ops)| { + let storage = router.contracts.get_mut(&k).unwrap().storage.as_mut(); + ops.commit(storage); + }); + } +} + +impl<'a> WasmCache<'a> { + fn new(router: &'a WasmRouter) -> Self { + WasmCache { + router, + state: WasmCacheState { + contracts: HashMap::new(), + contract_diffs: HashMap::new(), + }, + } + } + + /// When we want to commit the WasmCache, we need a 2 step process to satisfy Rust reference counting: + /// 1. prepare() consumes WasmCache, releasing &WasmRouter, and creating a self-owned update info. + /// 2. WasmOps::commit() can now take &mut WasmRouter and updates the underlying state + pub fn prepare(self) -> WasmOps { + self.state.prepare() + } + + /// This just creates an address and empty storage instance, returning the new address + /// You must call init after this to set up the contract properly. + /// These are separated into two steps to have cleaner return values. + pub fn register_contract(&mut self, code_id: usize) -> Result { + if !self.router.handlers.contains_key(&code_id) { + return Err("Cannot init contract with unregistered code id".to_string()); + } + let addr = self.next_address(); + let info = ContractData::new(code_id, (self.router.storage_factory)()); + self.state.contracts.insert(addr.clone(), info); + Ok(addr) + } + + // TODO: better addr generation + fn next_address(&self) -> HumanAddr { + let count = self.router.contracts.len() + self.state.contracts.len(); + HumanAddr::from(count.to_string()) + } + + pub fn handle( + &mut self, + address: HumanAddr, + querier: &dyn Querier, + info: MessageInfo, + msg: Vec, + ) -> Result { + let parent = &self.router.handlers; + let contracts = &self.router.contracts; + let env = self.router.get_env(address.clone()); + let api = self.router.api.as_ref(); + + self.state.with_storage( + querier, + contracts, + address, + env, + api, + |code_id, deps, env| { + let handler = parent + .get(&code_id) + .ok_or_else(|| "Unregistered code id".to_string())?; + handler.handle(deps, env, info, msg) + }, + ) + } + + pub fn init( + &mut self, + address: HumanAddr, + querier: &dyn Querier, + info: MessageInfo, + msg: Vec, + ) -> Result { + let parent = &self.router.handlers; + let contracts = &self.router.contracts; + let env = self.router.get_env(address.clone()); + let api = self.router.api.as_ref(); + + self.state.with_storage( + querier, + contracts, + address, + env, + api, + |code_id, deps, env| { + let handler = parent + .get(&code_id) + .ok_or_else(|| "Unregistered code id".to_string())?; + handler.init(deps, env, info, msg) + }, + ) + } +} + +impl<'a> WasmCacheState<'a> { + pub fn prepare(self) -> WasmOps { + let diffs: Vec<_> = self + .contract_diffs + .into_iter() + .map(|(k, store)| (k, store.prepare())) + .collect(); + + WasmOps { + new_contracts: self.contracts, + contract_diffs: diffs, + } + } + + fn get_contract<'b>( + &'b mut self, + parent: &'a HashMap, + addr: &HumanAddr, + ) -> Option<(usize, &'b mut dyn Storage)> { + // if we created this transaction + if let Some(x) = self.contracts.get_mut(addr) { + return Some((x.code_id, x.storage.as_mut())); + } + if let Some(c) = parent.get(addr) { + let code_id = c.code_id; + if self.contract_diffs.contains_key(addr) { + let storage = self.contract_diffs.get_mut(addr).unwrap(); + return Some((code_id, storage)); + } + // else make a new transaction + let wrap = StorageTransaction::new(c.storage.as_ref()); + self.contract_diffs.insert(addr.clone(), wrap); + Some((code_id, self.contract_diffs.get_mut(addr).unwrap())) + } else { + None + } + } + + fn with_storage( + &mut self, + querier: &dyn Querier, + parent: &'a HashMap, + address: HumanAddr, + env: Env, + api: &dyn Api, + action: F, + ) -> Result + where + F: FnOnce(usize, DepsMut, Env) -> Result, + { + let (code_id, storage) = self + .get_contract(parent, &address) + .ok_or_else(|| "Unregistered contract address".to_string())?; + let deps = DepsMut { + storage, + api, + querier: QuerierWrapper::new(querier), + }; + action(code_id, deps, env) + } +} + #[cfg(test)] mod test { use super::*; @@ -274,27 +439,28 @@ mod test { use cosmwasm_std::testing::{mock_env, mock_info, MockApi, MockQuerier, MockStorage}; use cosmwasm_std::{coin, to_vec, BankMsg, BlockInfo, CosmosMsg, Empty}; - fn mock_router() -> WasmRouter { + fn mock_router() -> WasmRouter { let env = mock_env(); let api = Box::new(MockApi::default()); - WasmRouter::::new(api, env.block) + WasmRouter::new(api, env.block, || Box::new(MockStorage::new())) } #[test] fn register_contract() { let mut router = mock_router(); let code_id = router.store_code(contract_error()); + let mut cache = router.cache(); // cannot register contract with unregistered codeId - router.register_contract(code_id + 1).unwrap_err(); + cache.register_contract(code_id + 1).unwrap_err(); // we can register a new instance of this code - let contract_addr = router.register_contract(code_id).unwrap(); + let contract_addr = cache.register_contract(code_id).unwrap(); // now, we call this contract and see the error message from the contract let querier: MockQuerier = MockQuerier::new(&[]); let info = mock_info("foobar", &[]); - let err = router + let err = cache .init(contract_addr, &querier, info, b"{}".to_vec()) .unwrap_err(); // StdError from contract_error auto-converted to string @@ -302,11 +468,14 @@ mod test { // and the error for calling an unregistered contract let info = mock_info("foobar", &[]); - let err = router + let err = cache .init("unregistered".into(), &querier, info, b"{}".to_vec()) .unwrap_err(); // Default error message from router when not found assert_eq!(err, "Unregistered contract address"); + + // and flush + cache.prepare().commit(&mut router); } #[test] @@ -325,7 +494,9 @@ mod test { fn contract_send_coins() { let mut router = mock_router(); let code_id = router.store_code(contract_payout()); - let contract_addr = router.register_contract(code_id).unwrap(); + let mut cache = router.cache(); + + let contract_addr = cache.register_contract(code_id).unwrap(); let querier: MockQuerier = MockQuerier::new(&[]); let payout = coin(100, "TGD"); @@ -336,14 +507,14 @@ mod test { payout: payout.clone(), }) .unwrap(); - let res = router + let res = cache .init(contract_addr.clone(), &querier, info, init_msg) .unwrap(); assert_eq!(0, res.messages.len()); // execute the contract let info = mock_info("foobar", &[]); - let res = router + let res = cache .handle(contract_addr.clone(), &querier, info, b"{}".to_vec()) .unwrap(); assert_eq!(1, res.messages.len()); @@ -360,9 +531,12 @@ mod test { m => panic!("Unexpected message {:?}", m), } + // and flush before query + cache.prepare().commit(&mut router); + // query the contract let data = router - .query(contract_addr.clone(), &querier, b"{}".to_vec()) + .query_smart(contract_addr.clone(), &querier, b"{}".to_vec()) .unwrap(); let res: PayoutMessage = from_slice(&data).unwrap(); assert_eq!(res.payout, payout);