diff --git a/.circleci/config.yml b/.circleci/config.yml index e9a3a574..8f29a39a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -30,7 +30,7 @@ workflows: jobs: build_and_test: docker: - - image: rust:1.65.0 + - image: rust:1.75 working_directory: ~/project steps: - checkout @@ -42,8 +42,8 @@ jobs: command: cargo update - restore_cache: keys: - - cargocache-v2-multi-test:1.65.0-{{ checksum "Cargo.lock" }} - - cargocache-v2-multi-test:1.65.0- + - cargocache-v2-multi-test:1.75-{{ checksum "Cargo.lock" }} + - cargocache-v2-multi-test:1.75- - run: name: Build library for native target command: cargo build --locked @@ -54,12 +54,12 @@ jobs: paths: - /usr/local/cargo/registry - target - key: cargocache-v2-multi-test:1.65.0-{{ checksum "Cargo.lock" }} + key: cargocache-v2-multi-test:1.75-{{ checksum "Cargo.lock" }} build_minimal: docker: - image: rustlang/rust:nightly - working_directory: ~/project/ + working_directory: ~/project steps: - checkout - run: @@ -68,25 +68,29 @@ jobs: - run: name: Remove Cargo.lock command: rm Cargo.lock + # Remove the following command after dependencies in crates ahash and num-bigint are upgraded! + - run: + name: Temporarily update problematic crates + command: cargo update -p ahash && cargo update -p num-bigint - restore_cache: keys: - - cargocache-v2-multi-test:1.65.0-minimal-{{ checksum "Cargo.toml" }} + - cargocache-v2-multi-test:1.75-minimal-{{ checksum "Cargo.toml" }} - run: name: Build library for native target - command: cargo build -Zminimal-versions --features backtrace,cosmwasm_1_1,cosmwasm_1_2,cosmwasm_1_3,cosmwasm_1_4 + command: cargo build -Zminimal-versions --all-features - run: name: Run unit tests - command: cargo test --workspace -Zminimal-versions --features backtrace,cosmwasm_1_1,cosmwasm_1_2,cosmwasm_1_3,cosmwasm_1_4 + command: cargo test --workspace -Zminimal-versions --all-features - save_cache: paths: - /usr/local/cargo/registry - target - key: cargocache-v2-multi-test:1.65.0-minimal-{{ checksum "Cargo.toml" }} + key: cargocache-v2-multi-test:1.75-minimal-{{ checksum "Cargo.toml" }} build_maximal: docker: - - image: rust:1.65.0 - working_directory: ~/project/ + - image: rust:1.75 + working_directory: ~/project steps: - checkout - run: @@ -97,22 +101,22 @@ jobs: command: cargo update - restore_cache: keys: - - cargocache-v2-multi-test:1.65.0-{{ checksum "Cargo.lock" }} + - cargocache-v2-multi-test:1.75-{{ checksum "Cargo.lock" }} - run: name: Build library for native target command: cargo build --locked --all-features - run: name: Run unit tests - command: cargo test --workspace --locked --features backtrace,cosmwasm_1_1,cosmwasm_1_2,cosmwasm_1_3,cosmwasm_1_4 + command: cargo test --workspace --locked --all-features - save_cache: paths: - /usr/local/cargo/registry - target - key: cargocache-v2-multi-test:1.65.0-{{ checksum "Cargo.lock" }} + key: cargocache-v2-multi-test:1.75-{{ checksum "Cargo.lock" }} lint: docker: - - image: rust:1.65.0 + - image: rust:1.75 steps: - checkout - run: @@ -123,8 +127,8 @@ jobs: command: cargo update - restore_cache: keys: - - cargocache-v2-lint-rust:1.65.0-{{ checksum "Cargo.lock" }} - - cargocache-v2-lint-rust:1.65.0- + - cargocache-v2-lint-rust:1.75-{{ checksum "Cargo.lock" }} + - cargocache-v2-lint-rust:1.75- - run: name: Add rustfmt component command: rustup component add rustfmt @@ -143,19 +147,19 @@ jobs: - target/debug/.fingerprint - target/debug/build - target/debug/deps - key: cargocache-v2-lint-rust:1.65.0-{{ checksum "Cargo.lock" }} + key: cargocache-v2-lint-rust:1.75-{{ checksum "Cargo.lock" }} coverage: # https://circleci.com/developer/images?imageType=machine machine: - image: ubuntu-2004:202201-02 + image: ubuntu-2404:2024.05.1 steps: - checkout - run: name: Run tests with coverage command: | mkdir -p cov - docker run --security-opt seccomp=unconfined -v "${PWD}:/volume" xd009642/tarpaulin \ - sh -c "cargo tarpaulin --workspace --features backtrace,cosmwasm_1_1,cosmwasm_1_2,cosmwasm_1_3,cosmwasm_1_4 --force-clean --engine llvm --out xml --output-dir cov" + docker run --security-opt seccomp=unconfined -v "${PWD}:/volume" xd009642/tarpaulin:0.31.0 \ + sh -c "cargo tarpaulin --workspace --all-features --force-clean --engine llvm --out xml --output-dir cov" - codecov/upload: file: cov/cobertura.xml diff --git a/CHANGELOG.md b/CHANGELOG.md index d84531fb..ac331a58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,137 @@ # Changelog +## [v2.1.1](https://github.com/CosmWasm/cw-multi-test/tree/v2.1.1) (2024-08-20) + +[Full Changelog](https://github.com/CosmWasm/cw-multi-test/compare/v2.1.0...v2.1.1) + +**Closed issues:** + +- Fix documentation metadata [\#203](https://github.com/CosmWasm/cw-multi-test/issues/203) + +**Merged pull requests:** + +- Fixed documentation metadata [\#204](https://github.com/CosmWasm/cw-multi-test/pull/204) ([DariuszDepta](https://github.com/DariuszDepta)) + +## [v2.1.0](https://github.com/CosmWasm/cw-multi-test/tree/v2.1.0) (2024-07-05) + +[Full Changelog](https://github.com/CosmWasm/cw-multi-test/compare/v2.0.1...v2.1.0) + +**Closed issues:** + +- `StakingSudo::ProcessQueue` is still necessary, despite deprecation warnings [\#127](https://github.com/CosmWasm/cw-multi-test/issues/127) +- Cosmwasm Std Empty String Attribute Causes Failure [\#178](https://github.com/CosmWasm/cw-multi-test/issues/178) +- Don't validate validator addresses [\#173](https://github.com/CosmWasm/cw-multi-test/issues/173) +- Using `addr_make` inside `AppBuilder::build` [\#180](https://github.com/CosmWasm/cw-multi-test/issues/188) + +**Merged pull requests:** + +- Removed `cosmwasm-std` features from dependencies [\#191](https://github.com/CosmWasm/cw-multi-test/pull/191) ([DariuszDepta](https://github.com/DariuszDepta)) +- Enabled using `addr_make` inside `AppBuilder::build` [\#189](https://github.com/CosmWasm/cw-multi-test/pull/189) ([DariuszDepta](https://github.com/DariuszDepta)) +- Do not validate validator addresses [\#183](https://github.com/CosmWasm/cw-multi-test/pull/183) ([DariuszDepta](https://github.com/DariuszDepta)) +- Refactoring staking module, part 2 [\#186](https://github.com/CosmWasm/cw-multi-test/pull/186) ([DariuszDepta](https://github.com/DariuszDepta)) +- Refactoring staking module, part 1 [\#185](https://github.com/CosmWasm/cw-multi-test/pull/185) ([DariuszDepta](https://github.com/DariuszDepta)) +- Removed validation of empty attribute value [\#180](https://github.com/CosmWasm/cw-multi-test/pull/180) ([DariuszDepta](https://github.com/DariuszDepta)) +- Improved unstake handling [\#179](https://github.com/CosmWasm/cw-multi-test/pull/179) ([DariuszDepta](https://github.com/DariuszDepta)) +- Remove default features [\#175](https://github.com/CosmWasm/cw-multi-test/pull/175) ([DariuszDepta](https://github.com/DariuszDepta)) +- Feature-gates in pattern matching of CosmWasm 2.0 Enum Variants [\#170](https://github.com/CosmWasm/cw-multi-test/pull/170) ([AmitPr](https://github.com/AmitPr)) +- Upgraded dependencies [\#168](https://github.com/CosmWasm/cw-multi-test/pull/168) ([DariuszDepta](https://github.com/DariuszDepta)) +- Added lacking changelog items [\#192](https://github.com/CosmWasm/cw-multi-test/pull/192) ([DariuszDepta](https://github.com/DariuszDepta)) + +## [v2.0.1](https://github.com/CosmWasm/cw-multi-test/tree/v2.0.1) (2024-04-22) + +[Full Changelog](https://github.com/CosmWasm/cw-multi-test/compare/v2.0.0...v2.0.1) + +**Closed issues:** + +- Restore support for `Stargate` messages [\#159](https://github.com/CosmWasm/cw-multi-test/issues/159) + +**Merged pull requests:** + +- Distinctive Stargate [\#163](https://github.com/CosmWasm/cw-multi-test/pull/163) ([DariuszDepta](https://github.com/DariuszDepta)) +- Expose contract storage [\#153](https://github.com/CosmWasm/cw-multi-test/pull/153) ([DariuszDepta](https://github.com/DariuszDepta)) +- Expose prefixed storage [\#154](https://github.com/CosmWasm/cw-multi-test/pull/154) ([DariuszDepta](https://github.com/DariuszDepta)) +- Upgraded dependencies [\#164](https://github.com/CosmWasm/cw-multi-test/pull/164) ([DariuszDepta](https://github.com/DariuszDepta)) +- Fixes typos [\#167](https://github.com/CosmWasm/cw-multi-test/pull/157) ([DariuszDepta](https://github.com/DariuszDepta)) + +## [v2.0.0](https://github.com/CosmWasm/cw-multi-test/tree/v2.0.0) (2024-03-22) + +[Full Changelog](https://github.com/CosmWasm/cw-multi-test/compare/v1.0.0...v2.0.0) + +**Closed issues:** + +- Forward port: Fixing contract wrapper [\#146](https://github.com/CosmWasm/cw-multi-test/issues/146) +- Forward port: `store_code_with_id` helper [#131](https://github.com/CosmWasm/cw-multi-test/issues/131) + +**Merged pull requests:** + +- V2: upgrading dependencies and refactoring [\#128](https://github.com/CosmWasm/cw-multi-test/pull/128) ([DariuszDepta](https://github.com/DariuszDepta)) + +## [v1.2.0](https://github.com/CosmWasm/cw-multi-test/tree/v1.2.0) (2024-06-12) + +[Full Changelog](https://github.com/CosmWasm/cw-multi-test/compare/v1.1.0...v1.2.0) + +**Merged pull requests:** + +- Backport: Removed validation of empty attribute value [\#181](https://github.com/CosmWasm/cw-multi-test/pull/181) ([DariuszDepta](https://github.com/DariuszDepta)) + +## [v1.1.0](https://github.com/CosmWasm/cw-multi-test/tree/v1.1.0) (2024-04-23) + +[Full Changelog](https://github.com/CosmWasm/cw-multi-test/compare/v1.0.0...v1.1.0) + +**Merged pull requests:** + +- Backport: Expose contract storage [\#155](https://github.com/CosmWasm/cw-multi-test/issues/155) ([DariuszDepta](https://github.com/DariuszDepta)) +- Backport: Expose prefixed storage [\#156](https://github.com/CosmWasm/cw-multi-test/issues/156) ([DariuszDepta](https://github.com/DariuszDepta)) + +## [v1.0.0](https://github.com/CosmWasm/cw-multi-test/tree/v1.0.0) (2024-03-22) + +[Full Changelog](https://github.com/CosmWasm/cw-multi-test/compare/v0.20.1...v1.0.0) + +**Closed issues:** + +- Forward port: Fixing contract wrapper [\#145](https://github.com/CosmWasm/cw-multi-test/issues/145) +- Implement `store_code_with_id` helper [\#22](https://github.com/CosmWasm/cw-multi-test/issues/22) +- New `App::store_code` function definition [\#69](https://github.com/CosmWasm/cw-multi-test/issues/69) (wontfix) +- Make `App::store_code_with_creator` deprecated [\#70](https://github.com/CosmWasm/cw-multi-test/issues/70) (wontfix) +- Remove function `next_address` from `AddressGenerator` trait [\#90](https://github.com/CosmWasm/cw-multi-test/issues/90) +- Remove `new_with_custom_address_generator` function from `WasmKeeper` [\#91](https://github.com/CosmWasm/cw-multi-test/issues/91) + +**Merged pull requests:** + +- Refactored contract wrapper [\#149](https://github.com/CosmWasm/cw-multi-test/pull/149) ([DariuszDepta](https://github.com/DariuszDepta)) +- Fixed contract wrapper [\#148](https://github.com/CosmWasm/cw-multi-test/pull/148) ([DariuszDepta](https://github.com/DariuszDepta)) +- Remove `Addr::unchecked` where possible [\#141](https://github.com/CosmWasm/cw-multi-test/pull/141) ([DariuszDepta](https://github.com/DariuszDepta)) +- Refactored wasm trait [\#139](https://github.com/CosmWasm/cw-multi-test/pull/139) ([DariuszDepta](https://github.com/DariuszDepta)) +- Added `IntoAddr` trait [\#138](https://github.com/CosmWasm/cw-multi-test/pull/138) ([DariuszDepta](https://github.com/DariuszDepta)) +- Removed `new_with_custom_address_generator` function [\#135](https://github.com/CosmWasm/cw-multi-test/pull/135) ([DariuszDepta](https://github.com/DariuszDepta)) +- Removed `next_address` function [\#134](https://github.com/CosmWasm/cw-multi-test/pull/134) ([DariuszDepta](https://github.com/DariuszDepta)) +- Added `store_code_with_id` function to `App` [\#117](https://github.com/CosmWasm/cw-multi-test/pull/117) ([DariuszDepta](https://github.com/DariuszDepta)) + +## [v0.20.1](https://github.com/CosmWasm/cw-multi-test/tree/v0.20.1) (2024-03-15) + +[Full Changelog](https://github.com/CosmWasm/cw-multi-test/compare/v0.20.0...v0.20.1) + +**Merged pull requests:** + +- Fixed contract wrapper [\#147](https://github.com/CosmWasm/cw-multi-test/pull/147) ([DariuszDepta](https://github.com/DariuszDepta)) + +## [v0.20.0](https://github.com/CosmWasm/cw-multi-test/tree/v0.20.0) (2023-12-06) + +[Full Changelog](https://github.com/CosmWasm/cw-multi-test/compare/v0.19.0...v0.20.0) + +**Closed issues:** + +- Unit testing IBC with mock module fails: Cannot execute Stargate [\#37](https://github.com/CosmWasm/cw-multi-test/issues/37) +- Allow mocking Stargate messages [\#40](https://github.com/CosmWasm/cw-multi-test/issues/40) +- MultiTest does not support CosmosMsg::Stargate [\#88](https://github.com/CosmWasm/cw-multi-test/issues/88) + +**Merged pull requests:** + +- Stargate mock support [\#106](https://github.com/CosmWasm/cw-multi-test/pull/106) ([DariuszDepta](https://github.com/DariuszDepta)) +- Made `no_init` function public [\#107](https://github.com/CosmWasm/cw-multi-test/pull/107) ([DariuszDepta](https://github.com/DariuszDepta)) +- Separated test helper contracts for Gov and IBC [\#108](https://github.com/CosmWasm/cw-multi-test/pull/108) ([DariuszDepta](https://github.com/DariuszDepta)) +- Add test to check custom `AddressGenerator` implementation [\#110](https://github.com/CosmWasm/cw-multi-test/pull/110) ([epanchee](https://github.com/epanchee)) + ## [v0.19.0](https://github.com/CosmWasm/cw-multi-test/tree/v0.19.0) (2023-11-28) [Full Changelog](https://github.com/CosmWasm/cw-multi-test/compare/v0.18.1...v0.19.0) @@ -68,6 +200,15 @@ - Adds BankQuery::Supply support [\#51](https://github.com/CosmWasm/cw-multi-test/pull/51) ([JakeHartnell](https://github.com/JakeHartnell)) - Remove direct k256 dependencies [\#47](https://github.com/CosmWasm/cw-multi-test/pull/47) ([webmaster128](https://github.com/webmaster128)) +## [v0.16.6](https://github.com/CosmWasm/cw-multi-test/tree/v0.16.6) (2024-03-15) + +[Full Changelog](https://github.com/CosmWasm/cw-multi-test/compare/v0.16.5...v0.16.6) + +**Merged pull requests:** + +- Fixed contract + wrapper [\#143](https://github.com/CosmWasm/cw-multi-test/pull/143) ([DariuszDepta](https://github.com/DariuszDepta)) + ## [v0.16.5](https://github.com/CosmWasm/cw-multi-test/tree/v0.16.5) (2023-06-07) [Full Changelog](https://github.com/CosmWasm/cw-multi-test/compare/v0.16.4...v0.16.5) @@ -131,6 +272,7 @@ changelog will be noisy - not everything is relevant to `cw-multi-test` there. ## [v0.16.1](https://github.com/CosmWasm/cw-plus/tree/v0.16.1) (2022-11-23) [Full Changelog](https://github.com/CosmWasm/cw-plus/compare/v0.16.0...v0.16.1) + - Modules for Stargate (IBC and Gov) messages - failing by default, but possible to exchange ## [v0.16.0](https://github.com/CosmWasm/cw-plus/tree/v0.16.0) (2022-10-14) @@ -329,7 +471,7 @@ changelog will be noisy - not everything is relevant to `cw-multi-test` there. - Update changelog add upcoming [\#675](https://github.com/CosmWasm/cw-plus/pull/675) ([maurolacy](https://github.com/maurolacy)) - Reject proposals early [\#668](https://github.com/CosmWasm/cw-plus/pull/668) ([Callum-A](https://github.com/Callum-A)) - cw20-base: validate addresses are unique in initial balances [\#659](https://github.com/CosmWasm/cw-plus/pull/659) ([harryscholes](https://github.com/harryscholes)) -- New SECURITY.md refering to wasmd [\#624](https://github.com/CosmWasm/cw-plus/pull/624) ([ethanfrey](https://github.com/ethanfrey)) +- New SECURITY.md referring to wasmd [\#624](https://github.com/CosmWasm/cw-plus/pull/624) ([ethanfrey](https://github.com/ethanfrey)) ## [v0.13.0](https://github.com/CosmWasm/cw-plus/tree/v0.13.0) (2022-03-09) @@ -389,7 +531,7 @@ changelog will be noisy - not everything is relevant to `cw-multi-test` there. **Merged pull requests:** - Prepare release v0.12.0 [\#654](https://github.com/CosmWasm/cw-plus/pull/654) ([uint](https://github.com/uint)) -- Ics20 same ack handling as ibctransfer [\#653](https://github.com/CosmWasm/cw-plus/pull/653) ([ethanfrey](https://github.com/ethanfrey)) +- Ics20 same ack handling as IBC transfer [\#653](https://github.com/CosmWasm/cw-plus/pull/653) ([ethanfrey](https://github.com/ethanfrey)) - packages: support custom queries [\#652](https://github.com/CosmWasm/cw-plus/pull/652) ([uint](https://github.com/uint)) - CW20 - Fix Docs URL [\#649](https://github.com/CosmWasm/cw-plus/pull/649) ([entrancedjames](https://github.com/entrancedjames)) - CW3: Add proposal\_id field to VoteInfo structure [\#648](https://github.com/CosmWasm/cw-plus/pull/648) ([ueco-jb](https://github.com/ueco-jb)) @@ -539,7 +681,7 @@ changelog will be noisy - not everything is relevant to `cw-multi-test` there. - ics20: Handle send errors with reply [\#520](https://github.com/CosmWasm/cw-plus/pull/520) ([ethanfrey](https://github.com/ethanfrey)) - Proper execute responses [\#519](https://github.com/CosmWasm/cw-plus/pull/519) ([ethanfrey](https://github.com/ethanfrey)) - Publish MsgInstantiate / Execute responses [\#518](https://github.com/CosmWasm/cw-plus/pull/518) ([maurolacy](https://github.com/maurolacy)) -- Fix instaniate reply data [\#517](https://github.com/CosmWasm/cw-plus/pull/517) ([ethanfrey](https://github.com/ethanfrey)) +- Fix instantiate reply data [\#517](https://github.com/CosmWasm/cw-plus/pull/517) ([ethanfrey](https://github.com/ethanfrey)) - Use protobuf de helpers [\#515](https://github.com/CosmWasm/cw-plus/pull/515) ([maurolacy](https://github.com/maurolacy)) - Add tests for the claims controller [\#514](https://github.com/CosmWasm/cw-plus/pull/514) ([sgoya](https://github.com/sgoya)) - Implement cw3-flex-multisig helper [\#479](https://github.com/CosmWasm/cw-plus/pull/479) ([orkunkl](https://github.com/orkunkl)) @@ -558,9 +700,9 @@ changelog will be noisy - not everything is relevant to `cw-multi-test` there. - Prepare 0.10.1 release [\#513](https://github.com/CosmWasm/cw-plus/pull/513) ([ethanfrey](https://github.com/ethanfrey)) - Added cw1-whitelist-ng to CI [\#512](https://github.com/CosmWasm/cw-plus/pull/512) ([hashedone](https://github.com/hashedone)) -- cw1-subkeys-ng: Additional follow up improvements [\#506](https://github.com/CosmWasm/cw-plus/pull/506) ([hashedone](https://github.com/hashedone)) +- cw1-subkeys-ng: Additional follow-up improvements [\#506](https://github.com/CosmWasm/cw-plus/pull/506) ([hashedone](https://github.com/hashedone)) - Parse reply helpers [\#502](https://github.com/CosmWasm/cw-plus/pull/502) ([maurolacy](https://github.com/maurolacy)) -- cw1-whitelist-ng: Contract implementation in terms of semantical structures [\#499](https://github.com/CosmWasm/cw-plus/pull/499) ([hashedone](https://github.com/hashedone)) +- cw1-whitelist-ng: Contract implementation in terms of semantic structures [\#499](https://github.com/CosmWasm/cw-plus/pull/499) ([hashedone](https://github.com/hashedone)) - range\_de for IndexMap [\#498](https://github.com/CosmWasm/cw-plus/pull/498) ([uint](https://github.com/uint)) - Implement range\_de for SnapshotMap [\#497](https://github.com/CosmWasm/cw-plus/pull/497) ([uint](https://github.com/uint)) - Fix publish script [\#486](https://github.com/CosmWasm/cw-plus/pull/486) ([ethanfrey](https://github.com/ethanfrey)) @@ -608,7 +750,7 @@ changelog will be noisy - not everything is relevant to `cw-multi-test` there. - Release v0.10.0-soon4 [\#477](https://github.com/CosmWasm/cw-plus/pull/477) ([ethanfrey](https://github.com/ethanfrey)) - Update to CosmWasm 1.0.0-soon2 [\#475](https://github.com/CosmWasm/cw-plus/pull/475) ([ethanfrey](https://github.com/ethanfrey)) - Allow error type conversions in ensure! and ensure\_eq! [\#474](https://github.com/CosmWasm/cw-plus/pull/474) ([webmaster128](https://github.com/webmaster128)) -- Improve error handling / remove FIXMEs [\#470](https://github.com/CosmWasm/cw-plus/pull/470) ([maurolacy](https://github.com/maurolacy)) +- Improve error handling / remove FIXME markers [\#470](https://github.com/CosmWasm/cw-plus/pull/470) ([maurolacy](https://github.com/maurolacy)) - Add ensure [\#469](https://github.com/CosmWasm/cw-plus/pull/469) ([ethanfrey](https://github.com/ethanfrey)) - Key deserializer improvements [\#467](https://github.com/CosmWasm/cw-plus/pull/467) ([maurolacy](https://github.com/maurolacy)) - Upgrade to cosmwasm/workspace-optimizer:0.12.3 [\#465](https://github.com/CosmWasm/cw-plus/pull/465) ([webmaster128](https://github.com/webmaster128)) @@ -659,7 +801,7 @@ changelog will be noisy - not everything is relevant to `cw-multi-test` there. - storage-plus: Improve in-code documentation of map primitives, in particular `MultiIndex` [\#407](https://github.com/CosmWasm/cw-plus/issues/407) - Remove use of dyn in multitest Router [\#404](https://github.com/CosmWasm/cw-plus/issues/404) - Define generic multitest module [\#387](https://github.com/CosmWasm/cw-plus/issues/387) -- Cw20 state key compatibity with previous versions [\#346](https://github.com/CosmWasm/cw-plus/issues/346) +- Cw20 state key compatibility with previous versions [\#346](https://github.com/CosmWasm/cw-plus/issues/346) - Refactor cw20-base to use controller pattern [\#205](https://github.com/CosmWasm/cw-plus/issues/205) **Merged pull requests:** @@ -715,7 +857,7 @@ changelog will be noisy - not everything is relevant to `cw-multi-test` there. - Snapshot item [\#409](https://github.com/CosmWasm/cw-plus/pull/409) ([maurolacy](https://github.com/maurolacy)) - cw20-base: upgrade helper.ts to cosmjs 0.26.0 [\#406](https://github.com/CosmWasm/cw-plus/pull/406) ([spacepotahto](https://github.com/spacepotahto)) - CW1-whitelist execute multitest [\#402](https://github.com/CosmWasm/cw-plus/pull/402) ([ueco-jb](https://github.com/ueco-jb)) -- Implementing all messages handling in mutlitest App [\#398](https://github.com/CosmWasm/cw-plus/pull/398) ([hashedone](https://github.com/hashedone)) +- Implementing all messages handling in MultiTest App [\#398](https://github.com/CosmWasm/cw-plus/pull/398) ([hashedone](https://github.com/hashedone)) - Make it easier to assert events on reply statements [\#395](https://github.com/CosmWasm/cw-plus/pull/395) ([ethanfrey](https://github.com/ethanfrey)) - Add helpers to check events [\#392](https://github.com/CosmWasm/cw-plus/pull/392) ([ethanfrey](https://github.com/ethanfrey)) - Switching from String to anyhow::Error for error type in multi-test [\#389](https://github.com/CosmWasm/cw-plus/pull/389) ([hashedone](https://github.com/hashedone)) @@ -788,7 +930,7 @@ changelog will be noisy - not everything is relevant to `cw-multi-test` there. - Responses validation in multi-test [\#373](https://github.com/CosmWasm/cw-plus/pull/373) ([hashedone](https://github.com/hashedone)) - Cw20 logo spec [\#370](https://github.com/CosmWasm/cw-plus/pull/370) ([ethanfrey](https://github.com/ethanfrey)) - Properly handling data in submessages in multi-test [\#369](https://github.com/CosmWasm/cw-plus/pull/369) ([hashedone](https://github.com/hashedone)) -- Abstracting API out of tests internals so it is clearly owned by `App` [\#368](https://github.com/CosmWasm/cw-plus/pull/368) ([hashedone](https://github.com/hashedone)) +- Abstracting API out of tests internals, so it is clearly owned by `App` [\#368](https://github.com/CosmWasm/cw-plus/pull/368) ([hashedone](https://github.com/hashedone)) - Storage plus doc correction [\#367](https://github.com/CosmWasm/cw-plus/pull/367) ([hashedone](https://github.com/hashedone)) - Multitest migrate support [\#366](https://github.com/CosmWasm/cw-plus/pull/366) ([ethanfrey](https://github.com/ethanfrey)) - Reorganizations of contracts in `multi-test::test_utils` [\#365](https://github.com/CosmWasm/cw-plus/pull/365) ([hashedone](https://github.com/hashedone)) @@ -841,7 +983,7 @@ changelog will be noisy - not everything is relevant to `cw-multi-test` there. - Proper event/data handling on reply in multitest [\#326](https://github.com/CosmWasm/cw-plus/issues/326) - Messages differ for cw20 & cw20\_base [\#320](https://github.com/CosmWasm/cw-plus/issues/320) - Upgrade cw20-staking to cw 15 [\#312](https://github.com/CosmWasm/cw-plus/issues/312) -- Uprade cw20-ics20 to cw 0.15 [\#311](https://github.com/CosmWasm/cw-plus/issues/311) +- Upgrade cw20-ics20 to cw 0.15 [\#311](https://github.com/CosmWasm/cw-plus/issues/311) - Upgrade cw20-escrow to 0.15 [\#309](https://github.com/CosmWasm/cw-plus/issues/309) - Upgrade cw20-bonding to 0.15 [\#307](https://github.com/CosmWasm/cw-plus/issues/307) - cw1-subkeys [\#305](https://github.com/CosmWasm/cw-plus/issues/305) @@ -911,7 +1053,7 @@ changelog will be noisy - not everything is relevant to `cw-multi-test` there. **Merged pull requests:** - Clarify index\_key\(\) range\(\) vs prefix\(\) behaviour [\#291](https://github.com/CosmWasm/cw-plus/pull/291) ([maurolacy](https://github.com/maurolacy)) -- Pkowned to vec u8 [\#290](https://github.com/CosmWasm/cw-plus/pull/290) ([maurolacy](https://github.com/maurolacy)) +- PkOwned to vec u8 [\#290](https://github.com/CosmWasm/cw-plus/pull/290) ([maurolacy](https://github.com/maurolacy)) - Update to CosmWasm v0.14.0 [\#289](https://github.com/CosmWasm/cw-plus/pull/289) ([ethanfrey](https://github.com/ethanfrey)) - Primary key / index key helpers [\#288](https://github.com/CosmWasm/cw-plus/pull/288) ([maurolacy](https://github.com/maurolacy)) @@ -961,7 +1103,7 @@ changelog will be noisy - not everything is relevant to `cw-multi-test` there. **Merged pull requests:** -- Bump dependency to cosmasm v0.14.0-beta3 [\#269](https://github.com/CosmWasm/cw-plus/pull/269) ([ethanfrey](https://github.com/ethanfrey)) +- Bump dependency to cosmwasm v0.14.0-beta3 [\#269](https://github.com/CosmWasm/cw-plus/pull/269) ([ethanfrey](https://github.com/ethanfrey)) - Remove unused PrimaryKey::parse\_key [\#267](https://github.com/CosmWasm/cw-plus/pull/267) ([webmaster128](https://github.com/webmaster128)) - Use workspace-optimizer:0.11.0 [\#262](https://github.com/CosmWasm/cw-plus/pull/262) ([webmaster128](https://github.com/webmaster128)) - Update cosmwasm-std [\#260](https://github.com/CosmWasm/cw-plus/pull/260) ([yihuang](https://github.com/yihuang)) @@ -1072,7 +1214,7 @@ changelog will be noisy - not everything is relevant to `cw-multi-test` there. - Don't use hooks for snapshotting on cw3-cw4 interface [\#162](https://github.com/CosmWasm/cw-plus/issues/162) - Refactor snapshotting into reusable module [\#161](https://github.com/CosmWasm/cw-plus/issues/161) - Distinguish between weight 0 and not member in cw3 queries [\#154](https://github.com/CosmWasm/cw-plus/issues/154) -- Migrate strorage-plus to v0.12.0 [\#149](https://github.com/CosmWasm/cw-plus/issues/149) +- Migrate storage-plus to v0.12.0 [\#149](https://github.com/CosmWasm/cw-plus/issues/149) - Asymmetries between query and execute in CW1 \(subkeys\) [\#145](https://github.com/CosmWasm/cw-plus/issues/145) - Add token-weighted group [\#142](https://github.com/CosmWasm/cw-plus/issues/142) - Multisig handles changes to group membership [\#141](https://github.com/CosmWasm/cw-plus/issues/141) @@ -1180,7 +1322,7 @@ changelog will be noisy - not everything is relevant to `cw-multi-test` there. **Closed issues:** -- Migration to 0.11: errors of shared functions accross contracts [\#103](https://github.com/CosmWasm/cw-plus/issues/103) +- Migration to 0.11: errors of shared functions across contracts [\#103](https://github.com/CosmWasm/cw-plus/issues/103) - Look at serde\(flatten\) to simplify return value composition [\#57](https://github.com/CosmWasm/cw-plus/issues/57) **Merged pull requests:** @@ -1222,7 +1364,7 @@ changelog will be noisy - not everything is relevant to `cw-multi-test` there. **Closed issues:** - Implement Copy for Coin / Vec\ [\#77](https://github.com/CosmWasm/cw-plus/issues/77) -- Why does not cw20 pass the received native token? [\#74](https://github.com/CosmWasm/cw-plus/issues/74) +- Why cw20 does not pass the received native token? [\#74](https://github.com/CosmWasm/cw-plus/issues/74) - Cw20Coin duplication [\#73](https://github.com/CosmWasm/cw-plus/issues/73) - Fix docker run script in all contract README [\#69](https://github.com/CosmWasm/cw-plus/issues/69) - Add cw20 support to atomic swap contract [\#27](https://github.com/CosmWasm/cw-plus/issues/27) @@ -1275,7 +1417,7 @@ changelog will be noisy - not everything is relevant to `cw-multi-test` there. - Bump all CosmWasm dependencies to 0.10.1 [\#56](https://github.com/CosmWasm/cw-plus/pull/56) ([ethanfrey](https://github.com/ethanfrey)) - Add new query to return all allowances on subkeys [\#54](https://github.com/CosmWasm/cw-plus/pull/54) ([ethanfrey](https://github.com/ethanfrey)) - Add CanSend query to the cw1 spec [\#53](https://github.com/CosmWasm/cw-plus/pull/53) ([ethanfrey](https://github.com/ethanfrey)) -- Add Expration to cw0 [\#51](https://github.com/CosmWasm/cw-plus/pull/51) ([ethanfrey](https://github.com/ethanfrey)) +- Add expiration to cw0 [\#51](https://github.com/CosmWasm/cw-plus/pull/51) ([ethanfrey](https://github.com/ethanfrey)) - Nft 721 spec [\#50](https://github.com/CosmWasm/cw-plus/pull/50) ([ethanfrey](https://github.com/ethanfrey)) - Add Subkeys helper [\#49](https://github.com/CosmWasm/cw-plus/pull/49) ([ethanfrey](https://github.com/ethanfrey)) - Add helpers to cw20-base [\#46](https://github.com/CosmWasm/cw-plus/pull/46) ([ethanfrey](https://github.com/ethanfrey)) @@ -1331,6 +1473,4 @@ changelog will be noisy - not everything is relevant to `cw-multi-test` there. - Define all Message and Query types [\#11](https://github.com/CosmWasm/cw-plus/pull/11) ([ethanfrey](https://github.com/ethanfrey)) - Set up basic CI script [\#10](https://github.com/CosmWasm/cw-plus/pull/10) ([ethanfrey](https://github.com/ethanfrey)) - - \* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)* diff --git a/CLA.md b/CLA.md deleted file mode 100644 index 17a701ee..00000000 --- a/CLA.md +++ /dev/null @@ -1,23 +0,0 @@ -## Contributor License Agreement -The following terms are used throughout this agreement: - -* You - the person or legal entity including its affiliates asked to accept this agreement. An affiliate is any entity that controls or is controlled by the legal entity, or is under common control with it. -* Project - is an umbrella term that refers to any and all Confio OÜ open source projects. -* Contribution - any type of work that is submitted to a Project, including any modifications or additions to existing work. -* Submitted - conveyed to a Project via a pull request, commit, issue, or any form of electronic, written, or verbal communication with Confio OÜ, contributors or maintainers. - -## 1. Grant of Copyright License. - -Subject to the terms and conditions of this agreement, You grant to the Projects’ maintainers, contributors, users and to Confio OÜ a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute Your contributions and such derivative works. Except for this license, You reserve all rights, title, and interest in your contributions. - -## 2. Grant of Patent License. - -Subject to the terms and conditions of this agreement, You grant to the Projects’ maintainers, contributors, users and to Confio OÜ a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer your contributions, where such license applies only to those patent claims licensable by you that are necessarily infringed by your contribution or by combination of your contribution with the project to which this contribution was submitted. - -If any entity institutes patent litigation - including cross-claim or counterclaim in a lawsuit - against You alleging that your contribution or any project it was submitted to constitutes or is responsible for direct or contributory patent infringement, then any patent licenses granted to that entity under this agreement shall terminate as of the date such litigation is filed. - -## 3. Source of Contribution. - -Your contribution is either your original creation, based upon previous work that, to the best of your knowledge, is covered under an appropriate open source license and you have the right under that license to submit that work with modifications, whether created in whole or in part by you, or you have clearly identified the source of the contribution and any license or other restriction (like related patents, trademarks, and license agreements) of which you are personally aware. - -_Based in [GitHub's CLA](https://cla.github.com/agreement)__ \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index d7c00dce..e4e8d901 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -643,10 +643,10 @@ dependencies = [ [[package]] name = "clone-cw-multi-test" -version = "0.6.5" +version = "0.7.0" dependencies = [ "anyhow", - "bech32 0.9.1", + "bech32 0.11.0", "cargo_metadata", "cosmrs", "cosmwasm-schema 2.2.0", @@ -657,12 +657,11 @@ dependencies = [ "cw-utils 2.0.0", "cw2 2.0.0", "cw20 2.0.0", - "derivative", "env_logger", "file-lock", "hex", "hex-literal", - "itertools 0.12.1", + "itertools 0.13.0", "log", "moneymarket", "num-bigint", @@ -2637,15 +2636,6 @@ dependencies = [ "either", ] -[[package]] -name = "itertools" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.13.0" diff --git a/Cargo.toml b/Cargo.toml index 7eef4874..aa8e73de 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,35 +1,44 @@ [package] name = "clone-cw-multi-test" -version = "0.6.5" -authors = ["Ethan Frey "] -edition = "2021" +version = "0.7.0" +authors = [ + "Ethan Frey ", + "Dariusz Depta ", +] description = "Testing tools for multi-contract interactions. Helps simulating chain behavior with on-chain storage locally" -license = "Apache-2.0" repository = "https://github.com/CosmWasm/cw-multi-test" homepage = "https://cosmwasm.com" +license = "Apache-2.0" +edition = "2021" + +[package.metadata.docs.rs] +all-features = true [features] default = [] backtrace = ["anyhow/backtrace"] +staking = ["cosmwasm-std/staking"] +stargate = ["cosmwasm-std/stargate"] +cosmwasm_1_1 = ["cosmwasm-std/cosmwasm_1_1"] +cosmwasm_1_2 = ["cosmwasm_1_1", "cosmwasm-std/cosmwasm_1_2"] +cosmwasm_1_3 = ["cosmwasm_1_2", "cosmwasm-std/cosmwasm_1_3"] +cosmwasm_1_4 = ["cosmwasm_1_3", "cosmwasm-std/cosmwasm_1_4"] +cosmwasm_2_0 = ["cosmwasm_1_4", "cosmwasm-std/cosmwasm_2_0"] +cosmwasm_2_1 = ["cosmwasm_2_0", "cosmwasm-std/cosmwasm_2_1"] [dependencies] -anyhow = "1.0.75" -bech32 = "0.9.1" -cosmwasm-std = { version = "2.1", features = [ - "iterator", - "staking", - "stargate", - "cosmwasm_2_0", -] } +anyhow = "1.0.89" +bech32 = "0.11.0" +cosmwasm-schema = "2.1.3" +cosmwasm-std = "2.1.4" cw-storage-plus = "2.0.0" cw-utils = "2.0.0" -derivative = "2.2.0" -itertools = "0.12.0" -prost = "0.13.0" -schemars = "0.8.16" -serde = "1.0.193" +itertools = "0.13.0" +prost = "0.13.3" +schemars = "0.8.21" +serde = "1.0.210" sha2 = "0.10.8" -thiserror = "1.0.50" +thiserror = "1.0.64" # Clone testing deps ## Network @@ -67,6 +76,8 @@ hex = "0.4.3" hex-literal = "0.4.1" once_cell = "1.19.0" +# Clone testing deps + # General env_logger = "0.10.0" cosmwasm-schema = "2.1.3" diff --git a/README.md b/README.md index cc028d95..f9898d6b 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# CosmWasm MultiTest +# CosmWasm MultiTest [![cw-multi-test on crates.io][crates-badge]][crates-url] [![docs][docs-badge]][docs-url] @@ -17,40 +17,51 @@ **Testing tools for multi-contract interactions** -## Introduction +## Introduction -**CosmWasm MultiTest** is a suite of testing tools designed for facilitating multi-contract +**CosmWasm MultiTest** is a suite of testing tools designed for facilitating multi-contract interactions within the [CosmWasm](https://github.com/CosmWasm) ecosystem. Its primary focus is on providing developers with a robust framework for simulating -complex contract interactions and bank operations. Currently, **CosmWasm MultiTest** -is in the _alpha_ stage, and primarily used internally for testing -[cw-plus](https://github.com/CosmWasm/cw-plus) contracts. +complex contract interactions and bank operations. -## Current Status - -### Internal Use and Refinement - -Internally, the **CosmWasm MultiTest** framework is an essential tool for the -testing of cw-plus contracts. Its development is focused on ensuring the reliability -and security of these contracts. The team is actively working on refactoring and enhancing -**CosmWasm MultiTest** to provide a more stable and feature-rich version for broader -community use in the future. - -### Framework Capabilities +## Library Capabilities **CosmWasm MultiTest** enables comprehensive unit testing, including scenarios where contracts -call other contracts and interact with the bank module. Its current implementation +call other contracts and interact with several modules like bank and staking. Its current implementation effectively handles these interactions, providing a realistic testing environment for contract developers. -The team is committed to extending **CosmWasm MultiTest**'s capabilities, making it a versatile tool +The team is committed to extending **CosmWasm MultiTest**'s capabilities, making it a versatile tool for various blockchain interaction tests. -## Conclusion +## Feature flags + +**CosmWasm MultiTest** library provides several feature flags that can be enabled like shown below: + +```toml +[dev-dependencies] +cw-multi-test = { version = "2.1.0", features = ["staking", "stargate", "cosmwasm_2_0"] } +``` + +Since version 2.1.0, **CosmWasm MultiTest** has no default features enabled. +The table below summarizes all available features: + +| Feature | Description | +|------------------|----------------------------------------------------------------------------------------------------| +| **backtrace** | Enables `backtrace` feature in **anyhow** dependency. | +| **staking** | Enables `staking` feature in **cosmwasm-std** dependency. | +| **stargate** | Enables `stargate` feature in **cosmwasm-std** dependency. | +| **cosmwasm_1_1** | Enables `cosmwasm_1_1` feature in **cosmwasm-std** dependency. | +| **cosmwasm_1_2** | Enables `cosmwasm_1_1` in **MultiTest** and `cosmwasm_1_2` feature in **cosmwasm-std** dependency. | +| **cosmwasm_1_3** | Enables `cosmwasm_1_2` in **MultiTest** and `cosmwasm_1_3` feature in **cosmwasm-std** dependency. | +| **cosmwasm_1_4** | Enables `cosmwasm_1_3` in **MultiTest** and `cosmwasm_1_4` feature in **cosmwasm-std** dependency. | +| **cosmwasm_2_0** | Enables `cosmwasm_1_4` in **MultiTest** and `cosmwasm_2_0` feature in **cosmwasm-std** dependency. | + +## Conclusion **CosmWasm MultiTest** stands as a vital development tool in the [CosmWasm](https://github.com/CosmWasm) ecosystem, especially for developers engaged in building complex decentralized applications. As the framework evolves, it is poised to become an even more integral part of the [CosmWasm](https://github.com/CosmWasm) development toolkit. -Users are encouraged to stay updated with its progress and contribute to its development. +Users are encouraged to stay updated with its progress and contribute to its development. ## License diff --git a/SECURITY.md b/SECURITY.md index 830ec171..e5fc15ed 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,16 +1,11 @@ # Security Policy This repository is maintained by Confio as part of the CosmWasm stack. -Please see https://github.com/CosmWasm/advisories/blob/main/SECURITY.md -for our security policy. -## Supported Versions - -cw-plus is still pre v1.0. A best effort has been made that the contracts here are secure, and we have moved the more -experimental contracts into community repositories like [cw-nfts](https://github.com/CosmWasm/cw-nfts) and -[cw-tokens](https://github.com/CosmWasm/cw-tokens). That said, we have not done an audit on them (formal or informal) -and you can use them at your own risk. We highly suggest doing your own audit on any contract you plan to deploy -with significant token value, and please inform us if it detects any issues so we can upstream them. - -Until v1.0 APIs are subject to change. The contracts APIs are pretty much stable, most work is currently -in `storage-plus` and `multi-test`. +The code here is not intended to be used in production +(i.e. cw-multi-test should only be used as a [Development dependency](https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#development-dependencies)). +Thus it is not covered by the Cosmos bug bounty program and is not treated as strict +as other components when it comes to bugs. +However, if you still think you found a security critical +issue please find the contact information at +https://github.com/CosmWasm/advisories/blob/main/SECURITY.md. diff --git a/examples/cavern_test_app.rs b/examples/cavern_test_app.rs index 02769a07..4a852c81 100644 --- a/examples/cavern_test_app.rs +++ b/examples/cavern_test_app.rs @@ -1,9 +1,6 @@ use clone_cw_multi_test::{ - addons::{MockAddressGenerator, MockApiBech32}, - wasm_emulation::{ - channel::RemoteChannel, contract::WasmContract, storage::analyzer::StorageAnalyzer, - }, - AppBuilder, BankKeeper, Executor, WasmKeeper, + wasm_emulation::{channel::RemoteChannel, storage::analyzer::StorageAnalyzer}, + AppBuilder, Executor, MockApiBech32, }; use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::{coins, Addr, BlockInfo, ContractInfoResponse, QueryRequest, WasmQuery}; @@ -65,12 +62,6 @@ pub fn test() -> anyhow::Result<()> { chain.network_info.pub_address_prefix, )?; - let wasm = WasmKeeper::::new() - .with_remote(remote_channel.clone()) - .with_address_generator(MockAddressGenerator); - - let bank = BankKeeper::new().with_remote(remote_channel.clone()); - let block = runtime.block_on( Node { channel: remote_channel.channel.clone(), @@ -80,8 +71,6 @@ pub fn test() -> anyhow::Result<()> { )?; // First we instantiate a new app let app = AppBuilder::default() - .with_wasm(wasm) - .with_bank(bank) .with_remote(remote_channel.clone()) .with_block(BlockInfo { height: block.height, @@ -89,7 +78,7 @@ pub fn test() -> anyhow::Result<()> { chain_id: chain.chain_id.to_string(), }) .with_api(MockApiBech32::new(chain.network_info.pub_address_prefix)); - let mut app = app.build(|_, _, _| {})?; + let mut app = app.build(|_, _, _| {}); // Then we send a message to the blockchain through the app // We query to verify the state changed @@ -129,7 +118,7 @@ pub fn test() -> anyhow::Result<()> { let code = std::fs::read( Path::new(env!("CARGO_MANIFEST_DIR")) .join("artifacts") - .join("counter_contract.wasm"), + .join("counter_contract_with_cousin.wasm"), ) .unwrap(); diff --git a/examples/counter/contract.rs b/examples/counter/contract.rs index d60a1065..3df53e71 100644 --- a/examples/counter/contract.rs +++ b/examples/counter/contract.rs @@ -48,9 +48,9 @@ pub fn execute( #[cfg_attr(feature = "export", entry_point)] pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { match msg { - QueryMsg::Count {} => to_json_binary(&query::count(deps)?), - QueryMsg::CousinCount {} => to_json_binary(&query::cousin_count(deps)?), - QueryMsg::RawCousinCount {} => to_json_binary(&query::raw_cousin_count(deps)?), + QueryMsg::GetCount {} => to_json_binary(&query::count(deps)?), + QueryMsg::GetCousinCount {} => to_json_binary(&query::cousin_count(deps)?), + QueryMsg::GetRawCousinCount {} => to_json_binary(&query::raw_cousin_count(deps)?), } } diff --git a/examples/counter/msg.rs b/examples/counter/msg.rs index 5b8d7dd3..bfd81a99 100644 --- a/examples/counter/msg.rs +++ b/examples/counter/msg.rs @@ -36,13 +36,13 @@ pub enum ExecuteMsg { pub enum QueryMsg { /// GetCount returns the current count as a json-encoded number #[returns(GetCountResponse)] - Count {}, + GetCount {}, /// GetCount returns the current count as a json-encoded number #[returns(GetCountResponse)] - CousinCount {}, + GetCousinCount {}, /// GetCount returns the current count as a json-encoded number #[returns(GetCountResponse)] - RawCousinCount {}, + GetRawCousinCount {}, } // Custom response for the query diff --git a/examples/counter/query.rs b/examples/counter/query.rs index ee237c59..31a1d585 100644 --- a/examples/counter/query.rs +++ b/examples/counter/query.rs @@ -15,7 +15,7 @@ pub fn cousin_count(deps: Deps) -> StdResult { let cousin_count: GetCountResponse = deps.querier.query(&QueryRequest::Wasm(WasmQuery::Smart { contract_addr: state.cousin.unwrap().to_string(), - msg: to_json_binary(&QueryMsg::Count {})?, + msg: to_json_binary(&QueryMsg::GetCount {})?, }))?; Ok(cousin_count) } diff --git a/examples/cousin_test.rs b/examples/cousin_test.rs index e7825329..e592dc38 100644 --- a/examples/cousin_test.rs +++ b/examples/cousin_test.rs @@ -5,9 +5,8 @@ use std::path::Path; use anyhow::Result as AnyResult; use clone_cw_multi_test::{ - addons::{MockAddressGenerator, MockApiBech32}, - wasm_emulation::channel::RemoteChannel, - App, AppBuilder, BankKeeper, ContractWrapper, Executor, WasmKeeper, + wasm_emulation::{channel::RemoteChannel, query::ContainsRemote}, + App, AppBuilder, BankKeeper, ContractWrapper, Executor, MockApiBech32, WasmKeeper, }; use cosmwasm_std::{Addr, Empty}; use counter::msg::{ExecuteMsg, GetCountResponse, QueryMsg}; @@ -31,7 +30,7 @@ fn increment(app: &mut App, contract: Addr) -> AnyRes fn count(app: &App, contract: Addr) -> AnyResult { Ok(app .wrap() - .query_wasm_smart(contract.clone(), &QueryMsg::Count {})?) + .query_wasm_smart(contract.clone(), &QueryMsg::GetCount {})?) } fn raw_cousin_count( @@ -40,7 +39,7 @@ fn raw_cousin_count( ) -> AnyResult { Ok(app .wrap() - .query_wasm_smart(contract.clone(), &QueryMsg::RawCousinCount {})?) + .query_wasm_smart(contract.clone(), &QueryMsg::GetRawCousinCount {})?) } fn cousin_count( @@ -49,7 +48,7 @@ fn cousin_count( ) -> AnyResult { Ok(app .wrap() - .query_wasm_smart(contract.clone(), &QueryMsg::CousinCount {})?) + .query_wasm_smart(contract.clone(), &QueryMsg::GetCousinCount {})?) } fn test() -> AnyResult<()> { @@ -76,9 +75,7 @@ fn test() -> AnyResult<()> { chain.network_info.pub_address_prefix, )?; - let wasm = WasmKeeper::::new() - .with_remote(remote_channel.clone()) - .with_address_generator(MockAddressGenerator); + let wasm = WasmKeeper::::new().with_remote(remote_channel.clone()); let bank = BankKeeper::new().with_remote(remote_channel.clone()); @@ -88,7 +85,7 @@ fn test() -> AnyResult<()> { .with_bank(bank) .with_remote(remote_channel) .with_api(MockApiBech32::new(chain.network_info.pub_address_prefix)) - .build(|_, _, _| {})?; + .build(|_, _, _| {}); let sender = Addr::unchecked(SENDER); let rust_code_id = app.store_code(Box::new(rust_contract)); diff --git a/examples/test_app.rs b/examples/test_app.rs index b464175c..7e46b5c4 100644 --- a/examples/test_app.rs +++ b/examples/test_app.rs @@ -1,3 +1,4 @@ +use clone_cw_multi_test::wasm_emulation::query::ContainsRemote; use clone_cw_multi_test::{ wasm_emulation::channel::RemoteChannel, AppBuilder, BankKeeper, Executor, WasmKeeper, }; @@ -35,7 +36,7 @@ pub fn test() -> anyhow::Result<()> { .with_wasm(wasm) .with_bank(bank) .with_remote(remote_channel) - .build(|_, _, _| {})?; + .build(|_, _, _| {}); // Then we send a message to the blockchain through the app let sender = "terra17c6ts8grcfrgquhj3haclg44le8s7qkx6l2yx33acguxhpf000xqhnl3je"; diff --git a/src/addons/addresses/mock.rs b/src/addons/addresses/mock.rs deleted file mode 100644 index 081199cc..00000000 --- a/src/addons/addresses/mock.rs +++ /dev/null @@ -1,148 +0,0 @@ -use crate::error::AnyResult; -use crate::AddressGenerator; -use cosmwasm_std::{instantiate2_address, Addr, Api, CanonicalAddr, Storage}; -use sha2::digest::Update; -use sha2::{Digest, Sha256}; - -/// Address generator that mimics the original `wasmd` behavior. -/// -/// [MockAddressGenerator] implements [AddressGenerator] trait in terms of -/// [`contract_address`](AddressGenerator::contract_address) and -/// [`predictable_contract_address`](AddressGenerator::predictable_contract_address) functions: -/// - `contract_address` generates non-predictable addresses for contracts, -/// using the same algorithm as `wasmd`, see: [`BuildContractAddressClassic`] for details. -/// - `predictable_contract_address` generates predictable addresses for contracts using -/// [`instantiate2_address`] function defined in `cosmwasm-std`. -/// -/// [`BuildContractAddressClassic`]:https://github.com/CosmWasm/wasmd/blob/3b6512c9f154995188ead84ab3bd9e034b49a0f3/x/wasm/keeper/addresses.go#L35-L41 -/// [`instantiate2_address`]:https://github.com/CosmWasm/cosmwasm/blob/8a652d7cd8071f71139deca6be8194ed4a278b2c/packages/std/src/addresses.rs#L309-L318 -#[derive(Default)] -pub struct MockAddressGenerator; - -impl AddressGenerator for MockAddressGenerator { - /// Generates a _non-predictable_ contract address, like `wasmd` does it in real-life chain. - /// - /// Note that addresses generated by `wasmd` may change and users **should not** - /// rely on this value in any extend. - /// - /// Returns the contract address after its instantiation. - /// Address generated by this function is returned as a result - /// of processing `WasmMsg::Instantiate` message. - /// - /// **NOTES** - /// > 👉 The canonical address generated by this function is humanized using the - /// > `Api::addr_humanize` function, so the resulting value depends on used `Api` implementation. - /// > The following example uses Bech32 format for humanizing canonical addresses. - /// - /// > 👉 Do NOT use this function **directly** to generate a contract address, - /// > pass this address generator to `WasmKeeper`: - /// > `WasmKeeper::new().with_address_generator(MockAddressGenerator::default());` - /// - /// # Example - /// - /// ``` - /// # use cosmwasm_std::testing::MockStorage; - /// # use cw_multi_test::AddressGenerator; - /// # use cw_multi_test::addons::{MockAddressGenerator, MockApiBech32}; - /// // use `Api` that implements Bech32 format - /// let api = MockApiBech32::new("juno"); - /// // prepare mock storage - /// let mut storage = MockStorage::default(); - /// // initialize the address generator - /// let address_generator = MockAddressGenerator::default(); - /// // generate the address - /// let addr = address_generator.contract_address(&api, &mut storage, 1, 1).unwrap(); - /// - /// assert_eq!(addr.to_string(), - /// "juno14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9skjuwg8"); - /// ``` - fn contract_address( - &self, - api: &dyn Api, - _storage: &mut dyn Storage, - code_id: u64, - instance_id: u64, - ) -> AnyResult { - let canonical_addr = instantiate_address(code_id, instance_id); - Ok(Addr::unchecked(api.addr_humanize(&canonical_addr)?)) - } - - /// Generates a _predictable_ contract address, like `wasmd` does it in real-life chain. - /// - /// Returns a contract address after its instantiation. - /// Address generated by this function is returned as a result - /// of processing `WasmMsg::Instantiate2` message. - /// - /// **NOTES** - /// > 👉 The canonical address generated by this function is humanized using the - /// > `Api::addr_humanize` function, so the resulting value depends on used `Api` implementation. - /// > The following example uses Bech32 format for humanizing canonical addresses. - /// - /// > 👉 Do NOT use this function **directly** to generate a contract address, - /// > pass this address generator to WasmKeeper: - /// > `WasmKeeper::new().with_address_generator(MockAddressGenerator::default());` - /// - /// # Example - /// - /// ``` - /// # use cosmwasm_std::Api; - /// # use cosmwasm_std::testing::MockStorage; - /// # use cw_multi_test::AddressGenerator; - /// # use cw_multi_test::addons::{MockAddressGenerator, MockApiBech32}; - /// // use `Api` that implements Bech32 format - /// let api = MockApiBech32::new("juno"); - /// // prepare mock storage - /// let mut storage = MockStorage::default(); - /// // initialize the address generator - /// let address_generator = MockAddressGenerator::default(); - /// // checksum of the contract code base - /// let checksum = [0, 1 ,2 ,3 ,4 ,5 ,6 ,7 ,8 ,9 ,10, - /// 11,12,13,14,15,16,17,18,19,20,21, - /// 22,23,24,25,26,27,28,29,30,31]; - /// // creator address - /// let creator = api.addr_canonicalize(api.addr_make("creator").as_str()).unwrap(); - /// // salt - /// let salt = [10,11,12]; - /// // generate the address - /// let addr = address_generator - /// .predictable_contract_address(&api, &mut storage, 1, 1, &checksum, &creator, &salt) - /// .unwrap(); - /// - /// assert_eq!(addr.to_string(), - /// "juno1sv3gjp85m3xxluxreruards8ruxk5ykys8qfljwrdj5tv8kqxuhsmlfyud"); - /// ``` - fn predictable_contract_address( - &self, - api: &dyn Api, - _storage: &mut dyn Storage, - _code_id: u64, - _instance_id: u64, - checksum: &[u8], - - creator: &CanonicalAddr, - salt: &[u8], - ) -> AnyResult { - let canonical_addr = instantiate2_address(checksum, creator, salt)?; - Ok(Addr::unchecked(api.addr_humanize(&canonical_addr)?)) - } -} - -/// Returns non-predictable contract address. -/// -/// Address is generated using the same algorithm as [`BuildContractAddressClassic`] -/// implementation in `wasmd`. -/// -/// [`BuildContractAddressClassic`]:https://github.com/CosmWasm/wasmd/blob/3b6512c9f154995188ead84ab3bd9e034b49a0f3/x/wasm/keeper/addresses.go#L35-L41 -fn instantiate_address(code_id: u64, instance_id: u64) -> CanonicalAddr { - let mut key = Vec::::new(); - key.extend_from_slice(b"wasm\0"); - key.extend_from_slice(&code_id.to_be_bytes()); - key.extend_from_slice(&instance_id.to_be_bytes()); - let module = Sha256::digest("module".as_bytes()); - Sha256::new() - .chain(module) - .chain(key) - .finalize() - .to_vec() - .into() -} diff --git a/src/addons/addresses/mod.rs b/src/addons/addresses/mod.rs deleted file mode 100644 index 9afc1d5e..00000000 --- a/src/addons/addresses/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod mock; diff --git a/src/addons/api/bech32.rs b/src/addons/api/bech32.rs deleted file mode 100644 index 55b21cc3..00000000 --- a/src/addons/api/bech32.rs +++ /dev/null @@ -1,187 +0,0 @@ -use bech32::{decode, encode, FromBase32, ToBase32, Variant}; -use cosmwasm_std::testing::MockApi; -use cosmwasm_std::{ - Addr, Api, CanonicalAddr, RecoverPubkeyError, StdError, StdResult, VerificationError, -}; -use sha2::{Digest, Sha256}; - -/// Implementation of the `Api` trait that uses [`Bech32`] format -/// for humanizing canonical addresses. -/// -/// [`Bech32`]:https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki -pub struct MockApiBech32 { - api: MockApi, - prefix: String, - variant: Variant, -} - -impl MockApiBech32 { - /// Returns `Api` implementation that uses specified prefix - /// to generate addresses in **Bech32** format. - /// - /// # Example - /// - /// ``` - /// use cw_multi_test::addons::MockApiBech32; - /// - /// let api = MockApiBech32::new("juno"); - /// let addr = api.addr_make("creator"); - /// assert_eq!(addr.as_str(), - /// "juno1h34lmpywh4upnjdg90cjf4j70aee6z8qqfspugamjp42e4q28kqsksmtyp"); - /// ``` - pub fn new(prefix: &str) -> Self { - Self::new_with_variant(prefix, Variant::Bech32) - } - - /// Creates `Api` implementation that uses specified prefix - /// to generate addresses in format defined by provided Bech32 variant. - pub(crate) fn new_with_variant(prefix: &str, variant: Variant) -> Self { - Self { - api: MockApi::default(), - prefix: prefix.to_string(), - variant, - } - } -} - -impl Api for MockApiBech32 { - /// Takes a human readable address in **Bech32** format and checks if it is valid. - /// - /// If the validation succeeds, an `Addr` containing the same string as the input is returned. - /// - /// # Example - /// - /// ``` - /// use cosmwasm_std::Api; - /// use cw_multi_test::addons::MockApiBech32; - /// - /// let api = MockApiBech32::new("juno"); - /// let addr = api.addr_make("creator"); - /// assert_eq!(api.addr_validate(addr.as_str()).unwrap().as_str(), - /// addr.as_str()); - /// ``` - fn addr_validate(&self, input: &str) -> StdResult { - self.addr_humanize(&self.addr_canonicalize(input)?) - } - - /// Takes a human readable address in **Bech32** format and returns - /// a canonical binary representation of it. - /// - /// # Example - /// - /// ``` - /// use cosmwasm_std::Api; - /// use cw_multi_test::addons::MockApiBech32; - /// - /// let api = MockApiBech32::new("juno"); - /// let addr = api.addr_make("creator"); - /// assert_eq!(api.addr_canonicalize(addr.as_str()).unwrap().to_string(), - /// "BC6BFD848EBD7819C9A82BF124D65E7F739D08E002601E23BB906AACD40A3D81"); - /// ``` - fn addr_canonicalize(&self, input: &str) -> StdResult { - if let Ok((prefix, decoded, variant)) = decode(input) { - if prefix == self.prefix && variant == self.variant { - if let Ok(bytes) = Vec::::from_base32(&decoded) { - return Ok(bytes.into()); - } - } - } - Err(StdError::generic_err("Invalid input")) - } - - /// Takes a canonical address and returns a human readable address in **Bech32** format. - /// - /// This is the inverse operation of [`addr_canonicalize`]. - /// - /// [`addr_canonicalize`]: MockApiBech32::addr_canonicalize - /// - /// # Example - /// - /// ``` - /// use cosmwasm_std::Api; - /// use cw_multi_test::addons::MockApiBech32; - /// - /// let api = MockApiBech32::new("juno"); - /// let addr = api.addr_make("creator"); - /// let canonical_addr = api.addr_canonicalize(addr.as_str()).unwrap(); - /// assert_eq!(api.addr_humanize(&canonical_addr).unwrap().as_str(), - /// addr.as_str()); - /// ``` - fn addr_humanize(&self, canonical: &CanonicalAddr) -> StdResult { - if let Ok(encoded) = encode(&self.prefix, canonical.as_slice().to_base32(), self.variant) { - Ok(Addr::unchecked(encoded)) - } else { - Err(StdError::generic_err("Invalid canonical address")) - } - } - - fn secp256k1_verify( - &self, - message_hash: &[u8], - signature: &[u8], - public_key: &[u8], - ) -> Result { - self.api - .secp256k1_verify(message_hash, signature, public_key) - } - - fn secp256k1_recover_pubkey( - &self, - message_hash: &[u8], - signature: &[u8], - recovery_param: u8, - ) -> Result, RecoverPubkeyError> { - self.api - .secp256k1_recover_pubkey(message_hash, signature, recovery_param) - } - - fn ed25519_verify( - &self, - message: &[u8], - signature: &[u8], - public_key: &[u8], - ) -> Result { - self.api.ed25519_verify(message, signature, public_key) - } - - fn ed25519_batch_verify( - &self, - messages: &[&[u8]], - signatures: &[&[u8]], - public_keys: &[&[u8]], - ) -> Result { - self.api - .ed25519_batch_verify(messages, signatures, public_keys) - } - - fn debug(&self, message: &str) { - self.api.debug(message) - } -} - -impl MockApiBech32 { - /// Returns an address in **Bech32** format, built from provided input string. - /// - /// # Example - /// - /// ``` - /// use cw_multi_test::addons::MockApiBech32; - /// - /// let api = MockApiBech32::new("juno"); - /// let addr = api.addr_make("creator"); - /// assert_eq!(addr.as_str(), - /// "juno1h34lmpywh4upnjdg90cjf4j70aee6z8qqfspugamjp42e4q28kqsksmtyp"); - /// ``` - /// - /// # Panics - /// - /// This function panics when generating a valid address in **Bech32** - /// format is not possible, especially when prefix is too long or empty. - pub fn addr_make(&self, input: &str) -> Addr { - let digest = Sha256::digest(input).to_vec(); - match encode(&self.prefix, digest.to_base32(), self.variant) { - Ok(address) => Addr::unchecked(address), - Err(reason) => panic!("Generating address failed with reason: {}", reason), - } - } -} diff --git a/src/addons/api/bech32m.rs b/src/addons/api/bech32m.rs deleted file mode 100644 index d630f2b0..00000000 --- a/src/addons/api/bech32m.rs +++ /dev/null @@ -1,154 +0,0 @@ -use crate::addons::MockApiBech32; -use bech32::Variant; -use cosmwasm_std::{Addr, Api, CanonicalAddr, RecoverPubkeyError, StdResult, VerificationError}; - -/// Implementation of the `Api` trait that uses [`Bech32m`] format -/// for humanizing canonical addresses. -/// -/// [`Bech32m`]:https://github.com/bitcoin/bips/blob/master/bip-0350.mediawiki -pub struct MockApiBech32m(MockApiBech32); - -impl MockApiBech32m { - /// Returns `Api` implementation that uses specified prefix - /// to generate addresses in **Bech32m** format. - /// - /// # Example - /// - /// ``` - /// use cw_multi_test::addons::MockApiBech32m; - /// - /// let api = MockApiBech32m::new("osmosis"); - /// let addr = api.addr_make("sender"); - /// assert_eq!(addr.as_str(), - /// "osmosis1pgm8hyk0pvphmlvfjc8wsvk4daluz5tgrw6pu5mfpemk74uxnx9qgv9940"); - /// ``` - pub fn new(prefix: &'static str) -> Self { - Self(MockApiBech32::new_with_variant(prefix, Variant::Bech32m)) - } -} - -impl Api for MockApiBech32m { - /// Takes a human readable address in **Bech32m** format and checks if it is valid. - /// - /// If the validation succeeds, an `Addr` containing the same string as the input is returned. - /// - /// # Example - /// - /// ``` - /// use cosmwasm_std::Api; - /// use cw_multi_test::addons::MockApiBech32m; - /// - /// let api = MockApiBech32m::new("osmosis"); - /// let addr = api.addr_make("sender"); - /// assert_eq!(api.addr_validate(addr.as_str()).unwrap().as_str(), - /// addr.as_str()); - /// ``` - fn addr_validate(&self, input: &str) -> StdResult { - self.0.addr_validate(input) - } - - /// Takes a human readable address in **Bech32m** format and returns - /// a canonical binary representation of it. - /// - /// # Example - /// - /// ``` - /// use cosmwasm_std::Api; - /// use cw_multi_test::addons::MockApiBech32; - /// - /// let api = MockApiBech32::new("osmosis"); - /// let addr = api.addr_make("sender"); - /// assert_eq!(api.addr_canonicalize(addr.as_str()).unwrap().to_string(), - /// "0A367B92CF0B037DFD89960EE832D56F7FC151681BB41E53690E776F5786998A"); - /// ``` - fn addr_canonicalize(&self, input: &str) -> StdResult { - self.0.addr_canonicalize(input) - } - - /// Takes a canonical address and returns a human readable address in **Bech32m** format. - /// - /// This is the inverse operation of [`addr_canonicalize`]. - /// - /// [`addr_canonicalize`]: MockApiBech32m::addr_canonicalize - /// - /// # Example - /// - /// ``` - /// use cosmwasm_std::Api; - /// use cw_multi_test::addons::MockApiBech32m; - /// - /// let api = MockApiBech32m::new("osmosis"); - /// let addr = api.addr_make("sender"); - /// let canonical_addr = api.addr_canonicalize(addr.as_str()).unwrap(); - /// assert_eq!(api.addr_humanize(&canonical_addr).unwrap().as_str(), - /// addr.as_str()); - /// ``` - fn addr_humanize(&self, canonical: &CanonicalAddr) -> StdResult { - self.0.addr_humanize(canonical) - } - - fn secp256k1_verify( - &self, - message_hash: &[u8], - signature: &[u8], - public_key: &[u8], - ) -> Result { - self.0.secp256k1_verify(message_hash, signature, public_key) - } - - fn secp256k1_recover_pubkey( - &self, - message_hash: &[u8], - signature: &[u8], - recovery_param: u8, - ) -> Result, RecoverPubkeyError> { - self.0 - .secp256k1_recover_pubkey(message_hash, signature, recovery_param) - } - - fn ed25519_verify( - &self, - message: &[u8], - signature: &[u8], - public_key: &[u8], - ) -> Result { - self.0.ed25519_verify(message, signature, public_key) - } - - fn ed25519_batch_verify( - &self, - messages: &[&[u8]], - signatures: &[&[u8]], - public_keys: &[&[u8]], - ) -> Result { - self.0 - .ed25519_batch_verify(messages, signatures, public_keys) - } - - fn debug(&self, message: &str) { - self.0.debug(message) - } -} - -impl MockApiBech32m { - /// Returns an address in **Bech32m** format, built from provided input string. - /// - /// # Example - /// - /// ``` - /// use cw_multi_test::addons::MockApiBech32m; - /// - /// let api = MockApiBech32m::new("osmosis"); - /// let addr = api.addr_make("sender"); - /// assert_eq!(addr.as_str(), - /// "osmosis1pgm8hyk0pvphmlvfjc8wsvk4daluz5tgrw6pu5mfpemk74uxnx9qgv9940"); - /// ``` - /// - /// # Panics - /// - /// This function panics when generating a valid address in **Bech32** - /// format is not possible, especially when prefix is too long or empty. - pub fn addr_make(&self, input: &str) -> Addr { - self.0.addr_make(input) - } -} diff --git a/src/addons/api/mod.rs b/src/addons/api/mod.rs deleted file mode 100644 index d59bbae5..00000000 --- a/src/addons/api/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod bech32; -pub mod bech32m; diff --git a/src/addons/mod.rs b/src/addons/mod.rs deleted file mode 100644 index c31658c3..00000000 --- a/src/addons/mod.rs +++ /dev/null @@ -1,11 +0,0 @@ -//! # MultiTest add-ons -//! -//! Additional components and functionalities used to enhance -//! or customize tests of CosmWasm smart contracts. - -mod addresses; -mod api; - -pub use addresses::mock::MockAddressGenerator; -pub use api::bech32::MockApiBech32; -pub use api::bech32m::MockApiBech32m; diff --git a/src/addresses.rs b/src/addresses.rs index 3a0b1669..43d49341 100644 --- a/src/addresses.rs +++ b/src/addresses.rs @@ -1,9 +1,78 @@ -//! # Implementation of address generators +//! # Implementation of address conversions and generators use crate::error::AnyResult; -use crate::prefixed_storage::prefixed_read; -use crate::wasm::{CONTRACTS, NAMESPACE_WASM}; -use cosmwasm_std::{Addr, Api, CanonicalAddr, HexBinary, Order, Storage}; +use crate::{MockApiBech32, MockApiBech32m}; +use cosmwasm_std::testing::MockApi; +use cosmwasm_std::{instantiate2_address, Addr, Api, CanonicalAddr, Storage}; +use sha2::digest::Update; +use sha2::{Digest, Sha256}; + +const DEFAULT_PREFIX: &str = "cosmwasm"; + +/// Defines conversions to [Addr], this conversion is format agnostic +/// and should be aligned with the format generated by [MockApi]. +/// +/// [MockApi]: https://github.com/CosmWasm/cosmwasm/blob/9a239838baba50f4f47230da306f39a8bb4ea697/packages/std/src/testing/mock.rs#L251-L257 +pub trait IntoAddr { + /// Converts into [Addr]. + fn into_addr(self) -> Addr; + + /// Converts into [Addr] with custom prefix. + fn into_addr_with_prefix(self, prefix: &'static str) -> Addr; +} + +impl IntoAddr for &str { + /// Converts [&str] into [Addr]. + fn into_addr(self) -> Addr { + MockApi::default().addr_make(self) + } + + /// Converts [&str] into [Addr] with custom prefix. + fn into_addr_with_prefix(self, prefix: &'static str) -> Addr { + MockApi::default().with_prefix(prefix).addr_make(self) + } +} + +/// Defines conversions to `Bech32` compatible addresses. +pub trait IntoBech32 { + /// Converts into [Addr] containing a string compatible with `Bech32` format with default prefix. + fn into_bech32(self) -> Addr; + + /// Converts into [Addr] containing a string compatible with `Bech32` format with custom prefix. + fn into_bech32_with_prefix(self, prefix: &'static str) -> Addr; +} + +impl IntoBech32 for &str { + /// Converts [&str] into [Addr] containing a string compatible with `Bech32` format with default prefix. + fn into_bech32(self) -> Addr { + MockApiBech32::new(DEFAULT_PREFIX).addr_make(self) + } + + /// Converts [&str] into [Addr] containing a string compatible with `Bech32` format with custom prefix. + fn into_bech32_with_prefix(self, prefix: &'static str) -> Addr { + MockApiBech32::new(prefix).addr_make(self) + } +} + +/// Defines conversions to `Bech32m` compatible addresses. +pub trait IntoBech32m { + /// Converts into [Addr] containing a string compatible with `Bech32m` format with default prefix. + fn into_bech32m(self) -> Addr; + /// Converts into [Addr] containing a string compatible with `Bech32m` format with custom prefix. + fn into_bech32m_with_prefix(self, prefix: &'static str) -> Addr; +} + +impl IntoBech32m for &str { + /// Converts [&str] into [Addr] containing a string compatible with `Bech32m` format with default prefix. + fn into_bech32m(self) -> Addr { + MockApiBech32m::new(DEFAULT_PREFIX).addr_make(self) + } + + /// Converts [&str] into [Addr] containing a string compatible with `Bech32m` format with custom prefix. + fn into_bech32m_with_prefix(self, prefix: &'static str) -> Addr { + MockApiBech32m::new(prefix).addr_make(self) + } +} /// Common address generator interface. /// @@ -12,30 +81,13 @@ use cosmwasm_std::{Addr, Api, CanonicalAddr, HexBinary, Order, Storage}; /// or [predictable_contract_address](AddressGenerator::predictable_contract_address) is used, /// but users should not make any assumptions about the value of the generated address. pub trait AddressGenerator { - #[deprecated( - since = "0.18.0", - note = "use `contract_address` or `predictable_contract_address` instead; will be removed in version 1.0.0" - )] - fn next_address(&self, storage: &mut dyn Storage) -> Addr { - //TODO After removing this function in version 1.0, make `CONTRACTS` and `NAMESPACE_WASM` private in `wasm.rs`. - let count = CONTRACTS - .range_raw( - &prefixed_read(storage, NAMESPACE_WASM), - None, - None, - Order::Ascending, - ) - .count(); - Addr::unchecked(format!("contract{}", count)) - } - /// Generates a _non-predictable_ contract address, just like the real-life chain /// returns contract address after its instantiation. /// Address generated by this function is returned as a result of processing /// `WasmMsg::Instantiate` message. /// /// The default implementation generates a contract address based - /// on contract's instance identifier only. + /// on contract's code and instance identifier. /// /// # Example /// @@ -50,26 +102,18 @@ pub trait AddressGenerator { /// /// let my_address_generator = MyAddressGenerator{}; /// - /// let addr = my_address_generator.contract_address(&api, &mut storage, 100, 0).unwrap(); - /// assert_eq!(addr.to_string(),"contract0"); - /// /// let addr = my_address_generator.contract_address(&api, &mut storage, 100, 1).unwrap(); - /// assert_eq!(addr.to_string(),"contract1"); - /// - /// let addr = my_address_generator.contract_address(&api, &mut storage, 200, 5).unwrap(); - /// assert_eq!(addr.to_string(),"contract5"); - /// - /// let addr = my_address_generator.contract_address(&api, &mut storage, 200, 6).unwrap(); - /// assert_eq!(addr.to_string(),"contract6"); + /// assert!(addr.as_str().starts_with("cosmwasm1")); /// ``` fn contract_address( &self, - _api: &dyn Api, + api: &dyn Api, _storage: &mut dyn Storage, - _code_id: u64, + code_id: u64, instance_id: u64, ) -> AnyResult { - Ok(Addr::unchecked(format!("contract{instance_id}"))) + let canonical_addr = instantiate_address(code_id, instance_id); + Ok(api.addr_humanize(&canonical_addr)?) } /// Generates a _predictable_ contract address, just like the real-life chain @@ -77,52 +121,76 @@ pub trait AddressGenerator { /// Address generated by this function is returned as a result of processing /// `WasmMsg::Instantiate2` message. /// - /// The default implementation generates a contract address based on provided salt only. + /// The default implementation generates a contract address based on provided + /// creator address and salt. /// /// # Example /// /// ``` - /// # use cosmwasm_std::Api; + /// # use cosmwasm_std::{Api, Checksum}; /// # use cosmwasm_std::testing::{MockApi, MockStorage}; /// # use cw_multi_test::{AddressGenerator, SimpleAddressGenerator}; /// # let api = MockApi::default(); /// # let mut storage = MockStorage::default(); - /// # let creator = api.addr_canonicalize("creator").unwrap(); /// struct MyAddressGenerator; /// /// impl AddressGenerator for MyAddressGenerator {} /// /// let my_address_generator = MyAddressGenerator{}; /// - /// let addr = my_address_generator.predictable_contract_address(&api, &mut storage, 100, 0, &[0], &creator, &[0]).unwrap(); - /// assert_eq!(addr.to_string(),"contract00"); - /// - /// let addr = my_address_generator.predictable_contract_address(&api, &mut storage, 100, 1, &[1], &creator, &[0]).unwrap(); - /// assert_eq!(addr.to_string(),"contract00"); - /// - /// let addr = my_address_generator.predictable_contract_address(&api, &mut storage, 200, 0, &[2], &creator, &[1]).unwrap(); - /// assert_eq!(addr.to_string(),"contract01"); - /// - /// let addr = my_address_generator.predictable_contract_address(&api, &mut storage, 200, 1, &[3], &creator, &[1]).unwrap(); - /// assert_eq!(addr.to_string(),"contract01"); + /// let creator1 = api.addr_canonicalize(&api.addr_make("creator1").to_string()).unwrap(); + /// let creator2 = api.addr_canonicalize(&api.addr_make("creator2").to_string()).unwrap(); + /// let salt1 = [0xc0,0xff,0xee]; + /// let salt2 = [0xbe,0xef]; + /// let chs = Checksum::generate(&[1]); + /// + /// let addr11 = my_address_generator.predictable_contract_address(&api, &mut storage, 1, 0, chs.as_slice(), &creator1, &salt1).unwrap(); + /// let addr12 = my_address_generator.predictable_contract_address(&api, &mut storage, 1, 0, chs.as_slice(), &creator1, &salt2).unwrap(); + /// let addr21 = my_address_generator.predictable_contract_address(&api, &mut storage, 1, 0, chs.as_slice(), &creator2, &salt1).unwrap(); + /// let addr22 = my_address_generator.predictable_contract_address(&api, &mut storage, 1, 0, chs.as_slice(), &creator2, &salt2).unwrap(); + /// + /// assert_ne!(addr11, addr12); + /// assert_ne!(addr11, addr21); + /// assert_ne!(addr11, addr22); + /// assert_ne!(addr12, addr21); + /// assert_ne!(addr12, addr22); + /// assert_ne!(addr21, addr22); /// ``` fn predictable_contract_address( &self, - _api: &dyn Api, + api: &dyn Api, _storage: &mut dyn Storage, _code_id: u64, _instance_id: u64, - _checksum: &[u8], - _creator: &CanonicalAddr, + checksum: &[u8], + creator: &CanonicalAddr, salt: &[u8], ) -> AnyResult { - Ok(Addr::unchecked(format!( - "contract{}", - HexBinary::from(salt).to_hex() - ))) + let canonical_addr = instantiate2_address(checksum, creator, salt)?; + Ok(api.addr_humanize(&canonical_addr)?) } } +/// Returns non-predictable contract address. +/// +/// Address is generated using the same algorithm as [`BuildContractAddressClassic`] +/// implementation in `wasmd`. +/// +/// [`BuildContractAddressClassic`]:https://github.com/CosmWasm/wasmd/blob/3b6512c9f154995188ead84ab3bd9e034b49a0f3/x/wasm/keeper/addresses.go#L35-L41 +fn instantiate_address(code_id: u64, instance_id: u64) -> CanonicalAddr { + let mut key = Vec::::new(); + key.extend_from_slice(b"wasm\0"); + key.extend_from_slice(&code_id.to_be_bytes()); + key.extend_from_slice(&instance_id.to_be_bytes()); + let module = Sha256::digest("module".as_bytes()); + Sha256::new() + .chain(module) + .chain(key) + .finalize() + .to_vec() + .into() +} + /// Default contract address generator used in [WasmKeeper](crate::WasmKeeper). pub struct SimpleAddressGenerator; diff --git a/src/api.rs b/src/api.rs new file mode 100644 index 00000000..bd3fa9ed --- /dev/null +++ b/src/api.rs @@ -0,0 +1,119 @@ +use bech32::primitives::decode::CheckedHrpstring; +use bech32::{encode, Bech32, Bech32m, Hrp}; +use cosmwasm_std::testing::MockApi; +use cosmwasm_std::{ + Addr, Api, CanonicalAddr, RecoverPubkeyError, StdError, StdResult, VerificationError, +}; +use sha2::{Digest, Sha256}; + +pub struct MockApiBech { + api: MockApi, + prefix: String, + _phantom_data: std::marker::PhantomData, +} + +impl MockApiBech { + /// Returns `Api` implementation that uses specified prefix + /// to generate addresses in `Bech32` or `Bech32m` format. + pub fn new(prefix: &str) -> Self { + Self { + api: MockApi::default(), + prefix: prefix.to_string(), + _phantom_data: std::marker::PhantomData, + } + } +} + +impl Api for MockApiBech { + fn addr_validate(&self, input: &str) -> StdResult { + self.addr_humanize(&self.addr_canonicalize(input)?) + } + + fn addr_canonicalize(&self, input: &str) -> StdResult { + if let Ok(s) = CheckedHrpstring::new::(input) { + if s.hrp().to_string() == self.prefix { + return Ok(s.byte_iter().collect::>().into()); + } + } + Err(StdError::generic_err("Invalid input")) + } + + fn addr_humanize(&self, canonical: &CanonicalAddr) -> StdResult { + let hrp = Hrp::parse(&self.prefix).map_err(|e| StdError::generic_err(e.to_string()))?; + if let Ok(encoded) = encode::(hrp, canonical.as_slice()) { + Ok(Addr::unchecked(encoded)) + } else { + Err(StdError::generic_err("Invalid canonical address")) + } + } + + fn secp256k1_verify( + &self, + message_hash: &[u8], + signature: &[u8], + public_key: &[u8], + ) -> Result { + self.api + .secp256k1_verify(message_hash, signature, public_key) + } + + fn secp256k1_recover_pubkey( + &self, + message_hash: &[u8], + signature: &[u8], + recovery_param: u8, + ) -> Result, RecoverPubkeyError> { + self.api + .secp256k1_recover_pubkey(message_hash, signature, recovery_param) + } + + fn ed25519_verify( + &self, + message: &[u8], + signature: &[u8], + public_key: &[u8], + ) -> Result { + self.api.ed25519_verify(message, signature, public_key) + } + + fn ed25519_batch_verify( + &self, + messages: &[&[u8]], + signatures: &[&[u8]], + public_keys: &[&[u8]], + ) -> Result { + self.api + .ed25519_batch_verify(messages, signatures, public_keys) + } + + fn debug(&self, message: &str) { + self.api.debug(message) + } +} + +impl MockApiBech { + /// Returns an address in `Bech32` or `Bech32m` format, built from provided input string. + /// + /// # Panics + /// + /// This function panics when generating a valid address in `Bech32` or `Bech32m` + /// format is not possible, especially when the prefix is too long or empty. + pub fn addr_make(&self, input: &str) -> Addr { + match Hrp::parse(&self.prefix) { + Ok(hrp) => Addr::unchecked(encode::(hrp, Sha256::digest(input).as_slice()).unwrap()), + Err(reason) => panic!("Generating address failed with reason: {}", reason), + } + } +} + +/// Implementation of the `cosmwasm_std::Api` trait that uses [Bech32] format +/// for humanizing canonical addresses. +/// +/// [Bech32]: https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki +pub type MockApiBech32 = MockApiBech; + +/// Implementation of the `cosmwasm_std::Api` trait that uses [Bech32m] format +/// for humanizing canonical addresses. +/// +/// [Bech32m]: https://github.com/bitcoin/bips/blob/master/bip-0350.mediawiki +pub type MockApiBech32m = MockApiBech; diff --git a/src/app.rs b/src/app.rs index 4d10958e..92242a61 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,38 +1,42 @@ -use crate::wasm_emulation::api::RealApi; use crate::wasm_emulation::channel::RemoteChannel; use crate::wasm_emulation::input::QuerierStorage; +use crate::wasm_emulation::query::ContainsRemote; use cosmwasm_std::CustomMsg; -use cw_storage_plus::Item; use crate::bank::{Bank, BankKeeper, BankSudo}; use crate::error::{bail, AnyResult}; use crate::executor::{AppResponse, Executor}; +use crate::featured::staking::{ + Distribution, DistributionKeeper, StakeKeeper, Staking, StakingSudo, +}; use crate::gov::Gov; use crate::ibc::Ibc; use crate::module::{FailingModule, Module}; -use crate::staking::{Distribution, DistributionKeeper, StakeKeeper, Staking, StakingSudo}; +use crate::prefixed_storage::{ + prefixed, prefixed_multilevel, prefixed_multilevel_read, prefixed_read, +}; use crate::transactions::transactional; use crate::wasm::{ContractData, Wasm, WasmKeeper, WasmSudo}; -use crate::{AppBuilder, Contract, GovFailingModule, IbcFailingModule}; +use crate::{AppBuilder, Contract, GovFailingModule, IbcFailingModule, Stargate, StargateFailing}; use cosmwasm_std::testing::{MockApi, MockStorage}; use cosmwasm_std::{ from_json, to_json_binary, Addr, Api, Binary, BlockInfo, ContractResult, CosmosMsg, CustomQuery, Empty, Querier, QuerierResult, QuerierWrapper, QueryRequest, Record, Storage, SystemError, SystemResult, }; -use schemars::JsonSchema; use serde::{de::DeserializeOwned, Serialize}; use std::fmt::Debug; use std::marker::PhantomData; -const ADDRESSES: Item> = Item::new("addresses"); - +/// Advances the blockchain environment to the next block in tests, enabling developers to simulate +/// time-dependent contract behaviors and block-related triggers efficiently. pub fn next_block(block: &mut BlockInfo) { block.time = block.time.plus_seconds(5); block.height += 1; } -/// Type alias for default build `App` to make its storing simpler in typical scenario +/// A type alias for the default-built App. It simplifies storage and handling in typical scenarios, +/// streamlining the use of the App structure in standard test setups. pub type BasicApp = App< BankKeeper, MockApi, @@ -43,11 +47,12 @@ pub type BasicApp = App< DistributionKeeper, IbcFailingModule, GovFailingModule, + StargateFailing, >; -/// 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. +/// # Blockchain application simulator +/// +/// This structure is the main component of the real-life blockchain simulator. #[derive(Clone)] pub struct App< Bank = BankKeeper, @@ -59,17 +64,52 @@ pub struct App< Distr = DistributionKeeper, Ibc = IbcFailingModule, Gov = GovFailingModule, + Stargate = StargateFailing, > { - pub(crate) router: Router, + pub(crate) router: Router, pub(crate) api: Api, pub(crate) storage: Storage, pub(crate) block: BlockInfo, pub(crate) remote: RemoteChannel, } +impl ContainsRemote + for App +{ + fn with_remote(self, remote: RemoteChannel) -> Self { + let Self { + router, + api, + storage, + block, + .. + } = self; + Self { + router, + api, + storage, + block, + remote, + } + } + + fn set_remote(&mut self, remote: RemoteChannel) { + self.remote = remote; + } +} + +/// No-op application initialization function. +pub fn no_init( + router: &mut Router, + api: &ApiT, + storage: &mut dyn Storage, +) { + let _ = (router, api, storage); +} + impl BasicApp { /// Creates new default `App` implementation working with Empty custom messages. - pub fn new(remote: RemoteChannel, init_fn: F) -> AnyResult + pub fn new(init_fn: F) -> Self where F: FnOnce( &mut Router< @@ -80,21 +120,19 @@ impl BasicApp { DistributionKeeper, IbcFailingModule, GovFailingModule, + StargateFailing, >, - &dyn Api, + &MockApi, &mut dyn Storage, ), { - AppBuilder::new().with_remote(remote).build(init_fn) + AppBuilder::new().build(init_fn) } } /// Creates new default `App` implementation working with customized exec and query messages. -/// Outside of `App` implementation to make type elision better. -pub fn custom_app( - remote: RemoteChannel, - init_fn: F, -) -> AnyResult> +/// Outside the `App` implementation to make type elision better. +pub fn custom_app(init_fn: F) -> BasicApp where ExecC: CustomMsg + DeserializeOwned + 'static, QueryC: Debug + CustomQuery + DeserializeOwned + 'static, @@ -107,18 +145,19 @@ where DistributionKeeper, IbcFailingModule, GovFailingModule, + StargateFailing, >, - &dyn Api, + &MockApi, &mut dyn Storage, ), { - AppBuilder::new_custom().with_remote(remote).build(init_fn) + AppBuilder::new_custom().build(init_fn) } -impl Querier - for App +impl Querier + for App where - CustomT::ExecT: Clone + Debug + PartialEq + JsonSchema + DeserializeOwned + 'static, + CustomT::ExecT: CustomMsg + DeserializeOwned + 'static, CustomT::QueryT: CustomQuery + DeserializeOwned + 'static, WasmT: Wasm, BankT: Bank, @@ -129,6 +168,7 @@ where DistrT: Distribution, IbcT: Ibc, GovT: Gov, + StargateT: Stargate, { fn raw_query(&self, bin_request: &[u8]) -> QuerierResult { self.router @@ -137,10 +177,11 @@ where } } -impl Executor - for App +impl + Executor + for App where - CustomT::ExecT: Clone + Debug + PartialEq + JsonSchema + DeserializeOwned + 'static, + CustomT::ExecT: CustomMsg + DeserializeOwned + 'static, CustomT::QueryT: CustomQuery + DeserializeOwned + 'static, WasmT: Wasm, BankT: Bank, @@ -151,6 +192,7 @@ where DistrT: Distribution, IbcT: Ibc, GovT: Gov, + StargateT: Stargate, { fn execute(&mut self, sender: Addr, msg: CosmosMsg) -> AnyResult { let mut all = self.execute_multi(sender, vec![msg])?; @@ -159,8 +201,8 @@ where } } -impl - App +impl + App where WasmT: Wasm, BankT: Bank, @@ -171,14 +213,16 @@ where DistrT: Distribution, IbcT: Ibc, GovT: Gov, - CustomT::QueryT: CustomQuery, + StargateT: Stargate, { /// Returns a shared reference to application's router. - pub fn router(&self) -> &Router { + pub fn router( + &self, + ) -> &Router { &self.router } - /// Returns a shared reference to application's api. + /// Returns a shared reference to application's API. pub fn api(&self) -> &ApiT { &self.api } @@ -193,22 +237,24 @@ where &mut self.storage } + /// Initializes modules. pub fn init_modules(&mut self, init_fn: F) -> T where F: FnOnce( - &mut Router, - &dyn Api, + &mut Router, + &ApiT, &mut dyn Storage, ) -> T, { init_fn(&mut self.router, &self.api, &mut self.storage) } + /// Queries a module. pub fn read_module(&self, query_fn: F) -> T where F: FnOnce( - &Router, - &dyn Api, + &Router, + &ApiT, &dyn Storage, ) -> T, { @@ -218,8 +264,8 @@ where // Helper functions to call some custom WasmKeeper logic. // They show how we can easily add such calls to other custom keepers (CustomT, StakingT, etc) -impl - App +impl + App where BankT: Bank, ApiT: Api, @@ -230,61 +276,163 @@ where DistrT: Distribution, IbcT: Ibc, GovT: Gov, + StargateT: Stargate, CustomT::ExecT: CustomMsg + DeserializeOwned + 'static, CustomT::QueryT: CustomQuery + DeserializeOwned + 'static, { /// Registers contract code (like uploading wasm bytecode on a chain), /// so it can later be used to instantiate a contract. - /// Only for wasm codes - pub fn store_wasm_code(&mut self, code: Vec) -> u64 { - self.init_modules(|router, _, _| { - router - .wasm - .store_wasm_code(Addr::unchecked("code-creator"), code) - }) + pub fn store_code(&mut self, code: Box>) -> u64 { + self.router + .wasm + .store_code(MockApi::default().addr_make("creator"), code) + } + + /// Registers contract code (like [store_code](Self::store_code)), + /// but takes the address of the code creator as an additional argument. + pub fn store_code_with_creator( + &mut self, + creator: Addr, + code: Box>, + ) -> u64 { + self.router.wasm.store_code(creator, code) } /// 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 { + /// Only for wasm codes + pub fn store_wasm_code(&mut self, code: Vec) -> u64 { self.init_modules(|router, _, _| { router .wasm - .store_code(Addr::unchecked("code-creator"), code) + .store_wasm_code(Addr::unchecked("code-creator"), code) }) } - /// Registers contract code (like [store_code](Self::store_code)), /// but takes the address of the code creator as an additional argument. pub fn store_wasm_code_with_creator(&mut self, creator: Addr, code: Vec) -> u64 { self.init_modules(|router, _, _| router.wasm.store_wasm_code(creator, code)) } - /// Registers contract code (like [store_code](Self::store_code)), - /// but takes the address of the code creator as an additional argument. - pub fn store_code_with_creator( + /// Registers contract code (like [store_code_with_creator](Self::store_code_with_creator)), + /// but takes the code identifier as an additional argument. + pub fn store_code_with_id( &mut self, creator: Addr, + code_id: u64, code: Box>, - ) -> u64 { - self.init_modules(|router, _, _| router.wasm.store_code(creator, code)) + ) -> AnyResult { + self.router.wasm.store_code_with_id(creator, code_id, code) + } + + /// Duplicates the contract code identified by `code_id` and returns + /// the identifier of the newly created copy of the contract code. + /// + /// # Examples + /// + /// ``` + /// use cosmwasm_std::Addr; + /// use cw_multi_test::App; + /// + /// // contract implementation + /// mod echo { + /// // contract entry points not shown here + /// # use std::todo; + /// # use cosmwasm_std::{Binary, Deps, DepsMut, Empty, Env, MessageInfo, Response, StdError, SubMsg, WasmMsg}; + /// # use serde::{Deserialize, Serialize}; + /// # use cw_multi_test::{Contract, ContractWrapper}; + /// # + /// # fn instantiate(_: DepsMut, _: Env, _: MessageInfo, _: Empty) -> Result { + /// # todo!() + /// # } + /// # + /// # fn execute(_: DepsMut, _: Env, _info: MessageInfo, msg: WasmMsg) -> Result { + /// # todo!() + /// # } + /// # + /// # fn query(_deps: Deps, _env: Env, _msg: Empty) -> Result { + /// # todo!() + /// # } + /// # + /// pub fn contract() -> Box> { + /// // should return the contract + /// # Box::new(ContractWrapper::new(execute, instantiate, query)) + /// } + /// } + /// + /// let mut app = App::default(); + /// + /// // store a new contract, save the code id + /// let code_id = app.store_code(echo::contract()); + /// + /// // duplicate the existing contract, duplicated contract has different code id + /// assert_ne!(code_id, app.duplicate_code(code_id).unwrap()); + /// + /// // zero is an invalid identifier for contract code, returns an error + /// assert_eq!("code id: invalid", app.duplicate_code(0).unwrap_err().to_string()); + /// + /// // there is no contract code with identifier 100 stored yet, returns an error + /// assert_eq!("code id 100: no such code", app.duplicate_code(100).unwrap_err().to_string()); + /// ``` + pub fn duplicate_code(&mut self, code_id: u64) -> AnyResult { + self.router.wasm.duplicate_code(code_id) } /// Returns `ContractData` for the contract with specified address. pub fn contract_data(&self, address: &Addr) -> AnyResult { - self.read_module(|router, _, storage| router.wasm.contract_data(storage, address)) + self.router.wasm.contract_data(&self.storage, address) } /// Returns a raw state dump of all key-values held by a contract with specified address. pub fn dump_wasm_raw(&self, address: &Addr) -> Vec { - self.read_module(|router, _, storage| router.wasm.dump_wasm_raw(storage, address)) + self.router.wasm.dump_wasm_raw(&self.storage, address) + } + + /// Returns **read-only** storage for a contract with specified address. + pub fn contract_storage<'a>(&'a self, contract_addr: &Addr) -> Box { + self.router + .wasm + .contract_storage(&self.storage, contract_addr) + } + + /// Returns **read-write** storage for a contract with specified address. + pub fn contract_storage_mut<'a>(&'a mut self, contract_addr: &Addr) -> Box { + self.router + .wasm + .contract_storage_mut(&mut self.storage, contract_addr) + } + + /// Returns **read-only** prefixed storage with specified namespace. + pub fn prefixed_storage<'a>(&'a self, namespace: &[u8]) -> Box { + Box::new(prefixed_read(&self.storage, namespace)) + } + + /// Returns **mutable** prefixed storage with specified namespace. + pub fn prefixed_storage_mut<'a>(&'a mut self, namespace: &[u8]) -> Box { + Box::new(prefixed(&mut self.storage, namespace)) + } + + /// Returns **read-only** prefixed, multilevel storage with specified namespaces. + pub fn prefixed_multilevel_storage<'a>( + &'a self, + namespaces: &[&[u8]], + ) -> Box { + Box::new(prefixed_multilevel_read(&self.storage, namespaces)) + } + + /// Returns **mutable** prefixed, multilevel storage with specified namespaces. + pub fn prefixed_multilevel_storage_mut<'a>( + &'a mut self, + namespaces: &[&[u8]], + ) -> Box { + Box::new(prefixed_multilevel(&mut self.storage, namespaces)) } } -impl - App +impl + App where - CustomT::ExecT: Debug + PartialEq + Clone + JsonSchema + DeserializeOwned + 'static, + CustomT::ExecT: CustomMsg + DeserializeOwned + 'static, CustomT::QueryT: CustomQuery + DeserializeOwned + 'static, WasmT: Wasm, BankT: Bank, @@ -295,47 +443,33 @@ where DistrT: Distribution, IbcT: Ibc, GovT: Gov, + StargateT: Stargate, { + /// Sets the initial block properties. pub fn set_block(&mut self, block: BlockInfo) { + self.block = block; self.router .staking .process_queue(&self.api, &mut self.storage, &self.router, &self.block) .unwrap(); - self.block = block; } - // this let's use use "next block" steps that add eg. one height and 5 seconds + /// Updates the current block applying the specified closure, usually [next_block]. pub fn update_block(&mut self, action: F) { + action(&mut self.block); self.router .staking .process_queue(&self.api, &mut self.storage, &self.router, &self.block) .unwrap(); - action(&mut self.block); } - /// Returns a copy of the current block_info + /// Returns a copy of the current block info. pub fn block_info(&self) -> BlockInfo { self.block.clone() } - /// Returns a new account address - pub fn next_address(&mut self) -> Addr { - let Self { - storage, remote, .. - } = self; - - let mut addresses = ADDRESSES.may_load(storage).unwrap().unwrap_or_default(); - - let new_address = - RealApi::new(&remote.pub_address_prefix.clone()).next_address(addresses.len()); - addresses.push(new_address.clone()); - ADDRESSES.save(storage, &addresses).unwrap(); - - new_address - } - /// Simple helper so we get access to all the QuerierWrapper helpers, - /// eg. wrap().query_wasm_smart, query_all_balances, ... + /// e.g. wrap().query_wasm_smart, query_all_balances, ... pub fn wrap(&self) -> QuerierWrapper { QuerierWrapper::new(self) } @@ -382,7 +516,10 @@ where contract_addr: U, msg: &T, ) -> AnyResult { - let msg = to_json_binary(msg)?; + let msg = WasmSudo { + contract_addr: contract_addr.into(), + message: to_json_binary(msg)?, + }; let Self { block, @@ -393,9 +530,7 @@ where } = self; transactional(&mut *storage, |write_cache, _| { - router - .wasm - .sudo(&*api, contract_addr.into(), write_cache, router, block, msg) + router.wasm.sudo(&*api, write_cache, router, block, msg) }) } @@ -419,25 +554,32 @@ where }) } } - +/// The Router plays a critical role in managing and directing +/// transactions within the Cosmos blockchain. #[derive(Clone)] -pub struct Router { - // this can remain crate-only as all special functions are wired up to app currently - // we need to figure out another format for wasm, as some like sudo need to be called after init +pub struct Router { + /// Wasm module instance to be used in this [Router]. pub(crate) wasm: Wasm, - // these must be pub so we can initialize them (super user) on build + /// Bank module instance to be used in this [Router]. pub bank: Bank, + /// Custom module instance to be used in this [Router]. pub custom: Custom, + /// Staking module instance to be used in this [Router]. pub staking: Staking, + /// Distribution module instance to be used in this [Router]. pub distribution: Distr, + /// IBC module instance to be used in this [Router]. pub ibc: Ibc, + /// Governance module instance to be used in this [Router]. pub gov: Gov, + /// Stargate handler instance to be used in this [Router]. + pub stargate: Stargate, } -impl - Router +impl + Router where - CustomT::ExecT: Clone + Debug + PartialEq + JsonSchema + DeserializeOwned + 'static, + CustomT::ExecT: CustomMsg + DeserializeOwned + 'static, CustomT::QueryT: CustomQuery + DeserializeOwned + 'static, CustomT: Module, WasmT: Wasm, @@ -446,7 +588,9 @@ where DistrT: Distribution, IbcT: Ibc, GovT: Gov, + StargateT: Stargate, { + /// Returns a querier populated with the instance of this [Router]. pub fn querier<'a>( &'a self, api: &'a dyn Api, @@ -465,9 +609,13 @@ where /// We use it to allow calling into modules from another module in sudo mode. /// Things like gov proposals belong here. pub enum SudoMsg { + /// Bank privileged actions. Bank(BankSudo), + /// Custom privileged actions. Custom(Empty), + /// Staking privileged actions. Staking(StakingSudo), + /// Wasm privileged actions. Wasm(WasmSudo), } @@ -488,11 +636,18 @@ impl From for SudoMsg { SudoMsg::Staking(staking) } } - +/// A trait representing the Cosmos based chain's router. +/// +/// This trait is designed for routing messages within the Cosmos ecosystem. +/// It is key to ensure that transactions and contract calls are directed to the +/// correct destinations during testing, simulating real-world blockchain operations. pub trait CosmosRouter { - type ExecC; + /// Type of the executed custom message. + type ExecC: CustomMsg; + /// Type of the query custom message. type QueryC: CustomQuery; + /// Executes messages. fn execute( &self, api: &dyn Api, @@ -502,6 +657,7 @@ pub trait CosmosRouter { msg: CosmosMsg, ) -> AnyResult; + /// Evaluates queries. fn query( &self, api: &dyn Api, @@ -510,6 +666,7 @@ pub trait CosmosRouter { request: QueryRequest, ) -> AnyResult; + /// Evaluates privileged actions. fn sudo( &self, api: &dyn Api, @@ -521,10 +678,10 @@ pub trait CosmosRouter { fn get_querier_storage(&self, storage: &dyn Storage) -> AnyResult; } -impl CosmosRouter - for Router +impl CosmosRouter + for Router where - CustomT::ExecT: Debug + Clone + PartialEq + JsonSchema + DeserializeOwned + 'static, + CustomT::ExecT: CustomMsg + DeserializeOwned + 'static, CustomT::QueryT: CustomQuery + DeserializeOwned + 'static, CustomT: Module, WasmT: Wasm, @@ -533,6 +690,7 @@ where DistrT: Distribution, IbcT: Ibc, GovT: Gov, + StargateT: Stargate, { type ExecC = CustomT::ExecT; type QueryC = CustomT::QueryT; @@ -549,17 +707,30 @@ where CosmosMsg::Wasm(msg) => self.wasm.execute(api, storage, self, block, sender, msg), CosmosMsg::Bank(msg) => self.bank.execute(api, storage, self, block, sender, msg), CosmosMsg::Custom(msg) => self.custom.execute(api, storage, self, block, sender, msg), + #[cfg(feature = "staking")] CosmosMsg::Staking(msg) => self.staking.execute(api, storage, self, block, sender, msg), + #[cfg(feature = "staking")] CosmosMsg::Distribution(msg) => self .distribution .execute(api, storage, self, block, sender, msg), + #[cfg(feature = "stargate")] CosmosMsg::Ibc(msg) => self.ibc.execute(api, storage, self, block, sender, msg), + #[cfg(feature = "stargate")] CosmosMsg::Gov(msg) => self.gov.execute(api, storage, self, block, sender, msg), + #[allow(deprecated)] + #[cfg(feature = "stargate")] + CosmosMsg::Stargate { type_url, value } => self + .stargate + .execute_stargate(api, storage, self, block, sender, type_url, value), + #[cfg(feature = "cosmwasm_2_0")] + CosmosMsg::Any(msg) => self + .stargate + .execute_any(api, storage, self, block, sender, msg), _ => bail!("Cannot execute {:?}", msg), } } - /// this is used by `RouterQuerier` to actual implement the `Querier` interface. + /// This is used by `RouterQuerier` to actual implement the `Querier` interface. /// you most likely want to use `router.querier(storage, block).wrap()` to get a /// QuerierWrapper to interact with fn query( @@ -574,8 +745,17 @@ where QueryRequest::Wasm(req) => self.wasm.query(api, storage, self, &querier, block, req), QueryRequest::Bank(req) => self.bank.query(api, storage, &querier, block, req), QueryRequest::Custom(req) => self.custom.query(api, storage, &querier, block, req), + #[cfg(feature = "staking")] QueryRequest::Staking(req) => self.staking.query(api, storage, &querier, block, req), + #[cfg(feature = "stargate")] QueryRequest::Ibc(req) => self.ibc.query(api, storage, &querier, block, req), + #[allow(deprecated)] + #[cfg(feature = "stargate")] + QueryRequest::Stargate { path, data } => self + .stargate + .query_stargate(api, storage, &querier, block, path, data), + #[cfg(feature = "cosmwasm_2_0")] + QueryRequest::Grpc(req) => self.stargate.query_grpc(api, storage, &querier, block, req), _ => unimplemented!(), } } @@ -588,13 +768,11 @@ where msg: SudoMsg, ) -> AnyResult { match msg { - SudoMsg::Wasm(msg) => { - self.wasm - .sudo(api, msg.contract_addr, storage, self, block, msg.msg) - } + SudoMsg::Wasm(msg) => self.wasm.sudo(api, storage, self, block, msg), SudoMsg::Bank(msg) => self.bank.sudo(api, storage, self, block, msg), + #[cfg(feature = "staking")] SudoMsg::Staking(msg) => self.staking.sudo(api, storage, self, block, msg), - SudoMsg::Custom(_) => unimplemented!(), + _ => unimplemented!(), } } @@ -625,6 +803,7 @@ impl MockRouter { impl CosmosRouter for MockRouter where + ExecC: CustomMsg, QueryC: CustomQuery, { type ExecC = ExecC; @@ -689,9 +868,9 @@ impl<'a, ExecC, QueryC> RouterQuerier<'a, ExecC, QueryC> { } } -impl<'a, ExecC, QueryC> Querier for RouterQuerier<'a, ExecC, QueryC> +impl Querier for RouterQuerier<'_, ExecC, QueryC> where - ExecC: Clone + Debug + PartialEq + JsonSchema + DeserializeOwned + 'static, + ExecC: CustomMsg + DeserializeOwned + 'static, QueryC: CustomQuery + DeserializeOwned + 'static, { fn raw_query(&self, bin_request: &[u8]) -> QuerierResult { diff --git a/src/app_builder.rs b/src/app_builder.rs index ad747509..94929310 100644 --- a/src/app_builder.rs +++ b/src/app_builder.rs @@ -1,11 +1,10 @@ -//! Implementation of the builder for [App]. - +//! AppBuilder helps you set up your test blockchain environment step by step [App]. +use crate::featured::staking::{Distribution, DistributionKeeper, StakeKeeper, Staking}; use crate::wasm_emulation::channel::RemoteChannel; use crate::{ - App, Bank, BankKeeper, Distribution, DistributionKeeper, FailingModule, Gov, GovFailingModule, - Ibc, IbcFailingModule, Module, Router, StakeKeeper, Staking, Wasm, WasmKeeper, + App, Bank, BankKeeper, FailingModule, Gov, GovFailingModule, Ibc, IbcFailingModule, Module, + Router, Stargate, StargateFailing, Wasm, WasmKeeper, }; -use anyhow::Result as AnyResult; use cosmwasm_std::testing::{mock_env, MockApi, MockStorage}; use cosmwasm_std::{Api, BlockInfo, CustomMsg, CustomQuery, Empty, Storage}; use serde::de::DeserializeOwned; @@ -17,15 +16,18 @@ use std::fmt::Debug; /// /// ``` /// # use cosmwasm_std::Empty; -/// # use cw_multi_test::{BasicAppBuilder, FailingModule, Module}; +/// # use cw_multi_test::{no_init, BasicAppBuilder, FailingModule, Module}; /// # type MyHandler = FailingModule; /// # type MyExecC = Empty; /// # type MyQueryC = Empty; /// /// let mut app = BasicAppBuilder::::new_custom() /// .with_custom(MyHandler::default()) -/// .build(|_, _, _| {}); +/// .build(no_init); /// ``` +/// This type alias is crucial for constructing a custom app with specific modules. +/// It provides a streamlined approach to building and configuring an App tailored to +/// particular testing needs or scenarios. pub type BasicAppBuilder = AppBuilder< BankKeeper, MockApi, @@ -36,11 +38,12 @@ pub type BasicAppBuilder = AppBuilder< DistributionKeeper, IbcFailingModule, GovFailingModule, + StargateFailing, >; /// Utility to build [App] in stages. /// When particular properties are not explicitly set, then default values are used. -pub struct AppBuilder { +pub struct AppBuilder { api: Api, block: BlockInfo, storage: Storage, @@ -52,6 +55,7 @@ pub struct AppBuilder, + stargate: Stargate, } impl Default @@ -65,6 +69,7 @@ impl Default DistributionKeeper, IbcFailingModule, GovFailingModule, + StargateFailing, > { fn default() -> Self { @@ -83,6 +88,7 @@ impl DistributionKeeper, IbcFailingModule, GovFailingModule, + StargateFailing, > { /// Creates builder with default components working with empty exec and query messages. @@ -93,11 +99,12 @@ impl storage: MockStorage::new(), bank: BankKeeper::new(), wasm: WasmKeeper::new(), - custom: FailingModule::new("custom"), + custom: FailingModule::new(), staking: StakeKeeper::new(), distribution: DistributionKeeper::new(), - ibc: IbcFailingModule::new("ibc"), - gov: GovFailingModule::new("gov"), + ibc: IbcFailingModule::new(), + gov: GovFailingModule::new(), + stargate: StargateFailing, remote: None, } } @@ -114,6 +121,7 @@ impl DistributionKeeper, IbcFailingModule, GovFailingModule, + StargateFailing, > where ExecC: CustomMsg + DeserializeOwned + 'static, @@ -128,21 +136,23 @@ where storage: MockStorage::new(), bank: BankKeeper::new(), wasm: WasmKeeper::new(), - custom: FailingModule::new("custom"), + custom: FailingModule::new(), staking: StakeKeeper::new(), distribution: DistributionKeeper::new(), - ibc: IbcFailingModule::new("ibc"), - gov: GovFailingModule::new("gov"), + ibc: IbcFailingModule::new(), + gov: GovFailingModule::new(), + stargate: StargateFailing, remote: None, } } } -impl - AppBuilder +impl + AppBuilder where CustomT: Module, WasmT: Wasm, + BankT: Bank, CustomT::QueryT: CustomQuery, { /// Overwrites the default wasm executor. @@ -152,8 +162,9 @@ where /// done on final building. pub fn with_wasm>( self, - wasm: NewWasm, - ) -> AppBuilder { + mut wasm: NewWasm, + ) -> AppBuilder + { let AppBuilder { bank, api, @@ -165,9 +176,12 @@ where ibc, gov, remote, + stargate, .. } = self; - + if let Some(remote) = remote.as_ref() { + wasm.set_remote(remote.clone()); + } AppBuilder { api, block, @@ -180,14 +194,16 @@ where ibc, gov, remote, + stargate, } } /// Overwrites the default bank interface. pub fn with_bank( self, - bank: NewBank, - ) -> AppBuilder { + mut bank: NewBank, + ) -> AppBuilder + { let AppBuilder { wasm, api, @@ -199,9 +215,13 @@ where ibc, gov, remote, + stargate, .. } = self; + if let Some(remote) = remote.as_ref() { + bank.set_remote(remote.clone()); + } AppBuilder { api, block, @@ -213,6 +233,7 @@ where distribution, ibc, gov, + stargate, remote, } } @@ -221,7 +242,8 @@ where pub fn with_api( self, api: NewApi, - ) -> AppBuilder { + ) -> AppBuilder + { let AppBuilder { wasm, bank, @@ -232,6 +254,7 @@ where distribution, ibc, gov, + stargate, remote, .. } = self; @@ -247,6 +270,7 @@ where distribution, ibc, gov, + stargate, remote, } } @@ -255,7 +279,8 @@ where pub fn with_storage( self, storage: NewStorage, - ) -> AppBuilder { + ) -> AppBuilder + { let AppBuilder { wasm, api, @@ -266,6 +291,7 @@ where distribution, ibc, gov, + stargate, remote, .. } = self; @@ -281,11 +307,12 @@ where distribution, ibc, gov, + stargate, remote, } } - /// Overwrites the default custom messages handler. + /// Overwrites the default handler for custom messages. /// /// At this point it is needed that new custom implements some `Module` trait, but it doesn't need /// to be bound to ExecC or QueryC yet - as those may change. The cross-components validation is @@ -293,7 +320,8 @@ where pub fn with_custom( self, custom: NewCustom, - ) -> AppBuilder { + ) -> AppBuilder + { let AppBuilder { wasm, bank, @@ -304,6 +332,7 @@ where distribution, ibc, gov, + stargate, remote, .. } = self; @@ -319,6 +348,7 @@ where distribution, ibc, gov, + stargate, remote, } } @@ -327,7 +357,8 @@ where pub fn with_staking( self, staking: NewStaking, - ) -> AppBuilder { + ) -> AppBuilder + { let AppBuilder { wasm, api, @@ -338,6 +369,7 @@ where distribution, ibc, gov, + stargate, remote, .. } = self; @@ -353,6 +385,7 @@ where distribution, ibc, gov, + stargate, remote, } } @@ -361,8 +394,18 @@ where pub fn with_distribution( self, distribution: NewDistribution, - ) -> AppBuilder - { + ) -> AppBuilder< + BankT, + ApiT, + StorageT, + CustomT, + WasmT, + StakingT, + NewDistribution, + IbcT, + GovT, + StargateT, + > { let AppBuilder { wasm, api, @@ -373,6 +416,7 @@ where bank, ibc, gov, + stargate, remote, .. } = self; @@ -388,6 +432,7 @@ where distribution, ibc, gov, + stargate, remote, } } @@ -402,7 +447,8 @@ where pub fn with_ibc( self, ibc: NewIbc, - ) -> AppBuilder { + ) -> AppBuilder + { let AppBuilder { wasm, api, @@ -414,6 +460,7 @@ where distribution, gov, remote, + stargate, .. } = self; @@ -425,6 +472,7 @@ where wasm, custom, staking, + stargate, distribution, ibc, gov, @@ -436,7 +484,8 @@ where pub fn with_gov( self, gov: NewGov, - ) -> AppBuilder { + ) -> AppBuilder + { let AppBuilder { wasm, api, @@ -448,6 +497,7 @@ where distribution, ibc, remote, + stargate, .. } = self; @@ -462,15 +512,29 @@ where distribution, ibc, gov, + stargate, remote, } } /// Sets the chain of the app pub fn with_remote( - self, + mut self, remote: RemoteChannel, - ) -> AppBuilder { + ) -> AppBuilder + { + self.remote = Some(remote.clone()); + self.wasm.set_remote(remote.clone()); + self.bank.set_remote(remote.clone()); + self + } + + /// Overwrites the default stargate interface. + pub fn with_stargate( + self, + stargate: NewStargate, + ) -> AppBuilder + { let AppBuilder { wasm, api, @@ -482,6 +546,7 @@ where distribution, ibc, gov, + remote, .. } = self; @@ -495,8 +560,9 @@ where staking, distribution, ibc, - remote: Some(remote), gov, + stargate, + remote, } } @@ -506,14 +572,13 @@ where self } - #[allow(clippy::type_complexity)] - /// Builds final `App`. At this point all components type have to be properly related to each - /// other. If there are some generics related compilation errors, make sure that all components - /// are properly relating to each other. + /// Builds the final [App] with initialization. + /// + /// At this point all component types have to be properly related to each other. pub fn build( self, init_fn: F, - ) -> AnyResult> + ) -> App where BankT: Bank, ApiT: Api, @@ -524,32 +589,35 @@ where DistrT: Distribution, IbcT: Ibc, GovT: Gov, + StargateT: Stargate, F: FnOnce( - &mut Router, - &dyn Api, + &mut Router, + &ApiT, &mut dyn Storage, ), { - let router = Router { - wasm: self.wasm, - bank: self.bank, - custom: self.custom, - staking: self.staking, - distribution: self.distribution, - ibc: self.ibc, - gov: self.gov, - }; - + // build the final application let mut app = App { - router, + router: Router { + wasm: self.wasm, + bank: self.bank, + custom: self.custom, + staking: self.staking, + distribution: self.distribution, + ibc: self.ibc, + gov: self.gov, + stargate: self.stargate, + }, api: self.api, block: self.block, storage: self.storage, - remote: self.remote.ok_or(anyhow::anyhow!( - "Remote has to be defined to use clone-testing" - ))?, + remote: self + .remote + .expect("Remote has to be defined to use clone-testing"), }; + // execute initialization provided by the caller app.init_modules(init_fn); - Ok(app) + // return already initialized application + app } } diff --git a/src/bank.rs b/src/bank.rs index ca08475f..14f8781f 100644 --- a/src/bank.rs +++ b/src/bank.rs @@ -5,51 +5,68 @@ use crate::module::Module; use crate::prefixed_storage::{prefixed, prefixed_read}; use crate::queries::bank::BankRemoteQuerier; use crate::wasm_emulation::channel::RemoteChannel; -use crate::wasm_emulation::input::BankStorage; -use crate::wasm_emulation::query::AllBankQuerier; +use crate::wasm_emulation::query::{AllBankQuerier, ContainsRemote}; use cosmwasm_std::{ coin, to_json_binary, Addr, AllBalanceResponse, Api, BalanceResponse, BankMsg, BankQuery, - Binary, BlockInfo, Coin, Event, Order, Querier, Storage, + Binary, BlockInfo, Coin, DenomMetadata, Event, Querier, Storage, }; -use cosmwasm_std::{StdResult, SupplyResponse, Uint128}; +#[cfg(feature = "cosmwasm_1_3")] +use cosmwasm_std::{AllDenomMetadataResponse, DenomMetadataResponse}; +#[cfg(feature = "cosmwasm_1_1")] +use cosmwasm_std::{Order, StdResult, SupplyResponse, Uint128}; use cw_storage_plus::Map; use cw_utils::NativeBalance; use itertools::Itertools; use schemars::JsonSchema; -pub(crate) const BALANCES: Map<&Addr, NativeBalance> = Map::new("balances"); +/// Collection of bank balances. +const BALANCES: Map<&Addr, NativeBalance> = Map::new("balances"); -pub const NAMESPACE_BANK: &[u8] = b"bank"; +/// Collection of metadata for denomination. +const DENOM_METADATA: Map = Map::new("metadata"); -#[derive(Clone, std::fmt::Debug, PartialEq, Eq, JsonSchema)] +/// Default storage namespace for bank module. +const NAMESPACE_BANK: &[u8] = b"bank"; + +/// A message representing privileged actions in bank module. +#[derive(Clone, Debug, PartialEq, Eq, JsonSchema)] pub enum BankSudo { + /// Minting privileged action. Mint { + /// Destination address the tokens will be minted for. to_address: String, + /// Amount of the minted tokens. amount: Vec, }, } +/// This trait defines the interface for simulating banking operations. +/// +/// In the test environment, it is essential for testing financial transactions, +/// like transfers and balance checks, within your smart contracts. +/// This trait implements all of these functionalities. pub trait Bank: - Module + AllBankQuerier + Module + AllBankQuerier + ContainsRemote { } +/// A structure representing a default bank keeper. +/// +/// Manages financial interactions in CosmWasm tests, such as simulating token transactions +/// and account balances. This is particularly important for contracts that deal with financial +/// operations in the Cosmos ecosystem. #[derive(Default)] pub struct BankKeeper { remote: Option, } impl BankKeeper { + /// Creates a new instance of a bank keeper with default settings. pub fn new() -> Self { - BankKeeper::default() - } - - pub fn with_remote(mut self, remote: RemoteChannel) -> Self { - self.remote = Some(remote); - self + Self::default() } - // this is an "admin" function to let us adjust bank accounts in genesis + /// Administration function for adjusting bank accounts in genesis. pub fn init_balance( &self, storage: &mut dyn Storage, @@ -60,7 +77,7 @@ impl BankKeeper { self.set_balance(&mut bank_storage, account, amount) } - // this is an "admin" function to let us adjust bank accounts + /// Administration function for adjusting bank accounts. fn set_balance( &self, bank_storage: &mut dyn Storage, @@ -74,6 +91,18 @@ impl BankKeeper { .map_err(Into::into) } + /// Administration function for adjusting denomination metadata. + pub fn set_denom_metadata( + &self, + bank_storage: &mut dyn Storage, + denom: String, + metadata: DenomMetadata, + ) -> AnyResult<()> { + DENOM_METADATA + .save(bank_storage, denom, &metadata) + .map_err(Into::into) + } + fn get_balance(&self, bank_storage: &dyn Storage, account: &Addr) -> AnyResult> { // If there is no balance present, we query it on the distant chain if let Some(val) = BALANCES.may_load(bank_storage, account)? { @@ -83,6 +112,7 @@ impl BankKeeper { } } + #[cfg(feature = "cosmwasm_1_1")] fn get_supply(&self, bank_storage: &dyn Storage, denom: String) -> AnyResult { let supply: Uint128 = BALANCES .range(bank_storage, None, None, Order::Ascending) @@ -136,7 +166,7 @@ impl BankKeeper { self.set_balance(bank_storage, &from_address, a.into_vec()) } - /// Filters out all 0 value coins and returns an error if the resulting Vec is empty + /// Filters out all `0` value coins and returns an error if the resulting vector is empty. fn normalize_amount(&self, amount: Vec) -> AnyResult> { let res: Vec<_> = amount.into_iter().filter(|x| !x.amount.is_zero()).collect(); if res.is_empty() { @@ -147,6 +177,17 @@ impl BankKeeper { } } +impl ContainsRemote for BankKeeper { + fn with_remote(mut self, remote: RemoteChannel) -> Self { + self.set_remote(remote); + self + } + + fn set_remote(&mut self, remote: RemoteChannel) { + self.remote = Some(remote) + } +} + fn coins_to_string(coins: &[Coin]) -> String { coins .iter() @@ -191,25 +232,7 @@ impl Module for BankKeeper { self.burn(&mut bank_storage, sender, amount)?; Ok(AppResponse::default()) } - m => bail!("Unsupported bank message: {:?}", m), - } - } - - fn sudo( - &self, - api: &dyn Api, - storage: &mut dyn Storage, - _router: &dyn CosmosRouter, - _block: &BlockInfo, - msg: BankSudo, - ) -> AnyResult { - let mut bank_storage = prefixed(storage, NAMESPACE_BANK); - match msg { - BankSudo::Mint { to_address, amount } => { - let to_address = api.addr_validate(&to_address)?; - self.mint(&mut bank_storage, to_address, amount)?; - Ok(AppResponse::default()) - } + other => unimplemented!("bank message: {other:?}"), } } @@ -227,7 +250,7 @@ impl Module for BankKeeper { let address = api.addr_validate(&address)?; let amount = self.get_balance(&bank_storage, &address)?; let res = AllBalanceResponse::new(amount); - Ok(to_json_binary(&res)?) + to_json_binary(&res).map_err(Into::into) } BankQuery::Balance { address, denom } => { let address = api.addr_validate(&address)?; @@ -237,24 +260,432 @@ impl Module for BankKeeper { .find(|c| c.denom == denom) .unwrap_or_else(|| coin(0, denom)); let res = BalanceResponse::new(amount); - Ok(to_json_binary(&res)?) + to_json_binary(&res).map_err(Into::into) } + #[cfg(feature = "cosmwasm_1_1")] BankQuery::Supply { denom } => { let amount = self.get_supply(&bank_storage, denom)?; let res = SupplyResponse::new(amount); - Ok(to_json_binary(&res)?) + to_json_binary(&res).map_err(Into::into) + } + #[cfg(feature = "cosmwasm_1_3")] + BankQuery::DenomMetadata { denom } => { + let meta = DENOM_METADATA.may_load(storage, denom)?.unwrap_or_default(); + let res = DenomMetadataResponse::new(meta); + to_json_binary(&res).map_err(Into::into) + } + #[cfg(feature = "cosmwasm_1_3")] + BankQuery::AllDenomMetadata { pagination: _ } => { + let mut metadata = vec![]; + for key in DENOM_METADATA.keys(storage, None, None, Order::Ascending) { + metadata.push(DENOM_METADATA.may_load(storage, key?)?.unwrap_or_default()); + } + let res = AllDenomMetadataResponse::new(metadata, None); + to_json_binary(&res).map_err(Into::into) + } + other => unimplemented!("bank query: {other:?}"), + } + } + + fn sudo( + &self, + api: &dyn Api, + storage: &mut dyn Storage, + _router: &dyn CosmosRouter, + _block: &BlockInfo, + msg: BankSudo, + ) -> AnyResult { + let mut bank_storage = prefixed(storage, NAMESPACE_BANK); + match msg { + BankSudo::Mint { to_address, amount } => { + let to_address = api.addr_validate(&to_address)?; + self.mint(&mut bank_storage, to_address, amount)?; + Ok(AppResponse::default()) } - q => bail!("Unsupported bank query: {:?}", q), } } } -impl AllBankQuerier for BankKeeper { - fn query_all(&self, storage: &dyn Storage) -> AnyResult { - let bank_storage = prefixed_read(storage, NAMESPACE_BANK); - let balances: Result, _> = BALANCES - .range(&bank_storage, None, None, Order::Ascending) - .collect(); - Ok(BankStorage { storage: balances? }) +pub mod storage_querier { + use anyhow::Result as AnyResult; + use cosmwasm_std::{Order, Storage}; + + use crate::{ + prefixed_storage::prefixed_read, + wasm_emulation::{input::BankStorage, query::AllBankQuerier}, + }; + + use super::{BankKeeper, BALANCES, NAMESPACE_BANK}; + + impl AllBankQuerier for BankKeeper { + fn query_all(&self, storage: &dyn Storage) -> AnyResult { + let bank_storage = prefixed_read(storage, NAMESPACE_BANK); + let balances: Result, _> = BALANCES + .range(&bank_storage, None, None, Order::Ascending) + .collect(); + Ok(BankStorage { storage: balances? }) + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + use crate::app::MockRouter; + use crate::tests::remote_channel; + use cosmwasm_std::testing::{mock_env, MockApi, MockQuerier, MockStorage}; + use cosmwasm_std::{coins, from_json, Empty, StdError}; + + fn query_balance( + bank: &BankKeeper, + api: &dyn Api, + store: &dyn Storage, + rcpt: &Addr, + ) -> Vec { + let req = BankQuery::AllBalances { + address: rcpt.clone().into(), + }; + let block = mock_env().block; + let querier: MockQuerier = MockQuerier::new(&[]); + + let raw = bank.query(api, store, &querier, &block, req).unwrap(); + let res: AllBalanceResponse = from_json(raw).unwrap(); + res.amount + } + + #[test] + #[cfg(feature = "cosmwasm_1_1")] + fn get_set_balance() { + let api = MockApi::default(); + let mut store = MockStorage::new(); + let block = mock_env().block; + let querier: MockQuerier = MockQuerier::new(&[]); + let router = MockRouter::default(); + + let owner = api.addr_make("owner"); + let rcpt = api.addr_make("receiver"); + let init_funds = vec![coin(100, "eth"), coin(20, "btc")]; + let norm = vec![coin(20, "btc"), coin(100, "eth")]; + + // set money + let bank = BankKeeper::new().with_remote(remote_channel()); + bank.init_balance(&mut store, &owner, init_funds).unwrap(); + let bank_storage = prefixed_read(&store, NAMESPACE_BANK); + + // get balance work + let rich = bank.get_balance(&bank_storage, &owner).unwrap(); + assert_eq!(rich, norm); + let poor = bank.get_balance(&bank_storage, &rcpt).unwrap(); + assert_eq!(poor, vec![]); + + // proper queries work + let req = BankQuery::AllBalances { + address: owner.clone().into(), + }; + let raw = bank.query(&api, &store, &querier, &block, req).unwrap(); + let res: AllBalanceResponse = from_json(raw).unwrap(); + assert_eq!(res.amount, norm); + + let req = BankQuery::AllBalances { + address: rcpt.clone().into(), + }; + let raw = bank.query(&api, &store, &querier, &block, req).unwrap(); + let res: AllBalanceResponse = from_json(raw).unwrap(); + assert_eq!(res.amount, vec![]); + + let req = BankQuery::Balance { + address: owner.clone().into(), + denom: "eth".into(), + }; + let raw = bank.query(&api, &store, &querier, &block, req).unwrap(); + let res: BalanceResponse = from_json(raw).unwrap(); + assert_eq!(res.amount, coin(100, "eth")); + + let req = BankQuery::Balance { + address: owner.into(), + denom: "foobar".into(), + }; + let raw = bank.query(&api, &store, &querier, &block, req).unwrap(); + let res: BalanceResponse = from_json(raw).unwrap(); + assert_eq!(res.amount, coin(0, "foobar")); + + let req = BankQuery::Balance { + address: rcpt.clone().into(), + denom: "eth".into(), + }; + let raw = bank.query(&api, &store, &querier, &block, req).unwrap(); + let res: BalanceResponse = from_json(raw).unwrap(); + assert_eq!(res.amount, coin(0, "eth")); + + // Query total supply of a denom + let req = BankQuery::Supply { + denom: "eth".into(), + }; + let raw = bank.query(&api, &store, &querier, &block, req).unwrap(); + let res: SupplyResponse = from_json(raw).unwrap(); + assert_eq!(res.amount, coin(100, "eth")); + + // Mint tokens for recipient account + let msg = BankSudo::Mint { + to_address: rcpt.to_string(), + amount: norm.clone(), + }; + bank.sudo(&api, &mut store, &router, &block, msg).unwrap(); + + // Check that the recipient account has the expected balance + let req = BankQuery::AllBalances { + address: rcpt.into(), + }; + let raw = bank.query(&api, &store, &querier, &block, req).unwrap(); + let res: AllBalanceResponse = from_json(raw).unwrap(); + assert_eq!(res.amount, norm); + + // Check that the total supply of a denom is updated + let req = BankQuery::Supply { + denom: "eth".into(), + }; + let raw = bank.query(&api, &store, &querier, &block, req).unwrap(); + let res: SupplyResponse = from_json(raw).unwrap(); + assert_eq!(res.amount, coin(200, "eth")); + } + + #[test] + fn send_coins() { + let api = MockApi::default(); + let mut store = MockStorage::new(); + let block = mock_env().block; + let router = MockRouter::default(); + + let owner = api.addr_make("owner"); + let rcpt = api.addr_make("receiver"); + let init_funds = vec![coin(20, "btc"), coin(100, "eth")]; + let rcpt_funds = vec![coin(5, "btc")]; + + // set money + let bank = BankKeeper::new().with_remote(remote_channel()); + bank.init_balance(&mut store, &owner, init_funds).unwrap(); + bank.init_balance(&mut store, &rcpt, rcpt_funds).unwrap(); + + // send both tokens + let to_send = vec![coin(30, "eth"), coin(5, "btc")]; + let msg = BankMsg::Send { + to_address: rcpt.clone().into(), + amount: to_send, + }; + bank.execute( + &api, + &mut store, + &router, + &block, + owner.clone(), + msg.clone(), + ) + .unwrap(); + let rich = query_balance(&bank, &api, &store, &owner); + assert_eq!(vec![coin(15, "btc"), coin(70, "eth")], rich); + let poor = query_balance(&bank, &api, &store, &rcpt); + assert_eq!(vec![coin(10, "btc"), coin(30, "eth")], poor); + + // can send from any account with funds + bank.execute(&api, &mut store, &router, &block, rcpt.clone(), msg) + .unwrap(); + + // cannot send too much + let msg = BankMsg::Send { + to_address: rcpt.into(), + amount: coins(20, "btc"), + }; + bank.execute(&api, &mut store, &router, &block, owner.clone(), msg) + .unwrap_err(); + + let rich = query_balance(&bank, &api, &store, &owner); + assert_eq!(vec![coin(15, "btc"), coin(70, "eth")], rich); + } + + #[test] + fn burn_coins() { + let api = MockApi::default(); + let mut store = MockStorage::new(); + let block = mock_env().block; + let router = MockRouter::default(); + + let owner = api.addr_make("owner"); + let rcpt = api.addr_make("recipient"); + let init_funds = vec![coin(20, "btc"), coin(100, "eth")]; + + // set money + let bank = BankKeeper::new().with_remote(remote_channel()); + bank.init_balance(&mut store, &owner, init_funds).unwrap(); + + // burn both tokens + let to_burn = vec![coin(30, "eth"), coin(5, "btc")]; + let msg = BankMsg::Burn { amount: to_burn }; + bank.execute(&api, &mut store, &router, &block, owner.clone(), msg) + .unwrap(); + let rich = query_balance(&bank, &api, &store, &owner); + assert_eq!(vec![coin(15, "btc"), coin(70, "eth")], rich); + + // cannot burn too much + let msg = BankMsg::Burn { + amount: coins(20, "btc"), + }; + let err = bank + .execute(&api, &mut store, &router, &block, owner.clone(), msg) + .unwrap_err(); + assert!(matches!(err.downcast().unwrap(), StdError::Overflow { .. })); + + let rich = query_balance(&bank, &api, &store, &owner); + assert_eq!(vec![coin(15, "btc"), coin(70, "eth")], rich); + + // cannot burn from empty account + let msg = BankMsg::Burn { + amount: coins(1, "btc"), + }; + let err = bank + .execute(&api, &mut store, &router, &block, rcpt, msg) + .unwrap_err(); + assert!(matches!(err.downcast().unwrap(), StdError::Overflow { .. })); + } + + #[test] + #[cfg(feature = "cosmwasm_1_3")] + fn set_get_denom_metadata_should_work() { + let api = MockApi::default(); + let mut store = MockStorage::new(); + let block = mock_env().block; + let querier: MockQuerier = MockQuerier::new(&[]); + let bank = BankKeeper::new().with_remote(remote_channel()); + // set metadata for Ether + let denom_eth_name = "eth".to_string(); + bank.set_denom_metadata( + &mut store, + denom_eth_name.clone(), + DenomMetadata { + name: denom_eth_name.clone(), + ..Default::default() + }, + ) + .unwrap(); + // query metadata + let req = BankQuery::DenomMetadata { + denom: denom_eth_name.clone(), + }; + let raw = bank.query(&api, &store, &querier, &block, req).unwrap(); + let res: DenomMetadataResponse = from_json(raw).unwrap(); + assert_eq!(res.metadata.name, denom_eth_name); + } + + #[test] + #[cfg(feature = "cosmwasm_1_3")] + fn set_get_all_denom_metadata_should_work() { + let api = MockApi::default(); + let mut store = MockStorage::new(); + let block = mock_env().block; + let querier: MockQuerier = MockQuerier::new(&[]); + let bank = BankKeeper::new().with_remote(remote_channel()); + // set metadata for Bitcoin + let denom_btc_name = "btc".to_string(); + bank.set_denom_metadata( + &mut store, + denom_btc_name.clone(), + DenomMetadata { + name: denom_btc_name.clone(), + ..Default::default() + }, + ) + .unwrap(); + // set metadata for Ether + let denom_eth_name = "eth".to_string(); + bank.set_denom_metadata( + &mut store, + denom_eth_name.clone(), + DenomMetadata { + name: denom_eth_name.clone(), + ..Default::default() + }, + ) + .unwrap(); + // query metadata + let req = BankQuery::AllDenomMetadata { pagination: None }; + let raw = bank.query(&api, &store, &querier, &block, req).unwrap(); + let res: AllDenomMetadataResponse = from_json(raw).unwrap(); + assert_eq!(res.metadata[0].name, denom_btc_name); + assert_eq!(res.metadata[1].name, denom_eth_name); + } + + #[test] + fn fail_on_zero_values() { + let api = MockApi::default(); + let mut store = MockStorage::new(); + let block = mock_env().block; + let router = MockRouter::default(); + + let owner = api.addr_make("owner"); + let rcpt = api.addr_make("recipient"); + let init_funds = vec![coin(5000, "atom"), coin(100, "eth")]; + + // set money + let bank = BankKeeper::new().with_remote(remote_channel()); + bank.init_balance(&mut store, &owner, init_funds).unwrap(); + + // can send normal amounts + let msg = BankMsg::Send { + to_address: rcpt.to_string(), + amount: coins(100, "atom"), + }; + bank.execute(&api, &mut store, &router, &block, owner.clone(), msg) + .unwrap(); + + // fails send on no coins + let msg = BankMsg::Send { + to_address: rcpt.to_string(), + amount: vec![], + }; + bank.execute(&api, &mut store, &router, &block, owner.clone(), msg) + .unwrap_err(); + + // fails send on 0 coins + let msg = BankMsg::Send { + to_address: rcpt.to_string(), + amount: coins(0, "atom"), + }; + bank.execute(&api, &mut store, &router, &block, owner.clone(), msg) + .unwrap_err(); + + // fails burn on no coins + let msg = BankMsg::Burn { amount: vec![] }; + bank.execute(&api, &mut store, &router, &block, owner.clone(), msg) + .unwrap_err(); + + // fails burn on 0 coins + let msg = BankMsg::Burn { + amount: coins(0, "atom"), + }; + bank.execute(&api, &mut store, &router, &block, owner, msg) + .unwrap_err(); + + // can mint via sudo + let msg = BankSudo::Mint { + to_address: rcpt.to_string(), + amount: coins(4321, "atom"), + }; + bank.sudo(&api, &mut store, &router, &block, msg).unwrap(); + + // mint fails with 0 tokens + let msg = BankSudo::Mint { + to_address: rcpt.to_string(), + amount: coins(0, "atom"), + }; + bank.sudo(&api, &mut store, &router, &block, msg) + .unwrap_err(); + + // mint fails with no tokens + let msg = BankSudo::Mint { + to_address: rcpt.to_string(), + amount: vec![], + }; + bank.sudo(&api, &mut store, &router, &block, msg) + .unwrap_err(); } } diff --git a/src/checksums.rs b/src/checksums.rs index f5c641f6..2f5a9781 100644 --- a/src/checksums.rs +++ b/src/checksums.rs @@ -2,10 +2,6 @@ use cosmwasm_std::{Addr, Checksum}; -/// Provides a custom interface for generating checksums for contract code. -/// This is crucial for ensuring code integrity and is particularly useful -/// in environments where code verification is a key part of the contract -/// deployment process. /// This trait defines a method to calculate checksum based on /// the creator's address and a unique code identifier. pub trait ChecksumGenerator { diff --git a/src/contracts.rs b/src/contracts.rs index e6d218ff..868c3146 100644 --- a/src/contracts.rs +++ b/src/contracts.rs @@ -1,80 +1,80 @@ -use std::{ - error::Error, - fmt::{self, Debug, Display}, -}; - -use schemars::JsonSchema; - +//! # Implementation of the contract trait and contract wrapper + +use crate::error::{anyhow, bail, AnyResult}; +use crate::wasm_emulation::query::mock_querier::ForkState; +use crate::wasm_emulation::query::MockQuerier; +use crate::wasm_emulation::storage::dual_std_storage::DualStorage; +use crate::wasm_emulation::storage::storage_wrappers::{ReadonlyStorageWrapper, StorageWrapper}; +use anyhow::Error as AnyError; use cosmwasm_std::{ - from_json, Binary, Checksum, CustomMsg, CustomQuery, Deps, DepsMut, Empty, Env, MessageInfo, QuerierWrapper, Reply, Response, StdError + from_json, Binary, Checksum, CosmosMsg, CustomMsg, CustomQuery, Deps, DepsMut, Empty, Env, + MessageInfo, QuerierWrapper, Reply, Response, SubMsg, }; - -use anyhow::Result as AnyResult; use serde::de::DeserializeOwned; +use std::fmt::{Debug, Display}; +use std::ops::Deref; -use crate::wasm_emulation::{ - query::{mock_querier::ForkState, MockQuerier}, - storage::{ - dual_std_storage::DualStorage, - storage_wrappers::{ReadonlyStorageWrapper, StorageWrapper}, - }, -}; -use anyhow::{anyhow, bail}; -/// Interface to call into a [Contract]. -pub trait Contract +/// This trait serves as a primary interface for interacting with contracts. +pub trait Contract where - T: CustomMsg + DeserializeOwned + Clone + std::fmt::Debug + PartialEq + JsonSchema, + C: CustomMsg, Q: CustomQuery + DeserializeOwned, { + /// Evaluates contract's `execute` entry-point. fn execute( &self, deps: DepsMut, env: Env, info: MessageInfo, msg: Vec, - fork_state: ForkState, - ) -> AnyResult>; + fork_state: ForkState, + ) -> AnyResult>; + /// Evaluates contract's `instantiate` entry-point. fn instantiate( &self, deps: DepsMut, env: Env, info: MessageInfo, msg: Vec, - fork_state: ForkState, - ) -> AnyResult>; + fork_state: ForkState, + ) -> AnyResult>; + /// Evaluates contract's `query` entry-point. fn query( &self, deps: Deps, env: Env, msg: Vec, - fork_state: ForkState, + fork_state: ForkState, ) -> AnyResult; + /// Evaluates contract's `sudo` entry-point. fn sudo( &self, deps: DepsMut, env: Env, msg: Vec, - fork_state: ForkState, - ) -> AnyResult>; + fork_state: ForkState, + ) -> AnyResult>; + /// Evaluates contract's `reply` entry-point. fn reply( &self, deps: DepsMut, env: Env, msg: Reply, - fork_state: ForkState, - ) -> AnyResult>; + fork_state: ForkState, + ) -> AnyResult>; + /// Evaluates contract's `migrate` entry-point. fn migrate( &self, deps: DepsMut, env: Env, msg: Vec, - fork_state: ForkState, - ) -> AnyResult>; + fork_state: ForkState, + ) -> AnyResult>; /// Returns the provided checksum of the contract's Wasm blob. fn checksum(&self) -> Option { @@ -82,20 +82,92 @@ where } } -type ContractFn = - fn(deps: DepsMut, env: Env, info: MessageInfo, msg: T) -> Result, E>; -type PermissionedFn = fn(deps: DepsMut, env: Env, msg: T) -> Result, E>; -type ReplyFn = fn(deps: DepsMut, env: Env, msg: Reply) -> Result, E>; -type QueryFn = fn(deps: Deps, env: Env, msg: T) -> Result; - -type ContractClosure = fn(DepsMut, Env, MessageInfo, T) -> Result, E>; -type PermissionedClosure = fn(DepsMut, Env, T) -> Result, E>; -type ReplyClosure = fn(DepsMut, Env, Reply) -> Result, E>; -type QueryClosure = fn(Deps, Env, T) -> Result; +#[rustfmt::skip] +mod closures { + use super::*; + + // function types + pub type ContractFn = fn(deps: DepsMut, env: Env, info: MessageInfo, msg: T) -> Result, E>; + pub type PermissionedFn = fn(deps: DepsMut, env: Env, msg: T) -> Result, E>; + pub type ReplyFn = fn(deps: DepsMut, env: Env, msg: Reply) -> Result, E>; + pub type QueryFn = fn(deps: Deps, env: Env, msg: T) -> Result; + + // closure types + pub type ContractClosure = Box, Env, MessageInfo, T) -> Result, E>>; + pub type PermissionedClosure = Box, Env, T) -> Result, E>>; + pub type ReplyClosure = Box, Env, Reply) -> Result, E>>; + pub type QueryClosure = Box, Env, T) -> Result>; +} -#[derive(Clone, Copy)] -/// Wraps the exported functions from a contract and provides the normalized format -/// Place T4 and E4 at the end, as we just want default placeholders for most contracts that don't have sudo +use closures::*; + +/// This structure wraps the [Contract] trait implementor +/// and provides generic access to the contract's entry-points. +/// +/// List of generic types used in [ContractWrapper]: +/// - **T1** type of message passed to [execute] entry-point. +/// - **T2** type of message passed to [instantiate] entry-point. +/// - **T3** type of message passed to [query] entry-point. +/// - **T4** type of message passed to [sudo] entry-point. +/// - instead of **~~T5~~**, always the `Reply` type is used in [reply] entry-point. +/// - **T6** type of message passed to [migrate] entry-point. +/// - **E1** type of error returned from [execute] entry-point. +/// - **E2** type of error returned from [instantiate] entry-point. +/// - **E3** type of error returned from [query] entry-point. +/// - **E4** type of error returned from [sudo] entry-point. +/// - **E5** type of error returned from [reply] entry-point. +/// - **E6** type of error returned from [migrate] entry-point. +/// - **C** type of custom message returned from all entry-points except [query]. +/// - **Q** type of custom query in `Querier` passed as 'Deps' or 'DepsMut' to all entry-points. +/// +/// The following table summarizes the purpose of all generic types used in [ContractWrapper]. +/// ```text +/// ┌─────────────┬────────────────┬─────────────────────┬─────────┬─────────┬───────┬───────┐ +/// │ Contract │ Contract │ │ │ │ │ │ +/// │ entry-point │ wrapper │ Closure type │ Message │ Message │ Error │ Query │ +/// │ │ member │ │ IN │ OUT │ OUT │ │ +/// ╞═════════════╪════════════════╪═════════════════════╪═════════╪═════════╪═══════╪═══════╡ +/// │ (1) │ │ │ │ │ │ │ +/// ╞═════════════╪════════════════╪═════════════════════╪═════════╪═════════╪═══════╪═══════╡ +/// │ execute │ execute_fn │ ContractClosure │ T1 │ C │ E1 │ Q │ +/// ├─────────────┼────────────────┼─────────────────────┼─────────┼─────────┼───────┼───────┤ +/// │ instantiate │ instantiate_fn │ ContractClosure │ T2 │ C │ E2 │ Q │ +/// ├─────────────┼────────────────┼─────────────────────┼─────────┼─────────┼───────┼───────┤ +/// │ query │ query_fn │ QueryClosure │ T3 │ Binary │ E3 │ Q │ +/// ├─────────────┼────────────────┼─────────────────────┼─────────┼─────────┼───────┼───────┤ +/// │ sudo │ sudo_fn │ PermissionedClosure │ T4 │ C │ E4 │ Q │ +/// ├─────────────┼────────────────┼─────────────────────┼─────────┼─────────┼───────┼───────┤ +/// │ reply │ reply_fn │ ReplyClosure │ Reply │ C │ E5 │ Q │ +/// ├─────────────┼────────────────┼─────────────────────┼─────────┼─────────┼───────┼───────┤ +/// │ migrate │ migrate_fn │ PermissionedClosure │ T6 │ C │ E6 │ Q │ +/// └─────────────┴────────────────┴─────────────────────┴─────────┴─────────┴───────┴───────┘ +/// ``` +/// The general schema depicting which generic type is used in entry points is shown below. +/// Entry point, when called, is provided minimum two arguments: custom query of type **Q** +/// (inside `Deps` or `DepsMut`) and input message of type **T1**, **T2**, **T3**, **T4**, +/// **Reply** or **T6**. As a result, entry point returns custom output message of type +/// Response<**C**> or **Binary** and an error of type **E1**, **E2**, **E3**, **E4**, **E5** +/// or **E6**. +/// +/// ```text +/// entry_point(query, .., message_in) -> Result +/// ┬ ┬ ┬ ┬ +/// Q >──┘ │ │ └──> E1,E2,E3,E4,E5,E6 +/// T1,T2,T3,T4,Reply,T6 >────┘ └─────────────> C,Binary +/// ``` +/// Generic type **C** defines a custom message that is specific for the **whole blockchain**. +/// Similarly, the generic type **Q** defines a custom query that is also specific +/// to the **whole blockchain**. Other generic types are specific to the implemented contract. +/// So all smart contracts used in the same blockchain will have the same types for **C** and **Q**, +/// but each contract may use different type for other generic types. +/// It means that e.g. **T1** in smart contract `A` may differ from **T1** in smart contract `B`. +/// +/// [execute]: Contract::execute +/// [instantiate]: Contract::instantiate +/// [query]: Contract::query +/// [sudo]: Contract::sudo +/// [reply]: Contract::reply +/// [migrate]: Contract::migrate pub struct ContractWrapper< T1, T2, @@ -106,28 +178,28 @@ pub struct ContractWrapper< C = Empty, Q = Empty, T4 = Empty, - E4 = StdError, - E5 = StdError, + E4 = AnyError, + E5 = AnyError, T6 = Empty, - E6 = StdError, + E6 = AnyError, > where - T1: DeserializeOwned + Debug, - T2: DeserializeOwned, - T3: DeserializeOwned, - T4: DeserializeOwned, - T6: DeserializeOwned, - E1: Display + Debug + Send + Sync + 'static, - E2: Display + Debug + Send + Sync + 'static, - E3: Display + Debug + Send + Sync + 'static, - E4: Display + Debug + Send + Sync + 'static, - E5: Display + Debug + Send + Sync + 'static, - E6: Display + Debug + Send + Sync + 'static, - C: Clone + fmt::Debug + PartialEq + JsonSchema, - Q: CustomQuery + DeserializeOwned + 'static, + T1: DeserializeOwned, // Type of message passed to `execute` entry-point. + T2: DeserializeOwned, // Type of message passed to `instantiate` entry-point. + T3: DeserializeOwned, // Type of message passed to `query` entry-point. + T4: DeserializeOwned, // Type of message passed to `sudo` entry-point. + T6: DeserializeOwned, // Type of message passed to `migrate` entry-point. + E1: Display + Debug + Send + Sync, // Type of error returned from `execute` entry-point. + E2: Display + Debug + Send + Sync, // Type of error returned from `instantiate` entry-point. + E3: Display + Debug + Send + Sync, // Type of error returned from `query` entry-point. + E4: Display + Debug + Send + Sync, // Type of error returned from `sudo` entry-point. + E5: Display + Debug + Send + Sync, // Type of error returned from `reply` entry-point. + E6: Display + Debug + Send + Sync, // Type of error returned from `migrate` entry-point. + C: CustomMsg, // Type of custom message returned from all entry-points except `query`. + Q: CustomQuery + DeserializeOwned, // Type of custom query in querier passed as deps/deps_mut to all entry-points. { execute_fn: ContractClosure, instantiate_fn: ContractClosure, - pub query_fn: QueryClosure, + query_fn: QueryClosure, sudo_fn: Option>, reply_fn: Option>, migrate_fn: Option>, @@ -136,24 +208,43 @@ pub struct ContractWrapper< impl ContractWrapper where - T1: DeserializeOwned + Debug + 'static, - T2: DeserializeOwned + 'static, - T3: DeserializeOwned + 'static, - E1: Display + Debug + Send + Sync + 'static, - E2: Display + Debug + Send + Sync + 'static, - E3: Display + Debug + Send + Sync + 'static, - C: Clone + fmt::Debug + PartialEq + JsonSchema + 'static, - Q: CustomQuery + DeserializeOwned + 'static, + T1: DeserializeOwned + 'static, // Type of message passed to `execute` entry-point. + T2: DeserializeOwned + 'static, // Type of message passed to `instantiate` entry-point. + T3: DeserializeOwned + 'static, // Type of message passed to `query` entry-point. + E1: Display + Debug + Send + Sync + 'static, // Type of error returned from `execute` entry-point. + E2: Display + Debug + Send + Sync + 'static, // Type of error returned from `instantiate` entry-point. + E3: Display + Debug + Send + Sync + 'static, // Type of error returned from `query` entry-point. + C: CustomMsg + 'static, // Type of custom message returned from all entry-points except `query`. + Q: CustomQuery + DeserializeOwned + 'static, // Type of custom query in querier passed as deps/deps_mut to all entry-points. { + /// Creates a new contract wrapper with default settings. pub fn new( execute_fn: ContractFn, instantiate_fn: ContractFn, query_fn: QueryFn, ) -> Self { Self { - execute_fn, - instantiate_fn, - query_fn, + execute_fn: Box::new(execute_fn), + instantiate_fn: Box::new(instantiate_fn), + query_fn: Box::new(query_fn), + sudo_fn: None, + reply_fn: None, + migrate_fn: None, + checksum: None, + } + } + + /// This will take a contract that returns `Response` and will _upgrade_ it + /// to `Response` if needed, to be compatible with a chain-specific extension. + pub fn new_with_empty( + execute_fn: ContractFn, + instantiate_fn: ContractFn, + query_fn: QueryFn, + ) -> Self { + Self { + execute_fn: customize_contract_fn(execute_fn), + instantiate_fn: customize_contract_fn(instantiate_fn), + query_fn: customize_query_fn(query_fn), sudo_fn: None, reply_fn: None, migrate_fn: None, @@ -162,23 +253,25 @@ where } } +#[allow(clippy::type_complexity)] impl ContractWrapper where - T1: DeserializeOwned + Debug + 'static, - T2: DeserializeOwned + 'static, - T3: DeserializeOwned + 'static, - T4: DeserializeOwned + 'static, - T6: DeserializeOwned + 'static, - E1: Display + Debug + Send + Sync + 'static, - E2: Display + Debug + Send + Sync + 'static, - E3: Display + Debug + Send + Sync + 'static, - E4: Display + Debug + Send + Sync + 'static, - E5: Display + Debug + Send + Sync + 'static, - E6: Display + Debug + Send + Sync + 'static, - C: Clone + fmt::Debug + PartialEq + JsonSchema + 'static, - Q: CustomQuery + DeserializeOwned + 'static, + T1: DeserializeOwned, // Type of message passed to `execute` entry-point. + T2: DeserializeOwned, // Type of message passed to `instantiate` entry-point. + T3: DeserializeOwned, // Type of message passed to `query` entry-point. + T4: DeserializeOwned, // Type of message passed to `sudo` entry-point. + T6: DeserializeOwned, // Type of message passed to `migrate` entry-point. + E1: Display + Debug + Send + Sync, // Type of error returned from `execute` entry-point. + E2: Display + Debug + Send + Sync, // Type of error returned from `instantiate` entry-point. + E3: Display + Debug + Send + Sync, // Type of error returned from `query` entry-point. + E4: Display + Debug + Send + Sync, // Type of error returned from `sudo` entry-point. + E5: Display + Debug + Send + Sync, // Type of error returned from `reply` entry-point. + E6: Display + Debug + Send + Sync, // Type of error returned from `migrate` entry-point. + C: CustomMsg + 'static, // Type of custom message returned from all entry-points except `query`. + Q: CustomQuery + DeserializeOwned + 'static, // Type of custom query in querier passed as deps/deps_mut to all entry-points. { + /// Populates [ContractWrapper] with contract's `sudo` entry-point and custom message type. pub fn with_sudo( self, sudo_fn: PermissionedFn, @@ -191,13 +284,34 @@ where execute_fn: self.execute_fn, instantiate_fn: self.instantiate_fn, query_fn: self.query_fn, - sudo_fn: Some(sudo_fn), + sudo_fn: Some(Box::new(sudo_fn)), reply_fn: self.reply_fn, migrate_fn: self.migrate_fn, checksum: None, } } + /// Populates [ContractWrapper] with contract's `sudo` entry-point and `Empty` as a custom message. + pub fn with_sudo_empty( + self, + sudo_fn: PermissionedFn, + ) -> ContractWrapper + where + T4A: DeserializeOwned + 'static, + E4A: Display + Debug + Send + Sync + 'static, + { + ContractWrapper { + execute_fn: self.execute_fn, + instantiate_fn: self.instantiate_fn, + query_fn: self.query_fn, + sudo_fn: Some(customize_permissioned_fn(sudo_fn)), + reply_fn: self.reply_fn, + migrate_fn: self.migrate_fn, + checksum: None, + } + } + + /// Populates [ContractWrapper] with contract's `reply` entry-point and custom message type. pub fn with_reply( self, reply_fn: ReplyFn, @@ -210,12 +324,32 @@ where instantiate_fn: self.instantiate_fn, query_fn: self.query_fn, sudo_fn: self.sudo_fn, - reply_fn: Some(reply_fn), + reply_fn: Some(Box::new(reply_fn)), migrate_fn: self.migrate_fn, checksum: None, } } + /// Populates [ContractWrapper] with contract's `reply` entry-point and `Empty` as a custom message. + pub fn with_reply_empty( + self, + reply_fn: ReplyFn, + ) -> ContractWrapper + where + E5A: Display + Debug + Send + Sync + 'static, + { + ContractWrapper { + execute_fn: self.execute_fn, + instantiate_fn: self.instantiate_fn, + query_fn: self.query_fn, + sudo_fn: self.sudo_fn, + reply_fn: Some(customize_permissioned_fn(reply_fn)), + migrate_fn: self.migrate_fn, + checksum: None, + } + } + + /// Populates [ContractWrapper] with contract's `migrate` entry-point and custom message type. pub fn with_migrate( self, migrate_fn: PermissionedFn, @@ -230,10 +364,31 @@ where query_fn: self.query_fn, sudo_fn: self.sudo_fn, reply_fn: self.reply_fn, - migrate_fn: Some(migrate_fn), + migrate_fn: Some(Box::new(migrate_fn)), + checksum: None, + } + } + + /// Populates [ContractWrapper] with contract's `migrate` entry-point and `Empty` as a custom message. + pub fn with_migrate_empty( + self, + migrate_fn: PermissionedFn, + ) -> ContractWrapper + where + T6A: DeserializeOwned + 'static, + E6A: Display + Debug + Send + Sync + 'static, + { + ContractWrapper { + execute_fn: self.execute_fn, + instantiate_fn: self.instantiate_fn, + query_fn: self.query_fn, + sudo_fn: self.sudo_fn, + reply_fn: self.reply_fn, + migrate_fn: Some(customize_permissioned_fn(migrate_fn)), checksum: None, } } + /// Populates [ContractWrapper] with the provided checksum of the contract's Wasm blob. pub fn with_checksum(mut self, checksum: Checksum) -> Self { self.checksum = Some(checksum); @@ -241,23 +396,138 @@ where } } +fn customize_contract_fn( + raw_fn: ContractFn, +) -> ContractClosure +where + T: DeserializeOwned + 'static, + E: Display + Debug + Send + Sync + 'static, + C: CustomMsg, + Q: CustomQuery + DeserializeOwned, +{ + Box::new( + move |mut deps: DepsMut, + env: Env, + info: MessageInfo, + msg: T| + -> Result, E> { + let deps = decustomize_deps_mut(&mut deps); + raw_fn(deps, env, info, msg).map(customize_response::) + }, + ) +} + +fn customize_query_fn(raw_fn: QueryFn) -> QueryClosure +where + T: DeserializeOwned + 'static, + E: Display + Debug + Send + Sync + 'static, + Q: CustomQuery + DeserializeOwned, +{ + Box::new( + move |deps: Deps, env: Env, msg: T| -> Result { + let deps = decustomize_deps(&deps); + raw_fn(deps, env, msg) + }, + ) +} + +fn customize_permissioned_fn( + raw_fn: PermissionedFn, +) -> PermissionedClosure +where + T: DeserializeOwned + 'static, + E: Display + Debug + Send + Sync + 'static, + C: CustomMsg, + Q: CustomQuery + DeserializeOwned, +{ + Box::new( + move |mut deps: DepsMut, env: Env, msg: T| -> Result, E> { + let deps = decustomize_deps_mut(&mut deps); + raw_fn(deps, env, msg).map(customize_response::) + }, + ) +} + +fn decustomize_deps_mut<'a, Q>(deps: &'a mut DepsMut) -> DepsMut<'a, Empty> +where + Q: CustomQuery + DeserializeOwned, +{ + DepsMut { + storage: deps.storage, + api: deps.api, + querier: QuerierWrapper::new(deps.querier.deref()), + } +} + +fn decustomize_deps<'a, Q>(deps: &'a Deps<'a, Q>) -> Deps<'a, Empty> +where + Q: CustomQuery + DeserializeOwned, +{ + Deps { + storage: deps.storage, + api: deps.api, + querier: QuerierWrapper::new(deps.querier.deref()), + } +} + +fn customize_response(resp: Response) -> Response +where + C: CustomMsg, +{ + let mut customized_resp = Response::::new() + .add_submessages(resp.messages.into_iter().map(customize_msg::)) + .add_events(resp.events) + .add_attributes(resp.attributes); + customized_resp.data = resp.data; + customized_resp +} + +fn customize_msg(msg: SubMsg) -> SubMsg +where + C: CustomMsg, +{ + SubMsg { + id: msg.id, + payload: Binary::default(), + msg: match msg.msg { + CosmosMsg::Wasm(wasm) => CosmosMsg::Wasm(wasm), + CosmosMsg::Bank(bank) => CosmosMsg::Bank(bank), + #[cfg(feature = "staking")] + CosmosMsg::Staking(staking) => CosmosMsg::Staking(staking), + #[cfg(feature = "staking")] + CosmosMsg::Distribution(distribution) => CosmosMsg::Distribution(distribution), + CosmosMsg::Custom(_) => unreachable!(), + #[cfg(feature = "stargate")] + CosmosMsg::Ibc(ibc) => CosmosMsg::Ibc(ibc), + #[cfg(feature = "cosmwasm_2_0")] + CosmosMsg::Any(any) => CosmosMsg::Any(any), + _ => panic!("unknown message variant {:?}", msg), + }, + gas_limit: msg.gas_limit, + reply_on: msg.reply_on, + } +} + impl Contract for ContractWrapper where - T1: DeserializeOwned + Debug + Clone, - T2: DeserializeOwned + Debug + Clone, - T3: DeserializeOwned + Debug + Clone, - T4: DeserializeOwned, - T6: DeserializeOwned, - E1: Display + Debug + Send + Sync + Error + 'static, - E2: Display + Debug + Send + Sync + Error + 'static, - E3: Display + Debug + Send + Sync + Error + 'static, - E4: Display + Debug + Send + Sync + 'static, - E5: Display + Debug + Send + Sync + 'static, - E6: Display + Debug + Send + Sync + 'static, - C: CustomMsg + DeserializeOwned + Clone + fmt::Debug + PartialEq + JsonSchema, - Q: CustomQuery + DeserializeOwned, + T1: DeserializeOwned, // Type of message passed to `execute` entry-point. + T2: DeserializeOwned, // Type of message passed to `instantiate` entry-point. + T3: DeserializeOwned, // Type of message passed to `query` entry-point. + T4: DeserializeOwned, // Type of message passed to `sudo` entry-point. + T6: DeserializeOwned, // Type of message passed to `migrate` entry-point. + E1: Display + Debug + Send + Sync + 'static, // Type of error returned from `execute` entry-point. + E2: Display + Debug + Send + Sync + 'static, // Type of error returned from `instantiate` entry-point. + E3: Display + Debug + Send + Sync + 'static, // Type of error returned from `query` entry-point. + E4: Display + Debug + Send + Sync + 'static, // Type of error returned from `sudo` entry-point. + E5: Display + Debug + Send + Sync + 'static, // Type of error returned from `reply` entry-point. + E6: Display + Debug + Send + Sync + 'static, // Type of error returned from `migrate` entry-point. + C: CustomMsg + DeserializeOwned, // Type of custom message returned from all entry-points except `query`. + Q: CustomQuery + DeserializeOwned, // Type of custom query in querier passed as deps/deps_mut to all entry-points. { + /// Calls [execute] on wrapped [Contract] trait implementor. + /// + /// [execute]: Contract::execute fn execute( &self, deps: DepsMut, @@ -279,9 +549,12 @@ where }; let msg: T1 = from_json(msg)?; - (self.execute_fn)(deps, env, info, msg).map_err(|err| anyhow!(err)) + (self.execute_fn)(deps, env, info, msg).map_err(|err: E1| anyhow!(err)) } + /// Calls [instantiate] on wrapped [Contract] trait implementor. + /// + /// [instantiate]: Contract::instantiate fn instantiate( &self, deps: DepsMut, @@ -302,9 +575,12 @@ where querier: QuerierWrapper::new(&querier), }; let msg: T2 = from_json(msg)?; - (self.instantiate_fn)(deps, env, info, msg).map_err(|err| anyhow!(err)) + (self.instantiate_fn)(deps, env, info, msg).map_err(|err: E2| anyhow!(err)) } + /// Calls [query] on wrapped [Contract] trait implementor. + /// + /// [query]: Contract::query fn query( &self, deps: Deps, @@ -324,10 +600,13 @@ where querier: QuerierWrapper::new(&querier), }; let msg: T3 = from_json(msg)?; - (self.query_fn)(deps, env, msg).map_err(|err| anyhow!(err)) + (self.query_fn)(deps, env, msg).map_err(|err: E3| anyhow!(err)) } - // this returns an error if the contract doesn't implement sudo + /// Calls [sudo] on wrapped [Contract] trait implementor. + /// Returns an error when the contract does not implement [sudo]. + /// + /// [sudo]: Contract::sudo fn sudo( &self, deps: DepsMut, @@ -348,12 +627,15 @@ where }; let msg = from_json(msg)?; match &self.sudo_fn { - Some(sudo) => sudo(deps, env, msg).map_err(|err| anyhow!(err)), - None => bail!("sudo not implemented for contract"), + Some(sudo) => sudo(deps, env, msg).map_err(|err: E4| anyhow!(err)), + None => bail!("sudo is not implemented for contract"), } } - // this returns an error if the contract doesn't implement reply + /// Calls [reply] on wrapped [Contract] trait implementor. + /// Returns an error when the contract does not implement [reply]. + /// + /// [reply]: Contract::reply fn reply( &self, deps: DepsMut, @@ -361,6 +643,7 @@ where reply_data: Reply, fork_state: ForkState, ) -> AnyResult> { + let msg: Reply = reply_data; let querier = MockQuerier::new(fork_state.clone()); let mut storage = DualStorage::new( fork_state.remote, @@ -373,12 +656,15 @@ where querier: QuerierWrapper::new(&querier), }; match &self.reply_fn { - Some(reply) => reply(deps, env, reply_data).map_err(|err| anyhow!(err)), - None => bail!("reply not implemented for contract"), + Some(reply) => reply(deps, env, msg).map_err(|err: E5| anyhow!(err)), + None => bail!("reply is not implemented for contract"), } } - // this returns an error if the contract doesn't implement migrate + /// Calls [migrate] on wrapped [Contract] trait implementor. + /// Returns an error when the contract does not implement [migrate]. + /// + /// [migrate]: Contract::migrate fn migrate( &self, deps: DepsMut, @@ -399,8 +685,8 @@ where }; let msg = from_json(msg)?; match &self.migrate_fn { - Some(migrate) => migrate(deps, env, msg).map_err(|err| anyhow!(err)), - None => bail!("migrate not implemented for contract"), + Some(migrate) => migrate(deps, env, msg).map_err(|err: E6| anyhow!(err)), + None => bail!("migrate is not implemented for contract"), } } @@ -409,55 +695,3 @@ where self.checksum } } - -#[cfg(test)] -pub mod test { - - use cosmwasm_std::{ - testing::{message_info, mock_dependencies, mock_env}, - to_json_binary, Addr, Binary, Deps, DepsMut, Empty, Env, MessageInfo, Response, StdResult, - }; - - use super::ContractWrapper; - - fn execute(_deps: DepsMut, _env: Env, _info: MessageInfo, _msg: Empty) -> StdResult { - Ok(Response::new()) - } - - fn query(_deps: Deps, _env: Env, _msg: Empty) -> StdResult { - to_json_binary("resp") - } - - fn instantiate( - _deps: DepsMut, - _env: Env, - _info: MessageInfo, - _msg: Empty, - ) -> StdResult { - Ok(Response::new()) - } - - #[test] - fn mock_contract() -> anyhow::Result<()> { - let contract = ContractWrapper::new(execute, instantiate, query); - - let clone = contract.execute_fn; - let second_clone = clone; - - clone( - mock_dependencies().as_mut(), - mock_env(), - message_info(&Addr::unchecked("sender"), &[]), - Empty {}, - )?; - - second_clone( - mock_dependencies().as_mut(), - mock_env(), - message_info(&Addr::unchecked("sender"), &[]), - Empty {}, - )?; - - Ok(()) - } -} diff --git a/src/custom_handler.rs b/src/custom_handler.rs index cb634b59..a9b0da2b 100644 --- a/src/custom_handler.rs +++ b/src/custom_handler.rs @@ -1,58 +1,91 @@ +//! # Custom message and query handler + use crate::app::CosmosRouter; use crate::error::{bail, AnyResult}; use crate::{AppResponse, Module}; use cosmwasm_std::{Addr, Api, Binary, BlockInfo, Empty, Querier, Storage}; -use derivative::Derivative; use std::cell::{Ref, RefCell}; use std::ops::Deref; use std::rc::Rc; -/// Internal state of `CachingCustomHandler` wrapping internal mutability so it is not exposed to -/// user. Those have to be shared internal state, as after mock is passed to app it is not -/// possible to access mock internals which are not exposed by API. -#[derive(Derivative)] -#[derivative(Default(bound = "", new = "true"), Clone(bound = ""))] -pub struct CachingCustomHandlerState { +/// A cache for messages and queries processes by the custom module. +#[derive(Default, Clone)] +pub struct CachingCustomHandlerState +where + ExecC: Default + Clone, + QueryC: Default + Clone, +{ + /// Cache for processes custom messages. execs: Rc>>, + /// Cache for processed custom queries. queries: Rc>>, } -impl CachingCustomHandlerState { +impl CachingCustomHandlerState +where + ExecC: Default + Clone, + QueryC: Default + Clone, +{ + /// Creates a new [CachingCustomHandlerState]. + pub fn new() -> Self { + Default::default() + } + + /// Returns a slice of processed custom messages. pub fn execs(&self) -> impl Deref + '_ { Ref::map(self.execs.borrow(), Vec::as_slice) } + /// Returns a slice of processed custom queries. pub fn queries(&self) -> impl Deref + '_ { Ref::map(self.queries.borrow(), Vec::as_slice) } + /// Clears the cache. pub fn reset(&self) { self.execs.borrow_mut().clear(); self.queries.borrow_mut().clear(); } } -/// Custom handler storing all the messages it received, so they can be later verified. -/// State is thin shared state, so it can be hold after mock is passed to App to read state. -#[derive(Clone, Derivative)] -#[derivative(Default(bound = "", new = "true"))] -pub struct CachingCustomHandler { +/// Custom handler that stores all received messages and queries. +/// +/// State is thin shared state, so it can be held after mock is passed to [App](crate::App) to read state. +#[derive(Default, Clone)] +pub struct CachingCustomHandler +where + ExecC: Default + Clone, + QueryC: Default + Clone, +{ + /// Cached state. state: CachingCustomHandlerState, } -impl CachingCustomHandler { +impl CachingCustomHandler +where + ExecC: Default + Clone, + QueryC: Default + Clone, +{ + /// Creates a new [CachingCustomHandler]. + pub fn new() -> Self { + Default::default() + } + + /// Returns the cached state. pub fn state(&self) -> CachingCustomHandlerState { self.state.clone() } } -impl Module for CachingCustomHandler { +impl Module for CachingCustomHandler +where + Exec: Default + Clone, + Query: Default + Clone, +{ type ExecT = Exec; type QueryT = Query; type SudoT = Empty; - // TODO: how to assert - // where ExecC: Exec, QueryC: Query fn execute( &self, _api: &dyn Api, @@ -66,17 +99,6 @@ impl Module for CachingCustomHandler { Ok(AppResponse::default()) } - fn sudo( - &self, - _api: &dyn Api, - _storage: &mut dyn Storage, - _router: &dyn CosmosRouter, - _block: &BlockInfo, - msg: Self::SudoT, - ) -> AnyResult { - bail!("Unexpected sudo msg {:?}", msg) - } - fn query( &self, _api: &dyn Api, @@ -88,4 +110,15 @@ impl Module for CachingCustomHandler { self.state.queries.borrow_mut().push(request); Ok(Binary::default()) } + + fn sudo( + &self, + _api: &dyn Api, + _storage: &mut dyn Storage, + _router: &dyn CosmosRouter, + _block: &BlockInfo, + msg: Self::SudoT, + ) -> AnyResult { + bail!("Unexpected custom sudo message {:?}", msg) + } } diff --git a/src/error.rs b/src/error.rs index 289a7949..b5c3c09a 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,59 +1,112 @@ +//! # Error definitions + pub use anyhow::{anyhow, bail, Context as AnyContext, Error as AnyError, Result as AnyResult}; use cosmwasm_std::{WasmMsg, WasmQuery}; use thiserror::Error; +/// An enumeration of errors reported across the **CosmWasm MultiTest** library. #[derive(Debug, Error, PartialEq, Eq)] pub enum Error { - #[error("Empty attribute key. Value: {value}")] - EmptyAttributeKey { value: String }, + /// Error variant for reporting an empty attribute key. + #[error("Empty attribute key. Value: {0}")] + EmptyAttributeKey(String), - #[error("Empty attribute value. Key: {key}")] - EmptyAttributeValue { key: String }, + /// Error variant for reporting an empty attribute value. + #[deprecated(note = "This error is not reported anymore. Will be removed in next release.")] + #[error("Empty attribute value. Key: {0}")] + EmptyAttributeValue(String), + /// Error variant for reporting a usage of reserved key prefix. #[error("Attribute key starts with reserved prefix _: {0}")] ReservedAttributeKey(String), + /// Error variant for reporting too short event types. #[error("Event type too short: {0}")] EventTypeTooShort(String), + /// Error variant for reporting that unsupported wasm query was encountered during processing. #[error("Unsupported wasm query: {0:?}")] UnsupportedWasmQuery(WasmQuery), + /// Error variant for reporting that unsupported wasm message was encountered during processing. #[error("Unsupported wasm message: {0:?}")] UnsupportedWasmMsg(WasmMsg), + /// Error variant for reporting invalid contract code. #[error("code id: invalid")] InvalidCodeId, + /// Error variant for reporting unregistered contract code. #[error("code id {0}: no such code")] UnregisteredCodeId(u64), + /// Error variant for reporting duplicated contract code identifier. + #[error("duplicated code id {0}")] + DuplicatedCodeId(u64), + + /// Error variant for reporting a situation when no more contract code identifiers are available. + #[error("no more code identifiers available")] + NoMoreCodeIdAvailable, + + /// Error variant for reporting duplicated contract addresses. #[error("Contract with this address already exists: {0}")] DuplicatedContractAddress(String), - - #[error("Unregistered contract address, not present locally or on-chain")] - UnregisteredContractAddress(String), } impl Error { + /// Creates an instance of the [Error](Self) for empty attribute key. pub fn empty_attribute_key(value: impl Into) -> Self { - Self::EmptyAttributeKey { - value: value.into(), - } + Self::EmptyAttributeKey(value.into()) } + #[deprecated(note = "This error is not reported anymore. Will be removed in next release.")] + /// Creates an instance of the [Error](Self) for empty attribute value. pub fn empty_attribute_value(key: impl Into) -> Self { - Self::EmptyAttributeValue { key: key.into() } + #[allow(deprecated)] + Self::EmptyAttributeValue(key.into()) } + /// Creates an instance of the [Error](Self) when reserved attribute key was used. pub fn reserved_attribute_key(key: impl Into) -> Self { Self::ReservedAttributeKey(key.into()) } + /// Creates an instance of the [Error](Self) for too short event types. pub fn event_type_too_short(ty: impl Into) -> Self { Self::EventTypeTooShort(ty.into()) } + /// Creates an instance of the [Error](Self) for unsupported wasm queries. + pub fn unsupported_wasm_query(query: WasmQuery) -> Self { + Self::UnsupportedWasmQuery(query) + } + + /// Creates an instance of the [Error](Self) for unsupported wasm messages. + pub fn unsupported_wasm_message(msg: WasmMsg) -> Self { + Self::UnsupportedWasmMsg(msg) + } + + /// Creates an instance of the [Error](Self) for invalid contract code identifier. + pub fn invalid_code_id() -> Self { + Self::InvalidCodeId + } + + /// Creates an instance of the [Error](Self) for unregistered contract code identifier. + pub fn unregistered_code_id(code_id: u64) -> Self { + Self::UnregisteredCodeId(code_id) + } + + /// Creates an instance of the [Error](Self) for duplicated contract code identifier. + pub fn duplicated_code_id(code_id: u64) -> Self { + Self::DuplicatedCodeId(code_id) + } + + /// Creates an instance of the [Error](Self) for exhausted contract code identifiers. + pub fn no_more_code_id_available() -> Self { + Self::NoMoreCodeIdAvailable + } + + /// Creates an instance of the [Error](Self) for duplicated contract addresses. pub fn duplicated_contract_address(address: impl Into) -> Self { Self::DuplicatedContractAddress(address.into()) } diff --git a/src/executor.rs b/src/executor.rs index 492be9bb..5269276b 100644 --- a/src/executor.rs +++ b/src/executor.rs @@ -1,30 +1,35 @@ use crate::error::AnyResult; use cosmwasm_std::{ - to_json_binary, Addr, Attribute, BankMsg, Binary, Coin, CosmosMsg, Event, SubMsgResponse, - WasmMsg, + to_json_binary, Addr, Attribute, BankMsg, Binary, Coin, CosmosMsg, CustomMsg, Event, + SubMsgResponse, WasmMsg, }; use cw_utils::{parse_execute_response_data, parse_instantiate_response_data}; -use schemars::JsonSchema; use serde::Serialize; use std::fmt::Debug; +/// A subset of data returned as a response of a contract entry point, +/// such as `instantiate`, `execute` or `migrate`. #[derive(Default, Clone, Debug)] pub struct AppResponse { + /// Response events. pub events: Vec, + /// Response data. pub data: Option, } impl AppResponse { - // Return all custom attributes returned by the contract in the `idx` event. - // We assert the type is wasm, and skip the contract_address attribute. + /// Returns all custom attributes returned by the contract in the `idx` event. + /// + /// We assert the type is wasm, and skip the contract_address attribute. #[track_caller] pub fn custom_attrs(&self, idx: usize) -> &[Attribute] { assert_eq!(self.events[idx].ty.as_str(), "wasm"); &self.events[idx].attributes[1..] } - /// Check if there is an Event that is a super-set of this. - /// It has the same type, and all compare.attributes are included in it as well. + /// Checks if there is an Event that is a super-set of this. + /// + /// It has the same type, and all compared attributes are included in it as well. /// You don't need to specify them all. pub fn has_event(&self, expected: &Event) -> bool { self.events.iter().any(|ev| { @@ -36,7 +41,7 @@ impl AppResponse { }) } - /// Like has_event but panics if no match + /// Like [has_event](Self::has_event) but panics if there is no match. #[track_caller] pub fn assert_event(&self, expected: &Event) { assert!( @@ -48,7 +53,7 @@ impl AppResponse { } } -/// They have the same shape, SubMsgExecutionResponse is what is returned in reply. +/// They have the same shape, SubMsgResponse is what is returned in reply. /// This is just to make some test cases easier. impl From for AppResponse { fn from(reply: SubMsgResponse) -> Self { @@ -59,14 +64,19 @@ impl From for AppResponse { } } } - +/// A trait defining a default behavior of the message executor. +/// +/// Defines the interface for executing transactions and contract interactions. +/// It is a central component in the testing framework, managing the operational +/// flow and ensuring that contract _calls_ are processed correctly. pub trait Executor where - C: Clone + Debug + PartialEq + JsonSchema + 'static, + C: CustomMsg + 'static, { - /// 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. + /// Processes (executes) an 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. fn execute(&mut self, sender: Addr, msg: CosmosMsg) -> AnyResult; /// Create a contract and get the new address. @@ -97,6 +107,7 @@ where /// Instantiates a new contract and returns its predictable address. /// This is a helper function around [execute][Self::execute] function /// with `WasmMsg::Instantiate2` message. + #[cfg(feature = "cosmwasm_1_2")] fn instantiate2_contract( &mut self, code_id: u64, @@ -128,8 +139,9 @@ where } /// Execute a contract and process all returned messages. - /// This is just a helper around execute(), - /// but we parse out the data field to that what is returned by the contract (not the protobuf wrapper) + /// This is just a helper function around [execute()](Self::execute) + /// with `WasmMsg::Execute` message, but in this case we parse out the data field + /// to that what is returned by the contract (not the protobuf wrapper). fn execute_contract( &mut self, sender: Addr, @@ -150,8 +162,10 @@ where Ok(res) } - /// Migrate a contract. Sender must be registered admin. - /// This is just a helper around execute() + /// Migrates a contract. + /// Sender must be registered admin. + /// This is just a helper function around [execute()](Self::execute) + /// with `WasmMsg::Migrate` message. fn migrate_contract( &mut self, sender: Addr, @@ -168,6 +182,9 @@ where self.execute(sender, msg.into()) } + /// Sends tokens to specified recipient. + /// This is just a helper function around [execute()](Self::execute) + /// with `BankMsg::Send` message. fn send_tokens( &mut self, sender: Addr, diff --git a/src/featured.rs b/src/featured.rs new file mode 100644 index 00000000..9d916182 --- /dev/null +++ b/src/featured.rs @@ -0,0 +1,43 @@ +//! # Definitions enabled or disabled by crate's features + +#[cfg(feature = "stargate")] +pub use cosmwasm_std::GovMsg; + +#[cfg(not(feature = "stargate"))] +pub use cosmwasm_std::Empty as GovMsg; + +#[cfg(feature = "staking")] +pub mod staking { + pub use crate::staking::{Distribution, DistributionKeeper, StakeKeeper, Staking, StakingSudo}; +} + +#[cfg(not(feature = "staking"))] +pub mod staking { + use crate::error::AnyResult; + use crate::{AppResponse, CosmosRouter, FailingModule, Module}; + use cosmwasm_std::{Api, BlockInfo, CustomMsg, CustomQuery, Empty, Storage}; + + pub enum StakingSudo {} + + pub trait Staking: Module { + fn process_queue( + &self, + _api: &dyn Api, + _storage: &mut dyn Storage, + _router: &dyn CosmosRouter, + _block: &BlockInfo, + ) -> AnyResult { + Ok(AppResponse::default()) + } + } + + pub type StakeKeeper = FailingModule; + + impl Staking for StakeKeeper {} + + pub trait Distribution: Module {} + + pub type DistributionKeeper = FailingModule; + + impl Distribution for DistributionKeeper {} +} diff --git a/src/gov.rs b/src/gov.rs index 286333a7..9d34b59b 100644 --- a/src/gov.rs +++ b/src/gov.rs @@ -1,12 +1,16 @@ +use crate::featured::GovMsg; use crate::{AcceptingModule, FailingModule, Module}; -use cosmwasm_std::{Empty, GovMsg}; +use cosmwasm_std::Empty; +/// This trait implements the interface of the governance module. pub trait Gov: Module {} +/// Implementation of the always accepting governance module. pub type GovAcceptingModule = AcceptingModule; impl Gov for GovAcceptingModule {} +/// Implementation of the always failing governance module. pub type GovFailingModule = FailingModule; impl Gov for GovFailingModule {} diff --git a/src/ibc.rs b/src/ibc.rs index 97e5b026..32236c34 100644 --- a/src/ibc.rs +++ b/src/ibc.rs @@ -1,12 +1,15 @@ use crate::{AcceptingModule, FailingModule, Module}; use cosmwasm_std::{Empty, IbcMsg, IbcQuery}; +/// This trait implements the interface for IBC functionalities. pub trait Ibc: Module {} +/// Implementation of the always accepting IBC module. pub type IbcAcceptingModule = AcceptingModule; impl Ibc for IbcAcceptingModule {} +/// implementation of the always failing IBC module. pub type IbcFailingModule = FailingModule; impl Ibc for IbcFailingModule {} diff --git a/src/lib.rs b/src/lib.rs index 99a926b7..4923842b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,34 +1,164 @@ -//! Multitest is a design to simulate a blockchain environment in pure Rust. -//! This allows us to run unit tests that involve contract -> contract, -//! and contract -> bank interactions. This is not intended to be a full blockchain app -//! but to simulate the Cosmos SDK x/wasm module close enough to gain confidence in -//! multi-contract deployments before testing them on a live blockchain. +//! # CosmWasm MultiTest //! -//! To understand the design of this module, please refer to `../DESIGN.md` +//! **CosmWasm MultiTest** is designed to simulate a blockchain environment in pure Rust. +//! This allows to run unit tests that involve **contract 🡘 contract**, +//! and **contract 🡘 module** interactions. **CosmWasm MultiTest** is not intended +//! to be a full blockchain application, but to simulate the Cosmos SDK x/wasm module close enough +//! to gain confidence in multi-contract deployments, before testing them on a live blockchain. +//! +//! The following sections explains some of the design for those who want to use the API, +//! as well as those who want to take a look under the hood of **CosmWasm MultiTest**. +//! +//! ## Key APIs +//! +//! ### App +//! +//! The main entry point to the system is called [App], which represents a blockchain application. +//! It maintains an idea of block height and time, which can be updated to simulate multiple +//! blocks. You can use application's [update_block](App::update_block) method to increment +//! the timestamp by 5 seconds and the height by 1 (simulating a new block) or you can write +//! any other mutator of [BlockInfo](cosmwasm_std::BlockInfo) to advance more. +//! +//! [App] exposes an entry point [execute](App::execute) that allows to execute +//! any [CosmosMsg](cosmwasm_std::CosmosMsg) and wraps it in an atomic transaction. +//! That is, only if [execute](App::execute) returns a success, then the state will be committed. +//! It returns the data and a list of [Event](cosmwasm_std::Event)s on successful execution +//! or an **`Err(String)`** on error. There are some helper methods tied to the [Executor] trait +//! that create the [CosmosMsg](cosmwasm_std::CosmosMsg) for you to provide a less verbose API. +//! [App]'s methods like [instantiate_contract](App::instantiate_contract), +//! [execute_contract](App::execute_contract), and [send_tokens](App::send_tokens) are exposed +//! for your convenience in writing tests. +//! Each method executes one [CosmosMsg](cosmwasm_std::CosmosMsg) atomically, as if it was submitted by a user. +//! You can also use [execute_multi](App::execute_multi) if you wish to execute multiple messages together +//! that revert the state as a whole in case of any failure. +//! +//! The other key entry point to [App] is the [Querier](cosmwasm_std::Querier) interface that it implements. +//! In particular, you can use [wrap](App::wrap) to get a [QuerierWrapper](cosmwasm_std::QuerierWrapper), +//! which provides all kinds of interesting APIs to query the blockchain, like +//! [query_all_balances](cosmwasm_std::QuerierWrapper::query_all_balances) and +//! [query_wasm_smart](cosmwasm_std::QuerierWrapper::query_wasm_smart). +//! Putting this all together, you have one [Storage](cosmwasm_std::Storage) wrapped into an application, +//! where you can execute contracts and bank, query them easily, and update the current +//! [BlockInfo](cosmwasm_std::BlockInfo), in an API that is not very verbose or cumbersome. +//! Under the hood it will process all messages returned from contracts, move _bank_ tokens +//! and call into other contracts. +//! +//! You can easily create an [App] for use in your testcode like shown below. +//! Having a single utility function for creating and configuring the [App] is the common +//! pattern while testing contracts with **CosmWasm MultiTest**. +//! +//! ``` +//! use cw_multi_test::App; +//! +//! fn mock_app() -> App { +//! App::default() +//! } +//! ``` +//! +//! The [App] maintains the root [Storage](cosmwasm_std::Storage), and the [BlockInfo](cosmwasm_std::BlockInfo) +//! for the current block. It also contains a [Router] (discussed below), which can process +//! any [CosmosMsg](cosmwasm_std::CosmosMsg) variant by passing it to the proper keeper. +//! +//! > **Note**: [App] properly handles submessages and reply blocks. +//! +//! > **Note**: While the API currently supports custom messages, we don't currently have an implementation +//! > of the default keeper, except of experimental [CachingCustomHandler](custom_handler::CachingCustomHandler). +//! +//! ### Contracts +//! +//! Before you can call contracts, you must **instantiate** them. And to instantiate them, you need a `code_id`. +//! In `wasmd`, this `code_id` points to some stored Wasm code that is then run. In multitest, we use it to +//! point to a `Box` that should be run. That is, you need to implement the [Contract] trait +//! and then add the contract to the [App] via [store_code](App::store_code) function. +//! +//! The [Contract] trait defines the major entry points to any CosmWasm contract: +//! [instantiate](Contract::instantiate), [execute](Contract::execute), [query](Contract::query), +//! [sudo](Contract::sudo), [reply](Contract::reply) (for submessages) and [migrate](Contract::migrate). +//! +//! In order to easily implement [Contract] from some existing contract code, we use the [ContractWrapper] struct, +//! which takes some function pointers and combines them. You can take a look at **test_helpers** module +//! for some examples or how to do so (and useful mocks for some test cases). +//! Here is an example of wrapping a CosmWasm contract into a [Contract] trait to be added to an [App]: +//! +//! ```ignore +//! use cosmwasm_std::Empty; +//! use cw1_whitelist::contract::{execute, instantiate, query}; +//! use cw_multi_test::{App, Contract, ContractWrapper}; +//! +//! pub fn contract_whitelist() -> Box> { +//! Box::new(ContractWrapper::new(execute, instantiate, query)) +//! } +//! +//! let mut app = App::default(); +//! let code_id = app.store_code(contract_whitelist()); +//! // use this code_id to instantiate a contract +//! ``` +//! +//! ### Modules +//! +//! There is only one root [Storage](cosmwasm_std::Storage), stored inside [App]. +//! This is wrapped into a transaction, and then passed down to other functions to work with. +//! The code that modifies the Storage is divided into _modules_ much like the CosmosSDK. +//! Currently, the message processing logic is divided into one _module_ for every [CosmosMsg](cosmwasm_std) variant. +//! [Bank] handles [BankMsg](cosmwasm_std::BankMsg) and [BankQuery](cosmwasm_std::BankQuery), +//! [Wasm] handles [WasmMsg](cosmwasm_std::WasmMsg) and [WasmQuery](cosmwasm_std::WasmQuery), etc. +//! +//! ### Router +//! +//! The [Router] groups all modules in the system into one "macro-module" that can handle +//! any [CosmosMsg](cosmwasm_std::CosmosMsg). While [Bank] handles [BankMsg](cosmwasm_std::BankMsg), +//! and [Wasm] handles [WasmMsg](cosmwasm_std::WasmMsg), we need to combine them into a larger composite +//! to let them process messages from [App]. This is the whole concept of the [Router]. +//! If you take a look at the [execute](Router::execute) method, you will see it is quite straightforward. +//! +//! Note that the only way one module can call or query another module is by dispatching messages via the [Router]. +//! This allows us to implement an independent [Wasm] in a way that it can process [SubMsg](cosmwasm_std::SubMsg) +//! that call into [Bank]. You can see an example of that in _send_ method of the [WasmKeeper], +//! where it moves bank tokens from one account to another. +//! +//! ### Addons +//! +//! (tbd) + +// #![deny(missing_docs)] +#![deny(rustdoc::broken_intra_doc_links)] +#![deny(rustdoc::missing_crate_level_docs)] -pub mod addons; mod addresses; +mod api; mod app; mod app_builder; mod bank; mod checksums; -#[allow(clippy::type_complexity)] mod contracts; pub mod custom_handler; pub mod error; mod executor; +mod featured; mod gov; mod ibc; mod module; pub(crate) mod prefixed_storage; -pub mod queries; +#[cfg(feature = "staking")] mod staking; +mod stargate; +mod test_helpers; +pub(crate) mod tests; mod transactions; mod wasm; + +// --- Clone Testing Modules --- // +pub mod queries; pub mod wasm_emulation; +// --- End --- // -pub use crate::addresses::{AddressGenerator, SimpleAddressGenerator}; -pub use crate::app::{custom_app, next_block, App, BasicApp, CosmosRouter, Router, SudoMsg}; +pub use crate::addresses::{ + AddressGenerator, IntoAddr, IntoBech32, IntoBech32m, SimpleAddressGenerator, +}; +pub use crate::api::{MockApiBech32, MockApiBech32m}; +pub use crate::app::{ + custom_app, next_block, no_init, App, BasicApp, CosmosRouter, Router, SudoMsg, +}; pub use crate::app_builder::{AppBuilder, BasicAppBuilder}; pub use crate::bank::{Bank, BankKeeper, BankSudo}; pub use crate::checksums::ChecksumGenerator; @@ -37,11 +167,10 @@ pub use crate::executor::{AppResponse, Executor}; pub use crate::gov::{Gov, GovAcceptingModule, GovFailingModule}; pub use crate::ibc::{Ibc, IbcAcceptingModule, IbcFailingModule}; pub use crate::module::{AcceptingModule, FailingModule, Module}; +#[cfg(feature = "staking")] pub use crate::staking::{ Distribution, DistributionKeeper, StakeKeeper, Staking, StakingInfo, StakingSudo, }; -pub use crate::wasm::{ - ContractData, Wasm, WasmKeeper, WasmSudo, LOCAL_RUST_CODE_OFFSET, LOCAL_WASM_CODE_OFFSET, -}; - -pub use prefixed_storage::{PrefixedStorage, ReadonlyPrefixedStorage}; +pub use crate::stargate::{Stargate, StargateAccepting, StargateFailing}; +pub use crate::wasm::{ContractData, Wasm, WasmKeeper, WasmSudo}; +pub use crate::wasm::{LOCAL_RUST_CODE_OFFSET, LOCAL_WASM_CODE_OFFSET}; diff --git a/src/module.rs b/src/module.rs index 470e8d97..cc73a4d6 100644 --- a/src/module.rs +++ b/src/module.rs @@ -1,16 +1,23 @@ use crate::app::CosmosRouter; use crate::error::{bail, AnyResult}; use crate::AppResponse; -use cosmwasm_std::{Addr, Api, Binary, BlockInfo, CustomQuery, Querier, Storage}; -use schemars::JsonSchema; +use cosmwasm_std::{Addr, Api, Binary, BlockInfo, CustomMsg, CustomQuery, Querier, Storage}; use serde::de::DeserializeOwned; use std::fmt::Debug; use std::marker::PhantomData; -/// Module interface. +/// # General module +/// +/// Provides a generic interface for modules within the test environment. +/// It is essential for creating modular and extensible testing setups, +/// allowing developers to integrate custom functionalities +/// or test specific scenarios. pub trait Module { + /// Type of messages processed by the module instance. type ExecT; + /// Type of queries processed by the module instance. type QueryT; + /// Type of privileged messages used by the module instance. type SudoT; /// Runs any [ExecT](Self::ExecT) message, @@ -25,7 +32,7 @@ pub trait Module { msg: Self::ExecT, ) -> AnyResult where - ExecC: Debug + Clone + PartialEq + JsonSchema + DeserializeOwned + 'static, + ExecC: CustomMsg + DeserializeOwned + 'static, QueryC: CustomQuery + DeserializeOwned + 'static; /// Runs any [QueryT](Self::QueryT) message, @@ -53,21 +60,26 @@ pub trait Module { msg: Self::SudoT, ) -> AnyResult where - ExecC: Debug + Clone + PartialEq + JsonSchema + DeserializeOwned + 'static, + ExecC: CustomMsg + DeserializeOwned + 'static, QueryC: CustomQuery + DeserializeOwned + 'static; } +/// # Always failing module +/// +/// This could be a diagnostic or testing tool within the Cosmos ecosystem, +/// designed to intentionally fail during processing any message, query or privileged action. +pub struct FailingModule(PhantomData<(ExecT, QueryT, SudoT)>); -pub struct FailingModule { - module_type: String, - _t: PhantomData<(ExecT, QueryT, SudoT)>, +impl FailingModule { + /// Creates an instance of a failing module. + pub fn new() -> Self { + Self(PhantomData) + } } -impl FailingModule { - pub fn new(module_type: &str) -> Self { - Self { - module_type: module_type.to_string(), - _t: PhantomData, - } +impl Default for FailingModule { + /// Creates a default instance of a failing module. + fn default() -> Self { + Self::new() } } @@ -91,12 +103,7 @@ where sender: Addr, msg: Self::ExecT, ) -> AnyResult { - bail!( - "Unexpected exec msg {:?} from {:?} on {} module", - msg, - sender, - self.module_type - ) + bail!("Unexpected exec msg {:?} from {:?}", msg, sender) } /// Runs any [QueryT](Self::QueryT) message, always returns an error. @@ -108,11 +115,7 @@ where _block: &BlockInfo, request: Self::QueryT, ) -> AnyResult { - bail!( - "Unexpected custom query {:?} on {} module", - request, - self.module_type - ) + bail!("Unexpected custom query {:?}", request) } /// Runs any [SudoT](Self::SudoT) privileged action, always returns an error. @@ -124,23 +127,24 @@ where _block: &BlockInfo, msg: Self::SudoT, ) -> AnyResult { - bail!( - "Unexpected sudo msg {:?} on {} module", - msg, - self.module_type - ) + bail!("Unexpected sudo msg {:?}", msg) } } - +/// # Always accepting module +/// +/// This struct represents a module in the Cosmos ecosystem designed to +/// always accept all processed messages, queries and privileged actions. pub struct AcceptingModule(PhantomData<(ExecT, QueryT, SudoT)>); impl AcceptingModule { + /// Creates an instance of an accepting module. pub fn new() -> Self { Self(PhantomData) } } impl Default for AcceptingModule { + /// Creates an instance of an accepting module with default settings. fn default() -> Self { Self::new() } @@ -169,7 +173,7 @@ where Ok(AppResponse::default()) } - /// Runs any [QueryT](Self::QueryT) message, always returns an empty binary. + /// Runs any [QueryT](Self::QueryT) message, always returns a default (empty) binary. fn query( &self, _api: &dyn Api, diff --git a/src/prefixed_storage/length_prefixed.rs b/src/prefixed_storage/length_prefixed.rs index 82eaf23b..931b8acd 100644 --- a/src/prefixed_storage/length_prefixed.rs +++ b/src/prefixed_storage/length_prefixed.rs @@ -102,7 +102,7 @@ mod tests { #[test] fn to_length_prefixed_calculates_capacity_correctly() { // Those tests cannot guarantee the required capacity was calculated correctly before - // the vector allocation but increase the likelyhood of a proper implementation. + // the vector allocation but increase the likelihood of a proper implementation. let key = to_length_prefixed(b""); assert_eq!(key.capacity(), key.len()); @@ -161,7 +161,7 @@ mod tests { #[test] fn to_length_prefixed_nested_calculates_capacity_correctly() { // Those tests cannot guarantee the required capacity was calculated correctly before - // the vector allocation but increase the likelyhood of a proper implementation. + // the vector allocation but increase the likelihood of a proper implementation. let key = to_length_prefixed_nested(&[]); assert_eq!(key.capacity(), key.len()); diff --git a/src/prefixed_storage/mod.rs b/src/prefixed_storage/mod.rs index 4506230a..ce96a655 100644 --- a/src/prefixed_storage/mod.rs +++ b/src/prefixed_storage/mod.rs @@ -7,16 +7,15 @@ mod length_prefixed; mod namespace_helpers; pub use length_prefixed::{ - contract_namespace, decode_length, get_full_contract_storage_namespace, to_length_prefixed, - CONTRACT_STORAGE_PREFIX, + decode_length, get_full_contract_storage_namespace, to_length_prefixed, CONTRACT_STORAGE_PREFIX, }; -/// An alias of PrefixedStorage::new for less verbose usage +/// An alias of [PrefixedStorage::new] for less verbose usage. pub fn prefixed<'a>(storage: &'a mut dyn Storage, namespace: &[u8]) -> PrefixedStorage<'a> { PrefixedStorage::new(storage, namespace) } -/// An alias of ReadonlyPrefixedStorage::new for less verbose usage +/// An alias of [ReadonlyPrefixedStorage::new] for less verbose usage. pub fn prefixed_read<'a>( storage: &'a dyn Storage, namespace: &[u8], @@ -24,12 +23,30 @@ pub fn prefixed_read<'a>( ReadonlyPrefixedStorage::new(storage, namespace) } +/// An alias of [PrefixedStorage::multilevel] for less verbose usage. +pub fn prefixed_multilevel<'a>( + storage: &'a mut dyn Storage, + namespaces: &[&[u8]], +) -> PrefixedStorage<'a> { + PrefixedStorage::multilevel(storage, namespaces) +} + +/// An alias of [ReadonlyPrefixedStorage::multilevel] for less verbose usage. +pub fn prefixed_multilevel_read<'a>( + storage: &'a dyn Storage, + namespaces: &[&[u8]], +) -> ReadonlyPrefixedStorage<'a> { + ReadonlyPrefixedStorage::multilevel(storage, namespaces) +} + +/// Prefixed, mutable storage. pub struct PrefixedStorage<'a> { storage: &'a mut dyn Storage, prefix: Vec, } impl<'a> PrefixedStorage<'a> { + /// Returns a mutable prefixed storage with specified namespace. pub fn new(storage: &'a mut dyn Storage, namespace: &[u8]) -> Self { PrefixedStorage { storage, @@ -37,8 +54,9 @@ impl<'a> PrefixedStorage<'a> { } } - // Nested namespaces as documented in - // https://github.com/webmaster128/key-namespacing#nesting + /// Returns a mutable prefixed storage with [nested namespaces]. + /// + /// [nested namespaces]: https://github.com/webmaster128/key-namespacing#nesting pub fn multilevel(storage: &'a mut dyn Storage, namespaces: &[&[u8]]) -> Self { PrefixedStorage { storage, @@ -47,21 +65,13 @@ impl<'a> PrefixedStorage<'a> { } } -impl<'a> Storage for PrefixedStorage<'a> { +impl Storage for PrefixedStorage<'_> { fn get(&self, key: &[u8]) -> Option> { get_with_prefix(self.storage, &self.prefix, key) } - fn set(&mut self, key: &[u8], value: &[u8]) { - set_with_prefix(self.storage, &self.prefix, key, value); - } - - fn remove(&mut self, key: &[u8]) { - remove_with_prefix(self.storage, &self.prefix, key); - } - - /// 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 + /// Range allows iteration over a set of keys, either forwards or backwards. + /// Uses standard rust range notation, and e.g. `db.range(b"foo"‥b"bar")` and also works reverse. fn range<'b>( &'b self, start: Option<&[u8]>, @@ -70,14 +80,24 @@ impl<'a> Storage for PrefixedStorage<'a> { ) -> Box + 'b> { range_with_prefix(self.storage, &self.prefix, start, end, order) } + + fn set(&mut self, key: &[u8], value: &[u8]) { + set_with_prefix(self.storage, &self.prefix, key, value); + } + + fn remove(&mut self, key: &[u8]) { + remove_with_prefix(self.storage, &self.prefix, key); + } } +/// Prefixed, read-only storage. pub struct ReadonlyPrefixedStorage<'a> { storage: &'a dyn Storage, prefix: Vec, } impl<'a> ReadonlyPrefixedStorage<'a> { + /// Returns a read-only prefixed storage with specified namespace. pub fn new(storage: &'a dyn Storage, namespace: &[u8]) -> Self { ReadonlyPrefixedStorage { storage, @@ -85,8 +105,9 @@ impl<'a> ReadonlyPrefixedStorage<'a> { } } - // Nested namespaces as documented in - // https://github.com/webmaster128/key-namespacing#nesting + /// Returns a read-only prefixed storage with [nested namespaces]. + /// + /// [nested namespaces]: https://github.com/webmaster128/key-namespacing#nesting pub fn multilevel(storage: &'a dyn Storage, namespaces: &[&[u8]]) -> Self { ReadonlyPrefixedStorage { storage, @@ -95,20 +116,12 @@ impl<'a> ReadonlyPrefixedStorage<'a> { } } -impl<'a> Storage for ReadonlyPrefixedStorage<'a> { +impl Storage for ReadonlyPrefixedStorage<'_> { fn get(&self, key: &[u8]) -> Option> { get_with_prefix(self.storage, &self.prefix, key) } - fn set(&mut self, _key: &[u8], _value: &[u8]) { - unimplemented!(); - } - - fn remove(&mut self, _key: &[u8]) { - unimplemented!(); - } - - /// range allows iteration over a set of keys, either forwards or backwards + /// Range allows iteration over a set of keys, either forwards or backwards. fn range<'b>( &'b self, start: Option<&[u8]>, @@ -117,6 +130,14 @@ impl<'a> Storage for ReadonlyPrefixedStorage<'a> { ) -> Box + 'b> { range_with_prefix(self.storage, &self.prefix, start, end, order) } + + fn set(&mut self, _key: &[u8], _value: &[u8]) { + unimplemented!(); + } + + fn remove(&mut self, _key: &[u8]) { + unimplemented!(); + } } #[cfg(test)] @@ -139,6 +160,35 @@ mod tests { assert_eq!(s2.get(b"elsewhere"), None); } + #[test] + fn prefixed_storage_range() { + // prepare prefixed storage + let mut storage = MockStorage::new(); + let mut ps1 = PrefixedStorage::new(&mut storage, b"foo"); + ps1.set(b"a", b"A"); + ps1.set(b"l", b"L"); + ps1.set(b"p", b"P"); + ps1.set(b"z", b"Z"); + assert_eq!(storage.get(b"\x00\x03fooa").unwrap(), b"A".to_vec()); + assert_eq!(storage.get(b"\x00\x03fool").unwrap(), b"L".to_vec()); + assert_eq!(storage.get(b"\x00\x03foop").unwrap(), b"P".to_vec()); + assert_eq!(storage.get(b"\x00\x03fooz").unwrap(), b"Z".to_vec()); + // query prefixed storage using range function + let ps2 = PrefixedStorage::new(&mut storage, b"foo"); + assert_eq!( + vec![b"A".to_vec(), b"L".to_vec(), b"P".to_vec()], + ps2.range(Some(b"a"), Some(b"z"), Order::Ascending) + .map(|(_, value)| value) + .collect::>>() + ); + assert_eq!( + vec![b"Z".to_vec(), b"P".to_vec(), b"L".to_vec(), b"A".to_vec()], + ps2.range(Some(b"a"), None, Order::Descending) + .map(|(_, value)| value) + .collect::>>() + ); + } + #[test] fn prefixed_storage_multilevel_set_and_get() { let mut storage = MockStorage::new(); @@ -172,6 +222,24 @@ mod tests { assert_eq!(s2.get(b"obar"), None); } + #[test] + #[should_panic(expected = "not implemented")] + #[allow(clippy::unnecessary_mut_passed)] + fn readonly_prefixed_storage_set() { + let mut storage = MockStorage::new(); + let mut rps = ReadonlyPrefixedStorage::new(&mut storage, b"foo"); + rps.set(b"bar", b"gotcha"); + } + + #[test] + #[should_panic(expected = "not implemented")] + #[allow(clippy::unnecessary_mut_passed)] + fn readonly_prefixed_storage_remove() { + let mut storage = MockStorage::new(); + let mut rps = ReadonlyPrefixedStorage::new(&mut storage, b"foo"); + rps.remove(b"gotcha"); + } + #[test] fn readonly_prefixed_storage_multilevel_get() { let mut storage = MockStorage::new(); diff --git a/src/queries/wasm.rs b/src/queries/wasm.rs index 9a3c2a10..c09aa7e3 100644 --- a/src/queries/wasm.rs +++ b/src/queries/wasm.rs @@ -30,6 +30,8 @@ impl WasmRemoteQuerier { admin: code_info.admin.map(Addr::unchecked), code_id: code_info.code_id, creator: Addr::unchecked(code_info.creator), + label: "Distant contract with no label".to_string(), + created: 0, }) } diff --git a/src/staking.rs b/src/staking.rs index fc0048dc..79783ae0 100644 --- a/src/staking.rs +++ b/src/staking.rs @@ -5,30 +5,37 @@ use crate::prefixed_storage::{prefixed, prefixed_read}; use crate::{BankSudo, Module}; use cosmwasm_std::{ coin, ensure, ensure_eq, to_json_binary, Addr, AllDelegationsResponse, AllValidatorsResponse, - Api, BankMsg, Binary, BlockInfo, BondedDenomResponse, Coin, CustomQuery, Decimal, Delegation, - DelegationResponse, DistributionMsg, Empty, Event, FullDelegation, Querier, StakingMsg, - StakingQuery, Storage, Timestamp, Uint128, Validator, ValidatorResponse, + Api, BankMsg, Binary, BlockInfo, BondedDenomResponse, Coin, CustomMsg, CustomQuery, Decimal, + Delegation, DelegationResponse, DistributionMsg, Empty, Event, FullDelegation, Querier, + StakingMsg, StakingQuery, Storage, Timestamp, Uint128, Validator, ValidatorResponse, }; use cw_storage_plus::{Deque, Item, Map}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::collections::{BTreeSet, VecDeque}; -// Contains some general staking parameters +/// Default denominator of the staking token. +const BONDED_DENOM: &str = "TOKEN"; + +/// One year expressed in seconds. +const YEAR: u64 = 60 * 60 * 24 * 365; + +/// A structure containing some general staking parameters. #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] pub struct StakingInfo { - /// The denominator of the staking token + /// The denominator of the staking token. pub bonded_denom: String, - /// Time between unbonding and receiving tokens in seconds + /// Time between unbonding and receiving tokens back (in seconds). pub unbonding_time: u64, - /// Interest rate per year (60 * 60 * 24 * 365 seconds) + /// Annual percentage rate (interest rate and any additional fees associated with bonding). pub apr: Decimal, } impl Default for StakingInfo { + /// Creates staking info with default settings. fn default() -> Self { StakingInfo { - bonded_denom: "TOKEN".to_string(), + bonded_denom: BONDED_DENOM.to_string(), unbonding_time: 60, apr: Decimal::percent(10), } @@ -43,16 +50,16 @@ struct Shares { } impl Shares { - /// Calculates the share of validator rewards that should be given to this staker. - pub fn share_of_rewards(&self, validator: &ValidatorInfo, rewards: Decimal) -> Decimal { - if validator.stake.is_zero() { + /// Calculates the share of validator's rewards that should be given to this staker. + pub fn share_of_rewards(&self, validator_info: &ValidatorInfo, rewards: Decimal) -> Decimal { + if validator_info.stake.is_zero() { return Decimal::zero(); } - rewards * self.stake / validator.stake + rewards * self.stake / validator_info.stake } } -/// Holds some operational data about a validator +/// Holds some operational data about a validator. #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] struct ValidatorInfo { /// The stakers that have staked with this validator. @@ -76,20 +83,24 @@ impl ValidatorInfo { #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] struct Unbonding { + /// Staker (delegator) address. pub delegator: Addr, - pub validator: Addr, + /// Validator address. + pub validator: String, + /// Amount of stakes to be unbonded. pub amount: Uint128, + /// Timestamp at which unbonding will take place (simulates unbonding timeout). pub payout_at: Timestamp, } const STAKING_INFO: Item = Item::new("staking_info"); /// (staker_addr, validator_addr) -> shares -const STAKES: Map<(&Addr, &Addr), Shares> = Map::new("stakes"); -const VALIDATOR_MAP: Map<&Addr, Validator> = Map::new("validator_map"); +const STAKES: Map<(&Addr, &str), Shares> = Map::new("stakes"); +const VALIDATOR_MAP: Map<&str, Validator> = Map::new("validator_map"); /// Additional vec of validators, in case the `iterator` feature is disabled const VALIDATORS: Deque = Deque::new("validators"); /// Contains additional info for each validator -const VALIDATOR_INFO: Map<&Addr, ValidatorInfo> = Map::new("validator_info"); +const VALIDATOR_INFO: Map<&str, ValidatorInfo> = Map::new("validator_info"); /// The queue of unbonding operations. This is needed because unbonding has a waiting time. See [`StakeKeeper`] const UNBONDING_QUEUE: Item> = Item::new("unbonding_queue"); /// (addr) -> addr. Maps addresses to the address they have delegated @@ -101,85 +112,97 @@ pub const NAMESPACE_STAKING: &[u8] = b"staking"; // https://github.com/cosmos/cosmos-sdk/blob/4f6f6c00021f4b5ee486bbb71ae2071a8ceb47c9/x/distribution/types/keys.go#L16 pub const NAMESPACE_DISTRIBUTION: &[u8] = b"distribution"; -// We need to expand on this, but we will need this to properly test out staking -#[derive(Clone, std::fmt::Debug, PartialEq, Eq, JsonSchema)] +/// Staking privileged action definition. +/// +/// We need to expand on this, but we will need this to properly test out staking +#[derive(Clone, Debug, PartialEq, Eq, JsonSchema)] pub enum StakingSudo { /// Slashes the given percentage of the validator's stake. /// For now, you cannot slash retrospectively in tests. Slash { + /// Validator's address. validator: String, + /// Percentage of the validator's stake. percentage: Decimal, }, - /// Causes the unbonding queue to be processed. - /// This needs to be triggered manually, since there is no good place to do this right now. - /// In cosmos-sdk, this is done in `EndBlock`, but we don't have that here. - #[deprecated(note = "This is not needed anymore. Just call `update_block`")] - ProcessQueue {}, } +/// A trait defining a behavior of the stake keeper. +/// +/// Manages staking operations, vital for testing contracts in proof-of-stake (PoS) blockchain environments. +/// This trait simulates staking behaviors, including delegation, validator operations, and reward mechanisms. pub trait Staking: Module { /// This is called from the end blocker (`update_block` / `set_block`) to process the - /// staking queue. - /// Needed because unbonding has a waiting time. + /// staking queue. Needed because unbonding has a waiting time. /// If you're implementing a dummy staking module, this can be a no-op. - fn process_queue( + fn process_queue( &self, - api: &dyn Api, - storage: &mut dyn Storage, - router: &dyn CosmosRouter, - block: &BlockInfo, - ) -> AnyResult; + _api: &dyn Api, + _storage: &mut dyn Storage, + _router: &dyn CosmosRouter, + _block: &BlockInfo, + ) -> AnyResult { + Ok(AppResponse::default()) + } } +/// A trait defining a behavior of the distribution keeper. pub trait Distribution: Module {} +/// A structure representing a default stake keeper. pub struct StakeKeeper { + /// Module address of a default stake keeper. module_addr: Addr, } impl Default for StakeKeeper { + /// Creates a new stake keeper with default settings. fn default() -> Self { - Self::new() + StakeKeeper { + // The address of the staking module. This holds all staked tokens. + module_addr: Addr::unchecked("staking_module"), + } } } impl StakeKeeper { + /// Creates a new stake keeper with default module address. pub fn new() -> Self { - StakeKeeper { - // The address of the staking module. This holds all staked tokens. - module_addr: Addr::unchecked("staking_module"), - } + Self::default() } /// Provides some general parameters to the stake keeper pub fn setup(&self, storage: &mut dyn Storage, staking_info: StakingInfo) -> AnyResult<()> { let mut storage = prefixed(storage, NAMESPACE_STAKING); - STAKING_INFO.save(&mut storage, &staking_info)?; Ok(()) } - /// Add a new validator available for staking + /// Add a new validator available for staking. pub fn add_validator( &self, - api: &dyn Api, + _api: &dyn Api, storage: &mut dyn Storage, block: &BlockInfo, validator: Validator, ) -> AnyResult<()> { let mut storage = prefixed(storage, NAMESPACE_STAKING); - - let val_addr = api.addr_validate(&validator.address)?; - if VALIDATOR_MAP.may_load(&storage, &val_addr)?.is_some() { + if VALIDATOR_MAP + .may_load(&storage, &validator.address)? + .is_some() + { bail!( "Cannot add validator {}, since a validator with that address already exists", - val_addr + validator.address ); } - - VALIDATOR_MAP.save(&mut storage, &val_addr, &validator)?; + VALIDATOR_MAP.save(&mut storage, &validator.address, &validator)?; VALIDATORS.push_back(&mut storage, &validator)?; - VALIDATOR_INFO.save(&mut storage, &val_addr, &ValidatorInfo::new(block.time))?; + VALIDATOR_INFO.save( + &mut storage, + &validator.address, + &ValidatorInfo::new(block.time), + )?; Ok(()) } @@ -187,16 +210,15 @@ impl StakeKeeper { Ok(STAKING_INFO.may_load(staking_storage)?.unwrap_or_default()) } - /// Returns the rewards of the given delegator at the given validator + /// Returns the rewards of the given delegator at the given validator. pub fn get_rewards( &self, storage: &dyn Storage, block: &BlockInfo, delegator: &Addr, - validator: &Addr, + validator: &str, ) -> AnyResult> { let staking_storage = prefixed_read(storage, NAMESPACE_STAKING); - let validator_obj = match self.get_validator(&staking_storage, validator)? { Some(validator) => validator, None => bail!("validator {} not found", validator), @@ -209,7 +231,6 @@ impl StakeKeeper { } }; let validator_info = VALIDATOR_INFO.load(&staking_storage, validator)?; - Self::get_rewards_internal( &staking_storage, block, @@ -263,20 +284,20 @@ impl StakeKeeper { let reward = Decimal::from_ratio(stake, 1u128) * interest_rate * Decimal::from_ratio(time_diff, 1u128) - / Decimal::from_ratio(60u128 * 60 * 24 * 365, 1u128); + / Decimal::from_ratio(YEAR, 1u128); let commission = reward * validator_commission; reward - commission } /// Updates the staking reward for the given validator and their stakers - /// It saves the validator info and it's stakers, so make sure not to overwrite that. + /// It saves the validator info and stakers, so make sure not to overwrite that. /// Always call this to update rewards before changing anything that influences future rewards. fn update_rewards( - api: &dyn Api, + _api: &dyn Api, staking_storage: &mut dyn Storage, block: &BlockInfo, - validator: &Addr, + validator: &str, ) -> AnyResult<()> { let staking_info = Self::get_staking_info(staking_storage)?; @@ -305,12 +326,11 @@ impl StakeKeeper { // update delegators if !new_rewards.is_zero() { - let validator_addr = api.addr_validate(&validator_obj.address)?; // update all delegators for staker in validator_info.stakers.iter() { STAKES.update( staking_storage, - (staker, &validator_addr), + (staker, &validator_obj.address), |shares| -> AnyResult<_> { let mut shares = shares.expect("all stakers in validator_info should exist"); @@ -323,11 +343,11 @@ impl StakeKeeper { Ok(()) } - /// Returns the single validator with the given address (or `None` if there is no such validator) + /// Returns the single validator with the given address (or `None` if there is no such validator). fn get_validator( &self, staking_storage: &dyn Storage, - address: &Addr, + address: &str, ) -> AnyResult> { Ok(VALIDATOR_MAP.may_load(staking_storage, address)?) } @@ -342,7 +362,7 @@ impl StakeKeeper { &self, staking_storage: &dyn Storage, account: &Addr, - validator: &Addr, + validator: &str, ) -> AnyResult> { let shares = STAKES.may_load(staking_storage, (account, validator))?; let staking_info = Self::get_staking_info(staking_storage)?; @@ -361,7 +381,7 @@ impl StakeKeeper { staking_storage: &mut dyn Storage, block: &BlockInfo, to_address: &Addr, - validator: &Addr, + validator: &str, amount: Coin, ) -> AnyResult<()> { self.validate_denom(staking_storage, &amount)?; @@ -382,7 +402,7 @@ impl StakeKeeper { staking_storage: &mut dyn Storage, block: &BlockInfo, from_address: &Addr, - validator: &Addr, + validator: &str, amount: Coin, ) -> AnyResult<()> { self.validate_denom(staking_storage, &amount)?; @@ -403,7 +423,7 @@ impl StakeKeeper { staking_storage: &mut dyn Storage, block: &BlockInfo, delegator: &Addr, - validator: &Addr, + validator: &str, amount: impl Into, sub: bool, ) -> AnyResult<()> { @@ -458,7 +478,7 @@ impl StakeKeeper { api: &dyn Api, staking_storage: &mut dyn Storage, block: &BlockInfo, - validator: &Addr, + validator: &str, percentage: Decimal, ) -> AnyResult<()> { // calculate rewards before slashing @@ -532,7 +552,7 @@ impl StakeKeeper { Ok(()) } - fn process_queue( + fn process_queue( &self, api: &dyn Api, storage: &mut dyn Storage, @@ -601,7 +621,7 @@ impl StakeKeeper { } impl Staking for StakeKeeper { - fn process_queue( + fn process_queue( &self, api: &dyn Api, storage: &mut dyn Storage, @@ -617,7 +637,7 @@ impl Module for StakeKeeper { type QueryT = StakingQuery; type SudoT = StakingSudo; - fn execute( + fn execute( &self, api: &dyn Api, storage: &mut dyn Storage, @@ -629,8 +649,6 @@ impl Module for StakeKeeper { let mut staking_storage = prefixed(storage, NAMESPACE_STAKING); match msg { StakingMsg::Delegate { validator, amount } => { - let validator = api.addr_validate(&validator)?; - // see https://github.com/cosmos/cosmos-sdk/blob/3c5387048f75d7e78b40c5b8d2421fdb8f5d973a/x/staking/types/msg.go#L202-L207 if amount.amount.is_zero() { bail!("invalid delegation amount"); @@ -664,7 +682,6 @@ impl Module for StakeKeeper { Ok(AppResponse { events, data: None }) } StakingMsg::Undelegate { validator, amount } => { - let validator = api.addr_validate(&validator)?; self.validate_denom(&staking_storage, &amount)?; // see https://github.com/cosmos/cosmos-sdk/blob/3c5387048f75d7e78b40c5b8d2421fdb8f5d973a/x/staking/types/msg.go#L292-L297 @@ -704,8 +721,6 @@ impl Module for StakeKeeper { dst_validator, amount, } => { - let src_validator = api.addr_validate(&src_validator)?; - let dst_validator = api.addr_validate(&dst_validator)?; // see https://github.com/cosmos/cosmos-sdk/blob/v0.46.1/x/staking/keeper/msg_server.go#L316-L322 let events = vec![Event::new("redelegate") .add_attribute("source_validator", &src_validator) @@ -735,32 +750,6 @@ impl Module for StakeKeeper { } } - fn sudo( - &self, - api: &dyn Api, - storage: &mut dyn Storage, - router: &dyn CosmosRouter, - block: &BlockInfo, - msg: StakingSudo, - ) -> AnyResult { - match msg { - StakingSudo::Slash { - validator, - percentage, - } => { - let mut staking_storage = prefixed(storage, NAMESPACE_STAKING); - let validator = api.addr_validate(&validator)?; - self.validate_percentage(percentage)?; - - self.slash(api, &mut staking_storage, block, &validator, percentage)?; - - Ok(AppResponse::default()) - } - #[allow(deprecated)] - StakingSudo::ProcessQueue {} => self.process_queue(api, storage, router, block), - } - } - fn query( &self, api: &dyn Api, @@ -784,11 +773,7 @@ impl Module for StakeKeeper { .filter_map(|validator| { let delegator = delegator.clone(); let amount = self - .get_stake( - &staking_storage, - &delegator, - &Addr::unchecked(&validator.address), - ) + .get_stake(&staking_storage, &delegator, &validator.address) .transpose()?; Some(amount.map(|amount| { @@ -803,18 +788,17 @@ impl Module for StakeKeeper { delegator, validator, } => { - let validator_addr = Addr::unchecked(&validator); - let validator_obj = match self.get_validator(&staking_storage, &validator_addr)? { + let validator_obj = match self.get_validator(&staking_storage, &validator)? { Some(validator) => validator, None => bail!("non-existent validator {}", validator), }; let delegator = api.addr_validate(&delegator)?; let shares = STAKES - .may_load(&staking_storage, (&delegator, &validator_addr))? + .may_load(&staking_storage, (&delegator, &validator))? .unwrap_or_default(); - let validator_info = VALIDATOR_INFO.load(&staking_storage, &validator_addr)?; + let validator_info = VALIDATOR_INFO.load(&staking_storage, &validator)?; let reward = Self::get_rewards_internal( &staking_storage, block, @@ -825,7 +809,7 @@ impl Module for StakeKeeper { let staking_info = Self::get_staking_info(&staking_storage)?; let amount = coin( - (Uint128::new(1).mul_floor(shares.stake)).u128(), + Uint128::new(1).mul_floor(shares.stake).u128(), staking_info.bonded_denom, ); @@ -853,19 +837,46 @@ impl Module for StakeKeeper { self.get_validators(&staking_storage)?, ))?), StakingQuery::Validator { address } => Ok(to_json_binary(&ValidatorResponse::new( - self.get_validator(&staking_storage, &Addr::unchecked(address))?, + self.get_validator(&staking_storage, &address)?, ))?), q => bail!("Unsupported staking sudo message: {:?}", q), } } + + fn sudo( + &self, + api: &dyn Api, + storage: &mut dyn Storage, + _router: &dyn CosmosRouter, + block: &BlockInfo, + msg: StakingSudo, + ) -> AnyResult { + match msg { + StakingSudo::Slash { + validator, + percentage, + } => { + let mut staking_storage = prefixed(storage, NAMESPACE_STAKING); + self.validate_percentage(percentage)?; + self.slash(api, &mut staking_storage, block, &validator, percentage)?; + Ok(AppResponse::default()) + } + } + } } +/// A structure representing a default distribution keeper. +/// +/// This module likely manages the distribution of rewards and fees within the blockchain network. +/// It could handle tasks like distributing block rewards to validators and delegators, +/// and managing community funding mechanisms. #[derive(Default)] pub struct DistributionKeeper {} impl DistributionKeeper { + /// Creates a new distribution keeper with default settings. pub fn new() -> Self { - DistributionKeeper {} + Self::default() } /// Removes all rewards from the given (delegator, validator) pair and returns the amount @@ -875,7 +886,7 @@ impl DistributionKeeper { storage: &mut dyn Storage, block: &BlockInfo, delegator: &Addr, - validator: &Addr, + validator: &str, ) -> AnyResult { let mut staking_storage = prefixed(storage, NAMESPACE_STAKING); // update the validator and staker rewards @@ -892,6 +903,7 @@ impl DistributionKeeper { Ok(rewards) } + /// Returns the withdrawal address for specified delegator. pub fn get_withdraw_address(storage: &dyn Storage, delegator: &Addr) -> AnyResult { Ok(match WITHDRAW_ADDRESS.may_load(storage, delegator)? { Some(a) => a, @@ -899,20 +911,22 @@ impl DistributionKeeper { }) } - // https://docs.cosmos.network/main/modules/distribution#msgsetwithdrawaddress + /// Sets (changes) the [withdraw address] of the delegator. + /// + /// [withdraw address]: https://docs.cosmos.network/main/modules/distribution#msgsetwithdrawaddress pub fn set_withdraw_address( storage: &mut dyn Storage, delegator: &Addr, - withdraw_address: &Addr, + withdraw_addr: &Addr, ) -> AnyResult<()> { - if delegator == withdraw_address { + if delegator == withdraw_addr { WITHDRAW_ADDRESS.remove(storage, delegator); Ok(()) } else { // technically we should require that this address is not // the address of a module. TODO: how? WITHDRAW_ADDRESS - .save(storage, delegator, withdraw_address) + .save(storage, delegator, withdraw_addr) .map_err(|e| e.into()) } } @@ -925,7 +939,7 @@ impl Module for DistributionKeeper { type QueryT = Empty; type SudoT = Empty; - fn execute( + fn execute( &self, api: &dyn Api, storage: &mut dyn Storage, @@ -936,10 +950,7 @@ impl Module for DistributionKeeper { ) -> AnyResult { match msg { DistributionMsg::WithdrawDelegatorReward { validator } => { - let validator_addr = api.addr_validate(&validator)?; - - let rewards = self.remove_rewards(api, storage, block, &sender, &validator_addr)?; - + let rewards = self.remove_rewards(api, storage, block, &sender, &validator)?; let staking_storage = prefixed_read(storage, NAMESPACE_STAKING); let distribution_storage = prefixed_read(storage, NAMESPACE_DISTRIBUTION); let staking_info = StakeKeeper::get_staking_info(&staking_storage)?; @@ -984,6 +995,17 @@ impl Module for DistributionKeeper { } } + fn query( + &self, + _api: &dyn Api, + _storage: &dyn Storage, + _querier: &dyn Querier, + _block: &BlockInfo, + _request: Empty, + ) -> AnyResult { + bail!("Something went wrong - Distribution doesn't have query messages") + } + fn sudo( &self, _api: &dyn Api, @@ -994,15 +1016,1534 @@ impl Module for DistributionKeeper { ) -> AnyResult { bail!("Something went wrong - Distribution doesn't have sudo messages") } +} - fn query( - &self, - _api: &dyn Api, - _storage: &dyn Storage, - _querier: &dyn Querier, - _block: &BlockInfo, - _request: Empty, - ) -> AnyResult { - bail!("Something went wrong - Distribution doesn't have query messages") +#[cfg(test)] +mod test { + use super::*; + use crate::{ + BankKeeper, FailingModule, GovFailingModule, IbcFailingModule, IntoBech32, Router, + StargateFailing, WasmKeeper, + }; + use cosmwasm_std::{ + coins, from_json, + testing::{mock_env, MockApi, MockStorage}, + BalanceResponse, BankQuery, QuerierWrapper, + }; + use serde::de::DeserializeOwned; + + /// Utility structure for combining validator properties, + /// used mainly for validator initialization. + struct ValidatorProperties { + /// Validator's commission. + commission: Decimal, + /// Validator's maximum commission. + max_commission: Decimal, + /// The maximum daily increase of the validator's commission. + max_change_rate: Decimal, + } + + /// Creates validator properties from values expressed as a percentage. + fn vp(commission: u64, max_commission: u64, max_change_rate: u64) -> ValidatorProperties { + ValidatorProperties { + commission: Decimal::percent(commission), + max_commission: Decimal::percent(max_commission), + max_change_rate: Decimal::percent(max_change_rate), + } + } + + /// Type alias for default build of [Router], to make its reference in typical test scenario. + type BasicRouter = Router< + BankKeeper, + FailingModule, + WasmKeeper, + StakeKeeper, + DistributionKeeper, + IbcFailingModule, + GovFailingModule, + StargateFailing, + >; + + /// Test environment that simplifies initialization of test cases. + struct TestEnv { + api: MockApi, + storage: MockStorage, + router: BasicRouter, + block: BlockInfo, + validator_addr_1: String, + validator_addr_2: String, + validator_addr_3: String, + delegator_addr_1: Addr, + delegator_addr_2: Addr, + user_addr_1: Addr, + } + + impl TestEnv { + /// Returns preconfigured test environment. + fn new(validator1: ValidatorProperties, validator2: ValidatorProperties) -> Self { + // Utility function for creating a validator's address, + // which has a different prefix from a user's address. + fn validator_address(value: &str) -> String { + value.into_bech32_with_prefix("cosmwasmvaloper").to_string() + } + + // Utility function for creating a user's address, + // which is in Bech32 format with the chain's prefix. + fn user_address(api: &MockApi, value: &str) -> Addr { + api.addr_make(value) + } + + let api = MockApi::default(); + let router = Router { + wasm: WasmKeeper::new(), + bank: BankKeeper::new(), + custom: FailingModule::new(), + staking: StakeKeeper::new(), + distribution: DistributionKeeper::new(), + ibc: IbcFailingModule::new(), + gov: GovFailingModule::new(), + stargate: StargateFailing, + }; + let mut storage = MockStorage::new(); + let block = mock_env().block; + + let validator_addr_1 = validator_address("validator1"); + let validator_addr_2 = validator_address("validator2"); + let validator_addr_3 = validator_address("validator3"); + + // configure basic staking parameters + router + .staking + .setup(&mut storage, StakingInfo::default()) + .unwrap(); + + // create validator no. 1 + let valoper1 = Validator::new( + validator_addr_1.to_string(), + validator1.commission, + validator1.max_commission, + validator1.max_change_rate, + ); + router + .staking + .add_validator(&api, &mut storage, &block, valoper1) + .unwrap(); + + // create validator no. 2 + let valoper2 = Validator::new( + validator_addr_2.to_string(), + validator2.commission, + validator2.max_commission, + validator2.max_change_rate, + ); + router + .staking + .add_validator(&api, &mut storage, &block, valoper2) + .unwrap(); + + // return testing environment + Self { + api, + storage, + router, + block, + validator_addr_1, + validator_addr_2, + validator_addr_3, + delegator_addr_1: user_address(&api, "delegator1"), + delegator_addr_2: user_address(&api, "delegator2"), + user_addr_1: user_address(&api, "user1"), + } + } + + /// Returns an address of EXISTING validator no. 1. + #[inline(always)] + fn validator_addr_1(&self) -> String { + self.validator_addr_1.clone() + } + + /// Returns an address of EXISTING validator no. 2. + #[inline(always)] + fn validator_addr_2(&self) -> String { + self.validator_addr_2.clone() + } + + /// Returns an address of NON-EXISTING validator no. 3. + #[inline(always)] + fn validator_addr_3(&self) -> String { + self.validator_addr_3.clone() + } + + /// Returns address of the delegator no. 1. + #[inline(always)] + fn delegator_addr_1(&self) -> Addr { + self.delegator_addr_1.clone() + } + + /// Returns address of the delegator no. 2. + #[inline(always)] + fn delegator_addr_2(&self) -> Addr { + self.delegator_addr_2.clone() + } + + /// Returns address of the user no. 1. + #[inline(always)] + fn user_addr_1(&self) -> Addr { + self.user_addr_1.clone() + } + } + + /// Executes staking message. + fn execute_stake(env: &mut TestEnv, sender: Addr, msg: StakingMsg) -> AnyResult { + env.router.staking.execute( + &env.api, + &mut env.storage, + &env.router, + &env.block, + sender, + msg, + ) + } + + /// Executes staking query. + fn query_stake(env: &TestEnv, msg: StakingQuery) -> AnyResult { + Ok(from_json(env.router.staking.query( + &env.api, + &env.storage, + &env.router.querier(&env.api, &env.storage, &env.block), + &env.block, + msg, + )?)?) + } + + /// Executes distribution message. + fn execute_distr( + env: &mut TestEnv, + sender: Addr, + msg: DistributionMsg, + ) -> AnyResult { + env.router.distribution.execute( + &env.api, + &mut env.storage, + &env.router, + &env.block, + sender, + msg, + ) + } + + /// Executes bank query. + fn query_bank(env: &TestEnv, msg: BankQuery) -> AnyResult { + Ok(from_json(env.router.bank.query( + &env.api, + &env.storage, + &env.router.querier(&env.api, &env.storage, &env.block), + &env.block, + msg, + )?)?) + } + + /// Initializes balance for specified address in staking denominator. + fn init_balance(env: &mut TestEnv, address: &Addr, amount: u128) { + init_balance_denom(env, address, amount, BONDED_DENOM); + } + + /// Initializes balance for specified address in any denominator. + fn init_balance_denom(env: &mut TestEnv, address: &Addr, amount: u128, denom: &str) { + env.router + .bank + .init_balance(&mut env.storage, address, coins(amount, denom)) + .unwrap(); + } + + /// Utility function for checking multiple balances in staking denominator. + fn assert_balances(env: &TestEnv, balances: impl IntoIterator) { + for (addr, amount) in balances { + let balance: BalanceResponse = query_bank( + env, + BankQuery::Balance { + address: addr.to_string(), + denom: BONDED_DENOM.to_string(), + }, + ) + .unwrap(); + assert_eq!(balance.amount.amount.u128(), amount); + } + } + + #[test] + fn add_get_validators() { + let mut env = TestEnv::new(vp(10, 100, 1), vp(0, 20, 1)); + + let validator_addr_3 = env.validator_addr_3(); + + // add a new validator (validator no. 3 does not exist yet) + let validator = Validator::new( + validator_addr_3.to_string(), + Decimal::percent(1), + Decimal::percent(10), + Decimal::percent(1), + ); + env.router + .staking + .add_validator(&env.api, &mut env.storage, &env.block, validator.clone()) + .unwrap(); + + // get the newly created validator + let staking_storage = prefixed_read(&env.storage, NAMESPACE_STAKING); + let val = env + .router + .staking + .get_validator(&staking_storage, &validator_addr_3) + .unwrap() + .unwrap(); + assert_eq!(val, validator); + + // try to create a validator with the same address as validator no. 3 + let validator_fake = Validator::new( + validator_addr_3.to_string(), + Decimal::percent(2), + Decimal::percent(20), + Decimal::percent(2), + ); + env.router + .staking + .add_validator(&env.api, &mut env.storage, &env.block, validator_fake) + .unwrap_err(); + + // validator no. 3 should still have the original values of its attributes + let staking_storage = prefixed_read(&env.storage, NAMESPACE_STAKING); + let val = env + .router + .staking + .get_validator(&staking_storage, &validator_addr_3) + .unwrap() + .unwrap(); + assert_eq!(val, validator); + } + + #[test] + fn validator_slashing() { + let mut env = TestEnv::new(vp(10, 20, 1), vp(10, 20, 1)); + + let validator_addr_1 = env.validator_addr_1(); + let delegator_addr_1 = env.delegator_addr_1(); + + // stake (delegate) 100 tokens from delegator to validator + let mut staking_storage = prefixed(&mut env.storage, NAMESPACE_STAKING); + env.router + .staking + .add_stake( + &env.api, + &mut staking_storage, + &env.block, + &delegator_addr_1, + &validator_addr_1, + coin(100, BONDED_DENOM), + ) + .unwrap(); + + // slash 50% of the stake of the validator + env.router + .staking + .sudo( + &env.api, + &mut env.storage, + &env.router, + &env.block, + StakingSudo::Slash { + validator: validator_addr_1.to_string(), + percentage: Decimal::percent(50), + }, + ) + .unwrap(); + + // check the remaining stake + let staking_storage = prefixed(&mut env.storage, NAMESPACE_STAKING); + let stake_left = env + .router + .staking + .get_stake(&staking_storage, &delegator_addr_1, &validator_addr_1) + .unwrap() + .unwrap(); + assert_eq!(50, stake_left.amount.u128()); + + // slash all + env.router + .staking + .sudo( + &env.api, + &mut env.storage, + &env.router, + &env.block, + StakingSudo::Slash { + validator: validator_addr_1.to_string(), + percentage: Decimal::percent(100), + }, + ) + .unwrap(); + + // check the current stake + let staking_storage = prefixed(&mut env.storage, NAMESPACE_STAKING); + let stake_left = env + .router + .staking + .get_stake(&staking_storage, &delegator_addr_1, &validator_addr_1) + .unwrap(); + assert_eq!(None, stake_left); + } + + #[test] + fn rewards_work_for_single_delegator() { + let mut env = TestEnv::new(vp(10, 20, 1), vp(10, 20, 1)); + + let validator_addr_1 = env.validator_addr_1(); + let delegator_addr_1 = env.delegator_addr_1(); + + let mut staking_storage = prefixed(&mut env.storage, NAMESPACE_STAKING); + // stake 200 tokens + env.router + .staking + .add_stake( + &env.api, + &mut staking_storage, + &env.block, + &delegator_addr_1, + &validator_addr_1, + coin(200, BONDED_DENOM), + ) + .unwrap(); + + // wait 1/2 year + env.block.time = env.block.time.plus_seconds(YEAR / 2); + + // should now have 200 * 10% / 2 - 10% commission = 9 tokens reward + let rewards = env + .router + .staking + .get_rewards( + &env.storage, + &env.block, + &delegator_addr_1, + &validator_addr_1, + ) + .unwrap() + .unwrap(); + assert_eq!(9, rewards.amount.u128()); + + // withdraw rewards + env.router + .distribution + .execute( + &env.api, + &mut env.storage, + &env.router, + &env.block, + delegator_addr_1.clone(), + DistributionMsg::WithdrawDelegatorReward { + validator: validator_addr_1.to_string(), + }, + ) + .unwrap(); + + // should have no rewards left + let rewards = env + .router + .staking + .get_rewards( + &env.storage, + &env.block, + &delegator_addr_1, + &validator_addr_1, + ) + .unwrap() + .unwrap(); + assert_eq!(0, rewards.amount.u128()); + + // wait another 1/2 year + env.block.time = env.block.time.plus_seconds(YEAR / 2); + // should now have 9 tokens again + let rewards = env + .router + .staking + .get_rewards( + &env.storage, + &env.block, + &delegator_addr_1, + &validator_addr_1, + ) + .unwrap() + .unwrap(); + assert_eq!(9, rewards.amount.u128()); + } + + #[test] + fn rewards_work_for_multiple_delegators() { + let mut env = TestEnv::new(vp(10, 100, 1), vp(10, 100, 1)); + + let validator_addr_1 = env.validator_addr_1(); + let delegator_addr_1 = env.delegator_addr_1(); + let delegator_addr_2 = env.delegator_addr_2(); + + let mut staking_storage = prefixed(&mut env.storage, NAMESPACE_STAKING); + + // add 100 stake to delegator1 and 200 to delegator2 + env.router + .staking + .add_stake( + &env.api, + &mut staking_storage, + &env.block, + &delegator_addr_1, + &validator_addr_1, + coin(100, BONDED_DENOM), + ) + .unwrap(); + env.router + .staking + .add_stake( + &env.api, + &mut staking_storage, + &env.block, + &delegator_addr_2, + &validator_addr_1, + coin(200, BONDED_DENOM), + ) + .unwrap(); + + // wait 1 year + env.block.time = env.block.time.plus_seconds(YEAR); + + // delegator1 should now have 100 * 10% - 10% commission = 9 tokens + let rewards = env + .router + .staking + .get_rewards( + &env.storage, + &env.block, + &delegator_addr_1, + &validator_addr_1, + ) + .unwrap() + .unwrap(); + assert_eq!(rewards.amount.u128(), 9); + + // delegator2 should now have 200 * 10% - 10% commission = 18 tokens + let rewards = env + .router + .staking + .get_rewards( + &env.storage, + &env.block, + &delegator_addr_2, + &validator_addr_1, + ) + .unwrap() + .unwrap(); + assert_eq!(rewards.amount.u128(), 18); + + // delegator1 stakes 100 more + let mut staking_storage = prefixed(&mut env.storage, NAMESPACE_STAKING); + env.router + .staking + .add_stake( + &env.api, + &mut staking_storage, + &env.block, + &delegator_addr_1, + &validator_addr_1, + coin(100, BONDED_DENOM), + ) + .unwrap(); + + // wait another year + env.block.time = env.block.time.plus_seconds(YEAR); + + // delegator1 should now have 9 + 200 * 10% - 10% commission = 27 tokens + let rewards = env + .router + .staking + .get_rewards( + &env.storage, + &env.block, + &delegator_addr_1, + &validator_addr_1, + ) + .unwrap() + .unwrap(); + assert_eq!(rewards.amount.u128(), 27); + + // delegator2 should now have 18 + 200 * 10% - 10% commission = 36 tokens + let rewards = env + .router + .staking + .get_rewards( + &env.storage, + &env.block, + &delegator_addr_2, + &validator_addr_1, + ) + .unwrap() + .unwrap(); + assert_eq!(rewards.amount.u128(), 36); + + // delegator2 unstakes 100 (has 100 left after that) + let mut staking_storage = prefixed(&mut env.storage, NAMESPACE_STAKING); + env.router + .staking + .remove_stake( + &env.api, + &mut staking_storage, + &env.block, + &delegator_addr_2, + &validator_addr_1, + coin(100, BONDED_DENOM), + ) + .unwrap(); + + // and delegator1 withdraws rewards + env.router + .distribution + .execute( + &env.api, + &mut env.storage, + &env.router, + &env.block, + delegator_addr_1.clone(), + DistributionMsg::WithdrawDelegatorReward { + validator: validator_addr_1.to_string(), + }, + ) + .unwrap(); + + let balance: BalanceResponse = from_json( + env.router + .bank + .query( + &env.api, + &env.storage, + &env.router.querier(&env.api, &env.storage, &env.block), + &env.block, + BankQuery::Balance { + address: delegator_addr_1.to_string(), + denom: BONDED_DENOM.to_string(), + }, + ) + .unwrap(), + ) + .unwrap(); + assert_eq!(27, balance.amount.amount.u128()); + + let rewards = env + .router + .staking + .get_rewards( + &env.storage, + &env.block, + &delegator_addr_1, + &validator_addr_1, + ) + .unwrap() + .unwrap(); + assert_eq!(0, rewards.amount.u128()); + + // wait another year + env.block.time = env.block.time.plus_seconds(YEAR); + + // delegator1 should now have 0 + 200 * 10% - 10% commission = 18 tokens + let rewards = env + .router + .staking + .get_rewards( + &env.storage, + &env.block, + &delegator_addr_1, + &validator_addr_1, + ) + .unwrap() + .unwrap(); + assert_eq!(18, rewards.amount.u128()); + + // delegator2 should now have 36 + 100 * 10% - 10% commission = 45 tokens + let rewards = env + .router + .staking + .get_rewards( + &env.storage, + &env.block, + &delegator_addr_2, + &validator_addr_1, + ) + .unwrap() + .unwrap(); + assert_eq!(45, rewards.amount.u128()); + } + + #[test] + fn execute() { + let mut env = TestEnv::new(vp(10, 100, 1), vp(0, 20, 1)); + + let validator_addr_1 = env.validator_addr_1(); + let validator_addr_2 = env.validator_addr_2(); + let delegator_addr_1 = env.delegator_addr_2(); + let reward_receiver_addr = env.user_addr_1(); + + // initialize balances + init_balance(&mut env, &delegator_addr_1, 1000); + + // delegate 100 tokens to validator 1 + execute_stake( + &mut env, + delegator_addr_1.clone(), + StakingMsg::Delegate { + validator: validator_addr_1.clone(), + amount: coin(100, BONDED_DENOM), + }, + ) + .unwrap(); + + // should now have 100 tokens less + assert_balances(&env, vec![(delegator_addr_1.clone(), 900)]); + + // wait a year + env.block.time = env.block.time.plus_seconds(YEAR); + + // change the withdrawal address + execute_distr( + &mut env, + delegator_addr_1.clone(), + DistributionMsg::SetWithdrawAddress { + address: reward_receiver_addr.to_string(), + }, + ) + .unwrap(); + + // withdraw rewards + execute_distr( + &mut env, + delegator_addr_1.clone(), + DistributionMsg::WithdrawDelegatorReward { + validator: validator_addr_1.clone(), + }, + ) + .unwrap(); + + // withdrawal address received rewards + assert_balances( + &env, + // one year, 10%apr, 10% commission, 100 tokens staked + vec![(reward_receiver_addr, 100 / 10 * 9 / 10)], + ); + + // redelegate to validator 2 + execute_stake( + &mut env, + delegator_addr_1.clone(), + StakingMsg::Redelegate { + src_validator: validator_addr_1, + dst_validator: validator_addr_2.clone(), + amount: coin(100, BONDED_DENOM), + }, + ) + .unwrap(); + + // should have same amount as before (rewards receiver received rewards) + assert_balances(&env, vec![(delegator_addr_1.clone(), 900)]); + + let delegations: AllDelegationsResponse = query_stake( + &env, + StakingQuery::AllDelegations { + delegator: delegator_addr_1.to_string(), + }, + ) + .unwrap(); + assert_eq!( + delegations.delegations, + [Delegation::new( + delegator_addr_1.clone(), + validator_addr_2.clone(), + coin(100, BONDED_DENOM), + )] + ); + + // undelegate all tokens + execute_stake( + &mut env, + delegator_addr_1.clone(), + StakingMsg::Undelegate { + validator: validator_addr_2, + amount: coin(100, BONDED_DENOM), + }, + ) + .unwrap(); + + // wait for unbonding period (60 seconds in default config) + env.block.time = env.block.time.plus_seconds(60); + + // need to manually cause queue to get processed + env.router + .staking + .process_queue(&env.api, &mut env.storage, &env.router, &env.block) + .unwrap(); + + // check bank balance + assert_balances(&env, vec![(delegator_addr_1.clone(), 1000)]); + } + + #[test] + fn can_set_withdraw_address() { + let mut env = TestEnv::new(vp(10, 100, 1), vp(10, 100, 1)); + + let validator_addr_1 = env.validator_addr_1(); + let delegator_addr_1 = env.delegator_addr_1(); + let reward_receiver_addr = env.user_addr_1(); + + // initialize balances + init_balance(&mut env, &delegator_addr_1, 100); + + // stake (delegate) 100 tokens to the validator + execute_stake( + &mut env, + delegator_addr_1.clone(), + StakingMsg::Delegate { + validator: validator_addr_1.clone(), + amount: coin(100, BONDED_DENOM), + }, + ) + .unwrap(); + + // change the receiver of rewards + execute_distr( + &mut env, + delegator_addr_1.clone(), + DistributionMsg::SetWithdrawAddress { + address: reward_receiver_addr.to_string(), + }, + ) + .unwrap(); + + // let one year pass + env.block.time = env.block.time.plus_seconds(YEAR); + + // Withdraw rewards to reward receiver. + execute_distr( + &mut env, + delegator_addr_1.clone(), + DistributionMsg::WithdrawDelegatorReward { + validator: validator_addr_1.clone(), + }, + ) + .unwrap(); + + // Change reward receiver back to delegator. + execute_distr( + &mut env, + delegator_addr_1.clone(), + DistributionMsg::SetWithdrawAddress { + address: delegator_addr_1.to_string(), + }, + ) + .unwrap(); + + // Another year passes. + env.block.time = env.block.time.plus_seconds(YEAR); + + // Withdraw rewards to delegator. + execute_distr( + &mut env, + delegator_addr_1.clone(), + DistributionMsg::WithdrawDelegatorReward { + validator: validator_addr_1, + }, + ) + .unwrap(); + + // one year, 10%apr, 10% commission, 100 tokens staked + let rewards_yr = 100 / 10 * 9 / 10; + + assert_balances( + &env, + vec![ + (reward_receiver_addr, rewards_yr), + (delegator_addr_1, rewards_yr), + ], + ); + } + + #[test] + fn cannot_steal() { + let mut env = TestEnv::new(vp(10, 100, 1), vp(0, 20, 1)); + + let validator_addr_1 = env.validator_addr_1(); + let validator_addr_2 = env.validator_addr_2(); + let delegator_addr_1 = env.delegator_addr_1(); + + // initialize balances + init_balance(&mut env, &delegator_addr_1, 100); + + // delegate 100 tokens to validator 1 + execute_stake( + &mut env, + delegator_addr_1.clone(), + StakingMsg::Delegate { + validator: validator_addr_1.clone(), + amount: coin(100, BONDED_DENOM), + }, + ) + .unwrap(); + + // undelegate more tokens than we have + let error_result = execute_stake( + &mut env, + delegator_addr_1.clone(), + StakingMsg::Undelegate { + validator: validator_addr_1.clone(), + amount: coin(200, BONDED_DENOM), + }, + ) + .unwrap_err(); + assert_eq!(error_result.to_string(), "invalid shares amount"); + + // redelegate more tokens than we have from validator 1 to validator 2 + let error_result = execute_stake( + &mut env, + delegator_addr_1.clone(), + StakingMsg::Redelegate { + src_validator: validator_addr_1, + dst_validator: validator_addr_2.clone(), + amount: coin(200, BONDED_DENOM), + }, + ) + .unwrap_err(); + assert_eq!(error_result.to_string(), "invalid shares amount"); + + // undelegate from non-existing delegation + let error_result = execute_stake( + &mut env, + delegator_addr_1.clone(), + StakingMsg::Undelegate { + validator: validator_addr_2, + amount: coin(100, BONDED_DENOM), + }, + ) + .unwrap_err(); + assert_eq!( + error_result.to_string(), + "no delegation for (address, validator) tuple" + ); + } + + #[test] + fn denom_validation() { + let mut env = TestEnv::new(vp(10, 100, 1), vp(10, 100, 1)); + + let validator_addr_1 = env.validator_addr_1(); + let delegator_addr_1 = env.delegator_addr_1(); + + // init balances + init_balance_denom(&mut env, &delegator_addr_1, 100, "FAKE"); + + // try to delegate 100 to validator + let error_result = execute_stake( + &mut env, + delegator_addr_1.clone(), + StakingMsg::Delegate { + validator: validator_addr_1, + amount: coin(100, "FAKE"), + }, + ) + .unwrap_err(); + assert_eq!( + error_result.to_string(), + "cannot delegate coins of denominator FAKE, only of TOKEN", + ); + } + + #[test] + fn cannot_slash_nonexistent() { + let mut env = TestEnv::new(vp(10, 100, 1), vp(10, 100, 1)); + + let validator_addr_3 = env.validator_addr_3(); + let delegator_addr_1 = env.delegator_addr_1(); + + // init balances + init_balance_denom(&mut env, &delegator_addr_1, 100, "FAKE"); + + // try to delegate 100 to non existing validator + let error_result = env + .router + .staking + .sudo( + &env.api, + &mut env.storage, + &env.router, + &env.block, + StakingSudo::Slash { + validator: validator_addr_3, + percentage: Decimal::percent(50), + }, + ) + .unwrap_err(); + assert_eq!(error_result.to_string(), "validator does not exist"); + } + + #[test] + fn non_existent_validator() { + let mut env = TestEnv::new(vp(10, 100, 1), vp(10, 100, 1)); + + let validator_addr_3 = env.validator_addr_3(); + let delegator_addr_1 = env.delegator_addr_1(); + + // initialize balances + init_balance(&mut env, &delegator_addr_1, 100); + + // try to delegate + let error_result = execute_stake( + &mut env, + delegator_addr_1.clone(), + StakingMsg::Delegate { + validator: validator_addr_3.clone(), + amount: coin(100, BONDED_DENOM), + }, + ) + .unwrap_err(); + assert_eq!(error_result.to_string(), "validator does not exist"); + + // try to undelegate + let error_result = execute_stake( + &mut env, + delegator_addr_1.clone(), + StakingMsg::Undelegate { + validator: validator_addr_3, + amount: coin(100, BONDED_DENOM), + }, + ) + .unwrap_err(); + assert_eq!(error_result.to_string(), "validator does not exist"); + } + + #[test] + fn zero_staking_forbidden() { + let mut env = TestEnv::new(vp(10, 100, 1), vp(10, 100, 1)); + + let validator_addr_1 = env.validator_addr_1(); + let delegator_addr_1 = env.delegator_addr_1(); + + // delegate 0 + let error_result = execute_stake( + &mut env, + delegator_addr_1.clone(), + StakingMsg::Delegate { + validator: validator_addr_1.clone(), + amount: coin(0, BONDED_DENOM), + }, + ) + .unwrap_err(); + assert_eq!(error_result.to_string(), "invalid delegation amount"); + + // undelegate 0 + let error_result = execute_stake( + &mut env, + delegator_addr_1, + StakingMsg::Undelegate { + validator: validator_addr_1, + amount: coin(0, BONDED_DENOM), + }, + ) + .unwrap_err(); + assert_eq!(error_result.to_string(), "invalid shares amount"); + } + + #[test] + fn query_staking() { + let mut env = TestEnv::new(vp(10, 100, 1), vp(0, 1, 1)); + + let validator_addr_1 = env.validator_addr_1(); + let validator_addr_2 = env.validator_addr_2(); + let delegator_addr_1 = env.delegator_addr_1(); + let delegator_addr_2 = env.delegator_addr_2(); + let user_addr_1 = env.user_addr_1(); + + // initialize balances + init_balance(&mut env, &delegator_addr_1, 260); + init_balance(&mut env, &delegator_addr_2, 150); + + // query validators + let valoper1: ValidatorResponse = query_stake( + &env, + StakingQuery::Validator { + address: validator_addr_1.to_string(), + }, + ) + .unwrap(); + let valoper2: ValidatorResponse = query_stake( + &env, + StakingQuery::Validator { + address: validator_addr_2.to_string(), + }, + ) + .unwrap(); + + let validators: AllValidatorsResponse = + query_stake(&env, StakingQuery::AllValidators {}).unwrap(); + assert_eq!( + validators.validators, + [valoper1.validator.unwrap(), valoper2.validator.unwrap()] + ); + + // query non-existent validator + let response = query_stake::( + &env, + StakingQuery::Validator { + address: user_addr_1.to_string(), + }, + ) + .unwrap(); + assert_eq!(response.validator, None); + + // query bonded denom + let response: BondedDenomResponse = + query_stake(&env, StakingQuery::BondedDenom {}).unwrap(); + assert_eq!(response.denom, BONDED_DENOM); + + // delegate some tokens with delegator1 and delegator2 + execute_stake( + &mut env, + delegator_addr_1.clone(), + StakingMsg::Delegate { + validator: validator_addr_1.to_string(), + amount: coin(100, BONDED_DENOM), + }, + ) + .unwrap(); + execute_stake( + &mut env, + delegator_addr_1.clone(), + StakingMsg::Delegate { + validator: validator_addr_2.to_string(), + amount: coin(160, BONDED_DENOM), + }, + ) + .unwrap(); + execute_stake( + &mut env, + delegator_addr_2.clone(), + StakingMsg::Delegate { + validator: validator_addr_1.to_string(), + amount: coin(150, BONDED_DENOM), + }, + ) + .unwrap(); + // unstake some again + execute_stake( + &mut env, + delegator_addr_1.clone(), + StakingMsg::Undelegate { + validator: validator_addr_1.to_string(), + amount: coin(50, BONDED_DENOM), + }, + ) + .unwrap(); + execute_stake( + &mut env, + delegator_addr_2.clone(), + StakingMsg::Undelegate { + validator: validator_addr_1.to_string(), + amount: coin(50, BONDED_DENOM), + }, + ) + .unwrap(); + + // query all delegations + let response1: AllDelegationsResponse = query_stake( + &env, + StakingQuery::AllDelegations { + delegator: delegator_addr_1.to_string(), + }, + ) + .unwrap(); + assert_eq!( + response1.delegations, + vec![ + Delegation::new( + delegator_addr_1.clone(), + validator_addr_1.to_string(), + coin(50, BONDED_DENOM), + ), + Delegation::new( + delegator_addr_1.clone(), + validator_addr_2, + coin(160, BONDED_DENOM), + ), + ] + ); + let response2: DelegationResponse = query_stake( + &env, + StakingQuery::Delegation { + delegator: delegator_addr_2.to_string(), + validator: validator_addr_1.clone(), + }, + ) + .unwrap(); + assert_eq!( + response2.delegation.unwrap(), + FullDelegation::new( + delegator_addr_2.clone(), + validator_addr_1, + coin(100, BONDED_DENOM), + coin(100, BONDED_DENOM), + vec![], + ), + ); + } + + #[test] + fn delegation_queries_unbonding() { + let mut env = TestEnv::new(vp(10, 100, 1), vp(10, 100, 1)); + + let validator_addr_1 = env.validator_addr_1(); + let delegator_addr_1 = env.delegator_addr_1(); + let delegator_addr_2 = env.delegator_addr_2(); + + // initialize balances + init_balance(&mut env, &delegator_addr_1, 100); + init_balance(&mut env, &delegator_addr_2, 150); + + // delegate some tokens with delegator1 and delegator2 + execute_stake( + &mut env, + delegator_addr_1.clone(), + StakingMsg::Delegate { + validator: validator_addr_1.to_string(), + amount: coin(100, BONDED_DENOM), + }, + ) + .unwrap(); + execute_stake( + &mut env, + delegator_addr_2.clone(), + StakingMsg::Delegate { + validator: validator_addr_1.to_string(), + amount: coin(150, BONDED_DENOM), + }, + ) + .unwrap(); + // unstake some of delegator1's stake + execute_stake( + &mut env, + delegator_addr_1.clone(), + StakingMsg::Undelegate { + validator: validator_addr_1.to_string(), + amount: coin(50, BONDED_DENOM), + }, + ) + .unwrap(); + // unstake all of delegator2's stake + execute_stake( + &mut env, + delegator_addr_2.clone(), + StakingMsg::Undelegate { + validator: validator_addr_1.to_string(), + amount: coin(150, BONDED_DENOM), + }, + ) + .unwrap(); + + // query all delegations + let response1: AllDelegationsResponse = query_stake( + &env, + StakingQuery::AllDelegations { + delegator: delegator_addr_1.to_string(), + }, + ) + .unwrap(); + assert_eq!( + response1.delegations, + vec![Delegation::new( + delegator_addr_1.clone(), + validator_addr_1.to_string(), + coin(50, BONDED_DENOM), + )] + ); + let response2: DelegationResponse = query_stake( + &env, + StakingQuery::Delegation { + delegator: delegator_addr_2.to_string(), + validator: validator_addr_1.to_string(), + }, + ) + .unwrap(); + assert_eq!(response2.delegation, None); + + // unstake rest of delegator1's stake in two steps + execute_stake( + &mut env, + delegator_addr_1.clone(), + StakingMsg::Undelegate { + validator: validator_addr_1.to_string(), + amount: coin(25, BONDED_DENOM), + }, + ) + .unwrap(); + env.block.time = env.block.time.plus_seconds(10); + execute_stake( + &mut env, + delegator_addr_1.clone(), + StakingMsg::Undelegate { + validator: validator_addr_1.to_string(), + amount: coin(25, BONDED_DENOM), + }, + ) + .unwrap(); + + // query all delegations again + let response1: DelegationResponse = query_stake( + &env, + StakingQuery::Delegation { + delegator: delegator_addr_1.to_string(), + validator: validator_addr_1, + }, + ) + .unwrap(); + let response2: AllDelegationsResponse = query_stake( + &env, + StakingQuery::AllDelegations { + delegator: delegator_addr_1.to_string(), + }, + ) + .unwrap(); + assert_eq!( + response1.delegation, None, + "delegator1 should have no delegations left" + ); + assert_eq!(response2.delegations, vec![]); + } + + #[test] + fn partial_unbonding_reduces_stake() { + let mut env = TestEnv::new(vp(10, 100, 1), vp(10, 100, 1)); + + let validator_addr_1 = env.validator_addr_1(); + let delegator_addr_1 = env.delegator_addr_1(); + + // initialize balances + init_balance(&mut env, &delegator_addr_1, 100); + + // delegate all tokens + execute_stake( + &mut env, + delegator_addr_1.clone(), + StakingMsg::Delegate { + validator: validator_addr_1.to_string(), + amount: coin(100, BONDED_DENOM), + }, + ) + .unwrap(); + // unstake in multiple steps + execute_stake( + &mut env, + delegator_addr_1.clone(), + StakingMsg::Undelegate { + validator: validator_addr_1.to_string(), + amount: coin(50, BONDED_DENOM), + }, + ) + .unwrap(); + env.block.time = env.block.time.plus_seconds(10); + execute_stake( + &mut env, + delegator_addr_1.clone(), + StakingMsg::Undelegate { + validator: validator_addr_1.to_string(), + amount: coin(30, BONDED_DENOM), + }, + ) + .unwrap(); + env.block.time = env.block.time.plus_seconds(10); + execute_stake( + &mut env, + delegator_addr_1.clone(), + StakingMsg::Undelegate { + validator: validator_addr_1.to_string(), + amount: coin(20, BONDED_DENOM), + }, + ) + .unwrap(); + + // wait for first unbonding to complete (but not the others) and process queue + env.block.time = env.block.time.plus_seconds(40); + env.router + .staking + .process_queue(&env.api, &mut env.storage, &env.router, &env.block) + .unwrap(); + + // query delegations + // we now have 0 stake, 50 unbonding and 50 completed unbonding + let response1: DelegationResponse = query_stake( + &env, + StakingQuery::Delegation { + delegator: delegator_addr_1.to_string(), + validator: validator_addr_1.to_string(), + }, + ) + .unwrap(); + let response2: AllDelegationsResponse = query_stake( + &env, + StakingQuery::AllDelegations { + delegator: delegator_addr_1.to_string(), + }, + ) + .unwrap(); + assert_eq!(response1.delegation, None); + assert_eq!(response2.delegations, vec![]); + + // wait for the rest to complete + env.block.time = env.block.time.plus_seconds(20); + env.router + .staking + .process_queue(&env.api, &mut env.storage, &env.router, &env.block) + .unwrap(); + + // query delegations again + let response1: DelegationResponse = query_stake( + &env, + StakingQuery::Delegation { + delegator: delegator_addr_1.to_string(), + validator: validator_addr_1, + }, + ) + .unwrap(); + let response2: AllDelegationsResponse = query_stake( + &env, + StakingQuery::AllDelegations { + delegator: delegator_addr_1.to_string(), + }, + ) + .unwrap(); + assert_eq!( + response1.delegation, None, + "delegator should have nothing left" + ); + assert!(response2.delegations.is_empty()); + } + + #[test] + fn delegations_slashed() { + let mut env = TestEnv::new(vp(10, 100, 1), vp(10, 100, 1)); + + let validator_addr_1 = env.validator_addr_1(); + let delegator_addr_1 = env.delegator_addr_1(); + + // initialize balances + init_balance(&mut env, &delegator_addr_1, 333); + + // stake (delegate) some tokens + execute_stake( + &mut env, + delegator_addr_1.clone(), + StakingMsg::Delegate { + validator: validator_addr_1.to_string(), + amount: coin(333, BONDED_DENOM), + }, + ) + .unwrap(); + + // unstake (undelegate) some tokens + execute_stake( + &mut env, + delegator_addr_1.clone(), + StakingMsg::Undelegate { + validator: validator_addr_1.to_string(), + amount: coin(111, BONDED_DENOM), + }, + ) + .unwrap(); + + // slash validator + env.router + .staking + .sudo( + &env.api, + &mut env.storage, + &env.router, + &env.block, + StakingSudo::Slash { + validator: validator_addr_1.to_string(), + percentage: Decimal::percent(50), + }, + ) + .unwrap(); + + // query all delegations + let response1: AllDelegationsResponse = query_stake( + &env, + StakingQuery::AllDelegations { + delegator: delegator_addr_1.to_string(), + }, + ) + .unwrap(); + assert_eq!( + response1.delegations[0], + Delegation::new( + delegator_addr_1.clone(), + validator_addr_1, + coin(111, BONDED_DENOM), + ) + ); + + // wait until unbonding is complete and check if amount was slashed + env.block.time = env.block.time.plus_seconds(60); + env.router + .staking + .process_queue(&env.api, &mut env.storage, &env.router, &env.block) + .unwrap(); + let balance = + QuerierWrapper::::new(&env.router.querier(&env.api, &env.storage, &env.block)) + .query_balance(delegator_addr_1, BONDED_DENOM) + .unwrap(); + assert_eq!(55, balance.amount.u128()); + } + + #[test] + fn rewards_initial_wait() { + let mut env = TestEnv::new(vp(0, 100, 1), vp(0, 100, 1)); + + let validator_addr_1 = env.validator_addr_1(); + let delegator_addr_1 = env.delegator_addr_1(); + + // initialize balances + init_balance(&mut env, &delegator_addr_1, 100); + + // wait one year before staking + env.block.time = env.block.time.plus_seconds(YEAR); + + // stake (delegate) 100 tokens to validator + execute_stake( + &mut env, + delegator_addr_1.clone(), + StakingMsg::Delegate { + validator: validator_addr_1.to_string(), + amount: coin(100, BONDED_DENOM), + }, + ) + .unwrap(); + + // wait another year + env.block.time = env.block.time.plus_seconds(YEAR); + + // query rewards + let response: DelegationResponse = query_stake( + &env, + StakingQuery::Delegation { + delegator: delegator_addr_1.to_string(), + validator: validator_addr_1, + }, + ) + .unwrap(); + + assert_eq!( + response.delegation.unwrap().accumulated_rewards, + vec![coin(10, BONDED_DENOM)] // 10% of 100 + ); } } diff --git a/src/stargate.rs b/src/stargate.rs new file mode 100644 index 00000000..7228d8a4 --- /dev/null +++ b/src/stargate.rs @@ -0,0 +1,145 @@ +//! # Handler for `CosmosMsg::Stargate`, `CosmosMsg::Any`, `QueryRequest::Stargate` and `QueryRequest::Grpc` messages + +use crate::error::AnyResult; +use crate::{AppResponse, CosmosRouter}; +use anyhow::bail; +use cosmwasm_std::{ + to_json_binary, Addr, AnyMsg, Api, Binary, BlockInfo, CustomMsg, CustomQuery, Empty, GrpcQuery, + Querier, Storage, +}; +use serde::de::DeserializeOwned; + +/// Interface of handlers for processing `Stargate`/`Any` message variants +/// and `Stargate`/`Grpc` queries. +pub trait Stargate { + /// Processes `CosmosMsg::Stargate` message variant. + fn execute_stargate( + &self, + _api: &dyn Api, + _storage: &mut dyn Storage, + _router: &dyn CosmosRouter, + _block: &BlockInfo, + sender: Addr, + type_url: String, + value: Binary, + ) -> AnyResult + where + ExecC: CustomMsg + DeserializeOwned + 'static, + QueryC: CustomQuery + DeserializeOwned + 'static, + { + bail!( + "Unexpected stargate execute: type_url={}, value={} from {}", + type_url, + value, + sender, + ) + } + + /// Processes `QueryRequest::Stargate` query. + fn query_stargate( + &self, + _api: &dyn Api, + _storage: &dyn Storage, + _querier: &dyn Querier, + _block: &BlockInfo, + path: String, + data: Binary, + ) -> AnyResult { + bail!("Unexpected stargate query: path={}, data={}", path, data) + } + + /// Processes `CosmosMsg::Any` message variant. + fn execute_any( + &self, + _api: &dyn Api, + _storage: &mut dyn Storage, + _router: &dyn CosmosRouter, + _block: &BlockInfo, + sender: Addr, + msg: AnyMsg, + ) -> AnyResult + where + ExecC: CustomMsg + DeserializeOwned + 'static, + QueryC: CustomQuery + DeserializeOwned + 'static, + { + bail!("Unexpected any execute: msg={:?} from {}", msg, sender) + } + + /// Processes `QueryRequest::Grpc` query. + fn query_grpc( + &self, + _api: &dyn Api, + _storage: &dyn Storage, + _querier: &dyn Querier, + _block: &BlockInfo, + request: GrpcQuery, + ) -> AnyResult { + bail!("Unexpected grpc query: request={:?}", request) + } +} + +/// Always failing handler for `Stargate`/`Any` message variants and `Stargate`/`Grpc` queries. +pub struct StargateFailing; + +impl Stargate for StargateFailing {} + +/// Always accepting handler for `Stargate`/`Any` message variants and `Stargate`/`Grpc` queries. +pub struct StargateAccepting; + +impl Stargate for StargateAccepting { + fn execute_stargate( + &self, + _api: &dyn Api, + _storage: &mut dyn Storage, + _router: &dyn CosmosRouter, + _block: &BlockInfo, + _sender: Addr, + _type_url: String, + _value: Binary, + ) -> AnyResult + where + ExecC: CustomMsg + DeserializeOwned + 'static, + QueryC: CustomQuery + DeserializeOwned + 'static, + { + Ok(AppResponse::default()) + } + + fn query_stargate( + &self, + _api: &dyn Api, + _storage: &dyn Storage, + _querier: &dyn Querier, + _block: &BlockInfo, + _path: String, + _data: Binary, + ) -> AnyResult { + to_json_binary(&Empty {}).map_err(Into::into) + } + + fn execute_any( + &self, + _api: &dyn Api, + _storage: &mut dyn Storage, + _router: &dyn CosmosRouter, + _block: &BlockInfo, + _sender: Addr, + _msg: AnyMsg, + ) -> AnyResult + where + ExecC: CustomMsg + DeserializeOwned + 'static, + QueryC: CustomQuery + DeserializeOwned + 'static, + { + Ok(AppResponse::default()) + } + + fn query_grpc( + &self, + _api: &dyn Api, + _storage: &dyn Storage, + _querier: &dyn Querier, + _block: &BlockInfo, + _request: GrpcQuery, + ) -> AnyResult { + Ok(Binary::default()) + } +} diff --git a/src/test_helpers/caller.rs b/src/test_helpers/caller.rs index c283cda5..691c3752 100644 --- a/src/test_helpers/caller.rs +++ b/src/test_helpers/caller.rs @@ -1,9 +1,8 @@ use crate::{Contract, ContractWrapper}; use cosmwasm_std::{ - Binary, Deps, DepsMut, Empty, Env, MessageInfo, Response, StdError, SubMsg, WasmMsg, + Binary, CustomMsg, Deps, DepsMut, Empty, Env, MessageInfo, Response, StdError, SubMsg, WasmMsg, }; -use schemars::JsonSchema; -use std::fmt::Debug; +use serde::de::DeserializeOwned; fn instantiate( _deps: DepsMut, @@ -33,7 +32,7 @@ fn query(_deps: Deps, _env: Env, _msg: Empty) -> Result { pub fn contract() -> Box> where - C: Clone + Debug + PartialEq + JsonSchema + 'static, + C: CustomMsg + DeserializeOwned + 'static, { let contract = ContractWrapper::new_with_empty(execute, instantiate, query); Box::new(contract) diff --git a/src/test_helpers/echo.rs b/src/test_helpers/echo.rs index 02ab2696..5da47d26 100644 --- a/src/test_helpers/echo.rs +++ b/src/test_helpers/echo.rs @@ -1,28 +1,26 @@ //! Very simple echoing contract which just returns incoming string if any, //! but performing sub call of given message to test response. //! -//! Additionally it bypasses all events and attributes send to it. +//! Additionally, it bypasses all events and attributes send to it. use crate::{Contract, ContractWrapper}; +use cosmwasm_schema::cw_serde; use cosmwasm_std::{ - to_json_binary, Attribute, Binary, Deps, DepsMut, Empty, Env, Event, MessageInfo, Reply, - Response, StdError, SubMsg, SubMsgResponse, SubMsgResult, + to_json_binary, Attribute, Binary, CustomMsg, Deps, DepsMut, Empty, Env, Event, MessageInfo, + Reply, Response, StdError, SubMsg, SubMsgResponse, SubMsgResult, }; use cw_utils::{parse_execute_response_data, parse_instantiate_response_data}; -use derivative::Derivative; -use schemars::JsonSchema; -use serde::{de::DeserializeOwned, Deserialize, Serialize}; -use std::fmt::Debug; +use serde::de::DeserializeOwned; // Choosing a reply id less than ECHO_EXECUTE_BASE_ID indicates an Instantiate message reply by convention. // An Execute message reply otherwise. pub const EXECUTE_REPLY_BASE_ID: u64 = i64::MAX as u64; -#[derive(Debug, Clone, Serialize, Deserialize, Derivative)] -#[derivative(Default(bound = "", new = "true"))] +#[cw_serde] +#[derive(Default)] pub struct Message where - ExecC: Debug + PartialEq + Clone + JsonSchema + 'static, + ExecC: CustomMsg + 'static, { pub data: Option, pub sub_msg: Vec>, @@ -30,18 +28,16 @@ where pub events: Vec, } -// This can take some data... but happy to accept {} -#[derive(Debug, Clone, Serialize, Deserialize, Derivative)] -#[derivative(Default(bound = "", new = "true"))] +#[cw_serde] +#[derive(Default)] pub struct InitMessage where - ExecC: Debug + PartialEq + Clone + JsonSchema + 'static, + ExecC: CustomMsg + 'static, { pub data: Option, pub sub_msg: Option>>, } -#[allow(clippy::unnecessary_wraps)] fn instantiate( _deps: DepsMut, _env: Env, @@ -49,7 +45,7 @@ fn instantiate( msg: InitMessage, ) -> Result, StdError> where - ExecC: Debug + PartialEq + Clone + JsonSchema + 'static, + ExecC: CustomMsg + 'static, { let mut res = Response::new(); if let Some(data) = msg.data { @@ -61,7 +57,6 @@ where Ok(res) } -#[allow(clippy::unnecessary_wraps)] fn execute( _deps: DepsMut, _env: Env, @@ -69,7 +64,7 @@ fn execute( msg: Message, ) -> Result, StdError> where - ExecC: Debug + PartialEq + Clone + JsonSchema + 'static, + ExecC: CustomMsg + 'static, { let mut resp = Response::new(); @@ -87,17 +82,18 @@ fn query(_deps: Deps, _env: Env, msg: Empty) -> Result { to_json_binary(&msg) } -#[allow(clippy::unnecessary_wraps)] fn reply(_deps: DepsMut, _env: Env, msg: Reply) -> Result, StdError> where - ExecC: Debug + PartialEq + Clone + JsonSchema + 'static, + ExecC: CustomMsg + 'static, { let res = Response::new(); + #[allow(deprecated)] if let Reply { id, result: SubMsgResult::Ok(SubMsgResponse { data: Some(data), .. }), + .. } = msg { // We parse out the WasmMsg::Execute wrapper... @@ -130,7 +126,7 @@ pub fn contract() -> Box> { pub fn custom_contract() -> Box> where - C: Clone + Debug + PartialEq + JsonSchema + DeserializeOwned + 'static, + C: CustomMsg + DeserializeOwned + 'static, { let contract = ContractWrapper::new(execute::, instantiate::, query).with_reply(reply::); diff --git a/src/test_helpers/error.rs b/src/test_helpers/error.rs index 033853de..7d0b7eb1 100644 --- a/src/test_helpers/error.rs +++ b/src/test_helpers/error.rs @@ -1,7 +1,6 @@ use crate::{Contract, ContractWrapper}; -use cosmwasm_std::{Binary, Deps, DepsMut, Empty, Env, MessageInfo, Response, StdError}; -use schemars::JsonSchema; -use std::fmt::Debug; +use cosmwasm_std::{Binary, CustomMsg, Deps, DepsMut, Empty, Env, MessageInfo, Response, StdError}; +use serde::de::DeserializeOwned; fn instantiate_err( _deps: DepsMut, @@ -36,7 +35,7 @@ fn query(_deps: Deps, _env: Env, _msg: Empty) -> Result { pub fn contract(instantiable: bool) -> Box> where - C: Clone + Debug + PartialEq + JsonSchema + 'static, + C: CustomMsg + DeserializeOwned + 'static, { let contract = if instantiable { ContractWrapper::new_with_empty(execute, instantiate_ok, query) diff --git a/src/test_helpers/gov.rs b/src/test_helpers/gov.rs new file mode 100644 index 00000000..0c885c3a --- /dev/null +++ b/src/test_helpers/gov.rs @@ -0,0 +1,27 @@ +use crate::{Contract, ContractWrapper}; +use cosmwasm_std::{ + Binary, CosmosMsg, Deps, DepsMut, Empty, Env, GovMsg, MessageInfo, Response, StdResult, +}; + +fn instantiate(_deps: DepsMut, _env: Env, _info: MessageInfo, _msg: Empty) -> StdResult { + Ok(Response::new()) +} + +fn execute(_deps: DepsMut, _env: Env, _info: MessageInfo, _msg: Empty) -> StdResult { + let msg: CosmosMsg = GovMsg::Vote { + proposal_id: 1, + option: cosmwasm_std::VoteOption::No, + } + .into(); + let resp = Response::new().add_message(msg); + Ok(resp) +} + +fn query(_deps: Deps, _env: Env, _msg: Empty) -> StdResult { + Ok(Binary::default()) +} + +pub fn contract() -> Box> { + let contract = ContractWrapper::new(execute, instantiate, query); + Box::new(contract) +} diff --git a/src/test_helpers/hackatom.rs b/src/test_helpers/hackatom.rs index 394c7351..ebc9a7c9 100644 --- a/src/test_helpers/hackatom.rs +++ b/src/test_helpers/hackatom.rs @@ -1,27 +1,26 @@ //! Simplified contract which when executed releases the funds to beneficiary use crate::{Contract, ContractWrapper}; +use cosmwasm_schema::cw_serde; use cosmwasm_std::{ - to_json_binary, BankMsg, Binary, Deps, DepsMut, Empty, Env, MessageInfo, Response, StdError, + to_json_binary, BankMsg, Binary, CustomMsg, Deps, DepsMut, Empty, Env, MessageInfo, Response, + StdError, }; use cw_storage_plus::Item; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use std::fmt::Debug; +use serde::de::DeserializeOwned; -#[derive(Debug, Clone, Serialize, Deserialize)] +#[cw_serde] pub struct InstantiateMsg { pub beneficiary: String, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[cw_serde] pub struct MigrateMsg { - // just use some other string so we see there are other types + // just use some other string, so we see there are other types pub new_guy: String, } -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] +#[cw_serde] pub enum QueryMsg { // returns InstantiateMsg Beneficiary {}, @@ -77,7 +76,7 @@ pub fn contract() -> Box> { #[allow(dead_code)] pub fn custom_contract() -> Box> where - C: Clone + Debug + PartialEq + JsonSchema + 'static, + C: CustomMsg + DeserializeOwned + 'static, { let contract = ContractWrapper::new_with_empty(execute, instantiate, query).with_migrate_empty(migrate); diff --git a/src/test_helpers/ibc.rs b/src/test_helpers/ibc.rs new file mode 100644 index 00000000..402edda6 --- /dev/null +++ b/src/test_helpers/ibc.rs @@ -0,0 +1,26 @@ +use crate::{Contract, ContractWrapper}; +use cosmwasm_std::{ + Binary, CosmosMsg, Deps, DepsMut, Empty, Env, IbcMsg, MessageInfo, Response, StdResult, +}; + +fn instantiate(_deps: DepsMut, _env: Env, _info: MessageInfo, _msg: Empty) -> StdResult { + Ok(Response::new()) +} + +fn execute(_deps: DepsMut, _env: Env, _info: MessageInfo, _msg: Empty) -> StdResult { + let msg: CosmosMsg = IbcMsg::CloseChannel { + channel_id: "channel".to_string(), + } + .into(); + let resp = Response::new().add_message(msg); + Ok(resp) +} + +fn query(_deps: Deps, _env: Env, _msg: Empty) -> StdResult { + Ok(Binary::default()) +} + +pub fn contract() -> Box> { + let contract = ContractWrapper::new(execute, instantiate, query); + Box::new(contract) +} diff --git a/src/test_helpers/mod.rs b/src/test_helpers/mod.rs index f67efbc5..c99736c9 100644 --- a/src/test_helpers/mod.rs +++ b/src/test_helpers/mod.rs @@ -1,24 +1,38 @@ #![cfg(test)] +use cosmwasm_schema::cw_serde; +use cosmwasm_std::CustomMsg; use cw_storage_plus::Item; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; pub mod caller; pub mod echo; pub mod error; +#[cfg(feature = "stargate")] +pub mod gov; pub mod hackatom; +#[cfg(feature = "stargate")] +pub mod ibc; pub mod payout; pub mod reflect; +#[cfg(feature = "stargate")] pub mod stargate; /// Custom message for testing purposes. -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[cw_serde] +#[derive(Default)] #[serde(rename = "snake_case")] -pub enum CustomMsg { - SetName { name: String }, - SetAge { age: u32 }, +pub enum CustomHelperMsg { + SetName { + name: String, + }, + SetAge { + age: u32, + }, + #[default] + NoOp, } +impl CustomMsg for CustomHelperMsg {} + /// Persisted counter for testing purposes. pub const COUNT: Item = Item::new("count"); diff --git a/src/test_helpers/payout.rs b/src/test_helpers/payout.rs index a1d13ea2..a2f58cd3 100644 --- a/src/test_helpers/payout.rs +++ b/src/test_helpers/payout.rs @@ -1,31 +1,30 @@ use crate::test_helpers::COUNT; use crate::{Contract, ContractWrapper}; +use cosmwasm_schema::cw_serde; use cosmwasm_std::{ - to_json_binary, BankMsg, Binary, Coin, Deps, DepsMut, Empty, Env, MessageInfo, Response, - StdError, + to_json_binary, BankMsg, Binary, Coin, CustomMsg, Deps, DepsMut, Empty, Env, MessageInfo, + Response, StdError, }; use cw_storage_plus::Item; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use std::fmt::Debug; +use serde::de::DeserializeOwned; -#[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[cw_serde] pub struct InstantiateMessage { pub payout: Coin, } -#[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[cw_serde] pub struct SudoMsg { pub set_count: u32, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[cw_serde] pub enum QueryMsg { Count {}, Payout {}, } -#[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[cw_serde] pub struct CountResponse { pub count: u32, } @@ -76,7 +75,7 @@ fn query(deps: Deps, _env: Env, msg: QueryMsg) -> Result { pub fn contract() -> Box> where - C: Clone + Debug + PartialEq + JsonSchema + 'static, + C: CustomMsg + DeserializeOwned + 'static, { let contract = ContractWrapper::new_with_empty(execute, instantiate, query).with_sudo_empty(sudo); diff --git a/src/test_helpers/reflect.rs b/src/test_helpers/reflect.rs index c7b37407..2bfb59ab 100644 --- a/src/test_helpers/reflect.rs +++ b/src/test_helpers/reflect.rs @@ -1,18 +1,18 @@ -use crate::test_helpers::{payout, CustomMsg, COUNT}; +use crate::test_helpers::{payout, CustomHelperMsg, COUNT}; use crate::{Contract, ContractWrapper}; +use cosmwasm_schema::cw_serde; use cosmwasm_std::{ to_json_binary, Binary, Deps, DepsMut, Empty, Env, Event, MessageInfo, Reply, Response, StdError, SubMsg, }; use cw_storage_plus::Map; -use serde::{Deserialize, Serialize}; -#[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[cw_serde] pub struct Message { - pub messages: Vec>, + pub messages: Vec>, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[cw_serde] pub enum QueryMsg { Count {}, Reply { id: u64 }, @@ -25,7 +25,7 @@ fn instantiate( _env: Env, _info: MessageInfo, _msg: Empty, -) -> Result, StdError> { +) -> Result, StdError> { COUNT.save(deps.storage, &0)?; Ok(Response::default()) } @@ -35,7 +35,7 @@ fn execute( _env: Env, _info: MessageInfo, msg: Message, -) -> Result, StdError> { +) -> Result, StdError> { COUNT.update::<_, StdError>(deps.storage, |old| Ok(old + 1))?; Ok(Response::new().add_submessages(msg.messages)) @@ -55,7 +55,7 @@ fn query(deps: Deps, _env: Env, msg: QueryMsg) -> Result { } } -fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result, StdError> { +fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result, StdError> { REFLECT.save(deps.storage, msg.id, &msg)?; // add custom event here to test let event = Event::new("custom") @@ -64,7 +64,7 @@ fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result, St Ok(Response::new().add_event(event)) } -pub fn contract() -> Box> { +pub fn contract() -> Box> { let contract = ContractWrapper::new(execute, instantiate, query).with_reply(reply); Box::new(contract) } diff --git a/src/test_helpers/stargate.rs b/src/test_helpers/stargate.rs index 0a31efcb..a4165354 100644 --- a/src/test_helpers/stargate.rs +++ b/src/test_helpers/stargate.rs @@ -1,35 +1,19 @@ use crate::{Contract, ContractWrapper}; use cosmwasm_std::{ - Binary, CosmosMsg, Deps, DepsMut, Empty, Env, GovMsg, IbcMsg, MessageInfo, Response, StdResult, + Binary, CosmosMsg, Deps, DepsMut, Empty, Env, MessageInfo, Response, StdResult, }; -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum ExecMsg { - Ibc {}, - Gov {}, -} fn instantiate(_deps: DepsMut, _env: Env, _info: MessageInfo, _msg: Empty) -> StdResult { Ok(Response::new()) } -fn execute(_deps: DepsMut, _env: Env, _info: MessageInfo, msg: ExecMsg) -> StdResult { - let msg: CosmosMsg = if let ExecMsg::Ibc {} = msg { - IbcMsg::CloseChannel { - channel_id: "channel".to_string(), - } - .into() - } else { - GovMsg::Vote { - proposal_id: 1, - vote: cosmwasm_std::VoteOption::No, - } - .into() +fn execute(_deps: DepsMut, _env: Env, _info: MessageInfo, _msg: Empty) -> StdResult { + #[allow(deprecated)] + let msg = CosmosMsg::Stargate { + type_url: "/this.is.a.stargate.test.helper".to_string(), + value: Default::default(), }; - - let resp = Response::new().add_message(msg); - Ok(resp) + Ok(Response::new().add_message(msg)) } fn query(_deps: Deps, _env: Env, _msg: Empty) -> StdResult { diff --git a/src/tests/mod.rs b/src/tests/mod.rs index 2a190c34..a313b471 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -1,6 +1,39 @@ #![cfg(test)] +use cosmwasm_std::Empty; +use cw_orch::{ + daemon::{networks::XION_TESTNET_1, RUNTIME}, + prelude::ChainInfo, +}; + +use crate::{ + no_init, + wasm_emulation::{channel::RemoteChannel, query::ContainsRemote}, + App, AppBuilder, WasmKeeper, +}; +pub const CHAIN: ChainInfo = XION_TESTNET_1; +pub fn remote_channel() -> RemoteChannel { + RemoteChannel::new( + &RUNTIME, + CHAIN.grpc_urls, + CHAIN.chain_id, + CHAIN.network_info.pub_address_prefix, + ) + .unwrap() +} + +pub fn default_app() -> App { + let remote_channel = remote_channel(); + let wasm = WasmKeeper::::new().with_remote(remote_channel.clone()); + AppBuilder::default() + .with_wasm(wasm) + .with_remote(remote_channel.clone()) + .build(no_init) +} + mod test_app; mod test_custom_handler; +mod test_error; mod test_gov; mod test_ibc; +mod test_stargate; diff --git a/src/tests/test_app.rs b/src/tests/test_app.rs index dad2e1b6..757e5dd2 100644 --- a/src/tests/test_app.rs +++ b/src/tests/test_app.rs @@ -1,20 +1,24 @@ -use crate::app::no_init; use crate::custom_handler::CachingCustomHandler; use crate::error::{bail, AnyResult}; +use crate::featured::staking::{Distribution, Staking}; use crate::test_helpers::echo::EXECUTE_REPLY_BASE_ID; -use crate::test_helpers::{caller, echo, error, hackatom, payout, reflect, CustomMsg}; +use crate::test_helpers::{caller, echo, error, hackatom, payout, reflect, CustomHelperMsg}; +use crate::tests::default_app; +use crate::tests::remote_channel; use crate::transactions::{transactional, StorageTransaction}; use crate::wasm::ContractData; -use crate::AppBuilder; +use crate::wasm_emulation::query::ContainsRemote; use crate::{ - custom_app, next_block, App, AppResponse, Bank, CosmosRouter, Distribution, Executor, Module, - Router, Staking, Wasm, WasmSudo, + custom_app, next_block, no_init, App, AppResponse, Bank, CosmosRouter, Executor, Module, + Router, Wasm, WasmSudo, }; +use crate::{AppBuilder, IntoAddr}; use cosmwasm_std::testing::{mock_env, MockQuerier}; use cosmwasm_std::{ coin, coins, from_json, to_json_binary, Addr, AllBalanceResponse, Api, Attribute, BankMsg, - BankQuery, Binary, BlockInfo, Coin, CosmosMsg, CustomQuery, Empty, Event, OverflowError, - OverflowOperation, Querier, Reply, StdError, StdResult, Storage, SubMsg, WasmMsg, + BankQuery, Binary, BlockInfo, Coin, CosmosMsg, CustomMsg, CustomQuery, Empty, Event, + OverflowError, OverflowOperation, Querier, Reply, StdError, StdResult, Storage, SubMsg, + WasmMsg, }; use cw_storage_plus::Item; use cw_utils::parse_instantiate_response_data; @@ -29,7 +33,7 @@ fn get_balance( addr: &Addr, ) -> Vec where - CustomT::ExecT: Clone + Debug + PartialEq + JsonSchema + DeserializeOwned + 'static, + CustomT::ExecT: CustomMsg + DeserializeOwned + 'static, CustomT::QueryT: CustomQuery + DeserializeOwned + 'static, WasmT: Wasm, BankT: Bank, @@ -40,14 +44,14 @@ where app.wrap().query_all_balances(addr).unwrap() } -fn query_router( - router: &Router, +fn query_router( + router: &Router, api: &dyn Api, storage: &dyn Storage, rcpt: &Addr, ) -> Vec where - CustomT::ExecT: Clone + Debug + PartialEq + JsonSchema, + CustomT::ExecT: CustomMsg, CustomT::QueryT: CustomQuery + DeserializeOwned, WasmT: Wasm, BankT: Bank, @@ -73,7 +77,7 @@ fn query_app( rcpt: &Addr, ) -> Vec where - CustomT::ExecT: Debug + PartialEq + Clone + JsonSchema + DeserializeOwned + 'static, + CustomT::ExecT: CustomMsg + DeserializeOwned + 'static, CustomT::QueryT: CustomQuery + DeserializeOwned + 'static, WasmT: Wasm, BankT: Bank, @@ -91,35 +95,40 @@ where val.amount } +/// Utility function for generating user addresses. +fn addr_make(addr: &str) -> Addr { + addr.into_addr() +} + #[test] fn update_block() { - let mut app = App::default(); - + let mut app = default_app(); let BlockInfo { time, height, .. } = app.block_info(); app.update_block(next_block); - assert_eq!(time.plus_seconds(5), app.block_info().time); assert_eq!(height + 1, app.block_info().height); } #[test] fn multi_level_bank_cache() { + // prepare user addresses + let owner_addr = addr_make("owner"); + let recipient_addr = addr_make("recipient"); + // set personal balance - let owner = Addr::unchecked("owner"); - let rcpt = Addr::unchecked("recipient"); let init_funds = vec![coin(20, "btc"), coin(100, "eth")]; - let mut app = App::new(|router, _, storage| { router .bank - .init_balance(storage, &owner, init_funds) + .init_balance(storage, &owner_addr, init_funds) .unwrap(); - }); + }) + .with_remote(remote_channel()); // cache 1 - send some tokens let mut cache = StorageTransaction::new(app.storage()); let msg = BankMsg::Send { - to_address: rcpt.clone().into(), + to_address: recipient_addr.clone().into(), amount: coins(25, "eth"), }; app.router() @@ -127,31 +136,31 @@ fn multi_level_bank_cache() { app.api(), &mut cache, &app.block_info(), - owner.clone(), + owner_addr.clone(), msg.into(), ) .unwrap(); // shows up in cache - let cached_rcpt = query_router(app.router(), app.api(), &cache, &rcpt); + let cached_rcpt = query_router(app.router(), app.api(), &cache, &recipient_addr); assert_eq!(coins(25, "eth"), cached_rcpt); - let router_rcpt = query_app(&app, &rcpt); + let router_rcpt = query_app(&app, &recipient_addr); assert_eq!(router_rcpt, vec![]); // now, second level cache transactional(&mut cache, |cache2, read| { let msg = BankMsg::Send { - to_address: rcpt.clone().into(), + to_address: recipient_addr.clone().into(), amount: coins(12, "eth"), }; app.router() - .execute(app.api(), cache2, &app.block_info(), owner, msg.into()) + .execute(app.api(), cache2, &app.block_info(), owner_addr, msg.into()) .unwrap(); // shows up in 2nd cache - let cached_rcpt = query_router(app.router(), app.api(), read, &rcpt); + let cached_rcpt = query_router(app.router(), app.api(), read, &recipient_addr); assert_eq!(coins(25, "eth"), cached_rcpt); - let cached2_rcpt = query_router(app.router(), app.api(), cache2, &rcpt); + let cached2_rcpt = query_router(app.router(), app.api(), cache2, &recipient_addr); assert_eq!(coins(37, "eth"), cached2_rcpt); Ok(()) }) @@ -160,14 +169,14 @@ fn multi_level_bank_cache() { // apply first to router cache.prepare().commit(app.storage_mut()); - let committed = query_app(&app, &rcpt); + let committed = query_app(&app, &recipient_addr); assert_eq!(coins(37, "eth"), committed); } #[test] fn duplicate_contract_code() { // set up the multi-test application - let mut app = App::default(); + let mut app = default_app(); // store the original contract code let code_id = app.store_code(payout::contract()); @@ -186,63 +195,68 @@ fn duplicate_contract_code() { #[test] fn send_tokens() { - let owner = Addr::unchecked("owner"); - let rcpt = Addr::unchecked("receiver"); + // prepare user addresses + let owner_addr = addr_make("owner"); + let recipient_addr = addr_make("recipient"); + + // set personal balance let init_funds = vec![coin(20, "btc"), coin(100, "eth")]; let rcpt_funds = vec![coin(5, "btc")]; - let mut app = App::new(|router, _, storage| { // initialization moved to App construction router .bank - .init_balance(storage, &owner, init_funds) + .init_balance(storage, &owner_addr, init_funds) .unwrap(); router .bank - .init_balance(storage, &rcpt, rcpt_funds) + .init_balance(storage, &recipient_addr, rcpt_funds) .unwrap(); - }); + }) + .with_remote(remote_channel()); // send both tokens let to_send = vec![coin(30, "eth"), coin(5, "btc")]; let msg: CosmosMsg = BankMsg::Send { - to_address: rcpt.clone().into(), + to_address: recipient_addr.clone().into(), amount: to_send, } .into(); - app.execute(owner.clone(), msg.clone()).unwrap(); - let rich = get_balance(&app, &owner); + app.execute(owner_addr.clone(), msg.clone()).unwrap(); + let rich = get_balance(&app, &owner_addr); assert_eq!(vec![coin(15, "btc"), coin(70, "eth")], rich); - let poor = get_balance(&app, &rcpt); + let poor = get_balance(&app, &recipient_addr); assert_eq!(vec![coin(10, "btc"), coin(30, "eth")], poor); // can send from other account (but funds will be deducted from sender) - app.execute(rcpt.clone(), msg).unwrap(); + app.execute(recipient_addr.clone(), msg).unwrap(); // cannot send too much let msg = BankMsg::Send { - to_address: rcpt.into(), + to_address: recipient_addr.into(), amount: coins(20, "btc"), } .into(); - app.execute(owner.clone(), msg).unwrap_err(); + app.execute(owner_addr.clone(), msg).unwrap_err(); - let rich = get_balance(&app, &owner); + let rich = get_balance(&app, &owner_addr); assert_eq!(vec![coin(15, "btc"), coin(70, "eth")], rich); } #[test] fn simple_contract() { + // prepare user addresses + let owner_addr = addr_make("owner"); + // set personal balance - let owner = Addr::unchecked("owner"); let init_funds = vec![coin(20, "btc"), coin(100, "eth")]; - let mut app = App::new(|router, _, storage| { router .bank - .init_balance(storage, &owner, init_funds) + .init_balance(storage, &owner_addr, init_funds) .unwrap(); - }); + }) + .with_remote(remote_channel()); // set up contract let code_id = app.store_code(payout::contract()); @@ -253,7 +267,7 @@ fn simple_contract() { let contract_addr = app .instantiate_contract( code_id, - owner.clone(), + owner_addr.clone(), &msg, &coins(23, "eth"), "Payout", @@ -266,7 +280,7 @@ fn simple_contract() { contract_data, ContractData { code_id, - creator: owner.clone(), + creator: owner_addr.clone(), admin: None, label: "Payout".to_owned(), created: app.block_info().height @@ -274,20 +288,20 @@ fn simple_contract() { ); // sender funds deducted - let sender = get_balance(&app, &owner); + let sender = get_balance(&app, &owner_addr); assert_eq!(sender, vec![coin(20, "btc"), coin(77, "eth")]); // get contract address, has funds let funds = get_balance(&app, &contract_addr); assert_eq!(funds, coins(23, "eth")); // create empty account - let random = Addr::unchecked("random"); - let funds = get_balance(&app, &random); + let random_addr = app.api().addr_make("random"); + let funds = get_balance(&app, &random_addr); assert_eq!(funds, vec![]); // do one payout and see money coming in let res = app - .execute_contract(random.clone(), contract_addr.clone(), &Empty {}, &[]) + .execute_contract(random_addr.clone(), contract_addr.clone(), &Empty {}, &[]) .unwrap(); assert_eq!(3, res.events.len()); @@ -305,13 +319,13 @@ fn simple_contract() { // then the transfer event let expected_transfer = Event::new("transfer") - .add_attribute("recipient", "random") + .add_attribute("recipient", &random_addr) .add_attribute("sender", &contract_addr) .add_attribute("amount", "5eth"); assert_eq!(&expected_transfer, &res.events[2]); // random got cash - let funds = get_balance(&app, &random); + let funds = get_balance(&app, &random_addr); assert_eq!(funds, coins(5, "eth")); // contract lost it let funds = get_balance(&app, &contract_addr); @@ -320,16 +334,21 @@ fn simple_contract() { #[test] fn reflect_success() { + // prepare user addresses + let owner_addr = addr_make("owner"); + let random_addr = addr_make("random"); + // set personal balance - let owner = Addr::unchecked("owner"); let init_funds = vec![coin(20, "btc"), coin(100, "eth")]; - - let mut app = custom_app::(|router, _, storage| { + let mut app = custom_app::(|router, _, storage| { router .bank - .init_balance(storage, &owner, init_funds) + .init_balance(storage, &owner_addr, init_funds) .unwrap(); - }); + router.bank.set_remote(remote_channel()); + router.wasm.set_remote(remote_channel()); + }) + .with_remote(remote_channel()); // set up payout contract let payout_id = app.store_code(payout::contract()); @@ -340,7 +359,7 @@ fn reflect_success() { let payout_addr = app .instantiate_contract( payout_id, - owner.clone(), + owner_addr.clone(), &msg, &coins(23, "eth"), "Payout", @@ -352,7 +371,7 @@ fn reflect_success() { let reflect_id = app.store_code(reflect::contract()); let reflect_addr = app - .instantiate_contract(reflect_id, owner, &Empty {}, &[], "Reflect", None) + .instantiate_contract(reflect_id, owner_addr, &Empty {}, &[], "Reflect", None) .unwrap(); // reflect account is empty @@ -375,7 +394,7 @@ fn reflect_success() { messages: vec![msg], }; let res = app - .execute_contract(Addr::unchecked("random"), reflect_addr.clone(), &msgs, &[]) + .execute_contract(random_addr, reflect_addr.clone(), &msgs, &[]) .unwrap(); // ensure the attributes were relayed from the sub-message @@ -426,16 +445,20 @@ fn reflect_success() { #[test] fn reflect_error() { + // prepare user addresses + let owner = addr_make("owner"); + // set personal balance - let owner = Addr::unchecked("owner"); let init_funds = vec![coin(20, "btc"), coin(100, "eth")]; - - let mut app = custom_app::(|router, _, storage| { + let mut app = custom_app::(|router, _, storage| { router .bank .init_balance(storage, &owner, init_funds) .unwrap(); - }); + router.bank.set_remote(remote_channel()); + router.wasm.set_remote(remote_channel()); + }) + .with_remote(remote_channel()); // set up reflect contract let reflect_id = app.store_code(reflect::contract()); @@ -454,18 +477,18 @@ fn reflect_error() { // reflect has 40 eth let funds = get_balance(&app, &reflect_addr); assert_eq!(funds, coins(40, "eth")); - let random = Addr::unchecked("random"); + let random_addr = app.api().addr_make("random"); // sending 7 eth works let msg = SubMsg::new(BankMsg::Send { - to_address: random.clone().into(), + to_address: random_addr.clone().into(), amount: coins(7, "eth"), }); let msgs = reflect::Message { messages: vec![msg], }; let res = app - .execute_contract(random.clone(), reflect_addr.clone(), &msgs, &[]) + .execute_contract(random_addr.clone(), reflect_addr.clone(), &msgs, &[]) .unwrap(); // no wasm events as no attributes assert_eq!(2, res.events.len()); @@ -478,7 +501,7 @@ fn reflect_error() { assert_eq!(transfer.ty.as_str(), "transfer"); // ensure random got paid - let funds = get_balance(&app, &random); + let funds = get_balance(&app, &random_addr); assert_eq!(funds, coins(7, "eth")); // reflect count should be updated to 1 @@ -490,26 +513,26 @@ fn reflect_error() { // sending 8 eth, then 3 btc should fail both let msg = SubMsg::new(BankMsg::Send { - to_address: random.clone().into(), + to_address: random_addr.clone().into(), amount: coins(8, "eth"), }); let msg2 = SubMsg::new(BankMsg::Send { - to_address: random.clone().into(), + to_address: random_addr.clone().into(), amount: coins(3, "btc"), }); let msgs = reflect::Message { messages: vec![msg, msg2], }; let err = app - .execute_contract(random.clone(), reflect_addr.clone(), &msgs, &[]) + .execute_contract(random_addr.clone(), reflect_addr.clone(), &msgs, &[]) .unwrap_err(); assert_eq!( - StdError::overflow(OverflowError::new(OverflowOperation::Sub, 0, 3)), + StdError::overflow(OverflowError::new(OverflowOperation::Sub)), err.downcast().unwrap() ); // first one should have been rolled-back on error (no second payment) - let funds = get_balance(&app, &random); + let funds = get_balance(&app, &random_addr); assert_eq!(funds, coins(7, "eth")); // failure should not update reflect count @@ -522,15 +545,18 @@ fn reflect_error() { #[test] fn sudo_works() { - let owner = Addr::unchecked("owner"); - let init_funds = vec![coin(100, "eth")]; + // prepare user addresses + let owner_addr = addr_make("owner"); + // set personal balance + let init_funds = vec![coin(100, "eth")]; let mut app = App::new(|router, _, storage| { router .bank - .init_balance(storage, &owner, init_funds) + .init_balance(storage, &owner_addr, init_funds) .unwrap(); - }); + }) + .with_remote(remote_channel()); let payout_id = app.store_code(payout::contract()); @@ -538,7 +564,14 @@ fn sudo_works() { payout: coin(5, "eth"), }; let payout_addr = app - .instantiate_contract(payout_id, owner, &msg, &coins(23, "eth"), "Payout", None) + .instantiate_contract( + payout_id, + owner_addr, + &msg, + &coins(23, "eth"), + "Payout", + None, + ) .unwrap(); // count is 1 @@ -563,7 +596,7 @@ fn sudo_works() { let msg = payout::SudoMsg { set_count: 49 }; let sudo_msg = WasmSudo { contract_addr: payout_addr.clone(), - msg: to_json_binary(&msg).unwrap(), + message: to_json_binary(&msg).unwrap(), }; app.sudo(sudo_msg.into()).unwrap(); @@ -576,17 +609,21 @@ fn sudo_works() { #[test] fn reflect_sub_message_reply_works() { + // prepare user addresses + let owner = addr_make("owner"); + let random = addr_make("random"); + // set personal balance - let owner = Addr::unchecked("owner"); - let random = Addr::unchecked("random"); let init_funds = vec![coin(20, "btc"), coin(100, "eth")]; - - let mut app = custom_app::(|router, _, storage| { + let mut app = custom_app::(|router, _, storage| { router .bank .init_balance(storage, &owner, init_funds) .unwrap(); - }); + router.bank.set_remote(remote_channel()); + router.wasm.set_remote(remote_channel()); + }) + .with_remote(remote_channel()); // set up reflect contract let reflect_id = app.store_code(reflect::contract()); @@ -602,7 +639,7 @@ fn reflect_sub_message_reply_works() { ) .unwrap(); - // no reply writen beforehand + // no reply written beforehand let query = reflect::QueryMsg::Reply { id: 123 }; let res: StdResult = app.wrap().query_wasm_smart(&reflect_addr, &query); res.unwrap_err(); @@ -672,11 +709,11 @@ fn send_update_admin_works() { // update admin succeeds if admin // update admin fails if not (new) admin // check admin set properly - let owner = Addr::unchecked("owner"); - let owner2 = Addr::unchecked("owner2"); - let beneficiary = Addr::unchecked("beneficiary"); + let mut app = default_app(); - let mut app = App::default(); + let owner = addr_make("owner"); + let owner2 = addr_make("owner2"); + let beneficiary = addr_make("beneficiary"); // create a hackatom contract with some funds let code_id = app.store_code(hackatom::contract()); @@ -736,16 +773,21 @@ fn sent_wasm_migration_works() { // migrate fails if not admin // migrate succeeds if admin // check beneficiary updated - let owner = Addr::unchecked("owner"); - let beneficiary = Addr::unchecked("beneficiary"); - let init_funds = coins(30, "btc"); + // prepare user addresses + let owner_addr = addr_make("owner"); + let beneficiary_addr = addr_make("beneficiary"); + let random_addr = addr_make("random"); + + // set personal balance + let init_funds = coins(30, "btc"); let mut app = App::new(|router, _, storage| { router .bank - .init_balance(storage, &owner, init_funds) + .init_balance(storage, &owner_addr, init_funds) .unwrap(); - }); + }) + .with_remote(remote_channel()); // create a hackatom contract with some funds let code_id = app.store_code(hackatom::contract()); @@ -753,40 +795,44 @@ fn sent_wasm_migration_works() { let contract = app .instantiate_contract( code_id, - owner.clone(), + owner_addr.clone(), &hackatom::InstantiateMsg { - beneficiary: beneficiary.as_str().to_owned(), + beneficiary: beneficiary_addr.as_str().to_owned(), }, &coins(20, "btc"), "Hackatom", - Some(owner.to_string()), + Some(owner_addr.to_string()), ) .unwrap(); // check admin set properly let info = app.contract_data(&contract).unwrap(); - assert_eq!(info.admin, Some(owner.clone())); + assert_eq!(info.admin, Some(owner_addr.clone())); // check beneficiary set properly let state: hackatom::InstantiateMsg = app .wrap() .query_wasm_smart(&contract, &hackatom::QueryMsg::Beneficiary {}) .unwrap(); - assert_eq!(state.beneficiary, beneficiary); + assert_eq!(state.beneficiary, beneficiary_addr.to_string()); // migrate fails if not admin - let random = Addr::unchecked("random"); let migrate_msg = hackatom::MigrateMsg { - new_guy: random.to_string(), + new_guy: random_addr.to_string(), }; - app.migrate_contract(beneficiary, contract.clone(), &migrate_msg, code_id) + app.migrate_contract(beneficiary_addr, contract.clone(), &migrate_msg, code_id) .unwrap_err(); // migrate fails if unregistered code id - app.migrate_contract(owner.clone(), contract.clone(), &migrate_msg, code_id + 7) - .unwrap_err(); + app.migrate_contract( + owner_addr.clone(), + contract.clone(), + &migrate_msg, + code_id + 7, + ) + .unwrap_err(); // migrate succeeds when the stars align - app.migrate_contract(owner, contract.clone(), &migrate_msg, code_id) + app.migrate_contract(owner_addr, contract.clone(), &migrate_msg, code_id) .unwrap(); // check beneficiary updated @@ -794,7 +840,7 @@ fn sent_wasm_migration_works() { .wrap() .query_wasm_smart(&contract, &hackatom::QueryMsg::Beneficiary {}) .unwrap(); - assert_eq!(state.beneficiary, random); + assert_eq!(state.beneficiary, random_addr.to_string()); } #[test] @@ -804,25 +850,29 @@ fn sent_funds_properly_visible_on_execution() { // additional 20btc. Then beneficiary balance is checked - expected value is 30btc. 10btc // would mean that sending tokens with message is not visible for this very message, and // 20btc means, that only such just send funds are visible. - let owner = Addr::unchecked("owner"); - let beneficiary = Addr::unchecked("beneficiary"); - let init_funds = coins(30, "btc"); + // prepare user addresses + let owner_addr = addr_make("owner"); + let beneficiary_addr = addr_make("beneficiary"); + + // set personal balance + let init_funds = coins(30, "btc"); let mut app = App::new(|router, _, storage| { router .bank - .init_balance(storage, &owner, init_funds) + .init_balance(storage, &owner_addr, init_funds) .unwrap(); - }); + }) + .with_remote(remote_channel()); let code_id = app.store_code(hackatom::contract()); let contract = app .instantiate_contract( code_id, - owner.clone(), + owner_addr.clone(), &hackatom::InstantiateMsg { - beneficiary: beneficiary.as_str().to_owned(), + beneficiary: beneficiary_addr.as_str().to_owned(), }, &coins(10, "btc"), "Hackatom", @@ -831,7 +881,7 @@ fn sent_funds_properly_visible_on_execution() { .unwrap(); app.execute_contract( - owner.clone(), + owner_addr.clone(), contract.clone(), &Empty {}, &coins(20, "btc"), @@ -840,32 +890,34 @@ fn sent_funds_properly_visible_on_execution() { // Check balance of all accounts to ensure no tokens where burned or created, and they are // in correct places - assert_eq!(get_balance(&app, &owner), &[]); + assert_eq!(get_balance(&app, &owner_addr), &[]); assert_eq!(get_balance(&app, &contract), &[]); - assert_eq!(get_balance(&app, &beneficiary), coins(30, "btc")); + assert_eq!(get_balance(&app, &beneficiary_addr), coins(30, "btc")); } /// Demonstrates that we can mint tokens and send from other accounts /// via a custom module, as an example of ability to do privileged actions. mod custom_handler { use super::*; - use crate::{BankSudo, BasicAppBuilder, CosmosRouter}; + use crate::{BankSudo, BasicAppBuilder}; const LOTTERY: Item = Item::new("lottery"); const PITY: Item = Item::new("pity"); #[derive(Clone, Debug, PartialEq, JsonSchema, Serialize, Deserialize)] - struct CustomMsg { + struct CustomLotteryMsg { // we mint LOTTERY tokens to this one lucky_winner: String, // we transfer PITY from lucky_winner to runner_up runner_up: String, } + impl CustomMsg for CustomLotteryMsg {} + struct CustomHandler {} impl Module for CustomHandler { - type ExecT = CustomMsg; + type ExecT = CustomLotteryMsg; type QueryT = Empty; type SudoT = Empty; @@ -879,7 +931,7 @@ mod custom_handler { msg: Self::ExecT, ) -> AnyResult where - ExecC: Debug + Clone + PartialEq + JsonSchema + DeserializeOwned + 'static, + ExecC: CustomMsg + DeserializeOwned + 'static, QueryC: CustomQuery + DeserializeOwned + 'static, { let lottery = LOTTERY.load(storage)?; @@ -912,7 +964,7 @@ mod custom_handler { _msg: Self::SudoT, ) -> AnyResult where - ExecC: Debug + Clone + PartialEq + JsonSchema + DeserializeOwned + 'static, + ExecC: CustomMsg + DeserializeOwned + 'static, QueryC: CustomQuery + DeserializeOwned + 'static, { bail!("sudo not implemented for CustomHandler") @@ -947,15 +999,12 @@ mod custom_handler { // let's call this custom handler #[test] fn dispatches_messages() { - let winner = "winner".to_string(); - let second = "second".to_string(); - // payments. note 54321 - 12321 = 42000 let denom = "tix"; let lottery = coin(54321, denom); let bonus = coin(12321, denom); - let mut app = BasicAppBuilder::::new_custom() + let mut app = BasicAppBuilder::::new_custom() .with_custom(CustomHandler {}) .build(|router, _, storage| { router @@ -964,16 +1013,20 @@ mod custom_handler { .unwrap(); }); + let winner = app.api().addr_make("winner"); + let second = app.api().addr_make("second"); + // query that balances are empty let start = app.wrap().query_balance(&winner, denom).unwrap(); assert_eq!(start, coin(0, denom)); // trigger the custom module - let msg = CosmosMsg::Custom(CustomMsg { - lucky_winner: winner.clone(), - runner_up: second.clone(), + let msg = CosmosMsg::Custom(CustomLotteryMsg { + lucky_winner: winner.to_string(), + runner_up: second.to_string(), }); - app.execute(Addr::unchecked("anyone"), msg).unwrap(); + let anyone = app.api().addr_make("anyone"); + app.execute(anyone, msg).unwrap(); // see if coins were properly added let big_win = app.wrap().query_balance(&winner, denom).unwrap(); @@ -985,9 +1038,6 @@ mod custom_handler { mod reply_data_overwrite { use super::*; - use cosmwasm_std::to_json_binary; - - use echo::EXECUTE_REPLY_BASE_ID; fn make_echo_submsg( contract: Addr, @@ -1031,9 +1081,9 @@ mod reply_data_overwrite { #[test] fn no_submsg() { - let mut app = App::default(); + let mut app = default_app(); - let owner = Addr::unchecked("owner"); + let owner = app.api().addr_make("owner"); let code_id = app.store_code(echo::contract()); @@ -1058,9 +1108,9 @@ mod reply_data_overwrite { #[test] fn single_submsg() { - let mut app = App::default(); + let mut app = default_app(); - let owner = Addr::unchecked("owner"); + let owner = app.api().addr_make("owner"); let code_id = app.store_code(echo::contract()); @@ -1091,9 +1141,9 @@ mod reply_data_overwrite { #[test] fn single_submsg_no_reply() { - let mut app = App::default(); + let mut app = default_app(); - let owner = Addr::unchecked("owner"); + let owner = app.api().addr_make("owner"); let code_id = app.store_code(echo::contract()); @@ -1119,9 +1169,9 @@ mod reply_data_overwrite { #[test] fn single_no_submsg_data() { - let mut app = App::default(); + let mut app = default_app(); - let owner = Addr::unchecked("owner"); + let owner = app.api().addr_make("owner"); let code_id = app.store_code(echo::contract()); @@ -1147,9 +1197,9 @@ mod reply_data_overwrite { #[test] fn single_no_top_level_data() { - let mut app = App::default(); + let mut app = default_app(); - let owner = Addr::unchecked("owner"); + let owner = app.api().addr_make("owner"); let code_id = app.store_code(echo::contract()); @@ -1179,16 +1229,20 @@ mod reply_data_overwrite { #[test] fn single_submsg_reply_returns_none() { + // prepare user addresses + let owner = addr_make("owner"); + // set personal balance - let owner = Addr::unchecked("owner"); let init_funds = coins(100, "tgd"); - - let mut app = custom_app::(|router, _, storage| { + let mut app = custom_app::(|router, _, storage| { router .bank .init_balance(storage, &owner, init_funds) .unwrap(); - }); + router.bank.set_remote(remote_channel()); + router.wasm.set_remote(remote_channel()); + }) + .with_remote(remote_channel()); // set up reflect contract let reflect_id = app.store_code(reflect::contract()); @@ -1235,9 +1289,9 @@ mod reply_data_overwrite { #[test] fn multiple_submsg() { - let mut app = App::default(); + let mut app = default_app(); - let owner = Addr::unchecked("owner"); + let owner = app.api().addr_make("owner"); let code_id = app.store_code(echo::contract()); @@ -1278,9 +1332,9 @@ mod reply_data_overwrite { #[test] fn multiple_submsg_no_reply() { - let mut app = App::default(); + let mut app = default_app(); - let owner = Addr::unchecked("owner"); + let owner = app.api().addr_make("owner"); let code_id = app.store_code(echo::contract()); @@ -1311,9 +1365,9 @@ mod reply_data_overwrite { #[test] fn multiple_submsg_mixed() { - let mut app = App::default(); + let mut app = default_app(); - let owner = Addr::unchecked("owner"); + let owner = app.api().addr_make("owner"); let code_id = app.store_code(echo::contract()); @@ -1349,9 +1403,9 @@ mod reply_data_overwrite { #[test] fn nested_submsg() { - let mut app = App::default(); + let mut app = default_app(); - let owner = Addr::unchecked("owner"); + let owner = app.api().addr_make("owner"); let code_id = app.store_code(echo::contract()); @@ -1402,9 +1456,9 @@ mod response_validation { #[test] fn empty_attribute_key() { - let mut app = App::default(); + let mut app = default_app(); - let owner = Addr::unchecked("owner"); + let owner = app.api().addr_make("owner"); let code_id = app.store_code(echo::contract()); @@ -1432,18 +1486,17 @@ mod response_validation { } #[test] - fn empty_attribute_value() { - let mut app = App::default(); - - let owner = Addr::unchecked("owner"); + fn empty_attribute_value_should_work() { + let mut app = default_app(); + let owner = app.api().addr_make("owner"); let code_id = app.store_code(echo::contract()); let contract = app .instantiate_contract(code_id, owner.clone(), &Empty {}, &[], "Echo", None) .unwrap(); - let err = app + assert!(app .execute_contract( owner, contract, @@ -1457,16 +1510,14 @@ mod response_validation { }, &[], ) - .unwrap_err(); - - assert_eq!(Error::empty_attribute_value("key"), err.downcast().unwrap()); + .is_ok()); } #[test] fn empty_event_attribute_key() { - let mut app = App::default(); + let mut app = default_app(); - let owner = Addr::unchecked("owner"); + let owner = app.api().addr_make("owner"); let code_id = app.store_code(echo::contract()); @@ -1493,18 +1544,17 @@ mod response_validation { } #[test] - fn empty_event_attribute_value() { - let mut app = App::default(); - - let owner = Addr::unchecked("owner"); + fn empty_event_attribute_value_should_work() { + let mut app = default_app(); + let owner = app.api().addr_make("owner"); let code_id = app.store_code(echo::contract()); let contract = app .instantiate_contract(code_id, owner.clone(), &Empty {}, &[], "Echo", None) .unwrap(); - let err = app + assert!(app .execute_contract( owner, contract, @@ -1517,16 +1567,14 @@ mod response_validation { }, &[], ) - .unwrap_err(); - - assert_eq!(Error::empty_attribute_value("key"), err.downcast().unwrap()); + .is_ok()); } #[test] fn too_short_event_type() { - let mut app = App::default(); + let mut app = default_app(); - let owner = Addr::unchecked("owner"); + let owner = app.api().addr_make("owner"); let code_id = app.store_code(echo::contract()); @@ -1552,26 +1600,29 @@ mod response_validation { } mod contract_instantiation { + #[test] fn instantiate2_works() { use super::*; // prepare application and actors - let mut app = App::default(); - let sender = Addr::unchecked("sender"); + let mut app = default_app(); + let sender = app.api().addr_make("sender"); + let creator = app.api().addr_make("creator"); // store contract's code - let code_id = app.store_code_with_creator(Addr::unchecked("creator"), echo::contract()); + let code_id = app.store_code_with_creator(creator, echo::contract()); // initialize the contract let init_msg = to_json_binary(&Empty {}).unwrap(); + let salt = cosmwasm_std::HexBinary::from_hex("010203040506").unwrap(); let msg = WasmMsg::Instantiate2 { admin: None, code_id, msg: init_msg, funds: vec![], label: "label".into(), - salt: [1, 2, 3, 4, 5, 6].as_slice().into(), + salt: salt.into(), }; let res = app.execute(sender, msg.into()).unwrap(); @@ -1581,7 +1632,10 @@ mod contract_instantiation { // assert contract's address is exactly the predicted one, // in default address generator, this is like `contract` + salt in hex - assert_eq!(parsed.contract_address, "contract010203040506"); + assert_eq!( + parsed.contract_address, + "cosmwasm167g7x7auj3l00lhdcevusncx565ytz6a6xvmx2f5xuy84re9ddrqczpzkm", + ); } } @@ -1590,18 +1644,19 @@ mod wasm_queries { #[test] fn query_existing_code_info() { use super::*; - let mut app = App::default(); - let code_id = app.store_code_with_creator(Addr::unchecked("creator"), echo::contract()); + let mut app = default_app(); + let creator = app.api().addr_make("creator"); + let code_id = app.store_code_with_creator(creator.clone(), echo::contract()); let code_info_response = app.wrap().query_wasm_code_info(code_id).unwrap(); assert_eq!(code_id, code_info_response.code_id); - assert_eq!("creator", code_info_response.creator); - assert!(!code_info_response.checksum.is_empty()); + assert_eq!(creator.to_string(), code_info_response.creator.to_string()); + assert_eq!(32, code_info_response.checksum.as_slice().len()); } #[test] fn query_non_existing_code_info() { use super::*; - let app = App::default(); + let app = default_app(); assert_eq!( "Generic error: Querier contract error: code id: invalid", app.wrap().query_wasm_code_info(0).unwrap_err().to_string() @@ -1618,15 +1673,16 @@ mod custom_messages { #[test] fn triggering_custom_msg() { - let custom_handler = CachingCustomHandler::::new(); + let custom_handler = CachingCustomHandler::::default(); let custom_handler_state = custom_handler.state(); let mut app = AppBuilder::new_custom() .with_custom(custom_handler) + .with_remote(remote_channel()) .build(no_init); - let sender = app.api().addr_validate("sender").unwrap(); - let owner = app.api().addr_validate("owner").unwrap(); + let sender = app.api().addr_make("sender"); + let owner = app.api().addr_make("owner"); let contract_id = app.store_code(echo::custom_contract()); @@ -1638,7 +1694,7 @@ mod custom_messages { sender, contract, &echo::Message { - sub_msg: vec![SubMsg::new(CosmosMsg::Custom(CustomMsg::SetAge { + sub_msg: vec![SubMsg::new(CosmosMsg::Custom(CustomHelperMsg::SetAge { age: 20, }))], ..Default::default() @@ -1649,7 +1705,7 @@ mod custom_messages { assert_eq!( custom_handler_state.execs().to_owned(), - vec![CustomMsg::SetAge { age: 20 }] + vec![CustomHelperMsg::SetAge { age: 20 }] ); assert!(custom_handler_state.queries().is_empty()); @@ -1658,20 +1714,23 @@ mod custom_messages { mod protobuf_wrapped_data { use super::*; - use crate::BasicApp; #[test] fn instantiate_wrapped_properly() { + // prepare user addresses + let owner = addr_make("owner"); + // set personal balance - let owner = Addr::unchecked("owner"); let init_funds = vec![coin(20, "btc")]; - - let mut app = custom_app::(|router, _, storage| { + let mut app = custom_app::(|router, _, storage| { router .bank .init_balance(storage, &owner, init_funds) .unwrap(); - }); + router.bank.set_remote(remote_channel()); + router.wasm.set_remote(remote_channel()); + }) + .with_remote(remote_channel()); // set up reflect contract let code_id = app.store_code(reflect::contract()); @@ -1700,8 +1759,9 @@ mod protobuf_wrapped_data { #[test] fn instantiate_with_data_works() { - let owner = Addr::unchecked("owner"); - let mut app = BasicApp::new(|_, _, _| {}); + let mut app = default_app(); + + let owner = app.api().addr_make("owner"); // set up echo contract let code_id = app.store_code(echo::contract()); @@ -1729,8 +1789,9 @@ mod protobuf_wrapped_data { #[test] fn instantiate_with_reply_works() { - let owner = Addr::unchecked("owner"); - let mut app = BasicApp::new(|_, _, _| {}); + let mut app = default_app(); + + let owner = app.api().addr_make("owner"); // set up echo contract let code_id = app.store_code(echo::contract()); @@ -1781,8 +1842,9 @@ mod protobuf_wrapped_data { #[test] fn execute_wrapped_properly() { - let owner = Addr::unchecked("owner"); - let mut app = BasicApp::new(|_, _, _| {}); + let mut app = default_app(); + + let owner = app.api().addr_make("owner"); // set up reflect contract let code_id = app.store_code(echo::contract()); @@ -1804,12 +1866,12 @@ mod protobuf_wrapped_data { mod errors { use super::*; - use cosmwasm_std::to_json_binary; #[test] fn simple_instantiation() { - let owner = Addr::unchecked("owner"); - let mut app = App::default(); + let mut app = default_app(); + + let owner = app.api().addr_make("owner"); // set up contract let code_id = app.store_code(error::contract(false)); @@ -1821,7 +1883,7 @@ mod errors { // we should be able to retrieve the original error by downcasting let source: &StdError = err.downcast_ref().unwrap(); - if let StdError::GenericErr { msg } = source { + if let StdError::GenericErr { msg, .. } = source { assert_eq!(msg, "Init failed"); } else { panic!("wrong StdError variant"); @@ -1834,8 +1896,10 @@ mod errors { #[test] fn simple_call() { - let owner = Addr::unchecked("owner"); - let mut app = App::default(); + let mut app = default_app(); + + let owner = app.api().addr_make("owner"); + let random_addr = app.api().addr_make("random"); // set up contract let code_id = app.store_code(error::contract(true)); @@ -1847,12 +1911,12 @@ mod errors { // execute should error let err = app - .execute_contract(Addr::unchecked("random"), contract_addr, &msg, &[]) + .execute_contract(random_addr, contract_addr, &msg, &[]) .unwrap_err(); // we should be able to retrieve the original error by downcasting let source: &StdError = err.downcast_ref().unwrap(); - if let StdError::GenericErr { msg } = source { + if let StdError::GenericErr { msg, .. } = source { assert_eq!(msg, "Handle failed"); } else { panic!("wrong StdError variant"); @@ -1865,8 +1929,10 @@ mod errors { #[test] fn nested_call() { - let owner = Addr::unchecked("owner"); - let mut app = App::default(); + let mut app = default_app(); + + let owner = app.api().addr_make("owner"); + let random_addr = app.api().addr_make("random"); let error_code_id = app.store_code(error::contract(true)); let caller_code_id = app.store_code(caller::contract()); @@ -1887,12 +1953,12 @@ mod errors { funds: vec![], }; let err = app - .execute_contract(Addr::unchecked("random"), caller_addr, &msg, &[]) + .execute_contract(random_addr, caller_addr, &msg, &[]) .unwrap_err(); - // we can downcast to get the original error + // we can get the original error by downcasting let source: &StdError = err.downcast_ref().unwrap(); - if let StdError::GenericErr { msg } = source { + if let StdError::GenericErr { msg, .. } = source { assert_eq!(msg, "Handle failed"); } else { panic!("wrong StdError variant"); @@ -1905,8 +1971,10 @@ mod errors { #[test] fn double_nested_call() { - let owner = Addr::unchecked("owner"); - let mut app = App::default(); + let mut app = default_app(); + + let owner_addr = app.api().addr_make("owner"); + let random_addr = app.api().addr_make("random"); let error_code_id = app.store_code(error::contract(true)); let caller_code_id = app.store_code(caller::contract()); @@ -1914,13 +1982,27 @@ mod errors { // set up contract_helpers let msg = Empty {}; let caller_addr1 = app - .instantiate_contract(caller_code_id, owner.clone(), &msg, &[], "caller", None) + .instantiate_contract( + caller_code_id, + owner_addr.clone(), + &msg, + &[], + "caller", + None, + ) .unwrap(); let caller_addr2 = app - .instantiate_contract(caller_code_id, owner.clone(), &msg, &[], "caller", None) + .instantiate_contract( + caller_code_id, + owner_addr.clone(), + &msg, + &[], + "caller", + None, + ) .unwrap(); let error_addr = app - .instantiate_contract(error_code_id, owner, &msg, &[], "error", None) + .instantiate_contract(error_code_id, owner_addr, &msg, &[], "error", None) .unwrap(); // caller1 calls caller2, caller2 calls error @@ -1935,15 +2017,15 @@ mod errors { funds: vec![], }; let err = app - .execute_contract(Addr::unchecked("random"), caller_addr1, &msg, &[]) + .execute_contract(random_addr, caller_addr1, &msg, &[]) .unwrap_err(); // uncomment to have the test fail and see how the error stringifies // panic!("{:?}", err); - // we can downcast to get the original error + // we can get the original error by downcasting let source: &StdError = err.downcast_ref().unwrap(); - if let StdError::GenericErr { msg } = source { + if let StdError::GenericErr { msg, .. } = source { assert_eq!(msg, "Handle failed"); } else { panic!("wrong StdError variant"); @@ -1954,29 +2036,3 @@ mod errors { assert_eq!(err.chain().count(), 4); } } - -mod api { - use super::*; - - #[test] - fn api_addr_validate_should_work() { - let app = App::default(); - let addr = app.api().addr_validate("creator").unwrap(); - assert_eq!(addr.to_string(), "creator"); - } - - #[test] - #[cfg(not(feature = "cosmwasm_1_5"))] - fn api_addr_canonicalize_should_work() { - let app = App::default(); - let canonical = app.api().addr_canonicalize("creator").unwrap(); - assert_eq!(canonical.to_string(), "0000000000000000000000000000726F0000000000000000000000000000000000000000006572000000000000000000000000000000000000000000610000000000000000000000000000000000000000006374000000000000"); - } - - #[test] - fn api_addr_humanize_should_work() { - let app = App::default(); - let canonical = app.api().addr_canonicalize("creator").unwrap(); - assert_eq!(app.api().addr_humanize(&canonical).unwrap(), "creator"); - } -} diff --git a/src/tests/test_custom_handler.rs b/src/tests/test_custom_handler.rs index 834f1e73..98a7a01c 100644 --- a/src/tests/test_custom_handler.rs +++ b/src/tests/test_custom_handler.rs @@ -1,17 +1,24 @@ use crate::custom_handler::CachingCustomHandler; -use crate::test_helpers::CustomMsg; -use crate::{App, Module}; +use crate::test_helpers::CustomHelperMsg; +use crate::tests::default_app; +use crate::Module; use cosmwasm_std::testing::MockStorage; -use cosmwasm_std::{Addr, Empty}; +use cosmwasm_std::Empty; +///Custom handlers in CosmWasm allow developers to incorporate their own unique logic into tests. +///This feature is valuable for tailoring the testing environment to reflect specific +/// use-cases or behaviors in a CosmWasm-based smart contract. #[test] fn custom_handler_works() { // prepare needed tools - let app = App::default(); + let app = default_app(); let mut storage = MockStorage::default(); // create custom handler - let custom_handler = CachingCustomHandler::::new(); + let custom_handler = CachingCustomHandler::::default(); + + // prepare user addresses + let sender_addr = app.api().addr_make("sender"); // run execute function let _ = custom_handler.execute( @@ -19,8 +26,8 @@ fn custom_handler_works() { &mut storage, app.router(), &app.block_info(), - Addr::unchecked("sender"), - CustomMsg::SetAge { age: 32 }, + sender_addr, + CustomHelperMsg::SetAge { age: 32 }, ); // run query function @@ -29,7 +36,7 @@ fn custom_handler_works() { &storage, &(*app.wrap()), &app.block_info(), - CustomMsg::SetName { + CustomHelperMsg::SetName { name: "John".to_string(), }, ); @@ -40,13 +47,13 @@ fn custom_handler_works() { // there should be one exec message assert_eq!( custom_handler_state.execs().to_owned(), - vec![CustomMsg::SetAge { age: 32 }] + vec![CustomHelperMsg::SetAge { age: 32 }] ); // there should be one query message assert_eq!( custom_handler_state.queries().to_owned(), - vec![CustomMsg::SetName { + vec![CustomHelperMsg::SetName { name: "John".to_string() }] ); @@ -60,15 +67,15 @@ fn custom_handler_works() { #[test] fn custom_handler_has_no_sudo() { // prepare needed tools - let app = App::default(); + let app = default_app(); let mut storage = MockStorage::default(); // create custom handler - let custom_handler = CachingCustomHandler::::new(); + let custom_handler = CachingCustomHandler::::default(); // run sudo function assert_eq!( - "Unexpected sudo msg Empty", + "Unexpected custom sudo message Empty", custom_handler .sudo( app.api(), diff --git a/src/tests/test_error.rs b/src/tests/test_error.rs new file mode 100644 index 00000000..d3f16290 --- /dev/null +++ b/src/tests/test_error.rs @@ -0,0 +1,43 @@ +use crate::error::Error; +use cosmwasm_std::{WasmMsg, WasmQuery}; + +#[test] +fn instantiating_error_should_work() { + assert_eq!( + "Empty attribute key. Value: alpha", + Error::empty_attribute_key("alpha").to_string() + ); + assert_eq!( + "Attribute key starts with reserved prefix _: gamma", + Error::reserved_attribute_key("gamma").to_string() + ); + assert_eq!( + "Event type too short: event_type", + Error::event_type_too_short("event_type").to_string() + ); + assert_eq!( + r#"Unsupported wasm query: ContractInfo { contract_addr: "contract1984" }"#, + Error::unsupported_wasm_query(WasmQuery::ContractInfo { + contract_addr: "contract1984".to_string() + }) + .to_string() + ); + assert_eq!( + r#"Unsupported wasm message: Migrate { contract_addr: "contract1984", new_code_id: 1984, msg: }"#, + Error::unsupported_wasm_message(WasmMsg::Migrate { + contract_addr: "contract1984".to_string(), + new_code_id: 1984, + msg: Default::default(), + }) + .to_string() + ); + assert_eq!("code id: invalid", Error::invalid_code_id().to_string()); + assert_eq!( + "code id 53: no such code", + Error::unregistered_code_id(53).to_string() + ); + assert_eq!( + "Contract with this address already exists: contract1984", + Error::duplicated_contract_address("contract1984").to_string() + ); +} diff --git a/src/tests/test_gov.rs b/src/tests/test_gov.rs index e900c40f..e4dcf4a1 100644 --- a/src/tests/test_gov.rs +++ b/src/tests/test_gov.rs @@ -1,99 +1,39 @@ -use crate::error::AnyResult; -use crate::test_helpers::{stargate, stargate::ExecMsg}; -use crate::{App, AppBuilder, AppResponse, CosmosRouter, Executor, Gov, Module}; -use cosmwasm_std::{Addr, Api, Binary, BlockInfo, CustomQuery, Empty, GovMsg, Querier, Storage}; -use schemars::JsonSchema; -use serde::de::DeserializeOwned; -use std::fmt::Debug; - -struct AcceptingModule; - -impl Module for AcceptingModule { - type ExecT = GovMsg; - type QueryT = Empty; - type SudoT = Empty; - - fn execute( - &self, - _api: &dyn Api, - _storage: &mut dyn Storage, - _router: &dyn CosmosRouter, - _block: &BlockInfo, - _sender: Addr, - _msg: Self::ExecT, - ) -> AnyResult - where - ExecC: Debug + Clone + PartialEq + JsonSchema + DeserializeOwned + 'static, - QueryC: CustomQuery + DeserializeOwned + 'static, - { - Ok(AppResponse::default()) - } - - fn sudo( - &self, - _api: &dyn Api, - _storage: &mut dyn Storage, - _router: &dyn CosmosRouter, - _block: &BlockInfo, - _msg: Self::SudoT, - ) -> AnyResult - where - ExecC: Debug + Clone + PartialEq + schemars::JsonSchema + DeserializeOwned + 'static, - QueryC: CustomQuery + DeserializeOwned + 'static, - { - Ok(AppResponse::default()) - } - - fn query( - &self, - _api: &dyn Api, - _storage: &dyn Storage, - _querier: &dyn Querier, - _block: &BlockInfo, - _request: Self::QueryT, - ) -> AnyResult { - Ok(Binary::default()) - } -} - -impl Gov for AcceptingModule {} +#![cfg(feature = "stargate")] +use crate::test_helpers::gov; +use crate::tests::default_app; +use crate::{no_init, AppBuilder, Executor, GovAcceptingModule}; +use cosmwasm_std::Empty; #[test] fn default_gov() { - let mut app = App::default(); - let code = app.store_code(stargate::contract()); + let mut app = default_app(); + + let creator_addr = app.api().addr_make("creator"); + let code = app.store_code_with_creator(creator_addr, gov::contract()); + + let owner_addr = app.api().addr_make("owner"); let contract = app - .instantiate_contract( - code, - Addr::unchecked("owner"), - &Empty {}, - &[], - "contract", - None, - ) + .instantiate_contract(code, owner_addr.clone(), &Empty {}, &[], "govenius", None) .unwrap(); - app.execute_contract(Addr::unchecked("owner"), contract, &ExecMsg::Gov {}, &[]) + app.execute_contract(owner_addr, contract, &Empty {}, &[]) .unwrap_err(); } #[test] -fn substituting_gov() { +fn accepting_gov() { let mut app = AppBuilder::new() - .with_gov(AcceptingModule) - .build(|_, _, _| ()); - let code = app.store_code(stargate::contract()); + .with_gov(GovAcceptingModule::new()) + .build(no_init); + + let creator_addr = app.api().addr_make("creator"); + let code = app.store_code_with_creator(creator_addr, gov::contract()); + + let owner_addr = app.api().addr_make("owner"); let contract = app - .instantiate_contract( - code, - Addr::unchecked("owner"), - &Empty {}, - &[], - "contract", - None, - ) + .instantiate_contract(code, owner_addr.clone(), &Empty {}, &[], "govenius", None) .unwrap(); - app.execute_contract(Addr::unchecked("owner"), contract, &ExecMsg::Gov {}, &[]) + app.execute_contract(owner_addr, contract, &Empty {}, &[]) .unwrap(); } diff --git a/src/tests/test_ibc.rs b/src/tests/test_ibc.rs index 2497478c..913318c7 100644 --- a/src/tests/test_ibc.rs +++ b/src/tests/test_ibc.rs @@ -1,43 +1,39 @@ -use crate::test_helpers::{stargate, stargate::ExecMsg}; -use crate::{App, AppBuilder, Executor, IbcAcceptingModule}; -use cosmwasm_std::{Addr, Empty}; +#![cfg(feature = "stargate")] +use crate::test_helpers::ibc; +use crate::tests::default_app; +use crate::{no_init, AppBuilder, Executor, IbcAcceptingModule}; +use cosmwasm_std::Empty; #[test] fn default_ibc() { - let mut app = App::default(); - let code = app.store_code(stargate::contract()); + let mut app = default_app(); + + let creator_addr = app.api().addr_make("creator"); + let code = app.store_code_with_creator(creator_addr, ibc::contract()); + + let owner_addr = app.api().addr_make("owner"); let contract = app - .instantiate_contract( - code, - Addr::unchecked("owner"), - &Empty {}, - &[], - "contract", - None, - ) + .instantiate_contract(code, owner_addr.clone(), &Empty {}, &[], "ibanera", None) .unwrap(); - app.execute_contract(Addr::unchecked("owner"), contract, &ExecMsg::Ibc {}, &[]) + app.execute_contract(owner_addr, contract, &Empty {}, &[]) .unwrap_err(); } #[test] -fn substituting_ibc() { - let mut app = AppBuilder::new() +fn accepting_ibc() { + let mut app = AppBuilder::default() .with_ibc(IbcAcceptingModule::new()) - .build(|_, _, _| ()); - let code = app.store_code(stargate::contract()); + .build(no_init); + + let creator_addr = app.api().addr_make("creator"); + let code = app.store_code_with_creator(creator_addr, ibc::contract()); + + let owner_addr = app.api().addr_make("owner"); let contract = app - .instantiate_contract( - code, - Addr::unchecked("owner"), - &Empty {}, - &[], - "contract", - None, - ) + .instantiate_contract(code, owner_addr.clone(), &Empty {}, &[], "ibanera", None) .unwrap(); - app.execute_contract(Addr::unchecked("owner"), contract, &ExecMsg::Ibc {}, &[]) + app.execute_contract(owner_addr, contract, &Empty {}, &[]) .unwrap(); } diff --git a/src/tests/test_stargate.rs b/src/tests/test_stargate.rs new file mode 100644 index 00000000..246536b4 --- /dev/null +++ b/src/tests/test_stargate.rs @@ -0,0 +1,56 @@ +#![cfg(feature = "stargate")] + +use crate::test_helpers::stargate; +use crate::tests::default_app; +use crate::{no_init, AppBuilder, Executor, StargateAccepting}; +use cosmwasm_std::Empty; +#[test] +fn default_failing_stargate_handler_should_work() { + let mut app = default_app(); + + // store the contract + let creator_addr = app.api().addr_make("creator"); + let code = app.store_code_with_creator(creator_addr, stargate::contract()); + + // instantiate contract + let owner_addr = app.api().addr_make("owner"); + let contract_addr = app + .instantiate_contract(code, owner_addr.clone(), &Empty {}, &[], "tauri", None) + .unwrap(); + + // execute empty message on the contract, this contract returns stargate message + // which is rejected by default failing stargate keeper + let err = app + .execute_contract(owner_addr, contract_addr, &Empty {}, &[]) + .unwrap_err(); + + // source error message comes from failing stargate keeper + assert!(err + .source() + .unwrap() + .to_string() + .starts_with("Unexpected stargate execute")); +} + +#[test] +fn accepting_stargate_handler_should_work() { + let mut app = AppBuilder::default() + .with_stargate(StargateAccepting) + .build(no_init); + + // store the contract + let creator_addr = app.api().addr_make("creator"); + let code = app.store_code_with_creator(creator_addr, stargate::contract()); + + // instantiate contract + let owner_addr = app.api().addr_make("owner"); + let contract_addr = app + .instantiate_contract(code, owner_addr.clone(), &Empty {}, &[], "tauri", None) + .unwrap(); + + // execute empty message on the contract, this contract returns stargate message + // which is just silently processed by accepting stargate keeper + assert!(app + .execute_contract(owner_addr, contract_addr, &Empty {}, &[]) + .is_ok()); +} diff --git a/src/transactions.rs b/src/transactions.rs index 4e8b2645..8c4aeb7d 100644 --- a/src/transactions.rs +++ b/src/transactions.rs @@ -45,7 +45,7 @@ impl<'a> StorageTransaction<'a> { } } -impl<'a> Storage for StorageTransaction<'a> { +impl Storage for StorageTransaction<'_> { fn get(&self, key: &[u8]) -> Option> { match self.local_state.get(key) { Some(val) => match val { @@ -56,23 +56,9 @@ impl<'a> Storage for StorageTransaction<'a> { } } - 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); - } - - /// 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 + /// Range allows iteration over a set of keys, either forwards or backwards + /// uses standard Rust range notation, e.g. `db.range(b"foo"‥b"bar")`, + /// works also in reverse order. fn range<'b>( &'b self, start: Option<&[u8]>, @@ -101,6 +87,21 @@ impl<'a> Storage for StorageTransaction<'a> { let merged = MergeOverlay::new(local, base, order); Box::new(merged) } + + 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); + } } pub struct RepLog { @@ -127,7 +128,7 @@ impl RepLog { } /// Op is the user operation, which can be stored in the RepLog. -/// Currently Set or Delete. +/// Currently: `Set` or `Delete`. enum Op { /// represents the `Set` operation for setting a key-value pair in storage Set { @@ -544,17 +545,17 @@ mod test { let mut base = MemoryStorage::new(); base.set(b"foo", b"bar"); - let mut stxn1 = StorageTransaction::new(&base); + let mut stx1 = StorageTransaction::new(&base); - assert_eq!(stxn1.get(b"foo"), Some(b"bar".to_vec())); + assert_eq!(stx1.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())); + stx1.set(b"subtx", b"works"); + assert_eq!(stx1.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); + stx1.prepare().commit(&mut base); assert_eq!(base.get(b"subtx"), Some(b"works".to_vec())); } diff --git a/src/wasm.rs b/src/wasm.rs index d7891f8a..487f5405 100644 --- a/src/wasm.rs +++ b/src/wasm.rs @@ -4,7 +4,6 @@ use crate::checksums::{ChecksumGenerator, SimpleChecksumGenerator}; use crate::contracts::Contract; use crate::error::{bail, AnyContext, AnyError, AnyResult, Error}; use crate::executor::AppResponse; -use crate::prefixed_storage::contract_namespace; use crate::prefixed_storage::{prefixed, prefixed_read, PrefixedStorage, ReadonlyPrefixedStorage}; use crate::queries::wasm::WasmRemoteQuerier; use crate::transactions::transactional; @@ -13,46 +12,52 @@ use crate::wasm_emulation::contract::{LocalWasmContract, WasmContract}; use crate::wasm_emulation::input::QuerierStorage; use crate::wasm_emulation::instance::create_module; use crate::wasm_emulation::query::mock_querier::{ForkState, LocalForkedState}; -use crate::wasm_emulation::query::AllWasmQuerier; +use crate::wasm_emulation::query::{AllWasmQuerier, ContainsRemote}; use cosmwasm_std::testing::mock_wasmd_attr; use cosmwasm_std::{ - to_json_binary, Addr, Api, Attribute, BankMsg, Binary, BlockInfo, Coin, ContractInfo, - ContractInfoResponse, CustomQuery, Deps, DepsMut, Env, Event, MessageInfo, Order, Querier, - QuerierWrapper, Record, Reply, ReplyOn, Response, StdResult, Storage, SubMsg, SubMsgResponse, - SubMsgResult, TransactionInfo, WasmMsg, WasmQuery, + to_json_binary, Addr, Api, Attribute, BankMsg, Binary, BlockInfo, Checksum, Coin, ContractInfo, + ContractInfoResponse, CustomMsg, CustomQuery, Deps, DepsMut, Env, Event, MessageInfo, Order, + Querier, QuerierWrapper, Record, Reply, ReplyOn, Response, StdResult, Storage, SubMsg, + SubMsgResponse, SubMsgResult, TransactionInfo, WasmMsg, WasmQuery, }; -use cosmwasm_std::{Checksum, CustomMsg}; use cw_storage_plus::Map; use prost::Message; use schemars::JsonSchema; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use std::cell::RefCell; +use std::collections::BTreeMap; use std::collections::HashMap; use std::fmt::Debug; -//TODO Make `CONTRACTS` private in version 1.0 when the function AddressGenerator::next_address will be removed. /// Contract state kept in storage, separate from the contracts themselves (contract code). -pub(crate) const CONTRACTS: Map<&Addr, ContractData> = Map::new("contracts"); +pub const CONTRACTS: Map<&Addr, ContractData> = Map::new("contracts"); -//TODO Make `NAMESPACE_WASM` private in version 1.0 when the function AddressGenerator::next_address will be removed. -pub(crate) const NAMESPACE_WASM: &[u8] = b"wasm"; -/// See +/// Wasm module namespace. +pub const NAMESPACE_WASM: &[u8] = b"wasm"; + +/// Contract [address namespace]. +/// +/// [address namespace]: https://github.com/CosmWasm/wasmd/blob/96e2b91144c9a371683555f3c696f882583cc6a2/x/wasm/types/events.go#L59 const CONTRACT_ATTR: &str = "_contract_address"; pub const LOCAL_WASM_CODE_OFFSET: usize = 5_000_000; pub const LOCAL_RUST_CODE_OFFSET: usize = 10_000_000; +/// A structure representing a privileged message. #[derive(Clone, Debug, PartialEq, Eq, JsonSchema)] pub struct WasmSudo { + /// Address of a contract the privileged action will be sent to. pub contract_addr: Addr, - pub msg: Binary, + /// Message representing privileged action to be executed by contract `sudo` entry-point. + pub message: Binary, } impl WasmSudo { + /// Creates a new privileged message for specified contract address and action to be executed. pub fn new(contract_addr: &Addr, msg: &T) -> StdResult { Ok(WasmSudo { contract_addr: contract_addr.clone(), - msg: to_json_binary(msg)?, + message: to_json_binary(msg)?, }) } } @@ -67,6 +72,10 @@ pub struct ContractData { pub creator: Addr, /// Optional address of account who can execute migrations pub admin: Option, + /// Metadata passed while contract instantiation + pub label: String, + /// Blockchain height in the moment of instantiating the contract + pub created: u64, } #[derive(Serialize, Deserialize, Debug, Clone)] @@ -77,21 +86,10 @@ pub struct CodeData { /// Checksum of the contract's code base. pub checksum: Checksum, /// Identifier of the code base where the contract code is stored in memory. - pub code_base_id: usize, + pub source_id: usize, } - -pub trait Wasm: AllWasmQuerier { - /// Handles all WasmQuery requests - fn query( - &self, - api: &dyn Api, - storage: &dyn Storage, - router: &dyn CosmosRouter, - querier: &dyn Querier, - block: &BlockInfo, - request: WasmQuery, - ) -> AnyResult; - +/// This trait implements the interface of the Wasm module. +pub trait Wasm: AllWasmQuerier + ContainsRemote { /// Handles all `WasmMsg` messages. fn execute( &self, @@ -103,15 +101,25 @@ pub trait Wasm: AllWasmQuerier { msg: WasmMsg, ) -> AnyResult; + /// Handles all `WasmQuery` requests. + fn query( + &self, + api: &dyn Api, + storage: &dyn Storage, + router: &dyn CosmosRouter, + querier: &dyn Querier, + block: &BlockInfo, + request: WasmQuery, + ) -> AnyResult; + /// Handles all sudo messages, this is an admin interface and can not be called via `CosmosMsg`. fn sudo( &self, api: &dyn Api, - contract_addr: Addr, storage: &mut dyn Storage, router: &dyn CosmosRouter, block: &BlockInfo, - msg: Binary, + msg: WasmSudo, ) -> AnyResult; /// Stores the contract's code and returns an identifier of the stored contract's code. @@ -119,23 +127,69 @@ pub trait Wasm: AllWasmQuerier { /// Stores the contract's code and returns an identifier of the stored contract's code. fn store_wasm_code(&mut self, creator: Addr, code: Vec) -> u64; + /// Stores the contract's code under specified identifier, + /// returns the same code identifier when successful. + fn store_code_with_id( + &mut self, + creator: Addr, + code_id: u64, + code: Box>, + ) -> AnyResult; + + /// Duplicates the contract's code with specified identifier + /// and returns an identifier of the copy of the contract's code. + fn duplicate_code(&mut self, code_id: u64) -> AnyResult; /// Returns `ContractData` for the contract with specified address. fn contract_data(&self, storage: &dyn Storage, address: &Addr) -> AnyResult; /// Returns a raw state dump of all key-values held by a contract with specified address. fn dump_wasm_raw(&self, storage: &dyn Storage, address: &Addr) -> Vec; + + /// Returns the namespace of the contract storage. + fn contract_namespace(&self, contract: &Addr) -> Vec { + let mut name = b"contract_data/".to_vec(); + name.extend_from_slice(contract.as_bytes()); + name + } + + /// Returns **read-only** (not mutable) contract storage. + fn contract_storage<'a>( + &self, + storage: &'a dyn Storage, + address: &Addr, + ) -> Box { + // We double-namespace this, once from global storage -> wasm_storage + // then from wasm_storage -> the contracts subspace + let namespace = self.contract_namespace(address); + let storage = ReadonlyPrefixedStorage::multilevel(storage, &[NAMESPACE_WASM, &namespace]); + Box::new(storage) + } + + /// Returns **read-write** (mutable) contract storage. + fn contract_storage_mut<'a>( + &self, + storage: &'a mut dyn Storage, + address: &Addr, + ) -> Box { + // We double-namespace this, once from global storage -> wasm_storage + // then from wasm_storage -> the contracts subspace + let namespace = self.contract_namespace(address); + let storage = PrefixedStorage::multilevel(storage, &[NAMESPACE_WASM, &namespace]); + Box::new(storage) + } } pub type LocalRustContract = *mut dyn Contract; +/// A structure representing a default wasm keeper. pub struct WasmKeeper { /// Contract codes that stand for wasm code in real-life blockchain. - pub code_base: RefCell>, + pub code_base: RefCell>, + /// Code data with code base identifier and additional attributes. + pub code_data: BTreeMap, /// Contract codes that stand for rust code living in the current instance /// We also associate the queries to them to make sure we are able to use them with the vm instance - pub rust_codes: HashMap>, - /// Code data with code base identifier and additional attributes. - pub code_data: HashMap, + pub rust_codes: HashMap>, /// Contract's address generator. address_generator: Box, /// Contract's code checksum generator. @@ -143,14 +197,14 @@ pub struct WasmKeeper { // chain on which the contract should be queried/tested against remote: Option, /// Just markers to make type elision fork when using it as `Wasm` trait - _p: std::marker::PhantomData<(ExecC, QueryC)>, + _p: std::marker::PhantomData, } impl Default for WasmKeeper { fn default() -> WasmKeeper { Self { - code_base: HashMap::new().into(), - code_data: HashMap::new(), + code_base: BTreeMap::default().into(), + code_data: BTreeMap::default(), address_generator: Box::new(SimpleAddressGenerator), checksum_generator: Box::new(SimpleChecksumGenerator), _p: std::marker::PhantomData, @@ -165,6 +219,22 @@ where ExecC: CustomMsg + DeserializeOwned + 'static, QueryC: CustomQuery + DeserializeOwned + 'static, { + fn execute( + &self, + api: &dyn Api, + storage: &mut dyn Storage, + router: &dyn CosmosRouter, + block: &BlockInfo, + sender: Addr, + msg: WasmMsg, + ) -> AnyResult { + self.execute_wasm(api, storage, router, block, sender.clone(), msg.clone()) + .context(format!( + "Error executing WasmMsg:\n sender: {}\n {:?}", + sender, msg + )) + } + fn query( &self, api: &dyn Api, @@ -203,118 +273,127 @@ where ); to_json_binary(&res).map_err(Into::into) } + #[cfg(feature = "cosmwasm_1_2")] WasmQuery::CodeInfo { code_id } => { let code_data = self.code_data(code_id)?; let res = cosmwasm_std::CodeInfoResponse::new( code_id, - code_data.creator, + code_data.creator.clone(), code_data.checksum, ); to_json_binary(&res).map_err(Into::into) } - other => bail!(Error::UnsupportedWasmQuery(other)), + _ => unimplemented!("{}", Error::unsupported_wasm_query(request)), } } - fn execute( - &self, - api: &dyn Api, - storage: &mut dyn Storage, - router: &dyn CosmosRouter, - block: &BlockInfo, - sender: Addr, - msg: WasmMsg, - ) -> AnyResult { - self.execute_wasm(api, storage, router, block, sender.clone(), msg.clone()) - .context(format!( - "Error executing WasmMsg:\n sender: {}\n {:?}", - sender, msg - )) - } - fn sudo( &self, api: &dyn Api, - contract: Addr, storage: &mut dyn Storage, router: &dyn CosmosRouter, block: &BlockInfo, - msg: Binary, + msg: WasmSudo, ) -> AnyResult { - let custom_event = Event::new("sudo").add_attribute(CONTRACT_ATTR, &contract); + let custom_event = Event::new("sudo").add_attribute(CONTRACT_ATTR, &msg.contract_addr); let querier_storage = router.get_querier_storage(storage)?; let res = self.call_sudo( - contract.clone(), + msg.contract_addr.clone(), api, storage, router, block, - msg.to_vec(), + msg.message.to_vec(), querier_storage, )?; - let (res, msgs) = self.build_app_response(&contract, custom_event, res); - self.process_response(api, router, storage, block, contract, res, msgs) + let (res, msgs) = self.build_app_response(&msg.contract_addr, custom_event, res); + self.process_response(api, router, storage, block, msg.contract_addr, res, msgs) } /// Stores the contract's code in the in-memory lookup table. /// Returns an identifier of the stored contract code. fn store_wasm_code(&mut self, creator: Addr, code: Vec) -> u64 { - let code_id = self.code_base.borrow().len() + 1 + LOCAL_WASM_CODE_OFFSET; + let code_id = self + .next_code_id() + .unwrap_or_else(|| panic!("{}", Error::NoMoreCodeIdAvailable)); let code = WasmContract::Local(LocalWasmContract { module: create_module(&code).unwrap(), code, }); + let checksum = >::checksum(&code) + .unwrap_or(self.checksum_generator.checksum(&creator, code_id)); self.code_base.borrow_mut().insert(code_id, code); - let checksum = self.checksum_generator.checksum(&creator, code_id as u64); self.code_data.insert( code_id, CodeData { creator, checksum, - code_base_id: code_id, + source_id: code_id as usize, }, ); - code_id as u64 + code_id } /// Stores the contract's code in the in-memory lookup table. /// Returns an identifier of the stored contract code. fn store_code(&mut self, creator: Addr, code: Box>) -> u64 { - let code_id = self.rust_codes.len() + 1 + LOCAL_RUST_CODE_OFFSET; - let checksum = code - .checksum() - .unwrap_or(self.checksum_generator.checksum(&creator, code_id as u64)); - let static_ref = Box::leak(code); + let code_id = self + .next_code_id() + .unwrap_or_else(|| panic!("{}", Error::NoMoreCodeIdAvailable)); + self.save_code(code_id, creator, code) + } - let raw_pointer = static_ref as *mut dyn Contract; - self.rust_codes.insert(code_id, raw_pointer); + /// Stores the contract's code in the in-memory lookup table. + /// Returns an identifier of the stored contract code. + fn store_code_with_id( + &mut self, + creator: Addr, + code_id: u64, + code: Box>, + ) -> AnyResult { + // validate provided contract code identifier + if self.code_data.contains_key(&code_id) { + bail!(Error::duplicated_code_id(code_id)); + } else if code_id == 0 { + bail!(Error::invalid_code_id()); + } + Ok(self.save_code(code_id, creator, code)) + } + + /// Duplicates the contract's code with specified identifier. + /// Returns an identifier of the copy of the contract's code. + fn duplicate_code(&mut self, code_id: u64) -> AnyResult { + let code_data = self.code_data(code_id)?; + let new_code_id = self + .next_code_id() + .ok_or_else(Error::no_more_code_id_available)?; self.code_data.insert( - code_id, + new_code_id, CodeData { - creator, - checksum, - code_base_id: code_id, + creator: code_data.creator.clone(), + checksum: code_data.checksum, + source_id: code_data.source_id, }, ); - code_id as u64 + Ok(new_code_id) } /// Returns `ContractData` for the contract with specified address. fn contract_data(&self, storage: &dyn Storage, address: &Addr) -> AnyResult { - let contract = CONTRACTS.load(&prefixed_read(storage, NAMESPACE_WASM), address); - if let Ok(local_contract) = contract { - Ok(local_contract) - } else { - WasmRemoteQuerier::load_distant_contract(self.remote.clone().unwrap(), address) - } + CONTRACTS + .load(&prefixed_read(storage, NAMESPACE_WASM), address) + .or_else(|_| { + WasmRemoteQuerier::load_distant_contract(self.remote.clone().unwrap(), address) + }) + .map_err(Into::into) } /// Returns a raw state dump of all key-values held by a contract with specified address. fn dump_wasm_raw(&self, storage: &dyn Storage, address: &Addr) -> Vec { - let storage = self.contract_storage_readonly(storage, address); + let storage = self.contract_storage(storage, address); storage.range(None, None, Order::Ascending).collect() } } @@ -348,24 +427,99 @@ where }, }) } + /// Creates a wasm keeper with default settings. + /// + /// # Example + /// + /// ``` + /// use cw_multi_test::{no_init, AppBuilder, WasmKeeper}; + /// + /// // create wasm keeper + /// let wasm_keeper = WasmKeeper::new(); + /// + /// // create and use the application with newly created wasm keeper + /// let mut app = AppBuilder::default().with_wasm(wasm_keeper).build(no_init); + /// ``` + pub fn new() -> Self { + Self::default() + } + + /// Populates an existing [WasmKeeper] with custom contract address generator. + /// + /// # Example + /// + /// ``` + /// use cosmwasm_std::{Addr, Api, Storage}; + /// use cw_multi_test::{no_init, AddressGenerator, AppBuilder, WasmKeeper}; + /// use cw_multi_test::error::AnyResult; + /// # use cosmwasm_std::testing::MockApi; + /// + /// struct CustomAddressGenerator; + /// + /// impl AddressGenerator for CustomAddressGenerator { + /// fn contract_address( + /// &self, + /// api: &dyn Api, + /// storage: &mut dyn Storage, + /// code_id: u64, + /// instance_id: u64, + /// ) -> AnyResult { + /// // here implement your address generation logic + /// # Ok(MockApi::default().addr_make("test_address")) + /// } + /// } + /// + /// // populate wasm with your custom address generator + /// let wasm_keeper = WasmKeeper::new().with_address_generator(CustomAddressGenerator); + /// + /// // create and use the application with customized wasm keeper + /// let mut app = AppBuilder::default().with_wasm(wasm_keeper).build(no_init); + /// ``` + pub fn with_address_generator( + mut self, + address_generator: impl AddressGenerator + 'static, + ) -> Self { + self.address_generator = Box::new(address_generator); + self + } + + /// Populates an existing [WasmKeeper] with custom checksum generator for the contract code. + /// + /// # Example + /// + /// ``` + /// use cosmwasm_std::{Addr, Checksum}; + /// use cw_multi_test::{no_init, AppBuilder, ChecksumGenerator, WasmKeeper}; + /// + /// struct MyChecksumGenerator; + /// + /// impl ChecksumGenerator for MyChecksumGenerator { + /// fn checksum(&self, creator: &Addr, code_id: u64) -> Checksum { + /// // here implement your custom checksum generator + /// # Checksum::from_hex("custom_checksum").unwrap() + /// } + /// } + /// + /// // populate wasm keeper with your custom checksum generator + /// let wasm_keeper = WasmKeeper::new().with_checksum_generator(MyChecksumGenerator); + /// + /// // create and use the application with customized wasm keeper + /// let mut app = AppBuilder::default().with_wasm(wasm_keeper).build(no_init); + /// ``` + pub fn with_checksum_generator( + mut self, + checksum_generator: impl ChecksumGenerator + 'static, + ) -> Self { + self.checksum_generator = Box::new(checksum_generator); + self + } /// Returns a handler to code of the contract with specified code id. - pub fn contract_code<'a, 'b>( - &'a self, - code_id: u64, - ) -> AnyResult> - where - 'a: 'b, - { - let code_data = self.code_data(code_id)?; - let code = self - .code_base - .borrow() - .get(&code_data.code_base_id) - .cloned(); + pub fn contract_code(&self, code_id: u64) -> AnyResult> { + let code = self.code_base.borrow().get(&code_id).cloned(); if let Some(code) = code { Ok(ContractBox::Owned(Box::new(code))) - } else if let Some(&rust_code) = self.rust_codes.get(&code_data.code_base_id) { + } else if let Some(&rust_code) = self.rust_codes.get(&code_id) { Ok(ContractBox::Borrowed(unsafe { rust_code.as_ref().unwrap() })) @@ -377,7 +531,7 @@ where // We save it in memory self.code_base .borrow_mut() - .insert(code_id as usize, wasm_contract.clone()); + .insert(code_id, wasm_contract.clone()); // And return a Owned reference Ok(ContractBox::Owned(Box::new(wasm_contract))) @@ -387,98 +541,29 @@ where /// Returns code data of the contract with specified code id. fn code_data(&self, code_id: u64) -> AnyResult { if code_id < 1 { - bail!(Error::InvalidCodeId); + bail!(Error::invalid_code_id()); } - if let Some(code_data) = self.code_data.get(&(code_id as usize)) { - Ok(code_data.clone()) - } else { - let code_info_response = - WasmRemoteQuerier::code_info(self.remote.clone().unwrap(), code_id)?; - Ok(CodeData { - creator: Addr::unchecked(code_info_response.creator), - checksum: code_info_response.checksum, - code_base_id: code_id as usize, + Ok(self + .code_data + .get(&code_id) + .cloned() + .ok_or_else(|| { + let code_info_response = + WasmRemoteQuerier::code_info(self.remote.clone().unwrap(), code_id)?; + Ok::<_, anyhow::Error>(CodeData { + creator: Addr::unchecked(code_info_response.creator), + checksum: code_info_response.checksum, + source_id: code_id as usize, + }) }) - } + .map_err(|_| Error::unregistered_code_id(code_id))?) } pub fn dump_wasm_raw(&self, storage: &dyn Storage, address: &Addr) -> Vec { - let storage = self.contract_storage_readonly(storage, address); + let storage = self.contract_storage(storage, address); storage.range(None, None, Order::Ascending).collect() } - fn contract_namespace(&self, contract: &Addr) -> Vec { - contract_namespace(contract) - } - - fn contract_storage<'a>( - &self, - storage: &'a mut dyn Storage, - address: &Addr, - ) -> Box { - // We double-namespace this, once from global storage -> wasm_storage - // then from wasm_storage -> the contracts subspace - let namespace = self.contract_namespace(address); - let storage = PrefixedStorage::multilevel(storage, &[NAMESPACE_WASM, &namespace]); - - Box::new(storage) - } - - // fails RUNTIME if you try to write. please don't - fn contract_storage_readonly<'a>( - &self, - storage: &'a dyn Storage, - address: &Addr, - ) -> Box { - // We double-namespace this, once from global storage -> wasm_storage - // then from wasm_storage -> the contracts subspace - let namespace = self.contract_namespace(address); - let storage = ReadonlyPrefixedStorage::multilevel(storage, &[NAMESPACE_WASM, &namespace]); - Box::new(storage) - } -} -impl WasmKeeper -where - ExecC: CustomMsg + DeserializeOwned + 'static, - QueryC: CustomQuery + DeserializeOwned + 'static, -{ - pub fn new() -> Self { - Self::default() - } - - #[deprecated( - since = "0.18.0", - note = "use `WasmKeeper::new().with_address_generator` instead; will be removed in version 1.0.0" - )] - pub fn new_with_custom_address_generator( - address_generator: impl AddressGenerator + 'static, - ) -> Self { - Self { - address_generator: Box::new(address_generator), - ..Default::default() - } - } - - pub fn with_remote(mut self, remote: RemoteChannel) -> Self { - self.remote = Some(remote); - self - } - pub fn with_address_generator( - mut self, - address_generator: impl AddressGenerator + 'static, - ) -> Self { - self.address_generator = Box::new(address_generator); - self - } - - pub fn with_checksum_generator( - mut self, - checksum_generator: impl ChecksumGenerator + 'static, - ) -> Self { - self.checksum_generator = Box::new(checksum_generator); - self - } - /// Validates all attributes. /// /// In `wasmd`, before version v0.45.0 empty attribute values were not allowed. @@ -515,6 +600,39 @@ where Ok(response) } + fn save_code( + &mut self, + code_id: u64, + creator: Addr, + code: Box>, + ) -> u64 { + // prepare the next identifier for the contract's code + let source_id = code_id as usize; + // prepare the contract's Wasm blob checksum + let checksum = code + .checksum() + .unwrap_or(self.checksum_generator.checksum(&creator, code_id)); + // store the 'source' code of the contract + let static_ref = Box::leak(code); + let raw_pointer = static_ref as *mut dyn Contract; + self.rust_codes.insert(code_id, raw_pointer); + // store the additional code attributes like creator address and checksum + self.code_data.insert( + code_id, + CodeData { + creator, + checksum, + source_id, + }, + ); + code_id + } + + /// Returns the next contract's code identifier. + fn next_code_id(&self) -> Option { + Some((self.code_base.borrow().len() + 1 + LOCAL_WASM_CODE_OFFSET) as u64) + } + /// Executes the contract's `query` entry-point. pub fn query_smart( &self, @@ -549,15 +667,17 @@ where ) } + /// Returns the value stored under specified key in contracts storage. pub fn query_raw(&self, address: Addr, storage: &dyn Storage, key: &[u8]) -> Binary { - let local_key = self.contract_storage_readonly(storage, &address).get(key); - if let Some(local_key) = local_key { - local_key.into() - } else { - WasmRemoteQuerier::raw_query(self.remote.clone().unwrap(), &address, key.into()) - .unwrap_or_default() - .into() - } + let storage = self.contract_storage(storage, &address); + let data = storage + .get(key) + .or_else(|| { + WasmRemoteQuerier::raw_query(self.remote.clone().unwrap(), &address, key.into()) + .ok() + }) + .unwrap_or_default(); + data.into() } fn send( @@ -622,9 +742,9 @@ where router: &dyn CosmosRouter, block: &BlockInfo, sender: Addr, - wasm_msg: WasmMsg, + msg: WasmMsg, ) -> AnyResult { - match wasm_msg { + match msg { WasmMsg::Execute { contract_addr, msg, @@ -661,7 +781,6 @@ where Event::new("execute").add_attribute(CONTRACT_ATTR, &contract_addr); let (res, msgs) = self.build_app_response(&contract_addr, custom_event, res); - let mut res = self.process_response(api, router, storage, block, contract_addr, res, msgs)?; res.data = execute_response(res.data); @@ -740,7 +859,7 @@ where WasmMsg::ClearAdmin { contract_addr } => { self.update_admin(api, storage, sender, &contract_addr, None) } - msg => bail!(Error::UnsupportedWasmMsg(msg)), + _ => unimplemented!("{}", Error::unsupported_wasm_message(msg)), } } @@ -804,7 +923,6 @@ where .add_attribute("code_id", code_id.to_string()); let (res, msgs) = self.build_app_response(&contract_addr, custom_event, res); - let mut res = self.process_response( api, router, @@ -850,6 +968,8 @@ where if matches!(reply_on, ReplyOn::Always | ReplyOn::Success) { let reply = Reply { id, + payload: Default::default(), + gas_used: 0, result: SubMsgResult::Ok( #[allow(deprecated)] SubMsgResponse { @@ -858,8 +978,6 @@ where msg_responses: vec![], }, ), - payload: Default::default(), - gas_used: 0, }; // do reply and combine it with the original response let reply_res = self.reply(api, router, storage, block, contract, reply)?; @@ -871,15 +989,14 @@ where // reply is not called, no data should be returned r.data = None; } - Ok(r) } else if let Err(e) = res { if matches!(reply_on, ReplyOn::Always | ReplyOn::Error) { let reply = Reply { id, - result: SubMsgResult::Err(format!("{:?}", e)), payload: Default::default(), gas_used: 0, + result: SubMsgResult::Err(format!("{:?}", e)), }; self.reply(api, router, storage, block, contract, reply) } else { @@ -995,8 +1112,8 @@ where code_id: u64, creator: Addr, admin: impl Into>, - _label: String, - _created: u64, + label: String, + created: u64, salt: impl Into>, ) -> AnyResult { // We don't error if the code id doesn't exist, it allows us to instantiate remote contracts @@ -1031,11 +1148,14 @@ where code_id, creator, admin: admin.into(), + label, + created, }; self.save_contract(storage, &addr, &info)?; Ok(addr) } + /// Executes contract's `execute` entry-point. pub fn call_execute( &self, api: &dyn Api, @@ -1072,6 +1192,7 @@ where )?) } + /// Executes contract's `instantiate` entry-point. pub fn call_instantiate( &self, address: Addr, @@ -1108,6 +1229,7 @@ where )?) } + /// Executes contract's `reply` entry-point. pub fn call_reply( &self, address: Addr, @@ -1141,6 +1263,7 @@ where )?) } + /// Executes contract's `sudo` entry-point. pub fn call_sudo( &self, address: Addr, @@ -1174,6 +1297,7 @@ where )?) } + /// Executes contract's `migrate` entry-point. pub fn call_migrate( &self, address: Addr, @@ -1217,8 +1341,8 @@ where } } - fn with_storage_readonly<'a, 'b, F, T>( - &'a self, + fn with_storage_readonly( + &self, api: &dyn Api, storage: &dyn Storage, querier: &dyn Querier, @@ -1227,12 +1351,11 @@ where action: F, ) -> AnyResult where - F: FnOnce(ContractBox<'b, ExecC, QueryC>, Deps, Env) -> AnyResult, - 'a: 'b, + F: FnOnce(ContractBox, Deps, Env) -> AnyResult, { let contract = self.contract_data(storage, &address)?; - let handler = self.contract_code::<'a, 'b>(contract.code_id)?; - let storage = self.contract_storage_readonly(storage, &address); + let handler = self.contract_code(contract.code_id)?; + let storage = self.contract_storage(storage, &address); let env = self.get_env(address, block); let deps = Deps { @@ -1243,8 +1366,8 @@ where action(handler, deps, env) } - fn with_storage<'a, 'b, F, T>( - &'a self, + fn with_storage( + &self, api: &dyn Api, storage: &mut dyn Storage, router: &dyn CosmosRouter, @@ -1253,8 +1376,7 @@ where action: F, ) -> AnyResult where - F: FnOnce(ContractBox<'b, ExecC, QueryC>, DepsMut, Env) -> AnyResult, - 'a: 'b, + F: FnOnce(ContractBox, DepsMut, Env) -> AnyResult, ExecC: DeserializeOwned, { let contract = self.contract_data(storage, &address)?; @@ -1265,7 +1387,7 @@ where // However, we need to get write and read access to the same storage in two different objects, // and this is the only way I know how to do so. transactional(storage, |write_cache, read_store| { - let mut contract_storage = self.contract_storage(write_cache, &address); + let mut contract_storage = self.contract_storage_mut(write_cache, &address); let querier = RouterQuerier::new(router, api, read_store, block); let env = self.get_env(address, block); @@ -1278,6 +1400,7 @@ where }) } + /// Saves contract data in a storage under specified address. pub fn save_contract( &self, storage: &mut dyn Storage, @@ -1302,17 +1425,29 @@ where } } -// TODO: replace with code in utils +impl ContainsRemote for WasmKeeper +where + ExecC: CustomMsg + DeserializeOwned + 'static, + QueryC: CustomQuery + DeserializeOwned + 'static, +{ + fn with_remote(mut self, remote: RemoteChannel) -> Self { + self.set_remote(remote); + self + } + + fn set_remote(&mut self, remote: RemoteChannel) { + self.remote = Some(remote) + } +} #[derive(Clone, PartialEq, Message)] struct InstantiateResponse { #[prost(string, tag = "1")] - pub address: ::prost::alloc::string::String, + pub address: String, #[prost(bytes, tag = "2")] - pub data: ::prost::alloc::vec::Vec, + pub data: Vec, } -// TODO: encode helpers in utils fn instantiate_response(data: Option, contact_address: &Addr) -> Binary { let data = data.unwrap_or_default().to_vec(); let init_data = InstantiateResponse { @@ -1328,7 +1463,7 @@ fn instantiate_response(data: Option, contact_address: &Addr) -> Binary #[derive(Clone, PartialEq, Message)] struct ExecuteResponse { #[prost(bytes, tag = "1")] - pub data: ::prost::alloc::vec::Vec, + pub data: Vec, } // empty return if no data present in original @@ -1341,3 +1476,924 @@ fn execute_response(data: Option) -> Option { new_data.into() }) } + +#[cfg(test)] +mod test { + use super::*; + use crate::app::Router; + use crate::bank::BankKeeper; + use crate::featured::staking::{DistributionKeeper, StakeKeeper}; + use crate::module::FailingModule; + use crate::test_helpers::{caller, error, payout}; + use crate::tests::remote_channel; + use crate::transactions::StorageTransaction; + use crate::{GovFailingModule, IbcFailingModule, StargateFailing}; + use cosmwasm_std::testing::{message_info, mock_env, MockApi, MockQuerier, MockStorage}; + #[cfg(feature = "cosmwasm_1_2")] + use cosmwasm_std::CodeInfoResponse; + use cosmwasm_std::{ + coin, from_json, to_json_vec, CanonicalAddr, CosmosMsg, Empty, HexBinary, StdError, + }; + + /// Type alias for default build `Router` to make its reference in typical scenario + type BasicRouter = Router< + BankKeeper, + FailingModule, + WasmKeeper, + StakeKeeper, + DistributionKeeper, + IbcFailingModule, + GovFailingModule, + StargateFailing, + >; + + fn wasm_keeper() -> WasmKeeper { + WasmKeeper::new().with_remote(remote_channel()) + } + + fn mock_router() -> BasicRouter { + Router { + wasm: WasmKeeper::new(), + bank: BankKeeper::new(), + custom: FailingModule::new(), + staking: StakeKeeper::new(), + distribution: DistributionKeeper::new(), + ibc: IbcFailingModule::new(), + gov: GovFailingModule::new(), + stargate: StargateFailing, + } + } + + #[test] + fn register_contract() { + let api = MockApi::default(); + + // prepare user addresses + let creator_addr = api.addr_make("creator"); + let user_addr = api.addr_make("foobar"); + let admin_addr = api.addr_make("admin"); + let unregistered_addr = api.addr_make("unregistered"); + + let mut wasm_storage = MockStorage::new(); + let mut wasm_keeper = wasm_keeper(); + let block = mock_env().block; + let code_id = wasm_keeper.store_code(creator_addr, error::contract(false)); + + transactional(&mut wasm_storage, |cache, _| { + // cannot register contract with unregistered codeId + wasm_keeper.register_contract( + &api, + cache, + code_id + 1, + user_addr.clone(), + admin_addr.clone(), + "label".to_owned(), + 1000, + None, + ) + }) + .unwrap_err(); + + let contract_addr = transactional(&mut wasm_storage, |cache, _| { + // we can register a new instance of this code + wasm_keeper.register_contract( + &api, + cache, + code_id, + user_addr.clone(), + admin_addr.clone(), + "label".to_owned(), + 1000, + None, + ) + }) + .unwrap(); + + // verify contract data are as expected + let contract_data = wasm_keeper + .contract_data(&wasm_storage, &contract_addr) + .unwrap(); + + assert_eq!( + contract_data, + ContractData { + code_id, + creator: user_addr.clone(), + admin: admin_addr.into(), + label: "label".to_owned(), + created: 1000, + } + ); + + let err = transactional(&mut wasm_storage, |cache, _| { + // now, we call this contract and see the error message from the contract + let info = message_info(&user_addr, &[]); + wasm_keeper.call_instantiate( + contract_addr.clone(), + &api, + cache, + &mock_router(), + &block, + info, + b"{}".to_vec(), + QuerierStorage::default(), + ) + }) + .unwrap_err(); + + // StdError from contract_error auto-converted to string + assert_eq!( + StdError::generic_err("Init failed"), + err.downcast().unwrap() + ); + + let err = transactional(&mut wasm_storage, |cache, _| { + // and the error for calling an unregistered contract + let info = message_info(&user_addr, &[]); + wasm_keeper.call_instantiate( + unregistered_addr, + &api, + cache, + &mock_router(), + &block, + info, + b"{}".to_vec(), + QuerierStorage::default(), + ) + }) + .unwrap_err(); + + // Default error message from router when not found + assert!(matches!(err.downcast().unwrap(), StdError::NotFound { .. })); + } + + #[test] + fn query_contract_info() { + let api = MockApi::default(); + + // prepare user addresses + let creator_addr = api.addr_make("creator"); + let admin_addr = api.addr_make("admin"); + + let mut wasm_storage = MockStorage::new(); + let mut wasm_keeper = wasm_keeper(); + let block = mock_env().block; + let code_id = wasm_keeper.store_code(creator_addr.clone(), payout::contract()); + assert_eq!(1, code_id); + + let contract_addr = wasm_keeper + .register_contract( + &api, + &mut wasm_storage, + code_id, + creator_addr.clone(), + admin_addr.clone(), + "label".to_owned(), + 1000, + None, + ) + .unwrap(); + + let querier: MockQuerier = MockQuerier::new(&[]); + let query = WasmQuery::ContractInfo { + contract_addr: contract_addr.into(), + }; + + let contract_info = wasm_keeper + .query(&api, &wasm_storage, &mock_router(), &querier, &block, query) + .unwrap(); + + let actual: ContractInfoResponse = from_json(contract_info).unwrap(); + let expected = + ContractInfoResponse::new(code_id, creator_addr, admin_addr.into(), false, None); + assert_eq!(expected, actual); + } + + #[test] + #[cfg(feature = "cosmwasm_1_2")] + fn query_code_info() { + let api = MockApi::default(); + let wasm_storage = MockStorage::new(); + let mut wasm_keeper = wasm_keeper(); + let block = mock_env().block; + let creator_addr = api.addr_make("creator"); + let code_id = wasm_keeper.store_code(creator_addr.clone(), payout::contract()); + let querier: MockQuerier = MockQuerier::new(&[]); + let query = WasmQuery::CodeInfo { code_id }; + let code_info = wasm_keeper + .query(&api, &wasm_storage, &mock_router(), &querier, &block, query) + .unwrap(); + let actual: CodeInfoResponse = from_json(code_info).unwrap(); + assert_eq!(code_id, actual.code_id); + assert_eq!(creator_addr.as_str(), actual.creator.as_str()); + assert_eq!(32, actual.checksum.as_slice().len()); + } + + #[test] + #[cfg(feature = "cosmwasm_1_2")] + fn different_contracts_must_have_different_checksum() { + let api = MockApi::default(); + let creator_addr = api.addr_make("creator"); + let wasm_storage = MockStorage::new(); + let mut wasm_keeper = wasm_keeper(); + let block = mock_env().block; + let code_id_payout = wasm_keeper.store_code(creator_addr.clone(), payout::contract()); + let code_id_caller = wasm_keeper.store_code(creator_addr, caller::contract()); + let querier: MockQuerier = MockQuerier::new(&[]); + let query_payout = WasmQuery::CodeInfo { + code_id: code_id_payout, + }; + let query_caller = WasmQuery::CodeInfo { + code_id: code_id_caller, + }; + let code_info_payout = wasm_keeper + .query( + &api, + &wasm_storage, + &mock_router(), + &querier, + &block, + query_payout, + ) + .unwrap(); + let code_info_caller = wasm_keeper + .query( + &api, + &wasm_storage, + &mock_router(), + &querier, + &block, + query_caller, + ) + .unwrap(); + let info_payout: CodeInfoResponse = from_json(code_info_payout).unwrap(); + let info_caller: CodeInfoResponse = from_json(code_info_caller).unwrap(); + assert_eq!(code_id_payout, info_payout.code_id); + assert_eq!(code_id_caller, info_caller.code_id); + assert_ne!(info_caller.code_id, info_payout.code_id); + assert_eq!(info_caller.creator, info_payout.creator); + assert_ne!(info_caller.checksum, info_payout.checksum); + } + + #[test] + #[cfg(feature = "cosmwasm_1_2")] + fn querying_invalid_code_info_must_fail() { + let api = MockApi::default(); + let wasm_storage = MockStorage::new(); + let wasm_keeper = wasm_keeper(); + let block = mock_env().block; + + let querier: MockQuerier = MockQuerier::new(&[]); + let query = WasmQuery::CodeInfo { code_id: 100 }; + + wasm_keeper + .query(&api, &wasm_storage, &mock_router(), &querier, &block, query) + .unwrap_err(); + } + + #[test] + fn can_dump_raw_wasm_state() { + let api = MockApi::default(); + + // prepare user addresses + let creator_addr = api.addr_make("creator"); + let admin_addr = api.addr_make("admin"); + let user_addr = api.addr_make("foobar"); + + let mut wasm_keeper = wasm_keeper(); + let block = mock_env().block; + let code_id = wasm_keeper.store_code(creator_addr, payout::contract()); + + let mut wasm_storage = MockStorage::new(); + + let contract_addr = wasm_keeper + .register_contract( + &api, + &mut wasm_storage, + code_id, + user_addr.clone(), + admin_addr, + "label".to_owned(), + 1000, + None, + ) + .unwrap(); + + // make a contract with state + let payout = coin(1500, "mlg"); + let msg = payout::InstantiateMessage { + payout: payout.clone(), + }; + wasm_keeper + .call_instantiate( + contract_addr.clone(), + &api, + &mut wasm_storage, + &mock_router(), + &block, + message_info(&user_addr, &[]), + to_json_vec(&msg).unwrap(), + QuerierStorage::default(), + ) + .unwrap(); + + // dump state + let state = wasm_keeper.dump_wasm_raw(&wasm_storage, &contract_addr); + assert_eq!(state.len(), 2); + // check contents + let (k, v) = &state[0]; + assert_eq!(k.as_slice(), b"count"); + let count: u32 = from_json(v).unwrap(); + assert_eq!(count, 1); + let (k, v) = &state[1]; + assert_eq!(k.as_slice(), b"payout"); + let stored_pay: payout::InstantiateMessage = from_json(v).unwrap(); + assert_eq!(stored_pay.payout, payout); + } + + #[test] + fn contract_send_coins() { + let api = MockApi::default(); + + // prepare user addresses + let creator_addr = api.addr_make("creator"); + let user_addr = api.addr_make("foobar"); + + let mut wasm_keeper = wasm_keeper(); + let block = mock_env().block; + let code_id = wasm_keeper.store_code(creator_addr, payout::contract()); + + let mut wasm_storage = MockStorage::new(); + let mut cache = StorageTransaction::new(&wasm_storage); + + let contract_addr = wasm_keeper + .register_contract( + &api, + &mut cache, + code_id, + user_addr.clone(), + None, + "label".to_owned(), + 1000, + None, + ) + .unwrap(); + + let payout = coin(100, "TGD"); + + // init the contract + let info = message_info(&user_addr, &[]); + let init_msg = to_json_vec(&payout::InstantiateMessage { + payout: payout.clone(), + }) + .unwrap(); + let res = wasm_keeper + .call_instantiate( + contract_addr.clone(), + &api, + &mut cache, + &mock_router(), + &block, + info, + init_msg, + QuerierStorage::default(), + ) + .unwrap(); + assert_eq!(0, res.messages.len()); + + // execute the contract + let info = message_info(&user_addr, &[]); + let res = wasm_keeper + .call_execute( + &api, + &mut cache, + contract_addr.clone(), + &mock_router(), + &block, + info, + b"{}".to_vec(), + QuerierStorage::default(), + ) + .unwrap(); + assert_eq!(1, res.messages.len()); + match &res.messages[0].msg { + CosmosMsg::Bank(BankMsg::Send { to_address, amount }) => { + assert_eq!(to_address.as_str(), user_addr.as_str()); + assert_eq!(amount.as_slice(), &[payout.clone()]); + } + m => panic!("Unexpected message {:?}", m), + } + + // and flush before query + cache.prepare().commit(&mut wasm_storage); + + // query the contract + let query = to_json_vec(&payout::QueryMsg::Payout {}).unwrap(); + let querier: MockQuerier = MockQuerier::new(&[]); + let data = wasm_keeper + .query_smart( + contract_addr, + &api, + &wasm_storage, + &querier, + &block, + query, + QuerierStorage::default(), + ) + .unwrap(); + let res: payout::InstantiateMessage = from_json(data).unwrap(); + assert_eq!(res.payout, payout); + } + + fn assert_payout( + router: &WasmKeeper, + storage: &mut dyn Storage, + contract_addr: &Addr, + payout: &Coin, + ) { + let api = MockApi::default(); + let user_addr = api.addr_make("silly"); + let info = message_info(&user_addr, &[]); + let res = router + .call_execute( + &api, + storage, + contract_addr.clone(), + &mock_router(), + &mock_env().block, + info, + b"{}".to_vec(), + QuerierStorage::default(), + ) + .unwrap(); + assert_eq!(1, res.messages.len()); + match &res.messages[0].msg { + CosmosMsg::Bank(BankMsg::Send { to_address, amount }) => { + assert_eq!(to_address.as_str(), user_addr.as_str()); + assert_eq!(amount.as_slice(), &[payout.clone()]); + } + m => panic!("Unexpected message {:?}", m), + } + } + + fn assert_no_contract(storage: &dyn Storage, contract_addr: &Addr) { + let contract = CONTRACTS.may_load(storage, contract_addr).unwrap(); + assert!(contract.is_none(), "{:?}", contract_addr); + } + + #[test] + fn multi_level_wasm_cache() { + let api = MockApi::default(); + + // prepare user addresses + let creator_addr = api.addr_make("creator"); + let user_addr = api.addr_make("foobar"); + let user_addr_1 = api.addr_make("johnny"); + + let mut wasm_keeper = wasm_keeper(); + let block = mock_env().block; + let code_id = wasm_keeper.store_code(creator_addr, payout::contract()); + + let mut wasm_storage = MockStorage::new(); + + let payout1 = coin(100, "TGD"); + + // set contract 1 and commit (on router) + let contract1 = transactional(&mut wasm_storage, |cache, _| { + let contract = wasm_keeper + .register_contract( + &api, + cache, + code_id, + user_addr.clone(), + None, + "".to_string(), + 1000, + None, + ) + .unwrap(); + let info = message_info(&user_addr, &[]); + let init_msg = to_json_vec(&payout::InstantiateMessage { + payout: payout1.clone(), + }) + .unwrap(); + wasm_keeper + .call_instantiate( + contract.clone(), + &api, + cache, + &mock_router(), + &block, + info, + init_msg, + QuerierStorage::default(), + ) + .unwrap(); + + Ok(contract) + }) + .unwrap(); + + let payout2 = coin(50, "BTC"); + let payout3 = coin(1234, "ATOM"); + + // create a new cache and check we can use contract 1 + let (contract2, contract3) = transactional(&mut wasm_storage, |cache, wasm_reader| { + assert_payout(&wasm_keeper, cache, &contract1, &payout1); + + // create contract 2 and use it + let contract2 = wasm_keeper + .register_contract( + &api, + cache, + code_id, + user_addr.clone(), + None, + "".to_owned(), + 1000, + None, + ) + .unwrap(); + let info = message_info(&user_addr, &[]); + let init_msg = to_json_vec(&payout::InstantiateMessage { + payout: payout2.clone(), + }) + .unwrap(); + let _res = wasm_keeper + .call_instantiate( + contract2.clone(), + &api, + cache, + &mock_router(), + &block, + info, + init_msg, + QuerierStorage::default(), + ) + .unwrap(); + assert_payout(&wasm_keeper, cache, &contract2, &payout2); + + // create a level2 cache and check we can use contract 1 and contract 2 + let contract3 = transactional(cache, |cache2, read| { + assert_payout(&wasm_keeper, cache2, &contract1, &payout1); + assert_payout(&wasm_keeper, cache2, &contract2, &payout2); + + // create a contract on level 2 + let contract3 = wasm_keeper + .register_contract( + &api, + cache2, + code_id, + user_addr, + None, + "".to_owned(), + 1000, + None, + ) + .unwrap(); + let info = message_info(&user_addr_1, &[]); + let init_msg = to_json_vec(&payout::InstantiateMessage { + payout: payout3.clone(), + }) + .unwrap(); + let _res = wasm_keeper + .call_instantiate( + contract3.clone(), + &api, + cache2, + &mock_router(), + &block, + info, + init_msg, + QuerierStorage::default(), + ) + .unwrap(); + assert_payout(&wasm_keeper, cache2, &contract3, &payout3); + + // ensure first cache still doesn't see this contract + assert_no_contract(read, &contract3); + Ok(contract3) + }) + .unwrap(); + + // after applying transaction, all contracts present on cache + assert_payout(&wasm_keeper, cache, &contract1, &payout1); + assert_payout(&wasm_keeper, cache, &contract2, &payout2); + assert_payout(&wasm_keeper, cache, &contract3, &payout3); + + // but not yet the root router + assert_no_contract(wasm_reader, &contract1); + assert_no_contract(wasm_reader, &contract2); + assert_no_contract(wasm_reader, &contract3); + + Ok((contract2, contract3)) + }) + .unwrap(); + + // ensure that it is now applied to the router + assert_payout(&wasm_keeper, &mut wasm_storage, &contract1, &payout1); + assert_payout(&wasm_keeper, &mut wasm_storage, &contract2, &payout2); + assert_payout(&wasm_keeper, &mut wasm_storage, &contract3, &payout3); + } + + fn assert_admin( + storage: &dyn Storage, + wasm_keeper: &WasmKeeper, + contract_addr: &impl ToString, + admin: Option, + ) { + let api = MockApi::default(); + let querier: MockQuerier = MockQuerier::new(&[]); + // query + let data = wasm_keeper + .query( + &api, + storage, + &mock_router(), + &querier, + &mock_env().block, + WasmQuery::ContractInfo { + contract_addr: contract_addr.to_string(), + }, + ) + .unwrap(); + let res: ContractInfoResponse = from_json(data).unwrap(); + assert_eq!(res.admin, admin); + } + + #[test] + fn update_clear_admin_works() { + let api = MockApi::default(); + let mut wasm_keeper = wasm_keeper(); + let block = mock_env().block; + let creator = api.addr_make("creator"); + let code_id = wasm_keeper.store_code(creator.clone(), caller::contract()); + + let mut wasm_storage = MockStorage::new(); + + let admin = api.addr_make("admin"); + let new_admin = api.addr_make("new_admin"); + let normal_user = api.addr_make("normal_user"); + + let contract_addr = wasm_keeper + .register_contract( + &api, + &mut wasm_storage, + code_id, + creator, + admin.clone(), + "label".to_owned(), + 1000, + None, + ) + .unwrap(); + + // init the contract + let info = message_info(&admin, &[]); + let init_msg = to_json_vec(&Empty {}).unwrap(); + let res = wasm_keeper + .call_instantiate( + contract_addr.clone(), + &api, + &mut wasm_storage, + &mock_router(), + &block, + info, + init_msg, + QuerierStorage::default(), + ) + .unwrap(); + assert_eq!(0, res.messages.len()); + + assert_admin( + &wasm_storage, + &wasm_keeper, + &contract_addr, + Some(admin.clone()), + ); + + // non-admin should not be allowed to become admin on their own + wasm_keeper + .execute_wasm( + &api, + &mut wasm_storage, + &mock_router(), + &block, + normal_user.clone(), + WasmMsg::UpdateAdmin { + contract_addr: contract_addr.to_string(), + admin: normal_user.to_string(), + }, + ) + .unwrap_err(); + + // should still be admin + assert_admin( + &wasm_storage, + &wasm_keeper, + &contract_addr, + Some(admin.clone()), + ); + + // admin should be allowed to transfer administration permissions + let res = wasm_keeper + .execute_wasm( + &api, + &mut wasm_storage, + &mock_router(), + &block, + admin, + WasmMsg::UpdateAdmin { + contract_addr: contract_addr.to_string(), + admin: new_admin.to_string(), + }, + ) + .unwrap(); + assert_eq!(res.events.len(), 0); + + // new_admin should now be admin + assert_admin( + &wasm_storage, + &wasm_keeper, + &contract_addr, + Some(new_admin.clone()), + ); + + // new_admin should now be able to clear to admin + let res = wasm_keeper + .execute_wasm( + &api, + &mut wasm_storage, + &mock_router(), + &block, + new_admin, + WasmMsg::ClearAdmin { + contract_addr: contract_addr.to_string(), + }, + ) + .unwrap(); + assert_eq!(res.events.len(), 0); + + // should have no admin now + assert_admin(&wasm_storage, &wasm_keeper, &contract_addr, None); + } + + #[test] + fn uses_simple_address_generator_by_default() { + let api = MockApi::default(); + let mut wasm_keeper = wasm_keeper(); + let creator_addr = api.addr_make("creator"); + let code_id = wasm_keeper.store_code(creator_addr.clone(), payout::contract()); + assert_eq!(1, code_id); + + let mut wasm_storage = MockStorage::new(); + + let admin = api.addr_make("admin"); + let contract_addr = wasm_keeper + .register_contract( + &api, + &mut wasm_storage, + code_id, + creator_addr.clone(), + admin.clone(), + "label".to_owned(), + 1000, + None, + ) + .unwrap(); + + assert_eq!( + contract_addr.as_str(), + "cosmwasm1mzdhwvvh22wrt07w59wxyd58822qavwkx5lcej7aqfkpqqlhaqfsgn6fq2", + "default address generator returned incorrect address" + ); + + let salt = HexBinary::from_hex("c0ffee").unwrap(); + + let contract_addr = wasm_keeper + .register_contract( + &api, + &mut wasm_storage, + code_id, + creator_addr.clone(), + admin.clone(), + "label".to_owned(), + 1000, + Binary::from(salt.clone()), + ) + .unwrap(); + + assert_eq!( + contract_addr.as_str(), + "cosmwasm1drhu6t78wacgm5qjzs4hvkv9fd9awa9henw7fh6vmzrhf7k2nkjsg3flns", + "default address generator returned incorrect address" + ); + + let code_id = wasm_keeper.store_code(creator_addr, payout::contract()); + assert_eq!(2, code_id); + + let user_addr = api.addr_make("boobaz"); + + let contract_addr = wasm_keeper + .register_contract( + &api, + &mut wasm_storage, + code_id, + user_addr, + admin, + "label".to_owned(), + 1000, + Binary::from(salt), + ) + .unwrap(); + + assert_eq!( + contract_addr.as_str(), + "cosmwasm13cfeertf2gny0rzp5jwqzst8crmfgvcd2lq5su0c9z66yxa45qdsdd0uxc", + "default address generator returned incorrect address" + ); + } + + struct TestAddressGenerator { + address: Addr, + predictable_address: Addr, + } + + impl AddressGenerator for TestAddressGenerator { + fn contract_address( + &self, + _api: &dyn Api, + _storage: &mut dyn Storage, + _code_id: u64, + _instance_id: u64, + ) -> AnyResult { + Ok(self.address.clone()) + } + + fn predictable_contract_address( + &self, + _api: &dyn Api, + _storage: &mut dyn Storage, + _code_id: u64, + _instance_id: u64, + _checksum: &[u8], + _creator: &CanonicalAddr, + _salt: &[u8], + ) -> AnyResult { + Ok(self.predictable_address.clone()) + } + } + + #[test] + fn can_use_custom_address_generator() { + let api = MockApi::default(); + let expected_addr = api.addr_make("address"); + let expected_predictable_addr = api.addr_make("predictable_address"); + let mut wasm_keeper: WasmKeeper = + WasmKeeper::new().with_address_generator(TestAddressGenerator { + address: expected_addr.clone(), + predictable_address: expected_predictable_addr.clone(), + }); + let creator = api.addr_make("creator"); + let code_id = wasm_keeper.store_code(creator.clone(), payout::contract()); + + let mut wasm_storage = MockStorage::new(); + + let admin = api.addr_make("admin"); + let contract_addr = wasm_keeper + .register_contract( + &api, + &mut wasm_storage, + code_id, + creator.clone(), + admin.clone(), + "label".to_owned(), + 1000, + None, + ) + .unwrap(); + + assert_eq!( + contract_addr, expected_addr, + "custom address generator returned incorrect address" + ); + + let contract_addr = wasm_keeper + .register_contract( + &api, + &mut wasm_storage, + code_id, + creator, + admin, + "label".to_owned(), + 1000, + Binary::from(HexBinary::from_hex("23A74B8C").unwrap()), + ) + .unwrap(); + + assert_eq!( + contract_addr, expected_predictable_addr, + "custom address generator returned incorrect address" + ); + } +} diff --git a/src/wasm_emulation/api/mod.rs b/src/wasm_emulation/api/mod.rs index 6e786427..707dea7f 100644 --- a/src/wasm_emulation/api/mod.rs +++ b/src/wasm_emulation/api/mod.rs @@ -1,5 +1,5 @@ use crate::wasm_emulation::query::gas::{GAS_COST_CANONICALIZE, GAS_COST_HUMANIZE}; -use bech32::{FromBase32, ToBase32, Variant}; +use bech32::{Bech32, Hrp}; use cosmwasm_std::Addr; use cosmwasm_vm::{BackendApi, BackendError, GasInfo}; use std::ops::AddAssign; @@ -7,23 +7,23 @@ use std::ops::AddAssign; const SHORT_CANON_LEN: usize = 20; const LONG_CANON_LEN: usize = 32; -pub fn bytes_from_bech32(address: &str, prefix: &str) -> Result, BackendError> { +pub fn bytes_from_bech32(address: &str, prefix: &Hrp) -> Result, BackendError> { if address.is_empty() { return Err(BackendError::Unknown { msg: "empty address string is not allowed".to_string(), }); } - let (hrp, data, _variant) = bech32::decode(address).map_err(|e| BackendError::Unknown { + let (hrp, data) = bech32::decode(address).map_err(|e| BackendError::Unknown { msg: format!("Invalid Bech32 address : Err {}", e), })?; - if hrp != prefix { + if hrp.ne(prefix) { return Err(BackendError::Unknown { msg: format!("invalid Bech32 prefix; expected {}, got {}", prefix, hrp), }); } - Ok(Vec::::from_base32(&data).unwrap()) + Ok(data) } pub const MAX_PREFIX_CHARS: usize = 10; @@ -47,7 +47,7 @@ impl RealApi { Self { prefix: api_prefix } } - pub fn get_prefix(&self) -> String { + pub fn get_prefix(&self) -> Result { let mut prefix = Vec::new(); for &c in self.prefix.iter() { @@ -55,7 +55,8 @@ impl RealApi { prefix.push(c); } } - prefix.iter().collect() + let prefix_string: String = prefix.into_iter().collect(); + Hrp::parse(&prefix_string).map_err(|e| BackendError::Unknown { msg: e.to_string() }) } pub fn next_address(&self, count: usize) -> Addr { @@ -112,7 +113,11 @@ impl BackendApi for RealApi { ); } - (bytes_from_bech32(human, &self.get_prefix()), gas_cost) + ( + self.get_prefix() + .and_then(|prefix| bytes_from_bech32(human, &prefix)), + gas_cost, + ) } fn addr_humanize(&self, canonical: &[u8]) -> cosmwasm_vm::BackendResult { @@ -131,8 +136,10 @@ impl BackendApi for RealApi { return (Ok("".to_string()), gas_cost); } - let human = bech32::encode(&self.get_prefix(), canonical.to_base32(), Variant::Bech32) - .map_err(|e| BackendError::Unknown { msg: e.to_string() }); + let human = self.get_prefix().and_then(|prefix| { + bech32::encode::(prefix, canonical) + .map_err(|e| BackendError::Unknown { msg: e.to_string() }) + }); (human, gas_cost) } @@ -149,7 +156,7 @@ mod test { let api = RealApi::new(prefix); let final_prefix = api.get_prefix(); - assert_eq!(prefix, final_prefix); + assert_eq!(prefix, final_prefix.unwrap().as_str()); } #[test] diff --git a/src/wasm_emulation/contract.rs b/src/wasm_emulation/contract.rs index 62a35cc2..72b74b14 100644 --- a/src/wasm_emulation/contract.rs +++ b/src/wasm_emulation/contract.rs @@ -1,48 +1,38 @@ -use crate::wasm_emulation::api::RealApi; -use crate::wasm_emulation::input::ReplyArgs; -use crate::wasm_emulation::instance::instance_from_reused_module; -use crate::wasm_emulation::output::StorageChanges; -use crate::wasm_emulation::query::MockQuerier; -use crate::wasm_emulation::storage::DualStorage; -use cosmwasm_std::Checksum; -use cosmwasm_std::CustomMsg; -use cosmwasm_std::StdError; -use cosmwasm_vm::WasmLimits; +use crate::wasm_emulation::{ + api::RealApi, + input::{InstanceArguments, ReplyArgs}, + instance::instance_from_reused_module, + output::{StorageChanges, WasmRunnerOutput}, + query::MockQuerier, + storage::DualStorage, +}; +use cosmwasm_std::{ + Binary, Checksum, CustomMsg, CustomQuery, Deps, DepsMut, Env, MessageInfo, Order, Reply, + Response, StdError, Storage, +}; use cosmwasm_vm::{ - call_execute, call_instantiate, call_migrate, call_query, call_reply, call_sudo, Backend, - BackendApi, Instance, InstanceOptions, Querier, + call_execute, call_instantiate, call_migrate, call_query, call_reply, call_sudo, + internals::check_wasm, Backend, BackendApi, Instance, InstanceOptions, Querier, WasmLimits, }; use cw_orch::daemon::queriers::CosmWasm; -use cosmwasm_std::Order; -use cosmwasm_std::Storage; - use serde::de::DeserializeOwned; use wasmer::Engine; use wasmer::Module; -use crate::wasm_emulation::input::InstanceArguments; -use crate::wasm_emulation::output::WasmRunnerOutput; - -use cosmwasm_vm::internals::check_wasm; use std::collections::HashSet; use crate::Contract; -use cosmwasm_std::{Binary, CustomQuery, Deps, DepsMut, Env, MessageInfo, Reply, Response}; - use anyhow::Result as AnyResult; -use super::channel::RemoteChannel; -use super::input::ExecuteArgs; -use super::input::InstantiateArgs; -use super::input::MigrateArgs; -use super::input::QueryArgs; -use super::input::SudoArgs; -use super::input::WasmFunction; -use super::instance::create_module; -use super::output::WasmOutput; -use super::query::mock_querier::ForkState; +use super::{ + channel::RemoteChannel, + input::{ExecuteArgs, InstantiateArgs, MigrateArgs, QueryArgs, SudoArgs, WasmFunction}, + instance::create_module, + output::WasmOutput, + query::mock_querier::ForkState, +}; fn apply_storage_changes(storage: &mut dyn Storage, output: &WasmRunnerOutput) { // We change all the values with the output diff --git a/src/wasm_emulation/input.rs b/src/wasm_emulation/input.rs index 2580b591..67cd832d 100644 --- a/src/wasm_emulation/input.rs +++ b/src/wasm_emulation/input.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::collections::{BTreeMap, HashMap}; use cosmwasm_std::Addr; use cosmwasm_std::{Env, MessageInfo, Reply}; @@ -13,8 +13,8 @@ use super::contract::WasmContract; #[derive(Debug, Clone, Default)] pub struct WasmStorage { pub contracts: HashMap, - pub codes: HashMap, - pub code_data: HashMap, + pub codes: BTreeMap, + pub code_data: BTreeMap, pub storage: Vec<(Vec, Vec)>, } diff --git a/src/wasm_emulation/query/mock_querier.rs b/src/wasm_emulation/query/mock_querier.rs index 972a237d..7e1ebf13 100644 --- a/src/wasm_emulation/query/mock_querier.rs +++ b/src/wasm_emulation/query/mock_querier.rs @@ -32,7 +32,7 @@ use super::gas::GAS_COST_QUERY_ERROR; #[derive(Clone)] pub struct LocalForkedState { - pub contracts: HashMap>, + pub contracts: HashMap>, pub env: Env, } @@ -198,6 +198,7 @@ impl< }), GasInfo::with_externally_used(GAS_COST_QUERY_ERROR), ), + #[cfg(feature = "cosmwasm_2_0")] QueryRequest::Grpc(_req) => ( SystemResult::Err(SystemError::UnsupportedRequest { kind: "Stargate".to_string(), diff --git a/src/wasm_emulation/query/mod.rs b/src/wasm_emulation/query/mod.rs index 258bc0b7..d483e656 100644 --- a/src/wasm_emulation/query/mod.rs +++ b/src/wasm_emulation/query/mod.rs @@ -9,7 +9,16 @@ pub mod gas; use anyhow::Result as AnyResult; -use super::input::{BankStorage, WasmStorage}; +use super::{ + channel::RemoteChannel, + input::{BankStorage, WasmStorage}, +}; + +pub trait ContainsRemote { + fn with_remote(self, remote: RemoteChannel) -> Self; + + fn set_remote(&mut self, remote: RemoteChannel); +} pub trait AllWasmQuerier { fn query_all(&self, storage: &dyn Storage) -> AnyResult; diff --git a/src/wasm_emulation/query/wasm.rs b/src/wasm_emulation/query/wasm.rs index 1ad075fa..eddbbd4e 100644 --- a/src/wasm_emulation/query/wasm.rs +++ b/src/wasm_emulation/query/wasm.rs @@ -1,6 +1,5 @@ use std::marker::PhantomData; -use crate::addons::MockApiBech32; use crate::prefixed_storage::get_full_contract_storage_namespace; use crate::queries::wasm::WasmRemoteQuerier; use crate::wasm_emulation::query::gas::{ @@ -8,7 +7,7 @@ use crate::wasm_emulation::query::gas::{ }; use crate::wasm_emulation::query::mock_querier::QueryResultWithGas; use crate::wasm_emulation::query::MockQuerier; -use crate::Contract; +use crate::{Contract, MockApiBech32}; use crate::wasm_emulation::contract::WasmContract; use cosmwasm_std::testing::MockStorage; @@ -161,48 +160,40 @@ impl< // Then, we get the corresponding wasm contract // If the contract data is already defined in our storage, we load it from there - let result = if let Some(code) = self - .fork_state - .querier_storage - .wasm - .codes - .get(&(code_id as usize)) - { - // Local Wasm Contract case - >::query( - code, - deps.as_ref(), - env, - msg.to_vec(), - self.fork_state.clone(), - ) - } else if let Some(local_contract) = self - .fork_state - .local_state - .contracts - .get(&(code_id as usize)) - { - // Local Rust Contract case - unsafe { - local_contract.as_ref().unwrap().query( + let result = + if let Some(code) = self.fork_state.querier_storage.wasm.codes.get(&code_id) { + // Local Wasm Contract case + >::query( + code, deps.as_ref(), env, msg.to_vec(), self.fork_state.clone(), ) - } - } else { - // Distant Registered Contract case - // TODO, this should be part of the cache as well - // However, it's not really possible to register that data inside the App, because this is deep in the execution layer - >::query( - &WasmContract::new_distant_code_id(code_id, remote.clone()), - deps.as_ref(), - env, - msg.to_vec(), - self.fork_state.clone(), - ) - }; + } else if let Some(local_contract) = + self.fork_state.local_state.contracts.get(&code_id) + { + // Local Rust Contract case + unsafe { + local_contract.as_ref().unwrap().query( + deps.as_ref(), + env, + msg.to_vec(), + self.fork_state.clone(), + ) + } + } else { + // Distant Registered Contract case + // TODO, this should be part of the cache as well + // However, it's not really possible to register that data inside the App, because this is deep in the execution layer + >::query( + &WasmContract::new_distant_code_id(code_id, remote.clone()), + deps.as_ref(), + env, + msg.to_vec(), + self.fork_state.clone(), + ) + }; let result = match result { Err(e) => { @@ -223,12 +214,7 @@ impl< ) } WasmQuery::CodeInfo { code_id } => { - let code_data = self - .fork_state - .querier_storage - .wasm - .code_data - .get(&(*code_id as usize)); + let code_data = self.fork_state.querier_storage.wasm.code_data.get(code_id); let res = if let Some(code_data) = code_data { cosmwasm_std::CodeInfoResponse::new( *code_id, diff --git a/src/wasm_emulation/storage/analyzer.rs b/src/wasm_emulation/storage/analyzer.rs index 660aa935..fa868193 100644 --- a/src/wasm_emulation/storage/analyzer.rs +++ b/src/wasm_emulation/storage/analyzer.rs @@ -1,7 +1,8 @@ use crate::{ + featured::staking::{Distribution, Staking}, prefixed_storage::{decode_length, to_length_prefixed, CONTRACT_STORAGE_PREFIX}, wasm_emulation::channel::RemoteChannel, - BankKeeper, Distribution, Gov, Ibc, Module, Staking, WasmKeeper, + BankKeeper, Gov, Ibc, Module, WasmKeeper, }; use cosmwasm_std::{Addr, Api, Coin, CustomMsg, CustomQuery, Storage}; use cw_orch::prelude::BankQuerier; diff --git a/src/wasm_emulation/storage/dual_std_storage.rs b/src/wasm_emulation/storage/dual_std_storage.rs index 4f5c317a..8469dcde 100644 --- a/src/wasm_emulation/storage/dual_std_storage.rs +++ b/src/wasm_emulation/storage/dual_std_storage.rs @@ -54,7 +54,7 @@ struct Iter<'a> { local_iter: Peekable + 'a>>, } -impl<'i> Iterator for Iter<'i> { +impl Iterator for Iter<'_> { type Item = Record; fn next(&mut self) -> Option { @@ -144,7 +144,7 @@ impl<'a> DualStorage<'a> { remote: RemoteChannel, contract_addr: String, local_storage: Box, - ) -> AnyResult { + ) -> AnyResult> { Ok(Self { local_storage, remote, @@ -154,7 +154,7 @@ impl<'a> DualStorage<'a> { } } -impl<'a> Storage for DualStorage<'a> { +impl Storage for DualStorage<'_> { fn get(&self, key: &[u8]) -> Option> { // First we try to get the value locally let mut value = self.local_storage.get(key); @@ -198,7 +198,7 @@ impl<'a> Storage for DualStorage<'a> { start.map(|s| s.to_vec()).unwrap_or_default() }; - return Box::new(Iter { + Box::new(Iter { distant_iter: DistantIter { remote: self.remote.clone(), contract_addr: self.contract_addr.clone(), @@ -210,6 +210,6 @@ impl<'a> Storage for DualStorage<'a> { reverse: order == Order::Descending, }, local_iter: self.local_storage.range(start, end, order).peekable(), - }); + }) } } diff --git a/src/wasm_emulation/storage/storage_wrappers.rs b/src/wasm_emulation/storage/storage_wrappers.rs index 33124f99..95e7bcbc 100644 --- a/src/wasm_emulation/storage/storage_wrappers.rs +++ b/src/wasm_emulation/storage/storage_wrappers.rs @@ -10,7 +10,7 @@ impl<'a> StorageWrapper<'a> { } } -impl<'a> Storage for StorageWrapper<'a> { +impl Storage for StorageWrapper<'_> { fn get(&self, key: &[u8]) -> Option> { self.storage.get(key) } @@ -45,7 +45,7 @@ impl<'a> ReadonlyStorageWrapper<'a> { } } -impl<'a> Storage for ReadonlyStorageWrapper<'a> { +impl Storage for ReadonlyStorageWrapper<'_> { fn get(&self, key: &[u8]) -> Option> { self.storage.get(key) } diff --git a/tests/mod.rs b/tests/mod.rs new file mode 100644 index 00000000..49ef6a7c --- /dev/null +++ b/tests/mod.rs @@ -0,0 +1,104 @@ +#![cfg(test)] + +use clone_cw_multi_test::wasm_emulation::channel::RemoteChannel; +use clone_cw_multi_test::{no_init, App}; +use cw_orch::daemon::RUNTIME; + +mod test_api; +mod test_app; +mod test_app_builder; +mod test_attributes; +mod test_bank; +mod test_contract_storage; +mod test_module; +mod test_prefixed_storage; +#[cfg(feature = "staking")] +mod test_staking; +mod test_wasm; + +extern crate clone_cw_multi_test as cw_multi_test; + +mod test_contracts { + + pub mod counter { + use clone_cw_multi_test::{Contract, ContractWrapper}; + use cosmwasm_schema::cw_serde; + use cosmwasm_std::{ + to_json_binary, Binary, Deps, DepsMut, Empty, Env, MessageInfo, Response, StdError, + WasmMsg, + }; + use cw_storage_plus::Item; + + const COUNTER: Item = Item::new("counter"); + + #[cw_serde] + pub enum CounterQueryMsg { + Counter {}, + } + + #[cw_serde] + pub struct CounterResponseMsg { + pub value: u64, + } + + fn instantiate( + deps: DepsMut, + _env: Env, + _info: MessageInfo, + _msg: Empty, + ) -> Result { + COUNTER.save(deps.storage, &1).unwrap(); + Ok(Response::default()) + } + + fn execute( + deps: DepsMut, + _env: Env, + _info: MessageInfo, + _msg: WasmMsg, + ) -> Result { + if let Some(mut counter) = COUNTER.may_load(deps.storage).unwrap() { + counter += 1; + COUNTER.save(deps.storage, &counter).unwrap(); + } + Ok(Response::default()) + } + + fn query(deps: Deps, _env: Env, msg: CounterQueryMsg) -> Result { + match msg { + CounterQueryMsg::Counter { .. } => Ok(to_json_binary(&CounterResponseMsg { + value: COUNTER.may_load(deps.storage).unwrap().unwrap(), + })?), + } + } + + pub fn contract() -> Box> { + Box::new(ContractWrapper::new_with_empty(execute, instantiate, query)) + } + + #[cfg(feature = "cosmwasm_1_2")] + pub fn contract_with_checksum() -> Box> { + Box::new( + ContractWrapper::new_with_empty(execute, instantiate, query).with_checksum( + cosmwasm_std::Checksum::generate(&[1, 2, 3, 4, 5, 6, 7, 8, 9]), + ), + ) + } + } +} + +use cw_orch::{daemon::networks::XION_TESTNET_1, prelude::ChainInfo}; +pub const CHAIN: ChainInfo = XION_TESTNET_1; +pub fn remote_channel() -> RemoteChannel { + RemoteChannel::new( + &RUNTIME, + CHAIN.grpc_urls, + CHAIN.chain_id, + CHAIN.network_info.pub_address_prefix, + ) + .unwrap() +} + +pub fn default_app() -> App { + App::new(remote_channel(), no_init) +} diff --git a/tests/test_api/mod.rs b/tests/test_api/mod.rs new file mode 100644 index 00000000..d429f30f --- /dev/null +++ b/tests/test_api/mod.rs @@ -0,0 +1,48 @@ +use cosmwasm_std::Api; +use hex_literal::hex; + +mod test_addr; +mod test_bech32; +mod test_bech32m; +mod test_prefixed; + +const SECP256K1_MSG_HASH: [u8; 32] = + hex!("5ae8317d34d1e595e3fa7247db80c0af4320cce1116de187f8f7e2e099c0d8d0"); +const SECP256K1_SIG: [u8; 64] = hex!("207082eb2c3dfa0b454e0906051270ba4074ac93760ba9e7110cd9471475111151eb0dbbc9920e72146fb564f99d039802bf6ef2561446eb126ef364d21ee9c4"); +const SECP256K1_PUBKEY: [u8;65] = hex!("04051c1ee2190ecfb174bfe4f90763f2b4ff7517b70a2aec1876ebcfd644c4633fb03f3cfbd94b1f376e34592d9d41ccaf640bb751b00a1fadeb0c01157769eb73"); +const SECP256K1_SIG_RECOVER: [u8; 64] = hex!("45c0b7f8c09a9e1f1cea0c25785594427b6bf8f9f878a8af0b1abbb48e16d0920d8becd0c220f67c51217eecfd7184ef0732481c843857e6bc7fc095c4f6b788"); +const SECP256K1_PUBKEY_RECOVER: [u8;65] = hex!("044a071e8a6e10aada2b8cf39fa3b5fb3400b04e99ea8ae64ceea1a977dbeaf5d5f8c8fbd10b71ab14cd561f7df8eb6da50f8a8d81ba564342244d26d1d4211595"); +const ED25519_MSG: [u8; 1] = hex!("72"); +const ED25519_SIG: [u8;64] = hex!("92a009a9f0d4cab8720e820b5f642540a2b27b5416503f8fb3762223ebdb69da085ac1e43e15996e458f3613d0f11d8c387b2eaeb4302aeeb00d291612bb0c00"); +const ED25519_PUBKEY: [u8; 32] = + hex!("3d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c"); + +fn assert_secp256k1_verify_works(api: &dyn Api) { + assert!(api + .secp256k1_verify(&SECP256K1_MSG_HASH, &SECP256K1_SIG, &SECP256K1_PUBKEY) + .unwrap()); +} + +fn assert_secp256k1_recover_pubkey_works(api: &dyn Api) { + assert_eq!( + api.secp256k1_recover_pubkey(&SECP256K1_MSG_HASH, &SECP256K1_SIG_RECOVER, 1) + .unwrap(), + SECP256K1_PUBKEY_RECOVER + ); +} + +fn assert_ed25519_verify_works(api: &dyn Api) { + assert!(api + .ed25519_verify(&ED25519_MSG, &ED25519_SIG, &ED25519_PUBKEY) + .unwrap()); +} + +fn assert_ed25519_batch_verify_works(api: &dyn Api) { + assert!(api + .ed25519_batch_verify(&[&ED25519_MSG], &[&ED25519_SIG], &[&ED25519_PUBKEY]) + .unwrap()); +} + +fn assert_debug_does_not_panic(api: &dyn Api) { + api.debug("debug should not panic"); +} diff --git a/tests/test_api/test_addr.rs b/tests/test_api/test_addr.rs new file mode 100644 index 00000000..015f338e --- /dev/null +++ b/tests/test_api/test_addr.rs @@ -0,0 +1,21 @@ +use cosmwasm_std::testing::MockApi; +use cw_multi_test::IntoAddr; + +#[test] +fn conversion_with_default_prefix_should_work() { + assert_eq!( + MockApi::default().addr_make("creator").as_str(), + "creator".into_addr().as_str(), + ); +} + +#[test] +fn conversion_with_custom_prefix_should_work() { + assert_eq!( + MockApi::default() + .with_prefix("juno") + .addr_make("sender") + .as_str(), + "sender".into_addr_with_prefix("juno").as_str(), + ); +} diff --git a/tests/test_api/test_bech32.rs b/tests/test_api/test_bech32.rs new file mode 100644 index 00000000..35e86234 --- /dev/null +++ b/tests/test_api/test_bech32.rs @@ -0,0 +1,138 @@ +use super::*; +use cosmwasm_std::CanonicalAddr; +use cw_multi_test::{IntoBech32, IntoBech32m, MockApiBech32, MockApiBech32m}; + +const ADDR_JUNO: &str = "juno1h34lmpywh4upnjdg90cjf4j70aee6z8qqfspugamjp42e4q28kqsksmtyp"; +const ADDR_DEFAULT: &str = "cosmwasm1h34lmpywh4upnjdg90cjf4j70aee6z8qqfspugamjp42e4q28kqs8s7vcp"; + +#[test] +fn new_api_bech32_should_work() { + assert_eq!( + MockApiBech32::new("juno").addr_make("creator").as_str(), + ADDR_JUNO + ); + assert_eq!( + "creator".into_bech32_with_prefix("juno").as_str(), + ADDR_JUNO + ); + assert_eq!("creator".into_bech32().as_str(), ADDR_DEFAULT); +} + +#[test] +fn api_bech32_should_differ_from_bech32m() { + assert_ne!( + MockApiBech32::new("juno").addr_make("sender").as_str(), + MockApiBech32m::new("juno").addr_make("sender").as_str(), + ); + assert_ne!( + "sender".into_bech32_with_prefix("juno").as_str(), + "sender".into_bech32m_with_prefix("juno").as_str() + ); + assert_ne!( + "sender".into_bech32().as_str(), + "sender".into_bech32m().as_str() + ); +} + +#[test] +fn address_validate_should_work() { + assert_eq!( + MockApiBech32::new("juno") + .addr_validate(ADDR_JUNO) + .unwrap() + .as_str(), + ADDR_JUNO + ) +} + +#[test] +fn address_validate_invalid_address() { + MockApiBech32::new("juno") + .addr_validate("creator") + .unwrap_err(); +} + +#[test] +fn addr_validate_invalid_prefix() { + MockApiBech32::new("juno") + .addr_validate(MockApiBech32m::new("osmosis").addr_make("creator").as_str()) + .unwrap_err(); +} + +#[test] +fn address_validate_invalid_variant() { + MockApiBech32::new("juno") + .addr_validate(MockApiBech32m::new("juno").addr_make("creator").as_str()) + .unwrap_err(); +} + +#[test] +fn address_canonicalize_humanize_should_work() { + let api = MockApiBech32::new("juno"); + assert_eq!( + api.addr_humanize(&api.addr_canonicalize(ADDR_JUNO).unwrap()) + .unwrap() + .as_str(), + ADDR_JUNO + ); +} + +#[test] +fn address_humanize_prefix_too_long() { + assert_eq!( + "Generic error: hrp is too long, found 85 characters, must be <= 126", + MockApiBech32::new( + "juno_juno_juno_juno_juno_juno_juno_juno_juno_juno_juno_juno_juno_juno_juno_juno_juno_", + ) + .addr_humanize(&CanonicalAddr::from([1, 2, 3, 4, 5])) + .unwrap_err() + .to_string() + ); +} + +#[test] +fn address_humanize_canonical_too_long() { + assert_eq!( + "Generic error: Invalid canonical address", + MockApiBech32::new("juno") + .addr_humanize(&CanonicalAddr::from([1; 1024])) + .unwrap_err() + .to_string() + ); +} + +#[test] +fn debug_should_not_panic() { + assert_debug_does_not_panic(&MockApiBech32::new("juno")); +} + +#[test] +#[should_panic( + expected = "Generating address failed with reason: hrp is too long, found 85 characters, must be <= 126" +)] +fn address_make_prefix_too_long() { + MockApiBech32::new( + "juno_juno_juno_juno_juno_juno_juno_juno_juno_juno_juno_juno_juno_juno_juno_juno_juno_", + ) + .addr_make("creator"); +} + +#[test] +fn secp256k1_verify_works() { + assert_secp256k1_verify_works(&MockApiBech32::new("juno")); +} + +#[test] +fn secp256k1_recover_pubkey_works() { + assert_secp256k1_recover_pubkey_works(&MockApiBech32::new("juno")); +} + +#[test] +fn ed25519_verify_works() { + assert_ed25519_verify_works(&MockApiBech32::new("juno")); +} + +#[test] +fn ed25519_batch_verify_works() { + assert_ed25519_batch_verify_works(&MockApiBech32::new("juno")); +} diff --git a/tests/test_api/test_bech32m.rs b/tests/test_api/test_bech32m.rs new file mode 100644 index 00000000..61af0177 --- /dev/null +++ b/tests/test_api/test_bech32m.rs @@ -0,0 +1,138 @@ +use super::*; +use cosmwasm_std::CanonicalAddr; +use cw_multi_test::{IntoBech32, IntoBech32m, MockApiBech32, MockApiBech32m}; + +const ADDR_JUNO: &str = "juno1h34lmpywh4upnjdg90cjf4j70aee6z8qqfspugamjp42e4q28kqsrvt8pr"; +const ADDR_DEFAULT: &str = "cosmwasm1h34lmpywh4upnjdg90cjf4j70aee6z8qqfspugamjp42e4q28kqsjvwqar"; + +#[test] +fn new_api_bech32m_should_work() { + assert_eq!( + MockApiBech32m::new("juno").addr_make("creator").as_str(), + ADDR_JUNO + ); + assert_eq!( + "creator".into_bech32m_with_prefix("juno").as_str(), + ADDR_JUNO + ); + assert_eq!("creator".into_bech32m().as_str(), ADDR_DEFAULT); +} + +#[test] +fn api_bech32m_should_differ_from_bech32() { + assert_ne!( + MockApiBech32m::new("juno").addr_make("sender").as_str(), + MockApiBech32::new("juno").addr_make("sender").as_str() + ); + assert_ne!( + "sender".into_bech32m_with_prefix("juno").as_str(), + "sender".into_bech32_with_prefix("juno").as_str() + ); + assert_ne!( + "sender".into_bech32m().as_str(), + "sender".into_bech32().as_str() + ); +} + +#[test] +fn address_validate_should_work() { + assert_eq!( + MockApiBech32m::new("juno") + .addr_validate(ADDR_JUNO) + .unwrap() + .as_str(), + ADDR_JUNO + ) +} + +#[test] +fn address_validate_invalid_address() { + MockApiBech32m::new("juno") + .addr_validate("creator") + .unwrap_err(); +} + +#[test] +fn addr_validate_invalid_prefix() { + MockApiBech32m::new("juno") + .addr_validate(MockApiBech32m::new("osmosis").addr_make("creator").as_str()) + .unwrap_err(); +} + +#[test] +fn address_validate_invalid_variant() { + MockApiBech32m::new("juno") + .addr_validate(MockApiBech32::new("juno").addr_make("creator").as_str()) + .unwrap_err(); +} + +#[test] +fn address_canonicalize_humanize_should_work() { + let api = MockApiBech32m::new("juno"); + assert_eq!( + api.addr_humanize(&api.addr_canonicalize(ADDR_JUNO).unwrap()) + .unwrap() + .as_str(), + ADDR_JUNO + ); +} + +#[test] +fn address_humanize_prefix_too_long() { + assert_eq!( + "Generic error: hrp is too long, found 85 characters, must be <= 126", + MockApiBech32m::new( + "juno_juno_juno_juno_juno_juno_juno_juno_juno_juno_juno_juno_juno_juno_juno_juno_juno_", + ) + .addr_humanize(&CanonicalAddr::from([1, 2, 3, 4, 5])) + .unwrap_err() + .to_string() + ); +} + +#[test] +fn address_humanize_canonical_too_long() { + assert_eq!( + "Generic error: Invalid canonical address", + MockApiBech32m::new("juno") + .addr_humanize(&CanonicalAddr::from([1; 1024])) + .unwrap_err() + .to_string() + ); +} + +#[test] +fn debug_should_not_panic() { + assert_debug_does_not_panic(&MockApiBech32m::new("juno")); +} + +#[test] +#[should_panic( + expected = "Generating address failed with reason: hrp is too long, found 85 characters, must be <= 126" +)] +fn address_make_prefix_too_long() { + MockApiBech32m::new( + "juno_juno_juno_juno_juno_juno_juno_juno_juno_juno_juno_juno_juno_juno_juno_juno_juno_", + ) + .addr_make("creator"); +} + +#[test] +fn secp256k1_verify_works() { + assert_secp256k1_verify_works(&MockApiBech32m::new("juno")); +} + +#[test] +fn secp256k1_recover_pubkey_works() { + assert_secp256k1_recover_pubkey_works(&MockApiBech32m::new("juno")); +} + +#[test] +fn ed25519_verify_works() { + assert_ed25519_verify_works(&MockApiBech32m::new("juno")); +} + +#[test] +fn ed25519_batch_verify_works() { + assert_ed25519_batch_verify_works(&MockApiBech32m::new("juno")); +} diff --git a/tests/test_api/test_prefixed.rs b/tests/test_api/test_prefixed.rs new file mode 100644 index 00000000..3890278c --- /dev/null +++ b/tests/test_api/test_prefixed.rs @@ -0,0 +1,97 @@ +use super::*; +use cosmwasm_std::testing::MockApi; +use cosmwasm_std::CanonicalAddr; + +const HUMAN_ADDRESS: &str = "juno1h34lmpywh4upnjdg90cjf4j70aee6z8qqfspugamjp42e4q28kqsksmtyp"; + +fn api_prefix(prefix: &'static str) -> MockApi { + MockApi::default().with_prefix(prefix) +} + +fn api_juno() -> MockApi { + api_prefix("juno") +} + +fn api_osmo() -> MockApi { + api_prefix("osmo") +} + +#[test] +fn new_api_bech32_should_work() { + let addr = api_juno().addr_make("creator"); + assert_eq!(HUMAN_ADDRESS, addr.as_str(),); +} + +#[test] +fn address_validate_should_work() { + assert_eq!( + api_juno().addr_validate(HUMAN_ADDRESS).unwrap().as_str(), + HUMAN_ADDRESS + ) +} + +#[test] +fn address_validate_invalid_address() { + api_juno().addr_validate("creator").unwrap_err(); +} + +#[test] +fn addr_validate_invalid_prefix() { + api_juno() + .addr_validate(api_osmo().addr_make("creator").as_str()) + .unwrap_err(); +} + +#[test] +fn address_canonicalize_humanize_should_work() { + let api = api_juno(); + assert_eq!( + api.addr_humanize(&api.addr_canonicalize(HUMAN_ADDRESS).unwrap()) + .unwrap() + .as_str(), + HUMAN_ADDRESS + ); +} + +#[test] +fn address_humanize_prefix_too_long() { + api_prefix( + "juno_juno_juno_juno_juno_juno_juno_juno_juno_juno_juno_juno_juno_juno_juno_juno_juno_", + ) + .addr_humanize(&CanonicalAddr::from([1, 2, 3, 4, 5])) + .unwrap_err(); +} + +#[test] +fn debug_should_not_panic() { + assert_debug_does_not_panic(&api_juno()); +} + +#[test] +#[should_panic] +fn address_make_prefix_too_long() { + api_prefix( + "juno_juno_juno_juno_juno_juno_juno_juno_juno_juno_juno_juno_juno_juno_juno_juno_juno_", + ) + .addr_make("creator"); +} + +#[test] +fn secp256k1_verify_works() { + assert_secp256k1_verify_works(&api_juno()); +} + +#[test] +fn secp256k1_recover_pubkey_works() { + assert_secp256k1_recover_pubkey_works(&api_juno()); +} + +#[test] +fn ed25519_verify_works() { + assert_ed25519_verify_works(&api_juno()); +} + +#[test] +fn ed25519_batch_verify_works() { + assert_ed25519_batch_verify_works(&api_juno()); +} diff --git a/tests/test_app/mod.rs b/tests/test_app/mod.rs new file mode 100644 index 00000000..165154af --- /dev/null +++ b/tests/test_app/mod.rs @@ -0,0 +1,6 @@ +mod test_block_info; +mod test_initialize_app; +mod test_instantiate2; +mod test_store_code; +mod test_store_code_with_creator; +mod test_store_code_with_id; diff --git a/tests/test_app/test_block_info.rs b/tests/test_app/test_block_info.rs new file mode 100644 index 00000000..7b5d9240 --- /dev/null +++ b/tests/test_app/test_block_info.rs @@ -0,0 +1,41 @@ +use cosmwasm_std::testing::mock_env; +use cosmwasm_std::{BlockInfo, Timestamp}; +use cw_multi_test::next_block; + +use crate::default_app; + +#[test] +fn default_block_info_should_work() { + let env = mock_env(); + let app = default_app(); + let block = app.block_info(); + assert_eq!(env.block.chain_id, block.chain_id); + assert_eq!(env.block.height, block.height); + assert_eq!(env.block.time, block.time); +} + +#[test] +fn setting_block_info_should_work() { + let initial_block = BlockInfo { + chain_id: "mainnet-fermentation".to_string(), + height: 273_094, + time: Timestamp::default().plus_days(366), + }; + let mut app = default_app(); + app.set_block(initial_block.clone()); + let block = app.block_info(); + assert_eq!(initial_block.chain_id, block.chain_id); + assert_eq!(initial_block.height, block.height); + assert_eq!(initial_block.time, block.time); +} + +#[test] +fn incrementing_block_info_should_work() { + let env = mock_env(); + let mut app = default_app(); + app.update_block(next_block); + let block = app.block_info(); + assert_eq!(env.block.chain_id, block.chain_id); + assert_eq!(env.block.height + 1, block.height); + assert_eq!(env.block.time.plus_seconds(5), block.time); +} diff --git a/tests/test_app/test_initialize_app.rs b/tests/test_app/test_initialize_app.rs new file mode 100644 index 00000000..3c2da198 --- /dev/null +++ b/tests/test_app/test_initialize_app.rs @@ -0,0 +1,23 @@ +use cw_multi_test::App; +use cw_storage_plus::Map; + +use crate::default_app; + +const USER: &str = "user"; +const USERS: Map<&str, u64> = Map::new("users"); +const AMOUNT: u64 = 100; + +#[test] +fn initializing_app_should_work() { + let mut app = default_app(); + let mut amount = 0; + app.init_modules(|_router, api, storage| { + USERS + .save(storage, api.addr_make(USER).as_str(), &AMOUNT) + .unwrap(); + }); + app.read_module(|_router, api, storage| { + amount = USERS.load(storage, api.addr_make(USER).as_str()).unwrap() + }); + assert_eq!(AMOUNT, amount); +} diff --git a/tests/test_app/test_instantiate2.rs b/tests/test_app/test_instantiate2.rs new file mode 100644 index 00000000..a881315e --- /dev/null +++ b/tests/test_app/test_instantiate2.rs @@ -0,0 +1,205 @@ +#![cfg(feature = "cosmwasm_1_2")] + +use crate::test_contracts::counter; +use cosmwasm_std::testing::MockApi; +use cosmwasm_std::{instantiate2_address, to_json_binary, Addr, Api, Coin, Empty, WasmMsg}; +use cw_multi_test::{no_init, App, AppBuilder, Executor}; +use cw_utils::parse_instantiate_response_data; + +const FUNDS: Vec = vec![]; +const SALT: &[u8] = "bad kids".as_bytes(); +const LABEL: &str = "label"; +const JUNO_1: &str = "juno1navvz5rjlvn43xjqxlpl7dunk6hglmhuh7c6a53eq6qamfam3dus7a220h"; +const JUNO_2: &str = "juno1qaygqu9plc7nqqgwt7d6dxhmej2tl0lu20j84l5pnz5p4th4zz5qwd77z5"; +const OSMO: &str = "osmo1navvz5rjlvn43xjqxlpl7dunk6hglmhuh7c6a53eq6qamfam3dusg94p04"; + +#[test] +fn instantiate2_works() { + // prepare the chain with custom Api and custom address generator + let mut app = AppBuilder::default() + .with_api(MockApi::default().with_prefix("juno")) + .build(no_init); + + // prepare addresses for sender and creator + let sender = app.api().addr_make("sender"); + let creator = app.api().addr_make("creator"); + + // store the contract's code (Wasm blob checksum is generated) + let code_id = app.store_code_with_creator(creator, counter::contract()); + + // instantiate the contract with predictable address + let init_msg = to_json_binary(&Empty {}).unwrap(); + let msg = WasmMsg::Instantiate2 { + admin: None, + code_id, + msg: init_msg, + funds: FUNDS, + label: LABEL.into(), + salt: SALT.into(), + }; + let res = app.execute(sender.clone(), msg.into()).unwrap(); + + // check the instantiate result + let parsed = parse_instantiate_response_data(res.data.unwrap().as_slice()).unwrap(); + assert!(parsed.data.is_none()); + + // check the resulting predictable contract's address + assert_eq!(parsed.contract_address, JUNO_1); // must be equal + assert_ne!(parsed.contract_address, JUNO_2); // must differ + + // comparing with the result from `cosmwasm_std::instantiate2_address` is done here + compare_with_cosmwasm_vm_address(&app, code_id, &sender, &parsed.contract_address); +} + +#[test] +fn instantiate2_works_with_checksum_provided_in_contract() { + // prepare the chain with custom API and custom address generator + let mut app = AppBuilder::default() + .with_api(MockApi::default().with_prefix("juno")) + .build(no_init); + + // prepare addresses for the sender and creator + let sender = app.api().addr_make("sender"); + let creator = app.api().addr_make("creator"); + + // store the contract's code (Wasm blob checksum is provided in contract) + let code_id = app.store_code_with_creator(creator, counter::contract_with_checksum()); + + // instantiate the contract with predictable address + let init_msg = to_json_binary(&Empty {}).unwrap(); + let msg = WasmMsg::Instantiate2 { + admin: None, + code_id, + msg: init_msg, + funds: FUNDS, + label: LABEL.into(), + salt: SALT.into(), + }; + let res = app.execute(sender.clone(), msg.into()).unwrap(); + + // check the instantiate result + let parsed = parse_instantiate_response_data(res.data.unwrap().as_slice()).unwrap(); + assert!(parsed.data.is_none()); + + // check the resulting predictable contract's address + assert_eq!(parsed.contract_address, JUNO_2); // must be equal + assert_ne!(parsed.contract_address, JUNO_1); // must differ + + // comparing with the result from `cosmwasm_std::instantiate2_address` is done here + compare_with_cosmwasm_vm_address(&app, code_id, &sender, &parsed.contract_address); +} + +#[test] +fn instantiate2_should_work_for_multiple_salts() { + // prepare the application with custom Api and custom address generator + let mut app = AppBuilder::default() + .with_api(MockApi::default().with_prefix("juno")) + .build(no_init); + + // prepare addresses for sender and creator + let sender = app.api().addr_make("sender"); + let creator = app.api().addr_make("creator"); + + // store the contract's code + let code_id = app.store_code_with_creator(creator, counter::contract()); + + let mut f = |salt: &str| { + // instantiate the contract with predictable address and provided salt, sender is the same + let msg = WasmMsg::Instantiate2 { + admin: None, + code_id, + msg: to_json_binary(&Empty {}).unwrap(), + funds: FUNDS, + label: LABEL.into(), + salt: salt.as_bytes().into(), + }; + let res = app.execute(sender.clone(), msg.into()).unwrap(); + let parsed = parse_instantiate_response_data(res.data.unwrap().as_slice()).unwrap(); + parsed.contract_address + }; + + // make sure, addresses generated for different salts are different + assert_ne!(f("bad kids 1"), f("bad kids 2")) +} + +#[test] +fn instantiate2_fails_for_duplicated_addresses() { + // prepare the application with custom Api and custom address generator + let mut app = AppBuilder::default() + .with_api(MockApi::default().with_prefix("osmo")) + .build(no_init); + + // prepare addresses for sender and creator + let sender = app.api().addr_make("sender"); + let creator = app.api().addr_make("creator"); + + // store the contract's code + let code_id = app.store_code_with_creator(creator, counter::contract()); + + // instantiate the contract with predictable address + let init_msg = to_json_binary(&Empty {}).unwrap(); + let msg = WasmMsg::Instantiate2 { + admin: None, + code_id, + msg: init_msg, + funds: FUNDS, + label: LABEL.into(), + salt: SALT.into(), + }; + let res = app.execute(sender.clone(), msg.clone().into()).unwrap(); + + // check the instantiate result + let parsed = parse_instantiate_response_data(res.data.unwrap().as_slice()).unwrap(); + assert!(parsed.data.is_none()); + + // check the resulting predictable contract's address + assert_eq!(parsed.contract_address, OSMO); + + // creating a new instance of the same contract with the same sender and salt + // should fail because the generated contract address is the same + app.execute(sender.clone(), msg.into()).unwrap_err(); + + // ---------------------------------------------------------------------- + // Below is an additional check, proving that the predictable address + // from contract instantiation is exactly the same when used with the + // cosmwasm_std::instantiate2_address twice (same sender and salt). + // ---------------------------------------------------------------------- + + // get the code info of the contract + let code_info_response = app.wrap().query_wasm_code_info(code_id).unwrap(); + + // retrieve the contract's code checksum + let checksum = code_info_response.checksum.as_slice(); + + // canonicalize the sender address (which is now in human Bech32 format) + let sender_addr = app.api().addr_canonicalize(sender.as_str()).unwrap(); + + // get the contract address using cosmwasm_std::instantiate2_address function twice + let contract_addr_1 = instantiate2_address(checksum, &sender_addr, SALT).unwrap(); + let contract_addr_2 = instantiate2_address(checksum, &sender_addr, SALT).unwrap(); + + // contract addresses should be the same + assert_eq!(contract_addr_1, contract_addr_2); +} + +/// Utility function proving that the predictable address from contract instantiation +/// is exactly the same as the address returned from the function `cosmwasm_std::instantiate2_address`. +fn compare_with_cosmwasm_vm_address(app: &App, code_id: u64, sender: &Addr, expected_addr: &str) { + // get the code info of the contract + let code_info_response = app.wrap().query_wasm_code_info(code_id).unwrap(); + + // retrieve the contract's code checksum + let checksum = code_info_response.checksum.as_slice(); + + // canonicalize the sender address (which is now in human Bech32 format) + let sender_addr = app.api().addr_canonicalize(sender.as_str()).unwrap(); + + // get the contract address using cosmwasm_std::instantiate2_address function + let contract_addr = instantiate2_address(checksum, &sender_addr, SALT).unwrap(); + + // humanize the address of the contract + let contract_human_addr = app.api().addr_humanize(&contract_addr).unwrap(); + + // check if the predictable contract's address matches the result from instantiate2_address function + assert_eq!(expected_addr, contract_human_addr.to_string()); +} diff --git a/tests/test_app/test_store_code.rs b/tests/test_app/test_store_code.rs new file mode 100644 index 00000000..4262c099 --- /dev/null +++ b/tests/test_app/test_store_code.rs @@ -0,0 +1,35 @@ +use crate::{default_app, test_contracts::counter}; +use cw_multi_test::App; + +#[test] +fn storing_code_assigns_consecutive_identifiers() { + // prepare the application + let mut app = default_app(); + + // storing contract's code assigns consecutive code identifiers + for i in 1..=10 { + assert_eq!(i, app.store_code(counter::contract())); + } +} + +#[test] +#[cfg(feature = "cosmwasm_1_2")] +fn store_code_generates_default_address_for_creator() { + use cosmwasm_std::testing::MockApi; + + // prepare the application + let mut app = default_app(); + + // store contract's code + let code_id = app.store_code(counter::contract()); + assert_eq!(1, code_id); + + // retrieve contract code info + let code_info_response = app.wrap().query_wasm_code_info(code_id).unwrap(); + + // address of the creator should be the default one + assert_eq!( + MockApi::default().addr_make("creator").as_str(), + code_info_response.creator.as_str() + ); +} diff --git a/tests/test_app/test_store_code_with_creator.rs b/tests/test_app/test_store_code_with_creator.rs new file mode 100644 index 00000000..5f70a1e4 --- /dev/null +++ b/tests/test_app/test_store_code_with_creator.rs @@ -0,0 +1,34 @@ +#![cfg(feature = "cosmwasm_1_2")] + +use crate::test_contracts::counter; +use cw_multi_test::{no_init, AppBuilder}; +use cw_multi_test::{MockApiBech32, MockApiBech32m}; + +#[test] +fn store_code_with_custom_creator_address_should_work() { + // prepare the application + let mut app = AppBuilder::default() + .with_api(MockApiBech32m::new("juno")) + .build(no_init); + + let creator = app.api().addr_make("zeus"); + + // store contract's code + let code_id = app.store_code_with_creator(creator, counter::contract()); + assert_eq!(1, code_id); + + // retrieve contract code info + let code_info_response = app.wrap().query_wasm_code_info(code_id).unwrap(); + + // address of the creator should be the custom one in Bech32m format + assert_eq!( + MockApiBech32m::new("juno").addr_make("zeus"), + code_info_response.creator + ); + + // address of the creator should be the custom one but not in Bech32 format + assert_ne!( + MockApiBech32::new("juno").addr_make("zeus"), + code_info_response.creator + ); +} diff --git a/tests/test_app/test_store_code_with_id.rs b/tests/test_app/test_store_code_with_id.rs new file mode 100644 index 00000000..843736b6 --- /dev/null +++ b/tests/test_app/test_store_code_with_id.rs @@ -0,0 +1,67 @@ +use crate::{default_app, test_contracts::counter}; +use cw_multi_test::App; + +#[test] +fn storing_code_with_custom_identifier_should_work() { + let mut app = default_app(); + let creator = app.api().addr_make("prometheus"); + assert_eq!( + 10, + app.store_code_with_id(creator.clone(), 10, counter::contract()) + .unwrap() + ); + assert_eq!( + u64::MAX, + app.store_code_with_id(creator, u64::MAX, counter::contract()) + .unwrap() + ); +} + +#[test] +fn zero_code_id_is_not_allowed() { + let mut app = default_app(); + let creator = app.api().addr_make("prometheus"); + app.store_code_with_id(creator, 0, counter::contract()) + .unwrap_err(); +} + +#[test] +fn storing_code_with_consecutive_identifiers() { + let mut app = default_app(); + let creator = app.api().addr_make("prometheus"); + assert_eq!( + 11, + app.store_code_with_id(creator, 11, counter::contract()) + .unwrap() + ); + for i in 12..=20 { + assert_eq!(i, app.store_code(counter::contract())); + } +} + +#[test] +fn storing_with_the_same_id_is_not_allowed() { + let mut app = default_app(); + let creator = app.api().addr_make("prometheus"); + let code_id = 2056; + assert_eq!( + code_id, + app.store_code_with_id(creator.clone(), code_id, counter::contract()) + .unwrap() + ); + app.store_code_with_id(creator, code_id, counter::contract()) + .unwrap_err(); +} + +#[test] +#[should_panic(expected = "no more code identifiers available")] +fn no_more_identifiers_available() { + let mut app = default_app(); + let creator = app.api().addr_make("prometheus"); + assert_eq!( + u64::MAX, + app.store_code_with_id(creator, u64::MAX, counter::contract()) + .unwrap() + ); + app.store_code(counter::contract()); +} diff --git a/tests/test_app_builder/mod.rs b/tests/test_app_builder/mod.rs new file mode 100644 index 00000000..4df6f918 --- /dev/null +++ b/tests/test_app_builder/mod.rs @@ -0,0 +1,89 @@ +use cosmwasm_std::{Addr, Api, Binary, BlockInfo, CustomMsg, CustomQuery, Querier, Storage}; +use cw_multi_test::error::{bail, AnyResult}; +use cw_multi_test::{AppResponse, CosmosRouter, Module}; +use serde::de::DeserializeOwned; +use std::fmt::Debug; +use std::marker::PhantomData; + +mod test_with_api; +mod test_with_bank; +mod test_with_block; +#[cfg(feature = "staking")] +mod test_with_distribution; +#[cfg(feature = "stargate")] +mod test_with_gov; +#[cfg(feature = "stargate")] +mod test_with_ibc; +#[cfg(feature = "staking")] +mod test_with_staking; +#[cfg(feature = "stargate")] +mod test_with_stargate; +mod test_with_storage; +#[cfg(feature = "cosmwasm_1_2")] +mod test_with_wasm; + +struct MyKeeper( + PhantomData<(ExecT, QueryT, SudoT)>, + &'static str, + &'static str, + &'static str, +); + +impl MyKeeper { + fn new(execute_msg: &'static str, query_msg: &'static str, sudo_msg: &'static str) -> Self { + Self(Default::default(), execute_msg, query_msg, sudo_msg) + } +} + +impl Module for MyKeeper +where + ExecT: Debug, + QueryT: Debug, + SudoT: Debug, +{ + type ExecT = ExecT; + type QueryT = QueryT; + type SudoT = SudoT; + + fn execute( + &self, + _api: &dyn Api, + _storage: &mut dyn Storage, + _router: &dyn CosmosRouter, + _block: &BlockInfo, + _sender: Addr, + _msg: Self::ExecT, + ) -> AnyResult + where + ExecC: CustomMsg + DeserializeOwned + 'static, + QueryC: CustomQuery + DeserializeOwned + 'static, + { + bail!(self.1); + } + + fn query( + &self, + _api: &dyn Api, + _storage: &dyn Storage, + _querier: &dyn Querier, + _block: &BlockInfo, + _request: Self::QueryT, + ) -> AnyResult { + bail!(self.2); + } + + fn sudo( + &self, + _api: &dyn Api, + _storage: &mut dyn Storage, + _router: &dyn CosmosRouter, + _block: &BlockInfo, + _msg: Self::SudoT, + ) -> AnyResult + where + ExecC: CustomMsg + DeserializeOwned + 'static, + QueryC: CustomQuery + DeserializeOwned + 'static, + { + bail!(self.3); + } +} diff --git a/tests/test_app_builder/test_with_api.rs b/tests/test_app_builder/test_with_api.rs new file mode 100644 index 00000000..91ee4de9 --- /dev/null +++ b/tests/test_app_builder/test_with_api.rs @@ -0,0 +1,37 @@ +use cosmwasm_std::testing::MockApi; +use cosmwasm_std::{Api, CanonicalAddr, HexBinary}; +use cw_multi_test::{no_init, AppBuilder}; + +#[test] +fn building_app_with_custom_api_should_work() { + // prepare test data + let human = "juno1h34lmpywh4upnjdg90cjf4j70aee6z8qqfspugamjp42e4q28kqsksmtyp"; + let hex = "bc6bfd848ebd7819c9a82bf124d65e7f739d08e002601e23bb906aacd40a3d81"; + + // create application with custom api that implements + // Bech32 address encoding with 'juno' prefix + let app = AppBuilder::default() + .with_api(MockApi::default().with_prefix("juno")) + .build(no_init); + + // check address validation function + assert_eq!(human, app.api().addr_validate(human).unwrap().as_str()); + + // check if address can be canonicalized + assert_eq!( + app.api().addr_canonicalize(human).unwrap(), + CanonicalAddr::from(HexBinary::from_hex(hex).unwrap()) + ); + + // check if address can be humanized + assert_eq!( + human, + app.api() + .addr_humanize(&app.api().addr_canonicalize(human).unwrap()) + .unwrap() + .as_str(), + ); + + // check extension function for creating Bech32 encoded addresses + assert_eq!(human, app.api().addr_make("creator").as_str()); +} diff --git a/tests/test_app_builder/test_with_bank.rs b/tests/test_app_builder/test_with_bank.rs new file mode 100644 index 00000000..b17906bc --- /dev/null +++ b/tests/test_app_builder/test_with_bank.rs @@ -0,0 +1,87 @@ +use crate::test_app_builder::MyKeeper; +use anyhow::bail; +use cosmwasm_std::{coins, BankMsg, BankQuery}; +use cw_multi_test::{ + no_init, + wasm_emulation::query::{AllBankQuerier, ContainsRemote}, + AppBuilder, Bank, BankSudo, Executor, +}; + +type MyBankKeeper = MyKeeper; + +impl Bank for MyBankKeeper {} +impl AllBankQuerier for MyBankKeeper { + fn query_all( + &self, + _storage: &dyn cosmwasm_std::Storage, + ) -> anyhow::Result { + bail!(self.1) + } +} +impl ContainsRemote for MyBankKeeper { + fn with_remote(self, _remote: cw_multi_test::wasm_emulation::channel::RemoteChannel) -> Self { + todo!() + } + + fn set_remote(&mut self, _remote: cw_multi_test::wasm_emulation::channel::RemoteChannel) { + todo!() + } +} + +const EXECUTE_MSG: &str = "bank execute called"; +const QUERY_MSG: &str = "bank query called"; +const SUDO_MSG: &str = "bank sudo called"; + +#[test] +fn building_app_with_custom_bank_should_work() { + // build custom bank keeper + let bank_keeper = MyBankKeeper::new(EXECUTE_MSG, QUERY_MSG, SUDO_MSG); + + // build the application with custom bank keeper + let mut app = AppBuilder::default().with_bank(bank_keeper).build(no_init); + + // prepare user addresses + let recipient_addr = app.api().addr_make("recipient"); + let sender_addr = app.api().addr_make("sender"); + + // prepare additional input data + let denom = "eth"; + + // executing bank message should return an error defined in custom keeper + assert_eq!( + EXECUTE_MSG, + app.execute( + sender_addr, + BankMsg::Send { + to_address: recipient_addr.clone().into(), + amount: coins(1, denom), + } + .into(), + ) + .unwrap_err() + .to_string() + ); + + // executing bank sudo should return an error defined in custom keeper + assert_eq!( + SUDO_MSG, + app.sudo( + BankSudo::Mint { + to_address: recipient_addr.clone().into(), + amount: vec![], + } + .into() + ) + .unwrap_err() + .to_string() + ); + + // executing bank query should return an error defined in custom keeper + assert_eq!( + format!("Generic error: Querier contract error: {}", QUERY_MSG), + app.wrap() + .query_balance(recipient_addr, denom) + .unwrap_err() + .to_string() + ); +} diff --git a/tests/test_app_builder/test_with_block.rs b/tests/test_app_builder/test_with_block.rs new file mode 100644 index 00000000..35faed2b --- /dev/null +++ b/tests/test_app_builder/test_with_block.rs @@ -0,0 +1,19 @@ +use cosmwasm_std::{BlockInfo, Timestamp}; +use cw_multi_test::{no_init, AppBuilder}; + +#[test] +fn building_app_with_custom_block_should_work() { + // prepare additional test data + let block_info = BlockInfo { + height: 20, + time: Timestamp::from_nanos(1_571_797_419_879_305_544), + chain_id: "my-testnet".to_string(), + }; + + // build the application with custom block + let app_builder = AppBuilder::default(); + let app = app_builder.with_block(block_info.clone()).build(no_init); + + // calling block_info should return the same block used during initialization + assert_eq!(block_info, app.block_info()); +} diff --git a/tests/test_app_builder/test_with_distribution.rs b/tests/test_app_builder/test_with_distribution.rs new file mode 100644 index 00000000..fc78fef5 --- /dev/null +++ b/tests/test_app_builder/test_with_distribution.rs @@ -0,0 +1,40 @@ +use crate::test_app_builder::MyKeeper; +use cosmwasm_std::{DistributionMsg, Empty}; +use cw_multi_test::{no_init, AppBuilder, Distribution, Executor}; + +type MyDistributionKeeper = MyKeeper; + +impl Distribution for MyDistributionKeeper {} + +const EXECUTE_MSG: &str = "distribution execute called"; + +#[test] +fn building_app_with_custom_distribution_should_work() { + // build custom distribution keeper + // which has no query or sudo messages + let distribution_keeper = MyDistributionKeeper::new(EXECUTE_MSG, "", ""); + + // build the application with custom distribution keeper + let app_builder = AppBuilder::default(); + let mut app = app_builder + .with_distribution(distribution_keeper) + .build(no_init); + + // prepare addresses + let recipient_addr = app.api().addr_make("recipient"); + let sender_addr = app.api().addr_make("sender"); + + // executing distribution message should return an error defined in custom keeper + assert_eq!( + EXECUTE_MSG, + app.execute( + sender_addr, + DistributionMsg::SetWithdrawAddress { + address: recipient_addr.into(), + } + .into(), + ) + .unwrap_err() + .to_string() + ); +} diff --git a/tests/test_app_builder/test_with_gov.rs b/tests/test_app_builder/test_with_gov.rs new file mode 100644 index 00000000..f7626eba --- /dev/null +++ b/tests/test_app_builder/test_with_gov.rs @@ -0,0 +1,36 @@ +use crate::test_app_builder::MyKeeper; +use cosmwasm_std::{Empty, GovMsg, VoteOption}; +use cw_multi_test::{no_init, AppBuilder, Executor, Gov}; + +type MyGovKeeper = MyKeeper; + +impl Gov for MyGovKeeper {} + +const EXECUTE_MSG: &str = "gov execute called"; + +#[test] +fn building_app_with_custom_gov_should_work() { + // build custom governance keeper (no query and sudo handling for gov module) + let gov_keeper = MyGovKeeper::new(EXECUTE_MSG, "", ""); + + // build the application with custom gov keeper + let mut app = AppBuilder::default().with_gov(gov_keeper).build(no_init); + + // prepare addresses + let sender_addr = app.api().addr_make("sender"); + + // executing governance message should return an error defined in custom keeper + assert_eq!( + EXECUTE_MSG, + app.execute( + sender_addr, + GovMsg::Vote { + proposal_id: 1, + option: VoteOption::Yes, + } + .into(), + ) + .unwrap_err() + .to_string() + ); +} diff --git a/tests/test_app_builder/test_with_ibc.rs b/tests/test_app_builder/test_with_ibc.rs new file mode 100644 index 00000000..c92e7c08 --- /dev/null +++ b/tests/test_app_builder/test_with_ibc.rs @@ -0,0 +1,47 @@ +use crate::test_app_builder::MyKeeper; +use cosmwasm_std::{Empty, IbcMsg, IbcQuery, QueryRequest}; +use cw_multi_test::{no_init, AppBuilder, Executor, Ibc}; + +type MyIbcKeeper = MyKeeper; + +impl Ibc for MyIbcKeeper {} + +const EXECUTE_MSG: &str = "ibc execute called"; +const QUERY_MSG: &str = "ibc query called"; + +#[test] +fn building_app_with_custom_ibc_should_work() { + // build custom ibc keeper (no sudo handling for ibc) + let ibc_keeper = MyIbcKeeper::new(EXECUTE_MSG, QUERY_MSG, ""); + + // build the application with custom ibc keeper + let mut app = AppBuilder::default().with_ibc(ibc_keeper).build(no_init); + + // prepare user addresses + let sender_addr = app.api().addr_make("sender"); + + // executing ibc message should return an error defined in custom keeper + assert_eq!( + EXECUTE_MSG, + app.execute( + sender_addr, + IbcMsg::CloseChannel { + channel_id: "my-channel".to_string() + } + .into(), + ) + .unwrap_err() + .to_string() + ); + + // executing ibc query should return an error defined in custom keeper + assert_eq!( + format!("Generic error: Querier contract error: {}", QUERY_MSG), + app.wrap() + .query::(&QueryRequest::Ibc(IbcQuery::ListChannels { + port_id: Some("my-port".to_string()) + })) + .unwrap_err() + .to_string() + ); +} diff --git a/tests/test_app_builder/test_with_staking.rs b/tests/test_app_builder/test_with_staking.rs new file mode 100644 index 00000000..3f49b1d4 --- /dev/null +++ b/tests/test_app_builder/test_with_staking.rs @@ -0,0 +1,61 @@ +use crate::test_app_builder::MyKeeper; +use cosmwasm_std::{Coin, StakingMsg, StakingQuery}; +use cw_multi_test::{no_init, AppBuilder, Executor, Staking, StakingSudo}; + +type MyStakeKeeper = MyKeeper; + +impl Staking for MyStakeKeeper {} + +const EXECUTE_MSG: &str = "staking execute called"; +const QUERY_MSG: &str = "staking query called"; +const SUDO_MSG: &str = "staking sudo called"; + +#[test] +fn building_app_with_custom_staking_should_work() { + // build custom stake keeper + let stake_keeper = MyStakeKeeper::new(EXECUTE_MSG, QUERY_MSG, SUDO_MSG); + + // build the application with custom stake keeper + let mut app = AppBuilder::default() + .with_staking(stake_keeper) + .build(no_init); + + // prepare addresses + let validator_addr = app.api().addr_make("validator"); + let sender_addr = app.api().addr_make("sender"); + + // executing staking message should return an error defined in custom keeper + assert_eq!( + EXECUTE_MSG, + app.execute( + sender_addr, + StakingMsg::Delegate { + validator: validator_addr.clone().into(), + amount: Coin::new(1_u32, "eth"), + } + .into(), + ) + .unwrap_err() + .to_string() + ); + + // executing staking sudo should return an error defined in custom keeper + assert_eq!( + SUDO_MSG, + app.sudo( + StakingSudo::Slash { + validator: validator_addr.into(), + percentage: Default::default(), + } + .into() + ) + .unwrap_err() + .to_string() + ); + + // executing staking query should return an error defined in custom keeper + assert_eq!( + format!("Generic error: Querier contract error: {}", QUERY_MSG), + app.wrap().query_all_validators().unwrap_err().to_string() + ); +} diff --git a/tests/test_app_builder/test_with_stargate.rs b/tests/test_app_builder/test_with_stargate.rs new file mode 100644 index 00000000..41e40f74 --- /dev/null +++ b/tests/test_app_builder/test_with_stargate.rs @@ -0,0 +1,273 @@ +use anyhow::bail; +use cosmwasm_std::{ + Addr, AnyMsg, Api, Binary, BlockInfo, CosmosMsg, CustomMsg, CustomQuery, Empty, Event, + GrpcQuery, Querier, QueryRequest, Storage, +}; +use cw_multi_test::error::AnyResult; +use cw_multi_test::{ + no_init, AppBuilder, AppResponse, CosmosRouter, Executor, Stargate, StargateAccepting, + StargateFailing, +}; +use serde::de::DeserializeOwned; + +const MSG_STARGATE_EXECUTE: &str = "stargate execute called"; +const MSG_STARGATE_QUERY: &str = "stargate query called"; +const MSG_ANY_EXECUTE: &str = "any execute called"; +const MSG_GRPC_QUERY: &str = "grpc query called"; + +struct StargateKeeper; + +impl Stargate for StargateKeeper { + fn execute_stargate( + &self, + _api: &dyn Api, + _storage: &mut dyn Storage, + _router: &dyn CosmosRouter, + _block: &BlockInfo, + _sender: Addr, + _type_url: String, + _value: Binary, + ) -> AnyResult + where + ExecC: CustomMsg + DeserializeOwned + 'static, + QueryC: CustomQuery + DeserializeOwned + 'static, + { + bail!(MSG_STARGATE_EXECUTE) + } + + fn query_stargate( + &self, + _api: &dyn Api, + _storage: &dyn Storage, + _querier: &dyn Querier, + _block: &BlockInfo, + _path: String, + _data: Binary, + ) -> AnyResult { + bail!(MSG_STARGATE_QUERY) + } + + fn execute_any( + &self, + _api: &dyn Api, + _storage: &mut dyn Storage, + _router: &dyn CosmosRouter, + _block: &BlockInfo, + _sender: Addr, + _msg: AnyMsg, + ) -> AnyResult + where + ExecC: CustomMsg + DeserializeOwned + 'static, + QueryC: CustomQuery + DeserializeOwned + 'static, + { + bail!(MSG_ANY_EXECUTE) + } + + fn query_grpc( + &self, + _api: &dyn Api, + _storage: &dyn Storage, + _querier: &dyn Querier, + _block: &BlockInfo, + _request: GrpcQuery, + ) -> AnyResult { + bail!(MSG_GRPC_QUERY) + } +} + +#[test] +fn building_app_with_custom_stargate_should_work() { + // build the application with custom stargate keeper + let app_builder = AppBuilder::default(); + let mut app = app_builder.with_stargate(StargateKeeper).build(no_init); + + // prepare user addresses + let sender_addr = app.api().addr_make("sender"); + + // executing `stargate` message should return an error defined in custom stargate keeper + #[allow(deprecated)] + let msg = CosmosMsg::Stargate { + type_url: "test".to_string(), + value: Default::default(), + }; + assert_eq!( + app.execute(sender_addr, msg).unwrap_err().to_string(), + MSG_STARGATE_EXECUTE, + ); + + // executing `stargate` query should return an error defined in custom stargate keeper + #[allow(deprecated)] + let request: QueryRequest = QueryRequest::Stargate { + path: "test".to_string(), + data: Default::default(), + }; + assert!(app + .wrap() + .query::(&request) + .unwrap_err() + .to_string() + .ends_with(MSG_STARGATE_QUERY)); +} + +#[test] +#[cfg(feature = "cosmwasm_2_0")] +fn building_app_with_custom_any_grpc_should_work() { + // build the application with custom stargate keeper + let mut app = AppBuilder::default() + .with_stargate(StargateKeeper) + .build(no_init); + + // prepare user addresses + let sender_addr = app.api().addr_make("sender"); + + // executing `any` message should return an error defined in custom stargate keeper + let msg = CosmosMsg::Any(AnyMsg { + type_url: "test".to_string(), + value: Default::default(), + }); + assert_eq!( + app.execute(sender_addr, msg).unwrap_err().to_string(), + MSG_ANY_EXECUTE, + ); + + // executing `grpc` query should return an error defined in custom stargate keeper + let request: QueryRequest = QueryRequest::Grpc(GrpcQuery { + path: "test".to_string(), + data: Default::default(), + }); + assert!(app + .wrap() + .query::(&request) + .unwrap_err() + .to_string() + .ends_with(MSG_GRPC_QUERY)); +} + +#[test] +fn building_app_with_accepting_stargate_should_work() { + let mut app = AppBuilder::default() + .with_stargate(StargateAccepting) + .build(no_init); + + // prepare user addresses + let sender_addr = app.api().addr_make("sender"); + + // executing `stargate` query should success and return empty values + #[allow(deprecated)] + let msg = CosmosMsg::Stargate { + type_url: "test".to_string(), + value: Default::default(), + }; + let AppResponse { events, data } = app.execute(sender_addr, msg).unwrap(); + assert_eq!(events, Vec::::new()); + assert_eq!(data, None); + + // executing `stargate` query should success and return Empty message + #[allow(deprecated)] + let request: QueryRequest = QueryRequest::Stargate { + path: "test".to_string(), + data: Default::default(), + }; + assert_eq!(app.wrap().query::(&request).unwrap(), Empty {}); +} + +#[test] +#[cfg(feature = "cosmwasm_2_0")] +fn building_app_with_accepting_any_grpc_should_work() { + let app_builder = AppBuilder::default(); + let mut app = app_builder.with_stargate(StargateAccepting).build(no_init); + + // prepare user addresses + let sender_addr = app.api().addr_make("sender"); + + use cosmwasm_std::to_json_vec; + + // executing `any` message should success and return empty values + let msg = CosmosMsg::Any(AnyMsg { + type_url: "test".to_string(), + value: Default::default(), + }); + let AppResponse { events, data } = app.execute(sender_addr, msg).unwrap(); + assert_eq!(events, Vec::::new()); + assert_eq!(data, None); + + // executing `grpc` query should success and return empty binary + let request: QueryRequest = QueryRequest::Grpc(GrpcQuery { + path: "test".to_string(), + data: Default::default(), + }); + assert_eq!( + app.wrap() + .raw_query(to_json_vec(&request).unwrap().as_slice()) + .unwrap() + .unwrap(), + Binary::default() + ); +} + +#[test] +#[cfg(feature = "stargate")] +fn default_failing_stargate_should_work() { + let app_builder = AppBuilder::default(); + let mut app = app_builder.with_stargate(StargateFailing).build(no_init); + + // prepare user addresses + let sender_addr = app.api().addr_make("sender"); + + #[allow(deprecated)] + let msg = CosmosMsg::Stargate { + type_url: "test".to_string(), + value: Default::default(), + }; + assert!(app + .execute(sender_addr, msg) + .unwrap_err() + .to_string() + .starts_with("Unexpected stargate execute")); + + #[allow(deprecated)] + let request: QueryRequest = QueryRequest::Stargate { + path: "test".to_string(), + data: Default::default(), + }; + assert!(app + .wrap() + .query::(&request) + .unwrap_err() + .to_string() + .contains("Unexpected stargate query")); +} + +#[test] +#[cfg(feature = "cosmwasm_2_0")] +fn default_failing_any_grpc_should_work() { + let mut app = AppBuilder::default() + .with_stargate(StargateFailing) + .build(no_init); + + // prepare user addresses + let sender_addr = app.api().addr_make("sender"); + + use cosmwasm_std::to_json_vec; + + let msg = CosmosMsg::Any(AnyMsg { + type_url: "test".to_string(), + value: Default::default(), + }); + assert!(app + .execute(sender_addr, msg) + .unwrap_err() + .to_string() + .starts_with("Unexpected any execute")); + + let request: QueryRequest = QueryRequest::Grpc(GrpcQuery { + path: "test".to_string(), + data: Default::default(), + }); + assert!(app + .wrap() + .raw_query(to_json_vec(&request).unwrap().as_slice()) + .unwrap() + .unwrap_err() + .starts_with("Unexpected grpc query")); +} diff --git a/tests/test_app_builder/test_with_storage.rs b/tests/test_app_builder/test_with_storage.rs new file mode 100644 index 00000000..6adf6909 --- /dev/null +++ b/tests/test_app_builder/test_with_storage.rs @@ -0,0 +1,93 @@ +use crate::test_contracts; +use crate::test_contracts::counter::{CounterQueryMsg, CounterResponseMsg}; +use cosmwasm_std::{to_json_binary, Empty, Order, Record, Storage, WasmMsg}; +use cw_multi_test::{no_init, AppBuilder, Executor}; +use std::collections::BTreeMap; +use std::iter; + +#[derive(Default)] +struct MyStorage(BTreeMap, Vec>); + +// Minimal implementation of custom storage. +impl Storage for MyStorage { + fn get(&self, key: &[u8]) -> Option> { + self.0.get::>(&key.into()).cloned() + } + + fn range<'a>( + &'a self, + _start: Option<&[u8]>, + _end: Option<&[u8]>, + _order: Order, + ) -> Box + 'a> { + Box::new(iter::empty()) + } + + fn set(&mut self, key: &[u8], value: &[u8]) { + self.0.insert(key.into(), value.into()); + } + + fn remove(&mut self, key: &[u8]) { + self.0.remove(key); + } +} + +#[test] +fn building_app_with_custom_storage_should_work() { + // prepare additional test input data + let msg = to_json_binary(&Empty {}).unwrap(); + let admin = None; + let funds = vec![]; + let label = "my-counter"; + + // build the application with custom storage + let mut app = AppBuilder::default() + .with_storage(MyStorage::default()) + .build(no_init); + + // prepare user addresses + let owner_addr = app.api().addr_make("owner"); + + // store a contract code + let code_id = app.store_code(test_contracts::counter::contract()); + + // instantiate contract, this initializes a counter with value 1 + let contract_addr = app + .instantiate_contract( + code_id, + owner_addr.clone(), + &WasmMsg::Instantiate { + admin: admin.clone(), + code_id, + msg: msg.clone(), + funds: funds.clone(), + label: label.into(), + }, + &funds, + label, + admin, + ) + .unwrap(); + + // execute contract, this increments a counter + app.execute_contract( + owner_addr, + contract_addr.clone(), + &WasmMsg::Execute { + contract_addr: contract_addr.clone().into(), + msg, + funds, + }, + &[], + ) + .unwrap(); + + // query contract for current counter value + let response: CounterResponseMsg = app + .wrap() + .query_wasm_smart(&contract_addr, &CounterQueryMsg::Counter {}) + .unwrap(); + + // counter should be 2 + assert_eq!(2, response.value); +} diff --git a/tests/test_app_builder/test_with_wasm.rs b/tests/test_app_builder/test_with_wasm.rs new file mode 100644 index 00000000..aa6fdc79 --- /dev/null +++ b/tests/test_app_builder/test_with_wasm.rs @@ -0,0 +1,192 @@ +use crate::cw_multi_test::wasm_emulation::input::WasmStorage; +use crate::cw_multi_test::wasm_emulation::query::AllWasmQuerier; +use crate::cw_multi_test::wasm_emulation::query::ContainsRemote; +use crate::test_app_builder::MyKeeper; +use crate::test_contracts; +use cosmwasm_std::{ + Addr, Api, Binary, BlockInfo, Empty, Querier, Record, Storage, WasmMsg, WasmQuery, +}; +use cw_multi_test::error::{bail, AnyResult}; +use cw_multi_test::{ + no_init, AppBuilder, AppResponse, Contract, ContractData, CosmosRouter, Executor, Wasm, + WasmKeeper, WasmSudo, +}; +use once_cell::sync::Lazy; + +const EXECUTE_MSG: &str = "wasm execute called"; +const QUERY_MSG: &str = "wasm query called"; +const SUDO_MSG: &str = "wasm sudo called"; +const DUPLICATE_CODE_MSG: &str = "wasm duplicate code called"; +const CONTRACT_DATA_MSG: &str = "wasm contract data called"; + +const CODE_ID: u64 = 154; + +static WASM_RAW: Lazy> = Lazy::new(|| vec![(vec![154u8], vec![155u8])]); + +// This is on purpose derived from module, to check if there are no compilation errors +// when custom wasm keeper implements also Module trait (although it is not needed). +type MyWasmKeeper = MyKeeper; + +impl ContainsRemote for MyWasmKeeper { + fn with_remote(self, _remote: cw_multi_test::wasm_emulation::channel::RemoteChannel) -> Self { + todo!() + } + + fn set_remote(&mut self, _remote: cw_multi_test::wasm_emulation::channel::RemoteChannel) { + todo!() + } +} + +impl Wasm for MyWasmKeeper { + fn execute( + &self, + _api: &dyn Api, + _storage: &mut dyn Storage, + _router: &dyn CosmosRouter, + _block: &BlockInfo, + _sender: Addr, + _msg: WasmMsg, + ) -> AnyResult { + bail!(self.1); + } + + fn query( + &self, + _api: &dyn Api, + _storage: &dyn Storage, + _router: &dyn CosmosRouter, + _querier: &dyn Querier, + _block: &BlockInfo, + _request: WasmQuery, + ) -> AnyResult { + bail!(self.2); + } + + fn sudo( + &self, + _api: &dyn Api, + _storage: &mut dyn Storage, + _router: &dyn CosmosRouter, + _block: &BlockInfo, + _msg: WasmSudo, + ) -> AnyResult { + bail!(self.3); + } + + fn store_code(&mut self, _creator: Addr, _code: Box>) -> u64 { + CODE_ID + } + + fn store_code_with_id( + &mut self, + _creator: Addr, + code_id: u64, + _code: Box>, + ) -> AnyResult { + Ok(code_id) + } + + fn duplicate_code(&mut self, _code_id: u64) -> AnyResult { + bail!(DUPLICATE_CODE_MSG); + } + + fn contract_data(&self, _storage: &dyn Storage, _address: &Addr) -> AnyResult { + bail!(CONTRACT_DATA_MSG); + } + + fn dump_wasm_raw(&self, _storage: &dyn Storage, _address: &Addr) -> Vec { + WASM_RAW.clone() + } + + /// Stores the contract's code and returns an identifier of the stored contract's code. + fn store_wasm_code(&mut self, _creator: Addr, _code: Vec) -> u64 { + CODE_ID + } +} + +impl AllWasmQuerier for MyWasmKeeper { + fn query_all(&self, _storage: &dyn Storage) -> AnyResult { + bail!(self.1) + } +} + +#[test] +fn building_app_with_custom_wasm_should_work() { + // build custom wasm keeper + let wasm_keeper = MyWasmKeeper::new(EXECUTE_MSG, QUERY_MSG, SUDO_MSG); + + // build the application with custom wasm keeper + let mut app = AppBuilder::default().with_wasm(wasm_keeper).build(no_init); + + // prepare addresses + let contract_addr = app.api().addr_make("contract"); + let sender_addr = app.api().addr_make("sender"); + + // calling store_code should return value defined in custom keeper + assert_eq!(CODE_ID, app.store_code(test_contracts::counter::contract())); + + // calling duplicate_code should return error defined in custom keeper + assert_eq!( + DUPLICATE_CODE_MSG, + app.duplicate_code(CODE_ID).unwrap_err().to_string() + ); + + // calling contract_data should return error defined in custom keeper + assert_eq!( + CONTRACT_DATA_MSG, + app.contract_data(&contract_addr).unwrap_err().to_string() + ); + + // calling dump_wasm_raw should return value defined in custom keeper + assert_eq!(*WASM_RAW, app.dump_wasm_raw(&contract_addr)); + + // executing wasm execute should return an error defined in custom keeper + assert_eq!( + EXECUTE_MSG, + app.execute( + sender_addr, + WasmMsg::Instantiate { + admin: None, + code_id: 0, + msg: Default::default(), + funds: vec![], + label: "".to_string(), + } + .into() + ) + .unwrap_err() + .to_string() + ); + + // executing wasm sudo should return an error defined in custom keeper + assert_eq!( + SUDO_MSG, + app.sudo( + WasmSudo { + contract_addr, + message: Default::default() + } + .into() + ) + .unwrap_err() + .to_string() + ); + + // executing wasm query should return an error defined in custom keeper + assert_eq!( + format!("Generic error: Querier contract error: {}", QUERY_MSG), + app.wrap() + .query_wasm_code_info(CODE_ID) + .unwrap_err() + .to_string() + ); +} + +#[test] +fn compiling_with_wasm_keeper_should_work() { + // this verifies only compilation errors + // while our WasmKeeper does not implement Module + let _ = AppBuilder::default() + .with_wasm(WasmKeeper::default()) + .build(no_init); +} diff --git a/tests/test_attributes/mod.rs b/tests/test_attributes/mod.rs new file mode 100644 index 00000000..94c1e819 --- /dev/null +++ b/tests/test_attributes/mod.rs @@ -0,0 +1 @@ +mod test_empty_attribute; diff --git a/tests/test_attributes/test_empty_attribute.rs b/tests/test_attributes/test_empty_attribute.rs new file mode 100644 index 00000000..d4bc8d89 --- /dev/null +++ b/tests/test_attributes/test_empty_attribute.rs @@ -0,0 +1,75 @@ +use cosmwasm_std::Empty; +use cw_multi_test::{Contract, ContractWrapper, Executor}; + +use crate::default_app; + +mod test_contract { + use cosmwasm_std::{Binary, Deps, DepsMut, Empty, Env, Event, MessageInfo, Response, StdError}; + + pub fn instantiate( + _deps: DepsMut, + _env: Env, + _info: MessageInfo, + _msg: Empty, + ) -> Result { + Ok(Response::default()) + } + + pub fn execute( + _deps: DepsMut, + _env: Env, + _info: MessageInfo, + _msg: Empty, + ) -> Result { + Ok(Response::::new() + .add_attribute("city", " ") + .add_attribute("street", "") + .add_event( + Event::new("location") + .add_attribute("longitude", " ") + .add_attribute("latitude", ""), + )) + } + + pub fn query(_deps: Deps, _env: Env, _msg: Empty) -> Result { + Ok(Binary::default()) + } +} + +fn contract() -> Box> { + Box::new(ContractWrapper::new_with_empty( + test_contract::execute, + test_contract::instantiate, + test_contract::query, + )) +} + +#[test] +fn empty_string_attribute_should_work() { + // prepare the blockchain + let mut app = default_app(); + + // prepare address for creator=owner=sender + let sender_addr = app.api().addr_make("sender"); + + // store the contract's code + let code_id = app.store_code_with_creator(sender_addr.clone(), contract()); + + // instantiate the contract + let contract_addr = app + .instantiate_contract( + code_id, + sender_addr.clone(), + &Empty {}, + &[], + "attributed", + None, + ) + .unwrap(); + + // execute message on the contract, this returns response + // with attributes having empty string values, which should not fail + assert!(app + .execute_contract(sender_addr, contract_addr, &Empty {}, &[]) + .is_ok()); +} diff --git a/tests/test_bank/mod.rs b/tests/test_bank/mod.rs new file mode 100644 index 00000000..dfe6392e --- /dev/null +++ b/tests/test_bank/mod.rs @@ -0,0 +1 @@ +mod test_init_balance; diff --git a/tests/test_bank/test_init_balance.rs b/tests/test_bank/test_init_balance.rs new file mode 100644 index 00000000..37a7c9b2 --- /dev/null +++ b/tests/test_bank/test_init_balance.rs @@ -0,0 +1,96 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Coin, CustomMsg, CustomQuery, Uint128}; +use cw_multi_test::{custom_app, App, AppBuilder, BasicApp}; + +use crate::{default_app, remote_channel}; + +const USER: &str = "user"; +const DENOM: &str = "denom"; +const AMOUNT: u128 = 100; + +fn assert_balance(coins: Vec) { + assert_eq!(1, coins.len()); + assert_eq!(AMOUNT, coins[0].amount.u128()); + assert_eq!(DENOM, coins[0].denom); +} + +fn coins() -> Vec { + vec![Coin { + denom: DENOM.to_string(), + amount: Uint128::new(AMOUNT), + }] +} + +#[test] +fn initializing_balance_should_work() { + let app = AppBuilder::new().build(|router, api, storage| { + router + .bank + .init_balance(storage, &api.addr_make(USER), coins()) + .unwrap(); + }); + assert_balance( + app.wrap() + .query_all_balances(app.api().addr_make(USER)) + .unwrap(), + ); +} + +#[test] +fn initializing_balance_without_builder_should_work() { + let app = App::new(remote_channel(), |router, api, storage| { + router + .bank + .init_balance(storage, &api.addr_make(USER), coins()) + .unwrap(); + }); + assert_balance( + app.wrap() + .query_all_balances(app.api().addr_make(USER)) + .unwrap(), + ); +} + +#[test] +fn initializing_balance_custom_app_should_work() { + #[cw_serde] + pub enum CustomHelperMsg { + HelperMsg, + } + impl CustomMsg for CustomHelperMsg {} + + #[cw_serde] + pub enum CustomHelperQuery { + HelperQuery, + } + impl CustomQuery for CustomHelperQuery {} + + let app: BasicApp = + custom_app(remote_channel(), |router, api, storage| { + router + .bank + .init_balance(storage, &api.addr_make(USER), coins()) + .unwrap(); + }); + assert_balance( + app.wrap() + .query_all_balances(app.api().addr_make(USER)) + .unwrap(), + ); +} + +#[test] +fn initializing_balance_later_should_work() { + let mut app = default_app(); + app.init_modules(|router, api, storage| { + router + .bank + .init_balance(storage, &api.addr_make(USER), coins()) + .unwrap(); + }); + assert_balance( + app.wrap() + .query_all_balances(app.api().addr_make(USER)) + .unwrap(), + ); +} diff --git a/tests/test_contract_storage/mod.rs b/tests/test_contract_storage/mod.rs new file mode 100644 index 00000000..43867d5a --- /dev/null +++ b/tests/test_contract_storage/mod.rs @@ -0,0 +1,77 @@ +use crate::default_app; +use crate::test_contracts::counter; +use crate::test_contracts::counter::{CounterQueryMsg, CounterResponseMsg}; +use cosmwasm_std::{to_json_binary, Empty, WasmMsg}; +use cw_multi_test::{App, Executor}; +use cw_storage_plus::Item; + +#[test] +fn read_write_contract_storage_should_work() { + // counter value saved in contract state + const COUNTER: Item = Item::new("counter"); + + // prepare the blockchain + let mut app = default_app(); + + // store the contract code + let creator_addr = app.api().addr_make("creator"); + let code_id = app.store_code_with_creator(creator_addr, counter::contract()); + assert_eq!(1, code_id); + + // instantiate a new contract + let owner_addr = app.api().addr_make("owner"); + let contract_addr = app + .instantiate_contract(code_id, owner_addr.clone(), &Empty {}, &[], "counter", None) + .unwrap(); + assert!(contract_addr.as_str().starts_with("cosmwasm1")); + + // `counter` contract should return value 1 after instantiation + let query_res: CounterResponseMsg = app + .wrap() + .query_wasm_smart(&contract_addr, &CounterQueryMsg::Counter {}) + .unwrap(); + assert_eq!(1, query_res.value); + + { + // read the counter value directly from contract storage + let storage = app.contract_storage(&contract_addr); + let value = COUNTER.load(&*storage).unwrap(); + assert_eq!(1, value); + } + + // execute `counter` contract - this increments a counter with one + let execute_msg = WasmMsg::Execute { + contract_addr: contract_addr.clone().into(), + msg: to_json_binary(&Empty {}).unwrap(), + funds: vec![], + }; + app.execute_contract(owner_addr, contract_addr.clone(), &execute_msg, &[]) + .unwrap(); + + // now the `counter` contract should return value 2 + let query_res: CounterResponseMsg = app + .wrap() + .query_wasm_smart(&contract_addr, &CounterQueryMsg::Counter {}) + .unwrap(); + assert_eq!(2, query_res.value); + + { + // read the counter value directly from contract storage + let storage = app.contract_storage(&contract_addr); + let value = COUNTER.load(&*storage).unwrap(); + assert_eq!(2, value); + } + + { + // write the counter value directly into contract storage + let mut storage = app.contract_storage_mut(&contract_addr); + COUNTER.save(&mut *storage, &100).unwrap(); + } + + // now the `counter` contract should return value 100 + let query_res: CounterResponseMsg = app + .wrap() + .query_wasm_smart(&contract_addr, &CounterQueryMsg::Counter {}) + .unwrap(); + assert_eq!(100, query_res.value); +} diff --git a/tests/test_module/mod.rs b/tests/test_module/mod.rs new file mode 100644 index 00000000..8ccf49ca --- /dev/null +++ b/tests/test_module/mod.rs @@ -0,0 +1,2 @@ +mod test_accepting_module; +mod test_failing_module; diff --git a/tests/test_module/test_accepting_module.rs b/tests/test_module/test_accepting_module.rs new file mode 100644 index 00000000..3ba23737 --- /dev/null +++ b/tests/test_module/test_accepting_module.rs @@ -0,0 +1,66 @@ +use cosmwasm_std::testing::MockStorage; +use cosmwasm_std::{Binary, Empty}; +use cw_multi_test::{AcceptingModule, App, AppResponse, Module}; + +use crate::default_app; + +/// Utility function for comparing responses. +fn eq(actual: AppResponse, expected: AppResponse) { + assert_eq!(actual.events, expected.events); + assert_eq!(actual.data, expected.data); +} + +/// Utility function for asserting default outputs returned from accepting module. +fn assert_results(accepting_module: AcceptingModule) { + let app = default_app(); + let sender_addr = app.api().addr_make("sender"); + let empty_msg = Empty {}; + let mut storage = MockStorage::default(); + eq( + AppResponse::default(), + accepting_module + .execute( + app.api(), + &mut storage, + app.router(), + &app.block_info(), + sender_addr, + empty_msg.clone(), + ) + .unwrap(), + ); + assert_eq!( + Binary::default(), + accepting_module + .query( + app.api(), + &storage, + &(*app.wrap()), + &app.block_info(), + empty_msg.clone() + ) + .unwrap() + ); + eq( + AppResponse::default(), + accepting_module + .sudo( + app.api(), + &mut storage, + app.router(), + &app.block_info(), + empty_msg, + ) + .unwrap(), + ); +} + +#[test] +fn accepting_module_default_works() { + assert_results(AcceptingModule::default()); +} + +#[test] +fn accepting_module_new_works() { + assert_results(AcceptingModule::new()); +} diff --git a/tests/test_module/test_failing_module.rs b/tests/test_module/test_failing_module.rs new file mode 100644 index 00000000..f9ac2c6e --- /dev/null +++ b/tests/test_module/test_failing_module.rs @@ -0,0 +1,63 @@ +use cosmwasm_std::testing::MockStorage; +use cosmwasm_std::Empty; +use cw_multi_test::{App, FailingModule, Module}; + +use crate::default_app; + +/// Utility function for asserting outputs returned from failing module. +fn assert_results(failing_module: FailingModule) { + let app = default_app(); + let sender_addr = app.api().addr_make("sender"); + let empty_msg = Empty {}; + let mut storage = MockStorage::default(); + assert_eq!( + format!(r#"Unexpected exec msg Empty from Addr("{}")"#, sender_addr), + failing_module + .execute( + app.api(), + &mut storage, + app.router(), + &app.block_info(), + sender_addr, + empty_msg.clone() + ) + .unwrap_err() + .to_string() + ); + assert_eq!( + "Unexpected custom query Empty", + failing_module + .query( + app.api(), + &storage, + &(*app.wrap()), + &app.block_info(), + empty_msg.clone() + ) + .unwrap_err() + .to_string() + ); + assert_eq!( + "Unexpected sudo msg Empty", + failing_module + .sudo( + app.api(), + &mut storage, + app.router(), + &app.block_info(), + empty_msg + ) + .unwrap_err() + .to_string() + ); +} + +#[test] +fn failing_module_default_works() { + assert_results(FailingModule::default()); +} + +#[test] +fn failing_module_new_works() { + assert_results(FailingModule::new()); +} diff --git a/tests/test_prefixed_storage/mod.rs b/tests/test_prefixed_storage/mod.rs new file mode 100644 index 00000000..aeda3e9d --- /dev/null +++ b/tests/test_prefixed_storage/mod.rs @@ -0,0 +1,2 @@ +mod test_prefixed_multilevel_storage; +mod test_prefixed_storage_bank; diff --git a/tests/test_prefixed_storage/test_prefixed_multilevel_storage.rs b/tests/test_prefixed_storage/test_prefixed_multilevel_storage.rs new file mode 100644 index 00000000..0978edda --- /dev/null +++ b/tests/test_prefixed_storage/test_prefixed_multilevel_storage.rs @@ -0,0 +1,42 @@ +use cosmwasm_std::{coin, Addr}; +use cw_multi_test::{App, IntoAddr}; +use cw_storage_plus::Map; +use cw_utils::NativeBalance; +use std::ops::{Deref, DerefMut}; + +use crate::default_app; + +const NAMESPACE_CENTRAL_BANK: &[u8] = b"central-bank"; +const NAMESPACE_NATIONAL_BANK: &[u8] = b"national-bank"; +const NAMESPACE_LOCAL_BANK: &[u8] = b"local-bank"; +const NAMESPACES: &[&[u8]] = &[ + NAMESPACE_NATIONAL_BANK, + NAMESPACE_CENTRAL_BANK, + NAMESPACE_LOCAL_BANK, +]; +const BALANCES: Map<&Addr, NativeBalance> = Map::new("balances"); + +#[test] +fn multilevel_storage_should_work() { + // prepare balance owner + let owner_addr = "owner".into_addr(); + // create the blockchain + let mut app = default_app(); + { + // get the mutable prefixed, multilevel storage for banks + let mut storage_mut = app.prefixed_multilevel_storage_mut(NAMESPACES); + // set balances manually + let mut balance = NativeBalance(vec![coin(111, "BTC"), coin(222, "ETH")]); + balance.normalize(); + BALANCES + .save(storage_mut.deref_mut(), &owner_addr, &balance) + .unwrap(); + } + { + // get the read-only prefixed, multilevel storage for banks + let storage = app.prefixed_multilevel_storage(NAMESPACES); + // read balances manually + let balances = BALANCES.load(storage.deref(), &owner_addr).unwrap(); + assert_eq!("BTC111ETH222", balances.to_string()); + } +} diff --git a/tests/test_prefixed_storage/test_prefixed_storage_bank.rs b/tests/test_prefixed_storage/test_prefixed_storage_bank.rs new file mode 100644 index 00000000..f24292f1 --- /dev/null +++ b/tests/test_prefixed_storage/test_prefixed_storage_bank.rs @@ -0,0 +1,51 @@ +use cosmwasm_std::{coin, Addr}; +use cw_multi_test::{App, IntoAddr}; +use cw_storage_plus::Map; +use cw_utils::NativeBalance; +use std::ops::{Deref, DerefMut}; + +use crate::{default_app, remote_channel}; + +const NAMESPACE_BANK: &[u8] = b"bank"; +const BALANCES: Map<&Addr, NativeBalance> = Map::new("balances"); + +#[test] +fn reading_bank_storage_should_work() { + // prepare balance owner + let owner_addr = "owner".into_addr(); + + // set balances + let init_funds = vec![coin(1, "BTC"), coin(2, "ETH")]; + let app = App::new(remote_channel(), |router, _, storage| { + router + .bank + .init_balance(storage, &owner_addr, init_funds) + .unwrap(); + }); + + // get the read-only prefixed storage for bank + let storage = app.prefixed_storage(NAMESPACE_BANK); + let balances = BALANCES.load(storage.deref(), &owner_addr).unwrap(); + assert_eq!("BTC1ETH2", balances.to_string()); +} + +#[test] +fn writing_bank_storage_should_work() { + // prepare balance owner + let owner_addr = "owner".into_addr(); + + let mut app = default_app(); + // get the mutable prefixed storage for bank + let mut storage = app.prefixed_storage_mut(NAMESPACE_BANK); + + // set balances manually + let mut balance = NativeBalance(vec![coin(3, "BTC"), coin(4, "ETH")]); + balance.normalize(); + BALANCES + .save(storage.deref_mut(), &owner_addr, &balance) + .unwrap(); + + // check balances + let balances = BALANCES.load(storage.deref(), &owner_addr).unwrap(); + assert_eq!("BTC3ETH4", balances.to_string()); +} diff --git a/tests/test_staking/mod.rs b/tests/test_staking/mod.rs new file mode 100644 index 00000000..39c2edb2 --- /dev/null +++ b/tests/test_staking/mod.rs @@ -0,0 +1 @@ +mod test_stake_unstake; diff --git a/tests/test_staking/test_stake_unstake.rs b/tests/test_staking/test_stake_unstake.rs new file mode 100644 index 00000000..92ab74ac --- /dev/null +++ b/tests/test_staking/test_stake_unstake.rs @@ -0,0 +1,131 @@ +use cosmwasm_std::testing::mock_env; +use cosmwasm_std::{coin, Decimal, StakingMsg, Validator}; +use cw_multi_test::{AppBuilder, Executor, IntoBech32, StakingInfo}; + +#[test] +fn stake_unstake_should_work() { + const BONDED_DENOM: &str = "stake"; // denominator of the staking token + const UNBONDING_TIME: u64 = 60; // time between unbonding and receiving tokens back (in seconds) + const DELEGATION_AMOUNT: u128 = 100; // amount of tokens to be (re)delegated + const INITIAL_AMOUNT: u128 = 1000; // initial amount of tokens for delegator + const FEWER_AMOUNT: u128 = INITIAL_AMOUNT - DELEGATION_AMOUNT; // amount of tokens after delegation + + let delegator_addr = "delegator".into_bech32(); + let validator_addr = "valoper".into_bech32(); + + let valoper = Validator::new( + validator_addr.to_string(), + Decimal::percent(10), + Decimal::percent(90), + Decimal::percent(1), + ); + + // prepare the blockchain configuration + let block = mock_env().block; + let mut app = AppBuilder::default().build(|router, api, storage| { + // set initial balance for the delegator + router + .bank + .init_balance( + storage, + &delegator_addr, + vec![coin(INITIAL_AMOUNT, BONDED_DENOM)], + ) + .unwrap(); + // setup staking parameters + router + .staking + .setup( + storage, + StakingInfo { + bonded_denom: BONDED_DENOM.to_string(), + unbonding_time: UNBONDING_TIME, + apr: Decimal::percent(10), + }, + ) + .unwrap(); + // add a validator + router + .staking + .add_validator(api, storage, &block, valoper) + .unwrap(); + }); + + // delegate tokens to validator + app.execute( + delegator_addr.clone(), + StakingMsg::Delegate { + validator: validator_addr.to_string(), + amount: coin(DELEGATION_AMOUNT, BONDED_DENOM), + } + .into(), + ) + .unwrap(); + + // delegation works immediately, so delegator should have now fewer tokens + let delegator_balance = app + .wrap() + .query_balance(delegator_addr.clone(), BONDED_DENOM) + .unwrap(); + assert_eq!(FEWER_AMOUNT, delegator_balance.amount.u128()); + + // validator should have now DELEGATION_AMOUNT of tokens assigned + let delegation = app + .wrap() + .query_delegation(delegator_addr.clone(), validator_addr.clone()) + .unwrap() + .unwrap(); + assert_eq!(DELEGATION_AMOUNT, delegation.amount.amount.u128()); + + // now, undelegate all bonded tokens + app.execute( + delegator_addr.clone(), + StakingMsg::Undelegate { + validator: validator_addr.to_string(), + amount: coin(DELEGATION_AMOUNT, BONDED_DENOM), + } + .into(), + ) + .unwrap(); + + // unbonding works with timeout, so tokens will be given back after unbonding time; + // while we do not change the block size or time, delegator should still have fewer tokens + let delegator_balance = app + .wrap() + .query_balance(delegator_addr.clone(), BONDED_DENOM) + .unwrap(); + assert_eq!(FEWER_AMOUNT, delegator_balance.amount.u128()); + + // now we update the block but with time that is shorter than unbonding time + app.update_block(|block| { + block.height += 1; + block.time = block.time.plus_seconds(UNBONDING_TIME - 1); + }); + + // delegator should still have fewer tokens + let delegator_balance = app + .wrap() + .query_balance(delegator_addr.clone(), BONDED_DENOM) + .unwrap(); + assert_eq!(FEWER_AMOUNT, delegator_balance.amount.u128()); + + // now we update the block so unbonding time is reached + app.update_block(|block| { + block.height += 1; + block.time = block.time.plus_seconds(1); + }); + + // delegator should have back the initial amount of tokens + let delegator_balance = app + .wrap() + .query_balance(delegator_addr.clone(), BONDED_DENOM) + .unwrap(); + assert_eq!(INITIAL_AMOUNT, delegator_balance.amount.u128()); + + // there should be no more delegations + let delegation = app + .wrap() + .query_delegation(delegator_addr, validator_addr) + .unwrap(); + assert_eq!(None, delegation); +} diff --git a/tests/test_wasm/mod.rs b/tests/test_wasm/mod.rs new file mode 100644 index 00000000..df641335 --- /dev/null +++ b/tests/test_wasm/mod.rs @@ -0,0 +1,3 @@ +mod test_with_addr_gen; +#[cfg(feature = "cosmwasm_1_2")] +mod test_with_checksum_gen; diff --git a/tests/test_wasm/test_with_addr_gen.rs b/tests/test_wasm/test_with_addr_gen.rs new file mode 100644 index 00000000..4ec6c829 --- /dev/null +++ b/tests/test_wasm/test_with_addr_gen.rs @@ -0,0 +1,180 @@ +use cosmwasm_std::testing::MockApi; +use cosmwasm_std::{Addr, Api, Empty, Storage}; +use cw_multi_test::error::AnyResult; +use cw_multi_test::{no_init, AddressGenerator, AppBuilder, Executor, WasmKeeper}; + +use crate::test_contracts; + +#[test] +fn contract_address_should_work() { + // prepare application with custom API + let mut app = AppBuilder::default() + .with_api(MockApi::default().with_prefix("purple")) + .build(no_init); + + // prepare user addresses + let creator_addr = app.api().addr_make("creator"); + + // store contract's code + let code_id = app.store_code_with_creator(creator_addr, test_contracts::counter::contract()); + + let owner = app.api().addr_make("owner"); + + let contract_addr_1 = app + .instantiate_contract(code_id, owner.clone(), &Empty {}, &[], "Counter", None) + .unwrap(); + + let contract_addr_2 = app + .instantiate_contract(code_id, owner, &Empty {}, &[], "Counter", None) + .unwrap(); + + // addresses of the two contract instances should be different + assert_ne!(contract_addr_1, contract_addr_2); + + // make sure that generated addresses are in valid Bech32 encoding + assert_eq!( + contract_addr_1.to_string(), + "purple1mzdhwvvh22wrt07w59wxyd58822qavwkx5lcej7aqfkpqqlhaqfs5efvjk" + ); + assert_eq!( + contract_addr_2.to_string(), + "purple14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9smc2vxm" + ); +} + +#[test] +fn custom_address_generator_should_work() { + // prepare custom address generator + struct CustomAddressGenerator; + + impl AddressGenerator for CustomAddressGenerator { + fn contract_address( + &self, + _api: &dyn Api, + _storage: &mut dyn Storage, + _code_id: u64, + _instance_id: u64, + ) -> AnyResult { + Ok(MockApi::default() + .with_prefix("osmo") + .addr_make("test_addr")) + } + } + + // prepare wasm module with custom address generator + let wasm_keeper = WasmKeeper::new().with_address_generator(CustomAddressGenerator); + + // prepare application with custom wasm keeper + let mut app = AppBuilder::default().with_wasm(wasm_keeper).build(no_init); + + // prepare user addresses + let owner_addr = app.api().addr_make("owner"); + + // store contract's code + let code_id = app.store_code(test_contracts::counter::contract()); + + let contract_addr = app + .instantiate_contract(code_id, owner_addr, &Empty {}, &[], "Counter", None) + .unwrap(); + + // make sure that contract address equals to value generated by custom address generator + assert_eq!( + contract_addr.as_str(), + MockApi::default() + .with_prefix("osmo") + .addr_make("test_addr") + .as_str() + ); +} + +#[test] +#[cfg(feature = "cosmwasm_1_2")] +fn predictable_contract_address_should_work() { + // prepare application with custom api + let mut app = AppBuilder::default() + .with_api(MockApi::default().with_prefix("juno")) + .build(no_init); + + let creator = app.api().addr_make("creator"); + + // store contract's code + let code_id = app.store_code_with_creator(creator.clone(), test_contracts::counter::contract()); + + let contract_addr_1 = app + .instantiate2_contract( + code_id, + creator.clone(), + &Empty {}, + &[], + "Counter", + None, + [1, 2, 3, 4, 5, 6], + ) + .unwrap(); + + let contract_addr_2 = app + .instantiate2_contract( + code_id, + creator.clone(), + &Empty {}, + &[], + "Counter", + None, + [11, 12, 13, 14, 15, 16], + ) + .unwrap(); + + // addresses of the two contract instances should be different + assert_ne!(contract_addr_1, contract_addr_2); + + // instantiating a contract with the same salt should fail + app.instantiate2_contract( + code_id, + creator, + &Empty {}, + &[], + "Counter", + None, + [1, 2, 3, 4, 5, 6], + ) + .unwrap_err(); +} + +#[test] +#[cfg(feature = "cosmwasm_1_2")] +fn creating_contract_with_the_same_predictable_address_should_fail() { + // prepare application with custom api + let mut app = AppBuilder::default() + .with_api(MockApi::default().with_prefix("juno")) + .build(no_init); + + let creator = app.api().addr_make("creator"); + + // store contract's code + let code_id = app.store_code_with_creator(creator.clone(), test_contracts::counter::contract()); + + // instantiating for the first time should work + app.instantiate2_contract( + code_id, + creator.clone(), + &Empty {}, + &[], + "Counter", + None, + [1, 2, 3, 4, 5, 6], + ) + .unwrap(); + + // this instantiating should fail, + // the same address is generated for a contract instance + app.instantiate2_contract( + code_id, + creator, + &Empty {}, + &[], + "Counter", + None, + [1, 2, 3, 4, 5, 6], + ) + .unwrap_err(); +} diff --git a/tests/test_wasm/test_with_checksum_gen.rs b/tests/test_wasm/test_with_checksum_gen.rs new file mode 100644 index 00000000..3c1bca3e --- /dev/null +++ b/tests/test_wasm/test_with_checksum_gen.rs @@ -0,0 +1,58 @@ +use crate::default_app; +use crate::test_contracts; +use cosmwasm_std::{Addr, Checksum}; +use cw_multi_test::{no_init, App, AppBuilder, ChecksumGenerator, WasmKeeper}; + +#[test] +fn default_checksum_generator_should_work() { + // prepare default application with default wasm keeper + let mut app = default_app(); + + // prepare user addresses + let creator_addr = app.api().addr_make("creator"); + + // store contract's code + let code_id = app.store_code_with_creator(creator_addr, test_contracts::counter::contract()); + + // get code info + let code_info_response = app.wrap().query_wasm_code_info(code_id).unwrap(); + + // this should be a default checksum + assert_eq!( + code_info_response.checksum.to_hex(), + "27095b438f70aed35405149bc5e8dfa1d461f7cd9c25359807ad66dcc1396fc7" + ); +} + +struct MyChecksumGenerator; + +impl ChecksumGenerator for MyChecksumGenerator { + fn checksum(&self, _creator: &Addr, _code_id: u64) -> Checksum { + Checksum::from_hex("c0ffee01c0ffee02c0ffee03c0ffee04c0ffee05c0ffee06c0ffee07c0ffee08") + .unwrap() + } +} + +#[test] +fn custom_checksum_generator_should_work() { + // prepare wasm keeper with custom checksum generator + let wasm_keeper = WasmKeeper::new().with_checksum_generator(MyChecksumGenerator); + + // prepare application with custom wasm keeper + let mut app = AppBuilder::default().with_wasm(wasm_keeper).build(no_init); + + // prepare user addresses + let creator_addr = app.api().addr_make("creator"); + + // store contract's code + let code_id = app.store_code_with_creator(creator_addr, test_contracts::counter::contract()); + + // get code info + let code_info_response = app.wrap().query_wasm_code_info(code_id).unwrap(); + + // this should be custom checksum + assert_eq!( + code_info_response.checksum.to_hex(), + "c0ffee01c0ffee02c0ffee03c0ffee04c0ffee05c0ffee06c0ffee07c0ffee08" + ); +}