├── .github └── workflows │ ├── publish.yml │ └── rust-checks.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── Makefile ├── README.md ├── drink-cli ├── Cargo.toml ├── README.md └── src │ ├── app_state │ ├── contracts.rs │ ├── mod.rs │ ├── output.rs │ ├── print.rs │ └── user_input.rs │ ├── cli.rs │ ├── executor │ ├── contract.rs │ ├── error.rs │ └── mod.rs │ ├── main.rs │ └── ui │ ├── contracts.rs │ ├── current_env.rs │ ├── footer.rs │ ├── help.rs │ ├── layout.rs │ ├── mod.rs │ ├── output.rs │ └── user_input.rs ├── drink ├── Cargo.toml ├── src │ ├── errors.rs │ ├── lib.rs │ ├── session.rs │ └── session │ │ ├── bundle.rs │ │ ├── error.rs │ │ ├── mock.rs │ │ ├── mock │ │ ├── contract.rs │ │ ├── error.rs │ │ └── extension.rs │ │ ├── mocking_api.rs │ │ ├── record.rs │ │ └── transcoding.rs ├── test-macro │ ├── Cargo.toml │ └── src │ │ ├── bundle_provision.rs │ │ ├── contract_building.rs │ │ └── lib.rs └── test-resources │ └── dummy.polkavm ├── examples ├── chain-extension │ ├── Cargo.lock │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── chain_extension_ink_side.rs │ │ ├── chain_extension_runtime_side.rs │ │ └── lib.rs ├── contract-events │ ├── Cargo.lock │ ├── Cargo.toml │ ├── README.md │ └── lib.rs ├── cross-contract-call-tracing │ ├── Cargo.lock │ ├── Cargo.toml │ ├── README.md │ └── lib.rs ├── dry-running │ ├── Cargo.lock │ ├── Cargo.toml │ ├── README.md │ └── lib.rs ├── flipper │ ├── Cargo.lock │ ├── Cargo.toml │ └── lib.rs ├── mocking │ ├── Cargo.lock │ ├── Cargo.toml │ ├── README.md │ └── lib.rs ├── multiple-contracts │ ├── Cargo.lock │ ├── Cargo.toml │ ├── README.md │ └── lib.rs ├── quick-start-with-drink │ ├── Cargo.lock │ ├── Cargo.toml │ ├── README.md │ └── lib.rs └── runtime-interaction │ ├── Cargo.lock │ ├── Cargo.toml │ ├── README.md │ ├── lib.rs │ └── test-resources │ └── dummy.polkavm ├── resources ├── blockchain-onion.svg └── testing-strategies.svg └── rustfmt.toml /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Publish to crates.io 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | - release-* 9 | pull_request: 10 | 11 | concurrency: 12 | group: ${{ github.ref }}-${{ github.workflow }} 13 | cancel-in-progress: true 14 | 15 | jobs: 16 | publish: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v4 20 | - uses: actions-rs/toolchain@v1 21 | with: 22 | toolchain: stable 23 | - uses: katyo/publish-crates@v2 24 | with: 25 | dry-run: ${{ github.event_name != 'push' }} 26 | registry-token: ${{ secrets.CRATES_IO_TOKEN }} 27 | ignore-unpublished-changes: true 28 | -------------------------------------------------------------------------------- /.github/workflows/rust-checks.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Rust checks 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | - release-* 9 | pull_request: 10 | workflow_dispatch: 11 | 12 | concurrency: 13 | group: ${{ github.ref }}-${{ github.workflow }} 14 | cancel-in-progress: true 15 | 16 | jobs: 17 | main: 18 | name: Run check, test and lints 19 | runs-on: ubuntu-latest 20 | env: 21 | CARGO_INCREMENTAL: 0 22 | steps: 23 | - name: Checkout source code 24 | uses: actions/checkout@v3 25 | 26 | - name: Install Rust toolchain 27 | uses: actions-rs/toolchain@v1 28 | with: 29 | toolchain: stable 30 | profile: minimal 31 | default: true 32 | components: clippy, rustfmt 33 | 34 | - name: Add rust-src 35 | shell: bash 36 | run: rustup component add rust-src 37 | 38 | - name: Run format checks 39 | uses: actions-rs/cargo@v1 40 | with: 41 | command: fmt 42 | args: --all 43 | 44 | - name: Run linter 45 | uses: actions-rs/cargo@v1 46 | with: 47 | command: clippy 48 | args: --all-targets -- --no-deps -D warnings 49 | 50 | - name: Run unit test suite 51 | uses: actions-rs/cargo@v1 52 | with: 53 | command: test 54 | 55 | - name: Run tests for examples 56 | env: 57 | CARGO_TARGET_DIR: ${{ github.workspace }}/target 58 | shell: bash 59 | run: | 60 | for dir in examples/*; do (echo "Running example: $dir" && cargo test --manifest-path $dir/Cargo.toml); done 61 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Cargo generated files 2 | **/debug/ 3 | **/target/ 4 | **/*.rs.bk 5 | *.pdb 6 | 7 | # Contracts' cargo locks 8 | examples/**/Cargo.lock 9 | 10 | # IDEs 11 | .idea/ 12 | .vscode/ -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## [0.18.0] 11 | 12 | ### Changed 13 | 14 | - For ink! 5.1 support: Bump `ink`, `cargo-contract`, `frame-` and `sp-` crates. 15 | - Use Rust 1.81 as the default toolchain. 16 | 17 | ## [0.17.0] 18 | 19 | ### Added 20 | 21 | - Return error when call was reverted. 22 | 23 | ## [0.16.0] 24 | 25 | ### Changed 26 | 27 | - Switch types from Rc to Arc. 28 | 29 | ## [0.15.0] 30 | 31 | ### Changed 32 | 33 | - Migrate `Sandbox` related code to `ink_sandbox` crate. 34 | 35 | ## [0.14.0] 36 | 37 | ### Changed 38 | 39 | - Bump `ink` to `5.0.0` and `cargo-contract` to `4.0.0` 40 | - Rework Sandbox API to better support custom Runtime 41 | 42 | ## [0.13.0] 43 | 44 | ### Changed 45 | 46 | - Bump `ink`, `cargo-contract`, `frame-` and `sp-` crates. 47 | 48 | ## [0.12.1] 49 | 50 | ### Added 51 | 52 | - Support dry running contract interactions 53 | 54 | ## [0.12.0] 55 | 56 | ### Changed 57 | 58 | - Hide macros behind dedicated `macros` (default) feature flag 59 | - Hide contract bundles behind `session` feature flag 60 | 61 | ## [0.11.1] 62 | 63 | ### Added 64 | 65 | - Respect features for the contract dependencies when building contracts via drink macros 66 | 67 | ## [0.11.0] 68 | 69 | ### Changed 70 | 71 | - Support `ink@5.0.0-rc.2` 72 | - Update `contract-*` crates to `4.0.0-rc.3` 73 | 74 | ### Changed 75 | 76 | ## [0.10.0] 77 | 78 | ### Changed 79 | 80 | - Update toolchain to `1.74.0` 81 | - Support `ink@5.0.0-rc.1` 82 | - Update `contract-*` crates to `4.0.0-rc.2` 83 | 84 | ## [0.9.0] 85 | 86 | ### Changed 87 | 88 | - Rework `Sandbox` API to ease working with custom runtimes 89 | 90 | ## [0.8.7] 91 | 92 | ### Changed 93 | 94 | - Migrate examples back to `ink@4.3.0` 95 | - Downgrade `contract-*` crates from `4.0.0-rc.1` to `3.2.0` 96 | - Bumped toolchain to `1.74.0` 97 | 98 | ### Fixed 99 | 100 | - Compilation issues due to the breaking changes in `contract-build` dependency 101 | 102 | ## [0.8.6] [YANKED] 103 | 104 | ### Added 105 | 106 | - Accessing events emitted by contracts 107 | - `#[drink::test]` creates and adds a `session: Session` argument to the test function 108 | 109 | ## [0.8.5] [YANKED] 110 | 111 | ### Changed 112 | 113 | - Update `contract-*` crates from `3.x.x` to `4.0.0-rc.1` 114 | - Migrate examples from `ink@4.2.1` to `ink@5.0.0-rc` 115 | 116 | ## [0.8.4] 117 | 118 | ### Added 119 | 120 | - `NO_SALT`, `NO_ENDOWMENT` contstants added 121 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | 4 | members = ["drink", "drink/test-macro", "drink-cli"] 5 | 6 | exclude = ["examples/"] 7 | 8 | [workspace.package] 9 | authors = ["Cardinal", "Use Ink "] 10 | edition = "2021" 11 | homepage = "https://github.com/use-ink/drink" 12 | license = "Apache-2.0" 13 | readme = "README.md" 14 | repository = "https://github.com/use-ink/drink" 15 | version = "0.19.0-alpha" 16 | 17 | [workspace.dependencies] 18 | anyhow = { version = "1.0.94" } 19 | cargo_metadata = { version = "0.18.1" } 20 | clap = { version = "4.3.4" } 21 | contract-build = { version = "6.0.0-alpha" } 22 | contract-metadata = { version = "6.0.0-alpha" } 23 | contract-transcode = { version = "6.0.0-alpha" } 24 | convert_case = { version = "0.6.0" } 25 | crossterm = { version = "0.26.0" } 26 | darling = { version = "0.20.3" } 27 | parity-scale-codec = { version = "3.6.9" } 28 | parity-scale-codec-derive = { version = "3.6.9" } 29 | paste = { version = "1.0.7" } 30 | proc-macro2 = { version = "1" } 31 | quote = { version = "1" } 32 | ratatui = { version = "0.21.0" } 33 | scale-info = { version = "2.10.0" } 34 | serde_json = { version = "1.0.133" } 35 | syn = { version = "2" } 36 | thiserror = { version = "1.0.40" } 37 | wat = { version = "1.0.71" } 38 | ink_sandbox = { version = "6.0.0-alpha" } 39 | ink_primitives = { version = "6.0.0-alpha" } 40 | 41 | # Substrate dependencies 42 | 43 | frame-system = { version = "40.1.0" } 44 | frame-support = { version = "40.1.0" } 45 | sp-runtime-interface = { version = "29.0.1" } 46 | 47 | # Local dependencies 48 | 49 | drink = { version = "=0.19.0-alpha", path = "drink" } 50 | drink-test-macro = { version = "=0.19.0-alpha", path = "drink/test-macro" } 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: run build lint test_examples clean help 2 | 3 | EXAMPLES = ./examples 4 | EXAMPLES_PATHS := $(shell find $(EXAMPLES) -mindepth 1 -maxdepth 2 -name Cargo.toml -exec dirname "{}" \;) 5 | EXAMPLES_TARGET = ./examples/target 6 | 7 | run: ## Run the project 8 | cargo run --release 9 | 10 | build: ## Build the project 11 | cargo build --release 12 | 13 | lint: ## Run the linter 14 | cargo +nightly fmt 15 | cargo clippy --release -- -D warnings 16 | 17 | test_examples: ## Run tests for the examples 18 | @mkdir -p $(EXAMPLES_TARGET) 19 | @for dir in $(EXAMPLES_PATHS); do \ 20 | echo "Processing $$dir"; \ 21 | cargo test --quiet --manifest-path $$dir/Cargo.toml --release --target-dir $(EXAMPLES_TARGET) || exit 1; \ 22 | done 23 | 24 | clean: ## Clean all the workspace build files 25 | cargo clean 26 | 27 | help: ## Displays this help 28 | @awk 'BEGIN {FS = ":.*##"; printf "Usage:\n make \033[1;36m\033[0m\n\nTargets:\n"} /^[a-zA-Z0-9_-]+:.*?##/ { printf " \033[1;36m%-25s\033[0m %s\n", $$1, $$2 }' $(MAKEFILE_LIST) 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Rust checks](https://github.com/use-ink/drink/actions/workflows/rust-checks.yml/badge.svg)](https://github.com/use-ink/drink/actions/workflows/rust-checks.yml) 2 | [![Built for ink!](https://raw.githubusercontent.com/paritytech/ink/master/.images/built-for-ink.svg)](https://github.com/paritytech/ink) 3 | 4 |

DRink!

5 |

Dechained Ready-to-play ink! playground

6 | 7 | # What is DRink!? 8 | 9 | ## In brief 10 | 11 | DRink! is a toolbox for ink! developers that allows for a fully functional ink! contract development without any running node. 12 | It provides you with a unique, yet very powerful environment for interacting with contracts: 13 | - deploy and call your contracts synchronously, **without any delays** related to block production or networking 14 | - gain access to **powerful features** that are not available with standard methods like **contract mocking, enhanced debugging and call tracing** 15 | - work with **multiple contracts** at the same time 16 | - work with **arbitrary runtime** configurations, including custom chain extensions and runtime calls 17 | - have **full control over runtime state**, including block number, timestamp, etc. 18 | 19 | ## In detail 20 | 21 | The key concept behind DRink! is to provide a nodeless environment. 22 | To understand it fully, we need to have a high-level overview of the Substrate architecture. 23 | 24 | _Note: While here we use Substrate-specific terms, these concepts are pretty universal and apply to at least most of the blockchain designs._ 25 | 26 | ### 'Blockchain onion' 27 | 28 | 29 | 30 | Any blockchain network participant runs a single binary, which is usually called a _node_ or a _host_. 31 | It is responsible for the fundamental operations like: 32 | - communication with other nodes (networking protocols, information dissemination, gossiping, etc.) 33 | - block production and finalization (consensus, block authoring, etc.) 34 | - storage (blockchain state, database, etc.) 35 | - sometimes also transaction pool, RPC, etc. 36 | 37 | When it receives a new transaction (or a block), it has to update the blockchain state. 38 | For that, it uses a _state transition function_, called a _runtime_. 39 | This is an auxiliary binary, which serves as the core logic function, taking as an input the current state and a transaction, and returning the updated state. 40 | 41 | In case the transaction is some smart contract interaction, the runtime has to execute it within an _isolated environment_. 42 | (This is where the _contract pallet_ comes into play and spawns a dedicated sandbox.) 43 | 44 | As a result, we have a layered architecture resembling an onion (actually, there are a few layers more, but we don't need to dig that deep). 45 | 46 | ### Testing strategies 47 | 48 | Depending on the part of technology stack involved, we can derive three main testing strategies for smart contracts. 49 | 50 | 51 | 52 | 53 | Before DRink!, you could have used ink!'s native test framework to execute either unit tests (with `#[ink::test]` macro) or end-to-end tests (with `#[ink_e2e::test]` macro). 54 | DRink! enabled the third option, i.e. _quasi-end-to-end_ testing. 55 | 56 | ### quasi-E2E testing 57 | 58 | This paradigm is a peculiar compromise between the two other strategies. 59 | We give up the node layer (including networking, block production etc.), but we still have a fully functional runtime with attached storage. 60 | In other words, we keep bare blockchain state in-memory, and we can interact with it directly however we want. 61 | 62 | This way, we gain full control over the runtime, sacrificing real simulation of the blockchain environment. 63 | However, usually, this is higly beneficial for the development process, as it allows for a much faster feedback loop, assisted with better insights into execution externalities. 64 | 65 | --- 66 | 67 | # How to use DRink!? 68 | 69 | You can use DRink! in three ways: 70 | 71 | ## Directly as a library 72 | 73 | This way you gain access to full DRink! power in your test suites. 74 | Check our helpful and verbose examples in the [examples](examples) directory. 75 | 76 | `drink` library is continuously published to [crates.io](https://crates.io/crates/drink), so you can use it in your project with either `cargo add drink` or by adding the following line to your `Cargo.toml`: 77 | ```toml 78 | drink = { version = "0.19.0-alpha" } 79 | ``` 80 | 81 | Full library documentation is available at: https://docs.rs/drink. 82 | 83 | **Quick start guide** is available [here](examples/quick-start-with-drink/README.md). 84 | 85 | ## As an alternative backend to ink!'s E2E testing framework 86 | 87 | DRink! is already integrated with ink! and can be used as a drop-in replacement for the standard E2E testing environment. 88 | Just use corresponding argument in the test macro: 89 | ```rust 90 | #[ink_e2e::test(backend = "runtime_only")] 91 | ``` 92 | to your test function and you have just switched from E2E testcase to quasi-E2E one, that doesn't use any running node in the background! 93 | 94 | For a full example check out [ink! repository](https://github.com/paritytech/ink/blob/master/integration-tests/e2e-runtime-only-backend/lib.rs). 95 | 96 | ## With a command line tool 97 | 98 | We provide a CLI which puts DRink! behind friendly TUI. 99 | Below you can find a short demo of how it works. 100 | For more details, consult [its README](drink-cli/README.md). 101 | 102 | https://github.com/use-ink/drink/assets/27450471/4a45ef8a-a7ec-4a2f-84ab-0a2a36c1cb4e 103 | 104 | Similarly to `drink` library, `drink-cli` is published to [crates.io](https://crates.io/crates/drink-cli) as well. 105 | You can install it with: 106 | ```shell 107 | cargo install drink-cli 108 | ``` 109 | -------------------------------------------------------------------------------- /drink-cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "drink-cli" 3 | authors.workspace = true 4 | edition.workspace = true 5 | homepage.workspace = true 6 | license.workspace = true 7 | readme.workspace = true 8 | repository.workspace = true 9 | version.workspace = true 10 | description = "A CLI for interacting with the `drink` environment" 11 | 12 | [dependencies] 13 | anyhow = { workspace = true } 14 | clap = { workspace = true, features = ["derive"] } 15 | crossterm = { workspace = true } 16 | contract-build = { workspace = true } 17 | contract-transcode = { workspace = true } 18 | ratatui = { workspace = true, features = ["all-widgets"] } 19 | thiserror = { workspace = true } 20 | 21 | ink_sandbox = { workspace = true } 22 | drink = { workspace = true, features = ["session"] } 23 | -------------------------------------------------------------------------------- /drink-cli/README.md: -------------------------------------------------------------------------------- 1 |

DRink! CLI

2 | 3 | https://github.com/use-ink/drink/assets/27450471/4a45ef8a-a7ec-4a2f-84ab-0a2a36c1cb4e 4 | 5 | We provide this simple command line tool to help you play with your local contracts in a convenient way. 6 | 7 | # Dependencies 8 | 9 | The only requirement for running DRInk! is having Rust installed. The code was tested with version `1.70`. 10 | All other dependencies are managed by Cargo and will be installed upon running `cargo build` or `cargo run`. 11 | 12 | # Running DRInk! CLI 13 | 14 | When you run the binary (`cargo run --release`) you'll see a DRink! TUI. 15 | You can also choose to start from a specific path by supplying the `--path` argument like: 16 | ```bash 17 | cargo run --release -- --path 18 | ``` 19 | 20 | ## CLI modes 21 | 22 | In a somewhat Vim-inspired way, the `drink-cli` allows you to work in two modes: the Managing mode and the Drinking mode. 23 | 24 | ### Managing mode 25 | 26 | This is the default mode, facilitating high-level interactions with the TUI itself. 27 | At any point, you can enter it by pressing the `Esc` key. Once in the Managing mode: 28 | - Press `h` to see a list of available commands with their brief descriptions; 29 | - Press `q` to quit the TUI; 30 | - Press `i` to enter the Drinking mode. 31 | 32 | ### Drinking mode 33 | 34 | This is the mode where you can interact with your environment and type your commands inside the `User input` field. 35 | When in Managing mode, you can enter the Drinking mode by pressing 'i' on your keyboard. 36 | 37 | You have several commands at your disposal (you can list them in the Managing mode by pressing the 'h' key): 38 | - `cd` and `clear` will, just like their Bash counterparts, change the directory and clear the output, respectively. You will see the current working directory as the first entry in the `Current environment` pane; 39 | - `build` command will build a contract from the sources in the current directory; 40 | - `deploy` command will deploy a contract from the current directory. Note that if your constructor takes arguments, you will need to supply them to this command, like: `deploy true` in the case of the Flipper example; 41 | - by pressing `Tab` you can switch between all deployed contracts (with automatic directory change); 42 | - `call` command will call a contract with the given message. Again, if the message takes arguments, they need to be supplied here; 43 | - `next-block` command will advance the current block number; 44 | - `add-tokens` command will add tokens to the given account. 45 | -------------------------------------------------------------------------------- /drink-cli/src/app_state/contracts.rs: -------------------------------------------------------------------------------- 1 | use std::{path::PathBuf, sync::Arc}; 2 | 3 | use contract_transcode::ContractMessageTranscoder; 4 | use drink::pallet_revive::evm::H160; 5 | use ContractIndex::NoContracts; 6 | 7 | use crate::app_state::ContractIndex::CurrentContract; 8 | 9 | pub struct Contract { 10 | pub name: String, 11 | pub address: H160, 12 | pub base_path: PathBuf, 13 | #[allow(dead_code)] 14 | pub transcoder: Arc, 15 | } 16 | 17 | #[derive(Copy, Clone, Eq, PartialEq, Debug, Default)] 18 | pub enum ContractIndex { 19 | #[default] 20 | NoContracts, 21 | CurrentContract(usize), 22 | } 23 | 24 | #[derive(Default)] 25 | pub struct ContractRegistry { 26 | contracts: Vec, 27 | index: ContractIndex, 28 | } 29 | 30 | impl ContractRegistry { 31 | pub fn add(&mut self, contract: Contract) { 32 | self.contracts.push(contract); 33 | self.index = CurrentContract(self.contracts.len() - 1); 34 | } 35 | 36 | pub fn current_index(&self) -> ContractIndex { 37 | self.index 38 | } 39 | 40 | pub fn current_contract(&self) -> Option<&Contract> { 41 | match self.index { 42 | NoContracts => None, 43 | CurrentContract(idx) => Some(&self.contracts[idx]), 44 | } 45 | } 46 | 47 | pub fn get_all(&self) -> &[Contract] { 48 | &self.contracts 49 | } 50 | 51 | pub fn next(&mut self) -> Option<&Contract> { 52 | let CurrentContract(old_index) = self.index else { 53 | return None; 54 | }; 55 | 56 | self.index = CurrentContract((old_index + 1) % self.contracts.len()); 57 | self.current_contract() 58 | } 59 | 60 | pub fn count(&self) -> usize { 61 | self.contracts.len() 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /drink-cli/src/app_state/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{env, path::PathBuf}; 2 | 3 | pub use contracts::{Contract, ContractIndex, ContractRegistry}; 4 | use drink::{minimal::MinimalSandbox, session::Session, AccountId32, Sandbox, Weight}; 5 | pub use user_input::UserInput; 6 | 7 | use crate::app_state::output::Output; 8 | 9 | mod contracts; 10 | mod output; 11 | pub mod print; 12 | mod user_input; 13 | 14 | #[derive(Clone, Eq, PartialEq, Debug)] 15 | pub struct ChainInfo { 16 | pub block_height: u32, 17 | pub actor: AccountId32, 18 | pub gas_limit: Weight, 19 | } 20 | 21 | impl Default for ChainInfo { 22 | fn default() -> Self { 23 | Self { 24 | block_height: 0, 25 | actor: MinimalSandbox::default_actor(), 26 | gas_limit: MinimalSandbox::default_gas_limit(), 27 | } 28 | } 29 | } 30 | 31 | #[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, Default)] 32 | pub enum Mode { 33 | #[default] 34 | Managing, 35 | Drinking, 36 | } 37 | 38 | #[derive(Clone, Eq, PartialEq, Debug)] 39 | pub struct UiState { 40 | pub cwd: PathBuf, 41 | pub mode: Mode, 42 | 43 | pub user_input: UserInput, 44 | pub output: Output, 45 | 46 | pub show_help: bool, 47 | } 48 | 49 | impl UiState { 50 | pub fn new(cwd_override: Option) -> Self { 51 | let cwd = cwd_override 52 | .unwrap_or_else(|| env::current_dir().expect("Failed to get current directory")); 53 | 54 | UiState { 55 | cwd, 56 | mode: Default::default(), 57 | user_input: Default::default(), 58 | output: Default::default(), 59 | show_help: false, 60 | } 61 | } 62 | } 63 | 64 | impl Default for UiState { 65 | fn default() -> Self { 66 | UiState::new(None) 67 | } 68 | } 69 | 70 | pub struct AppState { 71 | pub session: Session, 72 | pub chain_info: ChainInfo, 73 | pub ui_state: UiState, 74 | pub contracts: ContractRegistry, 75 | } 76 | 77 | impl AppState { 78 | pub fn new(cwd_override: Option) -> Self { 79 | AppState { 80 | session: Session::default(), 81 | chain_info: Default::default(), 82 | ui_state: UiState::new(cwd_override), 83 | contracts: Default::default(), 84 | } 85 | } 86 | } 87 | 88 | impl Default for AppState { 89 | fn default() -> Self { 90 | Self::new(None) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /drink-cli/src/app_state/output.rs: -------------------------------------------------------------------------------- 1 | use ratatui::text::Line; 2 | 3 | #[derive(Clone, Eq, PartialEq, Debug, Default)] 4 | pub struct Output { 5 | content: Vec>, 6 | offset: u16, 7 | scrolling: bool, 8 | window_height: u16, 9 | } 10 | 11 | impl Output { 12 | pub fn content(&self) -> &[Line<'static>] { 13 | &self.content 14 | } 15 | 16 | pub fn push(&mut self, line: Line<'static>) { 17 | self.content.push(line) 18 | } 19 | 20 | pub fn clear(&mut self) { 21 | *self = Default::default(); 22 | } 23 | 24 | pub fn offset(&self) -> u16 { 25 | self.offset 26 | } 27 | 28 | fn max_offset(&self) -> u16 { 29 | (self.content.len() as u16).saturating_sub(self.window_height) 30 | } 31 | 32 | pub fn note_display_height(&mut self, height: u16) { 33 | self.window_height = height; 34 | if !self.scrolling { 35 | self.offset = self.max_offset(); 36 | } 37 | } 38 | 39 | pub fn reset_scrolling(&mut self) { 40 | self.scrolling = false; 41 | } 42 | 43 | pub fn scroll_down(&mut self) { 44 | if self.offset < self.max_offset() { 45 | self.scrolling = true; 46 | self.offset += 1 47 | } 48 | } 49 | 50 | pub fn scroll_up(&mut self) { 51 | if self.offset > 0 { 52 | self.scrolling = true; 53 | self.offset -= 1; 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /drink-cli/src/app_state/print.rs: -------------------------------------------------------------------------------- 1 | use drink::pallet_revive::ContractResult; 2 | use ratatui::{ 3 | style::{Color, Modifier, Style}, 4 | text::Span, 5 | }; 6 | 7 | use crate::app_state::AppState; 8 | 9 | impl AppState { 10 | pub fn print_command(&mut self, command: &str) { 11 | self.ui_state.output.push("".into()); 12 | self.ui_state.output.push( 13 | Span::styled( 14 | format!("Executing `{command}`"), 15 | Style::default() 16 | .fg(Color::Blue) 17 | .add_modifier(Modifier::BOLD) 18 | .add_modifier(Modifier::ITALIC), 19 | ) 20 | .into(), 21 | ); 22 | } 23 | 24 | pub fn print(&mut self, msg: &str) { 25 | self.print_sequence( 26 | msg.split('\n'), 27 | Style::default() 28 | .fg(Color::White) 29 | .add_modifier(Modifier::BOLD), 30 | ); 31 | } 32 | 33 | pub fn print_error(&mut self, err: &str) { 34 | self.print_sequence( 35 | err.split('\n'), 36 | Style::default().fg(Color::Red).add_modifier(Modifier::BOLD), 37 | ); 38 | } 39 | 40 | fn print_sequence<'a, I: Iterator>(&mut self, seq: I, style: Style) { 41 | for line in seq { 42 | self.ui_state 43 | .output 44 | .push(Span::styled(line.to_string(), style).into()); 45 | } 46 | } 47 | } 48 | 49 | pub fn format_contract_action(result: &ContractResult) -> String { 50 | format!( 51 | "Gas consumed: {:?}\nGas required: {:?}\n", 52 | result.gas_consumed, result.gas_required 53 | ) 54 | } 55 | -------------------------------------------------------------------------------- /drink-cli/src/app_state/user_input.rs: -------------------------------------------------------------------------------- 1 | #[derive(Copy, Clone, Eq, PartialEq, Debug, Default)] 2 | enum Position { 3 | #[default] 4 | Fresh, 5 | History(usize), 6 | } 7 | 8 | #[derive(Clone, Eq, PartialEq, Debug, Default)] 9 | pub struct UserInput { 10 | history: Vec, 11 | position: Position, 12 | current_input: String, 13 | } 14 | 15 | impl UserInput { 16 | pub fn push(&mut self, c: char) { 17 | self.current_input.push(c); 18 | } 19 | 20 | pub fn pop(&mut self) { 21 | self.current_input.pop(); 22 | } 23 | 24 | pub fn set(&mut self, s: String) { 25 | self.current_input = s; 26 | self.position = Position::Fresh; 27 | } 28 | 29 | pub fn prev_input(&mut self) { 30 | match self.position { 31 | Position::Fresh if self.history.is_empty() => {} 32 | Position::Fresh => { 33 | self.position = Position::History(self.history.len() - 1); 34 | self.current_input = self.history[self.history.len() - 1].clone(); 35 | } 36 | Position::History(0) => {} 37 | Position::History(n) => { 38 | self.position = Position::History(n - 1); 39 | self.current_input = self.history[n - 1].clone(); 40 | } 41 | } 42 | } 43 | 44 | pub fn next_input(&mut self) { 45 | match self.position { 46 | Position::Fresh => {} 47 | Position::History(n) if n == self.history.len() - 1 => { 48 | self.position = Position::Fresh; 49 | self.current_input.clear(); 50 | } 51 | Position::History(n) => { 52 | self.position = Position::History(n + 1); 53 | self.current_input = self.history[n + 1].clone(); 54 | } 55 | } 56 | } 57 | 58 | pub fn apply(&mut self) { 59 | if !self.current_input.is_empty() 60 | && self.history.last().cloned().unwrap_or_default() != self.current_input 61 | { 62 | self.history.push(self.current_input.clone()); 63 | } 64 | self.current_input.clear(); 65 | self.position = Position::Fresh; 66 | } 67 | 68 | pub fn current_input(&self) -> &str { 69 | &self.current_input 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /drink-cli/src/cli.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use drink::{AccountId32, Ss58Codec}; 3 | 4 | #[derive(Parser)] 5 | pub enum CliCommand { 6 | #[clap(alias = "c")] 7 | Clear, 8 | #[clap(alias = "cd")] 9 | ChangeDir { 10 | path: String, 11 | }, 12 | 13 | #[clap(alias = "nb")] 14 | NextBlock { 15 | #[clap(default_value = "1")] 16 | count: u32, 17 | }, 18 | AddTokens { 19 | #[clap(value_parser = AccountId32::from_ss58check)] 20 | recipient: AccountId32, 21 | value: u128, 22 | }, 23 | SetActor { 24 | #[clap(value_parser = AccountId32::from_ss58check)] 25 | actor: AccountId32, 26 | }, 27 | SetGasLimit { 28 | ref_time: u64, 29 | proof_size: u64, 30 | }, 31 | 32 | #[clap(alias = "b")] 33 | Build, 34 | #[clap(alias = "d")] 35 | Deploy { 36 | #[clap(long, default_value = "new")] 37 | constructor: String, 38 | args: Vec, 39 | #[clap(long, default_values_t = Vec::::new(), value_delimiter = ',')] 40 | salt: Vec, 41 | }, 42 | Call { 43 | message: String, 44 | args: Vec, 45 | }, 46 | } 47 | 48 | #[cfg(test)] 49 | mod tests { 50 | use super::*; 51 | 52 | #[test] 53 | fn verify_cli() { 54 | use clap::CommandFactory; 55 | CliCommand::command().debug_assert() 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /drink-cli/src/executor/contract.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fs, 3 | path::{Path, PathBuf}, 4 | sync::Arc, 5 | }; 6 | 7 | use contract_build::{BuildMode, ExecuteArgs, ManifestPath, Verbosity}; 8 | use contract_transcode::ContractMessageTranscoder; 9 | 10 | use crate::{ 11 | app_state::{print::format_contract_action, AppState, Contract}, 12 | executor::error::BuildError, 13 | }; 14 | 15 | fn build_result(app_state: &mut AppState) -> Result { 16 | let path_to_cargo_toml = app_state.ui_state.cwd.join(Path::new("Cargo.toml")); 17 | let manifest_path = ManifestPath::new(path_to_cargo_toml.clone()).map_err(|err| { 18 | BuildError::InvalidManifest { 19 | manifest_path: path_to_cargo_toml, 20 | err, 21 | } 22 | })?; 23 | 24 | let args = ExecuteArgs { 25 | manifest_path, 26 | build_mode: BuildMode::Release, 27 | verbosity: Verbosity::Quiet, 28 | ..Default::default() 29 | }; 30 | 31 | contract_build::execute(args) 32 | .map_err(|err| BuildError::BuildFailed { err })? 33 | .dest_binary 34 | .ok_or(BuildError::WasmNotGenerated)? 35 | .canonicalize() 36 | .map_err(|err| BuildError::InvalidDestPath { err }) 37 | .map(|pb| pb.to_string_lossy().to_string()) 38 | } 39 | 40 | /// Build the contract in the current directory. 41 | pub fn build(app_state: &mut AppState) { 42 | match build_result(app_state) { 43 | Ok(res) => app_state.print(&format!("Contract built successfully {res}")), 44 | Err(msg) => app_state.print_error(&format!("{msg}")), 45 | } 46 | } 47 | 48 | pub fn deploy( 49 | app_state: &mut AppState, 50 | constructor: String, 51 | args: Vec, 52 | salt: Option<[u8; 32]>, 53 | ) { 54 | // Get raw contract bytes 55 | let Some((contract_name, contract_file)) = find_contract_blob(&app_state.ui_state.cwd) else { 56 | app_state.print_error("Failed to find contract file"); 57 | return; 58 | }; 59 | 60 | let contract_bytes = match fs::read(contract_file) { 61 | Ok(bytes) => bytes, 62 | Err(err) => { 63 | app_state.print_error(&format!("Failed to read contract bytes\n{err}")); 64 | return; 65 | } 66 | }; 67 | 68 | // Read contract metadata and prepare transcoder 69 | let metadata_path = app_state 70 | .ui_state 71 | .cwd 72 | .join(format!("target/ink/{contract_name}.json")); 73 | 74 | let Ok(transcoder) = ContractMessageTranscoder::load(metadata_path) else { 75 | app_state.print_error("Failed to create transcoder from metadata file."); 76 | return; 77 | }; 78 | let transcoder = Arc::new(transcoder); 79 | 80 | match app_state.session.deploy( 81 | contract_bytes, 82 | &constructor, 83 | args.as_slice(), 84 | salt, 85 | None, 86 | &transcoder, 87 | ) { 88 | Ok(address) => { 89 | app_state.contracts.add(Contract { 90 | name: contract_name, 91 | address, 92 | base_path: app_state.ui_state.cwd.clone(), 93 | transcoder, 94 | }); 95 | app_state.print("Contract deployed successfully"); 96 | } 97 | Err(err) => app_state.print_error(&format!("Failed to deploy contract\n{err}")), 98 | } 99 | 100 | if let Some(info) = app_state.session.record().deploy_results().last() { 101 | app_state.print(&format_contract_action(info)); 102 | } 103 | } 104 | 105 | pub fn call(app_state: &mut AppState, message: String, args: Vec) { 106 | let Some(contract) = app_state.contracts.current_contract() else { 107 | app_state.print_error("No deployed contract"); 108 | return; 109 | }; 110 | 111 | let address = contract.address; 112 | match app_state 113 | .session 114 | .call_with_address::<_, ()>(address, &message, &args, None) 115 | { 116 | Ok(result) => app_state.print(&format!("Result: {:?}", result)), 117 | Err(err) => app_state.print_error(&format!("Failed to call contract\n{err}")), 118 | }; 119 | 120 | if let Some(info) = app_state.session.record().call_results().last() { 121 | app_state.print(&format_contract_action(info)) 122 | } 123 | } 124 | 125 | fn find_contract_blob(cwd: &Path) -> Option<(String, PathBuf)> { 126 | let Ok(entries) = fs::read_dir(cwd.join("target/ink")) else { 127 | return None; 128 | }; 129 | let file = entries 130 | .into_iter() 131 | .filter_map(|e| e.ok()) 132 | .find(|e| e.path().extension().unwrap_or_default() == "polkavm")?; 133 | 134 | let raw_name = file 135 | .file_name() 136 | .into_string() 137 | .expect("Invalid file name") 138 | .strip_suffix(".polkavm") 139 | .expect("We have just checked file extension") 140 | .to_string(); 141 | 142 | Some((raw_name, file.path())) 143 | } 144 | -------------------------------------------------------------------------------- /drink-cli/src/executor/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | #[derive(Error, Debug)] 4 | pub(crate) enum BuildError { 5 | #[error("Invalid manifest path {manifest_path}: {err}")] 6 | InvalidManifest { 7 | manifest_path: std::path::PathBuf, 8 | err: anyhow::Error, 9 | }, 10 | #[error("Contract build failed: {err}")] 11 | BuildFailed { err: anyhow::Error }, 12 | #[error("Wasm code artifact not generated")] 13 | WasmNotGenerated, 14 | #[error("Invalid destination bundle path: {err}")] 15 | InvalidDestPath { err: std::io::Error }, 16 | } 17 | -------------------------------------------------------------------------------- /drink-cli/src/executor/mod.rs: -------------------------------------------------------------------------------- 1 | mod contract; 2 | mod error; 3 | 4 | use std::env; 5 | 6 | use anyhow::Result; 7 | use clap::Parser; 8 | use drink::{sandbox_api::prelude::*, AccountId32, Weight}; 9 | 10 | use crate::{app_state::AppState, cli::CliCommand}; 11 | 12 | fn vec_u8_to_array_32(bytes: Vec) -> [u8; 32] { 13 | let mut array = [0u8; 32]; 14 | let len = bytes.len().min(32); 15 | array[..len].copy_from_slice(&bytes[..len]); 16 | array 17 | } 18 | 19 | pub fn execute(app_state: &mut AppState) -> Result<()> { 20 | let command = app_state.ui_state.user_input.current_input().to_string(); 21 | app_state.print_command(&command); 22 | 23 | let command = command 24 | .split_ascii_whitespace() 25 | .map(|a| a.trim()) 26 | .collect::>(); 27 | let cli_command = match CliCommand::try_parse_from([vec![""], command].concat()) { 28 | Ok(cli_command) => cli_command, 29 | Err(_) => { 30 | app_state.print_error("Invalid command"); 31 | return Ok(()); 32 | } 33 | }; 34 | 35 | match cli_command { 36 | CliCommand::Clear => app_state.ui_state.output.clear(), 37 | CliCommand::ChangeDir { path } => { 38 | let target_dir = app_state.ui_state.cwd.join(path); 39 | match env::set_current_dir(target_dir) { 40 | Ok(_) => { 41 | app_state.ui_state.cwd = 42 | env::current_dir().expect("Failed to get current directory"); 43 | app_state.print("Directory changed"); 44 | } 45 | Err(err) => app_state.print_error(&err.to_string()), 46 | } 47 | } 48 | 49 | CliCommand::NextBlock { count } => build_blocks(app_state, count), 50 | CliCommand::AddTokens { recipient, value } => add_tokens(app_state, recipient, value)?, 51 | CliCommand::SetActor { actor } => { 52 | app_state.chain_info.actor = actor; 53 | app_state.print("Actor was set"); 54 | } 55 | CliCommand::SetGasLimit { 56 | ref_time, 57 | proof_size, 58 | } => { 59 | app_state.chain_info.gas_limit = Weight::from_parts(ref_time, proof_size); 60 | app_state.print("Gas limit was set"); 61 | } 62 | CliCommand::Build => contract::build(app_state), 63 | CliCommand::Deploy { 64 | constructor, 65 | args, 66 | salt, 67 | } => { 68 | contract::deploy(app_state, constructor, args, Some(vec_u8_to_array_32(salt))); 69 | } 70 | CliCommand::Call { message, args } => contract::call(app_state, message, args), 71 | } 72 | 73 | Ok(()) 74 | } 75 | 76 | fn build_blocks(app_state: &mut AppState, count: u32) { 77 | app_state.chain_info.block_height = app_state.session.sandbox().build_blocks(count); 78 | app_state.print(&format!("{count} blocks built")); 79 | } 80 | 81 | fn add_tokens(app_state: &mut AppState, recipient: AccountId32, value: u128) -> Result<()> { 82 | app_state 83 | .session 84 | .sandbox() 85 | .mint_into(&recipient, value) 86 | .map_err(|err| anyhow::format_err!("Failed to add token: {err:?}"))?; 87 | app_state.print(&format!("{value} tokens added to {recipient}",)); 88 | Ok(()) 89 | } 90 | -------------------------------------------------------------------------------- /drink-cli/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use anyhow::Result; 4 | use clap::Parser; 5 | 6 | use crate::ui::run_ui; 7 | 8 | mod app_state; 9 | mod cli; 10 | mod executor; 11 | mod ui; 12 | 13 | #[derive(Parser, Debug)] 14 | #[command(author, version, about, long_about = None)] 15 | struct Args { 16 | /// Starts the CLI in the provided directory 17 | #[arg(short, long, value_name = "DIRECTORY")] 18 | path: Option, 19 | } 20 | 21 | fn main() -> Result<()> { 22 | let args = Args::parse(); 23 | run_ui(args.path) 24 | } 25 | -------------------------------------------------------------------------------- /drink-cli/src/ui/contracts.rs: -------------------------------------------------------------------------------- 1 | use ratatui::{ 2 | style::{Color, Style}, 3 | text::{Line, Span}, 4 | widgets::{List, ListItem, Widget}, 5 | }; 6 | 7 | use crate::{ 8 | app_state::{AppState, ContractIndex}, 9 | ui::layout::section, 10 | }; 11 | 12 | pub(super) fn build(app_state: &mut AppState) -> impl Widget { 13 | let items = app_state 14 | .contracts 15 | .get_all() 16 | .iter() 17 | .enumerate() 18 | .map(|(idx, contract)| { 19 | let style = match app_state.contracts.current_index() { 20 | ContractIndex::CurrentContract(cc) if cc == idx => { 21 | Style::default().bg(Color::White).fg(Color::Black) 22 | } 23 | _ => Style::default(), 24 | }; 25 | 26 | let address = format!("{:?}", contract.address); 27 | ListItem::new(Line::from(Span::styled( 28 | format!("{} / {}", contract.name, &address[..8],), 29 | style, 30 | ))) 31 | }) 32 | .collect::>(); 33 | 34 | List::new(items).block(section("Deployed contracts")) 35 | } 36 | -------------------------------------------------------------------------------- /drink-cli/src/ui/current_env.rs: -------------------------------------------------------------------------------- 1 | use ratatui::{ 2 | layout::Alignment, 3 | widgets::{Paragraph, Widget, Wrap}, 4 | }; 5 | 6 | use crate::{app_state::AppState, ui::layout::section}; 7 | 8 | pub(super) fn build(app_state: &mut AppState) -> impl Widget { 9 | let current_contract_info = match app_state.contracts.current_contract() { 10 | Some(contract) => format!("name: {} | address: {}", contract.name, contract.address), 11 | None => "No deployed contract".to_string(), 12 | }; 13 | 14 | Paragraph::new(format!( 15 | r#"Current working directory: {} 16 | Block height: {} 17 | Deployed contracts: {} 18 | Current actor: {} 19 | Current contract: {{ {} }}"#, 20 | app_state.ui_state.cwd.to_str().unwrap(), 21 | app_state.chain_info.block_height, 22 | app_state.contracts.count(), 23 | app_state.chain_info.actor, 24 | current_contract_info 25 | )) 26 | .alignment(Alignment::Left) 27 | .wrap(Wrap { trim: false }) 28 | .block(section("Current environment")) 29 | } 30 | -------------------------------------------------------------------------------- /drink-cli/src/ui/footer.rs: -------------------------------------------------------------------------------- 1 | use ratatui::{ 2 | layout::Alignment, 3 | style::{Color, Style}, 4 | text::{Line, Span}, 5 | widgets::{Paragraph, Widget}, 6 | }; 7 | 8 | use crate::{ 9 | app_state::{AppState, Mode}, 10 | ui::layout::section, 11 | }; 12 | 13 | pub(super) fn build(app_state: &AppState) -> impl Widget { 14 | let instruction: Line = match app_state.ui_state.mode { 15 | Mode::Managing => alternate_help([ 16 | "Use arrows to scroll through output. Press ", 17 | "'q'", 18 | " to quit. Press ", 19 | "'h'", 20 | " to see help. Press ", 21 | "'i'", 22 | " to enter editing mode.", 23 | ]), 24 | Mode::Drinking => alternate_help([ 25 | "Press ", 26 | "'Esc'", 27 | " to quit editing mode. Use ", 28 | "'Tab'", 29 | " to switch between deployed contracts.", 30 | ]), 31 | }; 32 | 33 | Paragraph::new(vec![instruction]) 34 | .alignment(Alignment::Center) 35 | .block(section("Help")) 36 | } 37 | 38 | fn alternate_help>(items: I) -> Line<'static> { 39 | items 40 | .into_iter() 41 | .enumerate() 42 | .map(|(idx, item)| match idx % 2 { 43 | 0 => Span::raw(item), 44 | _ => Span::styled(item, Style::default().fg(Color::Yellow)), 45 | }) 46 | .collect::>() 47 | .into() 48 | } 49 | -------------------------------------------------------------------------------- /drink-cli/src/ui/help.rs: -------------------------------------------------------------------------------- 1 | use ratatui::{ 2 | style::{Color, Style}, 3 | text::{Line, Span}, 4 | widgets::{Paragraph, Widget}, 5 | }; 6 | 7 | use crate::{app_state::AppState, ui::layout::section}; 8 | 9 | pub(super) fn build(_app_state: &AppState) -> impl Widget { 10 | Paragraph::new(vec![ 11 | command("cd ", "change directory do "), 12 | command("clear / c", "clear output tab"), 13 | command( 14 | "build / b", 15 | "build contract from the sources in the current directory", 16 | ), 17 | command( 18 | "deploy / d [--constructor ] [--salt ]", 19 | "deploy contract using (`new` by default) and (empty by default)", 20 | ), 21 | command("call ", "call contract's message"), 22 | command( 23 | "next-block / nb [count]", 24 | "build next blocks (by default a single block)", 25 | ), 26 | command( 27 | "add-tokens ", 28 | "add tokens to ", 29 | ), 30 | command( 31 | "set-actor ", 32 | "set as the current actor (transaction sender)", 33 | ), 34 | command( 35 | "set-gas-limit ", 36 | "set gas limits to and ", 37 | ), 38 | ]) 39 | .block(section("Help")) 40 | } 41 | 42 | fn command(command: &'static str, description: &'static str) -> Line<'static> { 43 | Line::from(vec![ 44 | Span::styled(command, Style::default().fg(Color::Green)), 45 | format!(": {description}").into(), 46 | ]) 47 | } 48 | -------------------------------------------------------------------------------- /drink-cli/src/ui/layout.rs: -------------------------------------------------------------------------------- 1 | use ratatui::{ 2 | backend::Backend, 3 | layout::{Constraint, Direction, Layout, Margin}, 4 | widgets::{Block, BorderType, Borders, Padding}, 5 | Frame, 6 | }; 7 | 8 | use crate::{ 9 | app_state::AppState, 10 | ui::{contracts, current_env, footer, help, output, user_input}, 11 | }; 12 | 13 | pub(super) fn section(title: &str) -> Block { 14 | Block::default() 15 | .title(title) 16 | .borders(Borders::ALL) 17 | .border_type(BorderType::Rounded) 18 | .padding(Padding::horizontal(1)) 19 | } 20 | 21 | pub(super) fn layout(f: &mut Frame, app_state: &mut AppState) { 22 | let chunks = Layout::default() 23 | .direction(Direction::Vertical) 24 | .constraints([ 25 | // current env 26 | Constraint::Ratio(4, 20), 27 | // output / help 28 | Constraint::Ratio(12, 20), 29 | // user input 30 | Constraint::Length(3), 31 | // footer 32 | Constraint::Ratio(2, 20), 33 | ]) 34 | .split(f.size()); 35 | 36 | let subchunks = Layout::default() 37 | .direction(Direction::Horizontal) 38 | .constraints([Constraint::Percentage(70), Constraint::Percentage(30)]) 39 | .split(chunks[0].inner(&Margin { 40 | horizontal: 0, 41 | vertical: 0, 42 | })); 43 | f.render_widget(current_env::build(app_state), subchunks[0]); 44 | f.render_widget(contracts::build(app_state), subchunks[1]); 45 | 46 | if app_state.ui_state.show_help { 47 | f.render_widget(help::build(app_state), chunks[1]); 48 | } else { 49 | app_state 50 | .ui_state 51 | .output 52 | .note_display_height(chunks[1].height - 2); 53 | f.render_widget(output::build(app_state), chunks[1]); 54 | } 55 | 56 | f.render_widget(user_input::build(app_state), chunks[2]); 57 | f.render_widget(footer::build(app_state), chunks[3]); 58 | } 59 | -------------------------------------------------------------------------------- /drink-cli/src/ui/mod.rs: -------------------------------------------------------------------------------- 1 | mod contracts; 2 | mod current_env; 3 | mod footer; 4 | mod help; 5 | mod layout; 6 | mod output; 7 | mod user_input; 8 | 9 | use std::{io, io::Stdout, path::PathBuf}; 10 | 11 | use anyhow::{anyhow, Result}; 12 | use crossterm::{ 13 | event, 14 | event::{DisableMouseCapture, EnableMouseCapture, Event, KeyCode}, 15 | execute, 16 | terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, 17 | }; 18 | use layout::layout; 19 | use ratatui::backend::CrosstermBackend; 20 | 21 | use crate::{ 22 | app_state::{ 23 | AppState, 24 | Mode::{Drinking, Managing}, 25 | }, 26 | executor::execute, 27 | }; 28 | 29 | type Terminal = ratatui::Terminal>; 30 | 31 | pub fn run_ui(cwd: Option) -> Result<()> { 32 | let mut terminal = setup_dedicated_terminal()?; 33 | let app_result = run_ui_app(&mut terminal, cwd); 34 | restore_original_terminal(terminal)?; 35 | app_result 36 | } 37 | 38 | fn setup_dedicated_terminal() -> Result { 39 | enable_raw_mode()?; 40 | let mut stdout = io::stdout(); 41 | execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; 42 | let backend = CrosstermBackend::new(stdout); 43 | Terminal::new(backend).map_err(|e| anyhow!(e)) 44 | } 45 | 46 | fn restore_original_terminal(mut terminal: Terminal) -> Result<()> { 47 | disable_raw_mode()?; 48 | execute!( 49 | terminal.backend_mut(), 50 | LeaveAlternateScreen, 51 | DisableMouseCapture 52 | )?; 53 | terminal.show_cursor().map_err(|e| anyhow!(e)) 54 | } 55 | 56 | fn run_ui_app(terminal: &mut Terminal, cwd_override: Option) -> Result<()> { 57 | let mut app_state = AppState::new(cwd_override); 58 | 59 | loop { 60 | terminal.draw(|f| layout(f, &mut app_state))?; 61 | 62 | let mode = &mut app_state.ui_state.mode; 63 | if let Event::Key(key) = event::read()? { 64 | match (*mode, key.code) { 65 | (_, KeyCode::Esc) => *mode = Managing, 66 | 67 | (Managing, KeyCode::Char('q')) => break, 68 | (Managing, KeyCode::Char('i')) => { 69 | *mode = Drinking; 70 | app_state.ui_state.show_help = false; 71 | } 72 | (Managing, KeyCode::Char('h')) => { 73 | app_state.ui_state.show_help = !app_state.ui_state.show_help 74 | } 75 | (Managing, KeyCode::Down) => app_state.ui_state.output.scroll_down(), 76 | (Managing, KeyCode::Up) => app_state.ui_state.output.scroll_up(), 77 | 78 | (Drinking, KeyCode::Char(c)) => app_state.ui_state.user_input.push(c), 79 | (Drinking, KeyCode::Backspace) => { 80 | app_state.ui_state.user_input.pop(); 81 | } 82 | (Drinking, KeyCode::Tab) => { 83 | let prev_path = match app_state.contracts.current_contract() { 84 | Some(c) => c.base_path.clone(), 85 | None => continue, 86 | }; 87 | 88 | let new_path = &app_state 89 | .contracts 90 | .next() 91 | .expect("There is at least one contract - just checked") 92 | .base_path; 93 | 94 | if *new_path != prev_path { 95 | let base_path = new_path.to_str().unwrap(); 96 | app_state.ui_state.user_input.set(format!("cd {base_path}")); 97 | execute(&mut app_state)?; 98 | app_state.ui_state.user_input.set(String::new()); 99 | } 100 | } 101 | (Drinking, KeyCode::Up) => app_state.ui_state.user_input.prev_input(), 102 | (Drinking, KeyCode::Down) => app_state.ui_state.user_input.next_input(), 103 | (Drinking, KeyCode::Enter) => { 104 | execute(&mut app_state)?; 105 | app_state.ui_state.user_input.apply(); 106 | app_state.ui_state.output.reset_scrolling(); 107 | } 108 | 109 | _ => {} 110 | } 111 | } 112 | } 113 | Ok(()) 114 | } 115 | -------------------------------------------------------------------------------- /drink-cli/src/ui/output.rs: -------------------------------------------------------------------------------- 1 | use ratatui::widgets::{Paragraph, Widget}; 2 | 3 | use crate::{app_state::AppState, ui::layout::section}; 4 | 5 | pub(super) fn build(app_state: &AppState) -> impl Widget { 6 | Paragraph::new(app_state.ui_state.output.content().to_vec()) 7 | .block(section("Output")) 8 | .scroll((app_state.ui_state.output.offset(), 0)) 9 | } 10 | -------------------------------------------------------------------------------- /drink-cli/src/ui/user_input.rs: -------------------------------------------------------------------------------- 1 | use ratatui::{ 2 | style::{Color, Style}, 3 | widgets::{Paragraph, Widget}, 4 | }; 5 | 6 | use crate::{ 7 | app_state::{AppState, Mode}, 8 | ui::layout::section, 9 | }; 10 | 11 | pub(super) fn build(app_state: &mut AppState) -> impl Widget { 12 | let mut style = Style::default(); 13 | if app_state.ui_state.mode != Mode::Drinking { 14 | style = style.fg(Color::DarkGray); 15 | } 16 | 17 | let block = section("User input").style(style); 18 | Paragraph::new(app_state.ui_state.user_input.current_input().to_string()).block(block) 19 | } 20 | -------------------------------------------------------------------------------- /drink/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "drink" 3 | authors.workspace = true 4 | edition.workspace = true 5 | homepage.workspace = true 6 | license.workspace = true 7 | readme.workspace = true 8 | repository.workspace = true 9 | version.workspace = true 10 | description = "Minimal sufficient architecture that allows for a fully functional ink! contract development" 11 | 12 | [dependencies] 13 | contract-metadata = { workspace = true, optional = true } 14 | contract-transcode = { workspace = true, optional = true } 15 | frame-support = { workspace = true } 16 | frame-system = { workspace = true } 17 | parity-scale-codec = { workspace = true } 18 | parity-scale-codec-derive = { workspace = true } 19 | sp-runtime-interface = { workspace = true } 20 | ink_sandbox = { workspace = true } 21 | ink_primitives = { workspace = true } 22 | 23 | scale-info = { workspace = true } 24 | serde_json = { workspace = true, optional = true } 25 | thiserror = { workspace = true } 26 | wat = { workspace = true } 27 | 28 | drink-test-macro = { workspace = true } 29 | 30 | [features] 31 | default = [ 32 | # This is required for the runtime-interface to work properly in the std env. 33 | "std", 34 | "session", 35 | "macros", 36 | ] 37 | session = ["contract-metadata", "contract-transcode", "serde_json"] 38 | macros = ["contract-metadata", "contract-transcode", "serde_json"] 39 | std = [] 40 | -------------------------------------------------------------------------------- /drink/src/errors.rs: -------------------------------------------------------------------------------- 1 | //! Module gathering common error and result types. 2 | 3 | use thiserror::Error; 4 | 5 | /// Main error type for the drink crate. 6 | #[derive(Clone, Error, Debug)] 7 | pub enum Error { 8 | /// Externalities could not be initialized. 9 | #[error("Failed to build storage: {0}")] 10 | StorageBuilding(String), 11 | /// Bundle loading and parsing has failed 12 | #[error("Loading the contract bundle has failed: {0}")] 13 | BundleLoadFailed(String), 14 | } 15 | 16 | /// Every contract message wraps its return value in `Result`. This is the error 17 | /// type. 18 | /// 19 | /// Copied from ink primitives. 20 | #[non_exhaustive] 21 | #[repr(u32)] 22 | #[derive( 23 | Debug, 24 | Copy, 25 | Clone, 26 | PartialEq, 27 | Eq, 28 | parity_scale_codec::Encode, 29 | parity_scale_codec::Decode, 30 | scale_info::TypeInfo, 31 | Error, 32 | )] 33 | pub enum LangError { 34 | /// Failed to read execution input for the dispatchable. 35 | #[error("Failed to read execution input for the dispatchable.")] 36 | CouldNotReadInput = 1u32, 37 | } 38 | 39 | /// The `Result` type for ink! messages. 40 | pub type MessageResult = Result; 41 | -------------------------------------------------------------------------------- /drink/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! The drink crate provides a sandboxed runtime for testing smart contracts without a need for 2 | //! a running node. 3 | 4 | #![warn(missing_docs)] 5 | 6 | pub mod errors; 7 | #[cfg(feature = "session")] 8 | pub mod session; 9 | 10 | #[cfg(feature = "macros")] 11 | pub use drink_test_macro::{contract_bundle_provider, test}; 12 | pub use errors::Error; 13 | pub use frame_support; 14 | pub use ink_sandbox::{ 15 | api as sandbox_api, create_sandbox, pallet_balances, pallet_revive, pallet_timestamp, 16 | sp_externalities, AccountId32, DispatchError, Sandbox, Ss58Codec, Weight, 17 | }; 18 | #[cfg(feature = "session")] 19 | pub use session::mock::{mock_message, ContractMock, MessageMock, MockedCallResult, Selector}; 20 | 21 | /// Main result type for the drink crate. 22 | pub type DrinkResult = std::result::Result; 23 | 24 | /// Minimal Sandbox runtime used for testing contracts with drink!. 25 | #[allow(missing_docs)] 26 | pub mod minimal { 27 | use ink_sandbox::create_sandbox; 28 | 29 | create_sandbox!(MinimalSandbox); 30 | } 31 | -------------------------------------------------------------------------------- /drink/src/session.rs: -------------------------------------------------------------------------------- 1 | //! This module provides a context-aware interface for interacting with contracts. 2 | 3 | use std::{ 4 | fmt::Debug, 5 | mem, 6 | sync::{Arc, Mutex}, 7 | }; 8 | 9 | pub use contract_transcode; 10 | use contract_transcode::ContractMessageTranscoder; 11 | use error::SessionError; 12 | use frame_support::{sp_runtime::traits::Bounded, traits::fungible::Inspect, weights::Weight}; 13 | use ink_primitives::DepositLimit; 14 | use ink_sandbox::{ 15 | api::prelude::*, 16 | pallet_revive::{ 17 | evm::{H160, U256}, 18 | MomentOf, 19 | }, 20 | AccountIdFor, ContractExecResultFor, ContractResultInstantiate, Sandbox, H256, 21 | }; 22 | use parity_scale_codec::Decode; 23 | pub use record::{EventBatch, Record}; 24 | 25 | use crate::{minimal::MinimalSandboxRuntime, pallet_revive::Config, session::mock::MockRegistry}; 26 | 27 | pub mod bundle; 28 | pub mod error; 29 | pub mod mock; 30 | pub mod mocking_api; 31 | mod record; 32 | mod transcoding; 33 | 34 | pub use bundle::ContractBundle; 35 | 36 | use self::mocking_api::MockingApi; 37 | use crate::{ 38 | errors::MessageResult, 39 | // minimal::MinimalSandboxRuntime, 40 | session::transcoding::TranscoderRegistry, 41 | }; 42 | 43 | type BalanceOf = <::Currency as Inspect>>::Balance; 44 | type RuntimeOrigin = ::RuntimeOrigin; 45 | 46 | /// Convenient value for an empty sequence of call/instantiation arguments. 47 | /// 48 | /// Without it, you would have to specify explicitly a compatible type, like: 49 | /// `session.call::(.., &[], ..)`. 50 | pub const NO_ARGS: &[String] = &[]; 51 | /// Convenient value for an empty salt. 52 | pub const NO_SALT: Option<[u8; 32]> = None; 53 | /// Convenient value for no endowment. 54 | /// 55 | /// Compatible with any runtime with `u128` as the balance type. 56 | pub const NO_ENDOWMENT: Option> = None; 57 | 58 | /// Wrapper around `Sandbox` that provides a convenient API for interacting with multiple contracts. 59 | /// 60 | /// Instead of talking with a low-level `Sandbox`, you can use this struct to keep context, 61 | /// including: origin, gas_limit, transcoder and history of results. 62 | /// 63 | /// `Session` has two APIs: chain-ish and for singular actions. The first one can be used like: 64 | /// ```rust, no_run 65 | /// # use std::sync::Arc; 66 | /// # use contract_transcode::ContractMessageTranscoder; 67 | /// # use ink_sandbox::AccountId32; 68 | /// # use drink::{ 69 | /// # session::Session, 70 | /// # session::{NO_ARGS, NO_SALT, NO_ENDOWMENT}, 71 | /// # minimal::MinimalSandbox 72 | /// # }; 73 | /// # 74 | /// # fn get_transcoder() -> Arc { 75 | /// # Arc::new(ContractMessageTranscoder::load("").unwrap()) 76 | /// # } 77 | /// # fn contract_bytes() -> Vec { vec![] } 78 | /// # fn bob() -> AccountId32 { AccountId32::new([0; 32]) } 79 | /// 80 | /// # fn main() -> Result<(), drink::session::error::SessionError> { 81 | /// 82 | /// Session::::default() 83 | /// .deploy_and(contract_bytes(), "new", NO_ARGS, NO_SALT, NO_ENDOWMENT, &get_transcoder())? 84 | /// .call_and("foo", NO_ARGS, NO_ENDOWMENT)? 85 | /// .with_actor(bob()) 86 | /// .call_and("bar", NO_ARGS, NO_ENDOWMENT)?; 87 | /// # Ok(()) } 88 | /// ``` 89 | /// 90 | /// The second one serves for one-at-a-time actions: 91 | /// ```rust, no_run 92 | /// # use std::sync::Arc; 93 | /// # use contract_transcode::ContractMessageTranscoder; 94 | /// # use ink_sandbox::AccountId32; 95 | /// # use drink::{ 96 | /// # session::Session, 97 | /// # minimal::MinimalSandbox, 98 | /// # session::{NO_ARGS, NO_ENDOWMENT, NO_SALT} 99 | /// # }; 100 | /// # fn get_transcoder() -> Arc { 101 | /// # Arc::new(ContractMessageTranscoder::load("").unwrap()) 102 | /// # } 103 | /// # fn contract_bytes() -> Vec { vec![] } 104 | /// # fn bob() -> AccountId32 { AccountId32::new([0; 32]) } 105 | /// 106 | /// # fn main() -> Result<(), Box> { 107 | /// 108 | /// let mut session = Session::::default(); 109 | /// let _address = session.deploy(contract_bytes(), "new", NO_ARGS, NO_SALT, NO_ENDOWMENT, &get_transcoder())?; 110 | /// let _result: u32 = session.call("foo", NO_ARGS, NO_ENDOWMENT)??; 111 | /// session.set_actor(bob()); 112 | /// session.call::<_, ()>("bar", NO_ARGS, NO_ENDOWMENT)??; 113 | /// # Ok(()) } 114 | /// ``` 115 | /// 116 | /// You can also work with `.contract` bundles like so: 117 | /// ```rust, no_run 118 | /// # use drink::{ 119 | /// # local_contract_file, 120 | /// # session::Session, 121 | /// # session::{ContractBundle, NO_ARGS, NO_SALT, NO_ENDOWMENT}, 122 | /// # minimal::MinimalSandbox 123 | /// # }; 124 | /// 125 | /// # fn main() -> Result<(), drink::session::error::SessionError> { 126 | /// // Simplest way, loading a bundle from the project's directory: 127 | /// Session::::default() 128 | /// .deploy_bundle_and(local_contract_file!(), "new", NO_ARGS, NO_SALT, NO_ENDOWMENT)?; /* ... */ 129 | /// 130 | /// // Or choosing the file explicitly: 131 | /// let contract = ContractBundle::load("path/to/your.contract")?; 132 | /// Session::::default() 133 | /// .deploy_bundle_and(contract, "new", NO_ARGS, NO_SALT, NO_ENDOWMENT)?; /* ... */ 134 | /// # Ok(()) } 135 | /// ``` 136 | pub struct Session 137 | where 138 | T::Runtime: Config, 139 | { 140 | sandbox: T, 141 | 142 | actor: AccountIdFor, 143 | origin: RuntimeOrigin, 144 | gas_limit: Weight, 145 | storage_deposit_limit: BalanceOf, 146 | 147 | transcoders: TranscoderRegistry, 148 | record: Record, 149 | mocks: Arc>, 150 | } 151 | 152 | impl Default for Session 153 | where 154 | T::Runtime: Config, 155 | T: Default, 156 | BalanceOf: Into + TryFrom + Bounded, 157 | MomentOf: Into, 158 | <::Runtime as frame_system::Config>::Hash: frame_support::traits::IsType, 159 | { 160 | fn default() -> Self { 161 | let mocks = Arc::new(Mutex::new(MockRegistry::new())); 162 | let mut sandbox = T::default(); 163 | 164 | let actor = T::default_actor(); 165 | let origin = T::convert_account_to_origin(actor.clone()); 166 | sandbox.map_account(origin.clone()).expect("cannot map"); 167 | 168 | Self { 169 | sandbox, 170 | mocks, 171 | actor, 172 | origin, 173 | gas_limit: T::default_gas_limit(), 174 | storage_deposit_limit: BalanceOf::::max_value(), 175 | transcoders: TranscoderRegistry::new(), 176 | record: Default::default(), 177 | } 178 | } 179 | } 180 | 181 | impl Session 182 | where 183 | T::Runtime: Config, 184 | BalanceOf: Into + TryFrom + Bounded, 185 | MomentOf: Into, 186 | <::Runtime as frame_system::Config>::Hash: frame_support::traits::IsType, 187 | { 188 | /// Sets a new actor and returns updated `self`. 189 | pub fn with_actor(self, actor: AccountIdFor) -> Self { 190 | Self { actor, ..self } 191 | } 192 | 193 | /// Returns currently set actor. 194 | pub fn get_actor(&self) -> AccountIdFor { 195 | self.actor.clone() 196 | } 197 | 198 | /// Sets a new actor and returns the old one. 199 | pub fn set_actor(&mut self, actor: AccountIdFor) -> AccountIdFor { 200 | let actor = mem::replace(&mut self.actor, actor); 201 | let origin = T::convert_account_to_origin(actor.clone()); 202 | self.sandbox 203 | .map_account(origin.clone()) 204 | .expect("Failed to map actor account"); 205 | let _ = mem::replace(&mut self.origin, origin); 206 | actor 207 | } 208 | 209 | /// Sets a new gas limit and returns updated `self`. 210 | pub fn with_gas_limit(self, gas_limit: Weight) -> Self { 211 | Self { gas_limit, ..self } 212 | } 213 | 214 | /// Sets a new gas limit and returns the old one. 215 | pub fn set_gas_limit(&mut self, gas_limit: Weight) -> Weight { 216 | mem::replace(&mut self.gas_limit, gas_limit) 217 | } 218 | 219 | /// Returns currently set gas limit. 220 | pub fn get_gas_limit(&self) -> Weight { 221 | self.gas_limit 222 | } 223 | 224 | /// Sets a new storage deposit limit and returns updated `self`. 225 | pub fn with_storage_deposit_limit(self, storage_deposit_limit: BalanceOf) -> Self { 226 | Self { 227 | storage_deposit_limit, 228 | ..self 229 | } 230 | } 231 | 232 | /// Sets a new storage deposit limit and returns the old one. 233 | pub fn set_storage_deposit_limit( 234 | &mut self, 235 | storage_deposit_limit: BalanceOf, 236 | ) -> BalanceOf { 237 | mem::replace(&mut self.storage_deposit_limit, storage_deposit_limit) 238 | } 239 | 240 | /// Returns currently set storage deposit limit. 241 | pub fn get_storage_deposit_limit(&self) -> BalanceOf { 242 | self.storage_deposit_limit 243 | } 244 | 245 | /// Register a transcoder for a particular contract and returns updated `self`. 246 | pub fn with_transcoder( 247 | mut self, 248 | contract_address: H160, 249 | transcoder: &Arc, 250 | ) -> Self { 251 | self.set_transcoder(contract_address, transcoder); 252 | self 253 | } 254 | 255 | /// Registers a transcoder for a particular contract. 256 | pub fn set_transcoder( 257 | &mut self, 258 | contract_address: H160, 259 | transcoder: &Arc, 260 | ) { 261 | self.transcoders.register(contract_address, transcoder); 262 | } 263 | 264 | /// The underlying `Sandbox` instance. 265 | pub fn sandbox(&mut self) -> &mut T { 266 | &mut self.sandbox 267 | } 268 | 269 | /// Returns a reference to the record of the session. 270 | pub fn record(&self) -> &Record { 271 | &self.record 272 | } 273 | 274 | /// Returns a reference for mocking API. 275 | pub fn mocking_api(&mut self) -> &mut impl MockingApi { 276 | self 277 | } 278 | 279 | /// Deploys a contract with a given constructor, arguments, salt and endowment. In case of 280 | /// success, returns `self`. 281 | pub fn deploy_and + Debug>( 282 | mut self, 283 | contract_bytes: Vec, 284 | constructor: &str, 285 | args: &[S], 286 | salt: Option<[u8; 32]>, 287 | endowment: Option>, 288 | transcoder: &Arc, 289 | ) -> Result { 290 | self.deploy( 291 | contract_bytes, 292 | constructor, 293 | args, 294 | salt, 295 | endowment, 296 | transcoder, 297 | ) 298 | .map(|_| self) 299 | } 300 | fn record_events(&mut self, recording: impl FnOnce(&mut Self) -> V) -> V { 301 | let start = self.sandbox.events().len(); 302 | let result = recording(self); 303 | let events = self.sandbox.events()[start..].to_vec(); 304 | self.record.push_event_batches(events); 305 | result 306 | } 307 | 308 | /// Deploys a contract with a given constructor, arguments, salt and endowment. In case of 309 | /// success, returns the address of the deployed contract. 310 | pub fn deploy + Debug>( 311 | &mut self, 312 | contract_bytes: Vec, 313 | constructor: &str, 314 | args: &[S], 315 | salt: Option<[u8; 32]>, 316 | endowment: Option>, 317 | transcoder: &Arc, 318 | ) -> Result { 319 | let data = transcoder 320 | .encode(constructor, args) 321 | .map_err(|err| SessionError::Encoding(err.to_string()))?; 322 | 323 | let result = self.record_events(|session| { 324 | session.sandbox.deploy_contract( 325 | contract_bytes, 326 | endowment.unwrap_or_default(), 327 | data, 328 | salt, 329 | session.origin.clone(), 330 | session.gas_limit, 331 | DepositLimit::Balance(session.storage_deposit_limit), 332 | ) 333 | }); 334 | 335 | let ret = match &result.result { 336 | Ok(exec_result) if exec_result.result.did_revert() => { 337 | Err(SessionError::DeploymentReverted) 338 | } 339 | Ok(exec_result) => { 340 | let address = exec_result.addr; 341 | self.record.push_deploy_return(address); 342 | self.transcoders.register(address, transcoder); 343 | 344 | Ok(address) 345 | } 346 | Err(err) => Err(SessionError::DeploymentFailed(*err)), 347 | }; 348 | 349 | self.record.push_deploy_result(result); 350 | ret 351 | } 352 | 353 | /// Similar to `deploy` but takes the parsed contract file (`ContractBundle`) as a first argument. 354 | /// 355 | /// You can get it with `ContractBundle::load("some/path/your.contract")` or `local_contract_file!()` 356 | pub fn deploy_bundle + Debug>( 357 | &mut self, 358 | contract_file: ContractBundle, 359 | constructor: &str, 360 | args: &[S], 361 | salt: Option<[u8; 32]>, 362 | endowment: Option>, 363 | ) -> Result { 364 | self.deploy( 365 | contract_file.binary, 366 | constructor, 367 | args, 368 | salt, 369 | endowment, 370 | &contract_file.transcoder, 371 | ) 372 | } 373 | 374 | /// Performs a dry run of the deployment of a contract. 375 | pub fn dry_run_deployment + Debug>( 376 | &mut self, 377 | contract_file: ContractBundle, 378 | constructor: &str, 379 | args: &[S], 380 | salt: Option<[u8; 32]>, 381 | endowment: Option>, 382 | ) -> Result, SessionError> { 383 | let data = contract_file 384 | .transcoder 385 | .encode(constructor, args) 386 | .map_err(|err| SessionError::Encoding(err.to_string()))?; 387 | 388 | Ok(self.sandbox.dry_run(|sandbox| { 389 | sandbox.deploy_contract( 390 | contract_file.binary, 391 | endowment.unwrap_or_default(), 392 | data, 393 | salt, 394 | self.origin.clone(), 395 | self.gas_limit, 396 | DepositLimit::Balance(self.storage_deposit_limit), 397 | ) 398 | })) 399 | } 400 | 401 | /// Similar to `deploy_and` but takes the parsed contract file (`ContractBundle`) as a first argument. 402 | /// 403 | /// You can get it with `ContractBundle::load("some/path/your.contract")` or `local_contract_file!()` 404 | pub fn deploy_bundle_and + Debug>( 405 | mut self, 406 | contract_file: ContractBundle, 407 | constructor: &str, 408 | args: &[S], 409 | salt: Option<[u8; 32]>, 410 | endowment: Option>, 411 | ) -> Result { 412 | self.deploy_bundle(contract_file, constructor, args, salt, endowment) 413 | .map(|_| self) 414 | } 415 | 416 | /// Uploads a raw contract code. In case of success, returns `self`. 417 | pub fn upload_and(mut self, contract_bytes: Vec) -> Result { 418 | self.upload(contract_bytes).map(|_| self) 419 | } 420 | 421 | /// Uploads a raw contract code. In case of success returns the code hash. 422 | pub fn upload(&mut self, contract_bytes: Vec) -> Result { 423 | let result = self.sandbox.upload_contract( 424 | contract_bytes, 425 | self.origin.clone(), 426 | self.storage_deposit_limit, 427 | ); 428 | 429 | result 430 | .map(|upload_result| upload_result.code_hash) 431 | .map_err(SessionError::UploadFailed) 432 | } 433 | 434 | /// Similar to `upload_and` but takes the contract bundle as the first argument. 435 | /// 436 | /// You can obtain it using `ContractBundle::load("some/path/your.contract")` or `local_contract_file!()` 437 | pub fn upload_bundle_and(self, contract_file: ContractBundle) -> Result { 438 | self.upload_and(contract_file.binary) 439 | } 440 | 441 | /// Similar to `upload` but takes the contract bundle as the first argument. 442 | /// 443 | /// You can obtain it using `ContractBundle::load("some/path/your.contract")` or `local_contract_file!()` 444 | pub fn upload_bundle(&mut self, contract_file: ContractBundle) -> Result { 445 | self.upload(contract_file.binary) 446 | } 447 | 448 | /// Calls a contract with a given address. In case of a successful call, returns `self`. 449 | pub fn call_and + Debug>( 450 | mut self, 451 | message: &str, 452 | args: &[S], 453 | endowment: Option>, 454 | ) -> Result { 455 | // We ignore result, so we can pass `()` as the message result type, which will never fail 456 | // at decoding. 457 | self.call_internal::<_, ()>(None, message, args, endowment) 458 | .map(|_| self) 459 | } 460 | 461 | /// Calls the last deployed contract. In case of a successful call, returns `self`. 462 | pub fn call_with_address_and + Debug>( 463 | mut self, 464 | address: H160, 465 | message: &str, 466 | args: &[S], 467 | endowment: Option>, 468 | ) -> Result { 469 | // We ignore result, so we can pass `()` as the message result type, which will never fail 470 | // at decoding. 471 | self.call_internal::<_, ()>(Some(address), message, args, endowment) 472 | .map(|_| self) 473 | } 474 | 475 | /// Calls the last deployed contract. In case of a successful call, returns the encoded result. 476 | pub fn call + Debug, V: Decode>( 477 | &mut self, 478 | message: &str, 479 | args: &[S], 480 | endowment: Option>, 481 | ) -> Result, SessionError> { 482 | self.call_internal::<_, V>(None, message, args, endowment) 483 | } 484 | 485 | /// Calls the last deployed contract. Expect it to be reverted and the message result to be of 486 | /// type `Result<_, E>`. 487 | pub fn call_and_expect_error + Debug, E: Debug + Decode>( 488 | &mut self, 489 | message: &str, 490 | args: &[S], 491 | endowment: Option>, 492 | ) -> Result { 493 | Ok(self 494 | .call_internal::<_, Result<(), E>>(None, message, args, endowment) 495 | .expect_err("Call should fail") 496 | .decode_revert::>()? 497 | .expect("Call should return an error") 498 | .expect_err("Call should return an error")) 499 | } 500 | 501 | /// Calls a contract with a given address. In case of a successful call, returns the encoded 502 | /// result. 503 | pub fn call_with_address + Debug, V: Decode>( 504 | &mut self, 505 | address: H160, 506 | message: &str, 507 | args: &[S], 508 | endowment: Option>, 509 | ) -> Result, SessionError> { 510 | self.call_internal(Some(address), message, args, endowment) 511 | } 512 | 513 | /// Performs a dry run of a contract call. 514 | pub fn dry_run_call + Debug>( 515 | &mut self, 516 | address: H160, 517 | message: &str, 518 | args: &[S], 519 | endowment: Option>, 520 | ) -> Result, SessionError> { 521 | let data = self 522 | .transcoders 523 | .get(&address) 524 | .as_ref() 525 | .ok_or(SessionError::NoTranscoder)? 526 | .encode(message, args) 527 | .map_err(|err| SessionError::Encoding(err.to_string()))?; 528 | 529 | Ok(self.sandbox.dry_run(|sandbox| { 530 | sandbox.call_contract( 531 | address, 532 | endowment.unwrap_or_default(), 533 | data, 534 | self.origin.clone(), 535 | self.gas_limit, 536 | DepositLimit::Balance(self.storage_deposit_limit), 537 | ) 538 | })) 539 | } 540 | 541 | fn call_internal + Debug, V: Decode>( 542 | &mut self, 543 | address: Option, 544 | message: &str, 545 | args: &[S], 546 | endowment: Option>, 547 | ) -> Result, SessionError> { 548 | let address = match address { 549 | Some(address) => address, 550 | None => *self 551 | .record 552 | .deploy_returns() 553 | .last() 554 | .ok_or(SessionError::NoContract)?, 555 | }; 556 | 557 | let data = self 558 | .transcoders 559 | .get(&address) 560 | .as_ref() 561 | .ok_or(SessionError::NoTranscoder)? 562 | .encode(message, args) 563 | .map_err(|err| SessionError::Encoding(err.to_string()))?; 564 | 565 | let result = self.record_events(|session| { 566 | session.sandbox.call_contract( 567 | address, 568 | endowment.unwrap_or_default(), 569 | data, 570 | session.origin.clone(), 571 | session.gas_limit, 572 | DepositLimit::Balance(session.storage_deposit_limit), 573 | ) 574 | }); 575 | 576 | let ret = match &result.result { 577 | Ok(exec_result) if exec_result.did_revert() => { 578 | Err(SessionError::CallReverted(exec_result.data.clone())) 579 | } 580 | Ok(exec_result) => { 581 | self.record.push_call_return(exec_result.data.clone()); 582 | self.record.last_call_return_decoded::() 583 | } 584 | Err(err) => Err(SessionError::CallFailed(*err)), 585 | }; 586 | 587 | self.record.push_call_result(result); 588 | ret 589 | } 590 | } 591 | -------------------------------------------------------------------------------- /drink/src/session/bundle.rs: -------------------------------------------------------------------------------- 1 | //! This module provides simple utilities for loading and parsing `.contract` files in context of `drink` tests. 2 | 3 | use std::{path::PathBuf, sync::Arc}; 4 | 5 | use contract_metadata::ContractMetadata; 6 | use contract_transcode::ContractMessageTranscoder; 7 | 8 | use crate::{DrinkResult, Error}; 9 | 10 | /// A struct representing the result of parsing a `.contract` bundle file. 11 | /// 12 | /// It can be used with the following methods of the `Session` struct: 13 | /// - `deploy_bundle` 14 | /// - `deploy_bundle_and` 15 | /// - `upload_bundle` 16 | /// - `upload_bundle_and` 17 | #[derive(Clone)] 18 | pub struct ContractBundle { 19 | /// Binary of the contract. 20 | pub binary: Vec, 21 | /// Transcoder derived from the ABI/metadata 22 | pub transcoder: Arc, 23 | } 24 | 25 | impl ContractBundle { 26 | /// Load and parse the information in a `.contract` bundle under `path`, producing a 27 | /// `ContractBundle` struct. 28 | pub fn load

(path: P) -> DrinkResult 29 | where 30 | P: AsRef, 31 | { 32 | let metadata: ContractMetadata = ContractMetadata::load(&path).map_err(|e| { 33 | Error::BundleLoadFailed(format!("Failed to load the contract file:\n{e:?}")) 34 | })?; 35 | 36 | let ink_metadata = serde_json::from_value(serde_json::Value::Object(metadata.abi)) 37 | .map_err(|e| { 38 | Error::BundleLoadFailed(format!( 39 | "Failed to parse metadata from the contract file:\n{e:?}" 40 | )) 41 | })?; 42 | 43 | let transcoder = Arc::new(ContractMessageTranscoder::new(ink_metadata)); 44 | 45 | let binary = metadata 46 | .source 47 | .contract_binary 48 | .ok_or(Error::BundleLoadFailed( 49 | "Failed to get the WASM blob from the contract file".to_string(), 50 | ))? 51 | .0; 52 | 53 | Ok(Self { binary, transcoder }) 54 | } 55 | 56 | /// Load the `.contract` bundle (`contract_file_name`) located in the `project_dir`` working directory. 57 | /// 58 | /// This is meant to be used predominantly by the `local_contract_file!` macro. 59 | pub fn local(project_dir: &str, contract_file_name: String) -> Self { 60 | let mut path = PathBuf::from(project_dir); 61 | path.push("target"); 62 | path.push("ink"); 63 | path.push(contract_file_name); 64 | Self::load(path).expect("Loading the local bundle failed") 65 | } 66 | } 67 | 68 | /// A convenience macro that allows you to load a bundle found in the target directory 69 | /// of the current project. 70 | #[macro_export] 71 | macro_rules! local_contract_file { 72 | () => { 73 | drink::session::ContractBundle::local( 74 | env!("CARGO_MANIFEST_DIR"), 75 | env!("CARGO_CRATE_NAME").to_owned() + ".contract", 76 | ) 77 | }; 78 | } 79 | -------------------------------------------------------------------------------- /drink/src/session/error.rs: -------------------------------------------------------------------------------- 1 | //! Module exposing errors and result types for the session API. 2 | 3 | use frame_support::sp_runtime::DispatchError; 4 | use parity_scale_codec::Decode; 5 | use thiserror::Error; 6 | 7 | use crate::errors::MessageResult; 8 | 9 | /// Session specific errors. 10 | #[derive(Clone, Error, Debug)] 11 | pub enum SessionError { 12 | /// Encoding data failed. 13 | #[error("Encoding call data failed: {0}")] 14 | Encoding(String), 15 | /// Decoding data failed. 16 | #[error("Decoding call data failed: {0}")] 17 | Decoding(String), 18 | /// Crate-specific error. 19 | #[error("{0:?}")] 20 | Drink(#[from] crate::Error), 21 | /// Deployment has been reverted by the contract. 22 | #[error("Contract deployment has been reverted")] 23 | DeploymentReverted, 24 | /// Deployment failed (aborted by the pallet). 25 | #[error("Contract deployment failed before execution: {0:?}")] 26 | DeploymentFailed(DispatchError), 27 | /// Code upload failed (aborted by the pallet). 28 | #[error("Code upload failed: {0:?}")] 29 | UploadFailed(DispatchError), 30 | /// Call has been reverted by the contract. 31 | #[error("Contract call has been reverted. Encoded error: {0:?}")] 32 | CallReverted(Vec), 33 | /// Contract call failed (aborted by the pallet). 34 | #[error("Contract call failed before execution: {0:?}")] 35 | CallFailed(DispatchError), 36 | /// There is no deployed contract to call. 37 | #[error("No deployed contract")] 38 | NoContract, 39 | /// There is no registered transcoder to encode/decode messages for the called contract. 40 | #[error("Missing transcoder")] 41 | NoTranscoder, 42 | } 43 | 44 | impl SessionError { 45 | /// Check if the error is a revert error and if so, decode the error message. 46 | pub fn decode_revert(&self) -> Result, Self> { 47 | match self { 48 | SessionError::CallReverted(error) => { 49 | Ok(MessageResult::decode(&mut &error[..]).expect("Failed to decode error")) 50 | } 51 | _ => Err(self.clone()), 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /drink/src/session/mock.rs: -------------------------------------------------------------------------------- 1 | //! Mocking utilities for contract calls. 2 | 3 | mod contract; 4 | mod error; 5 | mod extension; 6 | use std::collections::BTreeMap; 7 | 8 | pub use contract::{mock_message, ContractMock, MessageMock, Selector}; 9 | use error::MockingError; 10 | use ink_sandbox::pallet_revive::evm::H160; 11 | 12 | /// Untyped result of a mocked call. 13 | pub type MockedCallResult = Result, MockingError>; 14 | 15 | /// A registry of mocked contracts. 16 | pub(crate) struct MockRegistry { 17 | mocked_contracts: BTreeMap, 18 | nonce: u8, 19 | } 20 | 21 | impl MockRegistry { 22 | /// Creates a new registry. 23 | pub fn new() -> Self { 24 | Self { 25 | mocked_contracts: BTreeMap::new(), 26 | nonce: 0u8, 27 | } 28 | } 29 | 30 | /// Returns the salt for the next contract. 31 | pub fn salt(&mut self) -> [u8; 32] { 32 | self.nonce += 1; 33 | [self.nonce; 32] 34 | } 35 | 36 | /// Registers `mock` for `address`. Returns the previous mock, if any. 37 | pub fn register(&mut self, address: H160, mock: ContractMock) -> Option { 38 | self.mocked_contracts.insert(address, mock) 39 | } 40 | 41 | /// Returns the mock for `address`, if any. 42 | #[allow(dead_code)] // FIXME: Remove when mocking extension is replaced 43 | pub fn get(&self, address: &H160) -> Option<&ContractMock> { 44 | self.mocked_contracts.get(address) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /drink/src/session/mock/contract.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | 3 | use parity_scale_codec::{Decode, Encode}; 4 | 5 | use crate::{ 6 | errors::LangError, 7 | session::mock::{error::MockingError, MockedCallResult}, 8 | }; 9 | 10 | /// Alias for a 4-byte selector. 11 | pub type Selector = [u8; 4]; 12 | /// An untyped message mock. 13 | /// 14 | /// Notice that in the end, we cannot operate on specific argument/return types. Rust won't let us 15 | /// have a collection of differently typed closures. Fortunately, we can assume that all types are 16 | /// en/decodable, so we can use `Vec` as a common denominator. 17 | pub type MessageMock = Box) -> MockedCallResult + Send + Sync>; 18 | 19 | /// A contract mock. 20 | pub struct ContractMock { 21 | messages: BTreeMap, 22 | } 23 | 24 | impl ContractMock { 25 | /// Creates a new mock without any message. 26 | pub fn new() -> Self { 27 | Self { 28 | messages: BTreeMap::new(), 29 | } 30 | } 31 | 32 | /// Adds a message mock. 33 | pub fn with_message(mut self, selector: Selector, message: MessageMock) -> Self { 34 | self.messages.insert(selector, message); 35 | self 36 | } 37 | 38 | /// Try to call a message mock. Returns an error if there is no message mock for `selector`. 39 | pub fn call(&self, selector: Selector, input: Vec) -> MockedCallResult { 40 | match self.messages.get(&selector) { 41 | None => Err(MockingError::MessageNotFound(selector)), 42 | Some(message) => message(input), 43 | } 44 | } 45 | } 46 | 47 | impl Default for ContractMock { 48 | fn default() -> Self { 49 | Self::new() 50 | } 51 | } 52 | 53 | /// A helper function to create a message mock out of a typed closure. 54 | /// 55 | /// In particular, it takes care of decoding the input and encoding the output. Also, wraps the 56 | /// return value in a `Result`, which is normally done implicitly by ink!. 57 | pub fn mock_message Ret + Send + Sync + 'static>( 58 | body: Body, 59 | ) -> MessageMock { 60 | Box::new(move |encoded_input| { 61 | let input = Decode::decode(&mut &*encoded_input).map_err(MockingError::ArgumentDecoding)?; 62 | Ok(Ok::(body(input)).encode()) 63 | }) 64 | } 65 | -------------------------------------------------------------------------------- /drink/src/session/mock/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | use crate::session::mock::Selector; 4 | 5 | /// Error type for mocking operations. 6 | #[derive(Error, Debug)] 7 | pub enum MockingError { 8 | #[error("Message not found (unknown selector: {0:?})")] 9 | MessageNotFound(Selector), 10 | #[error("Decoding message arguments failed: {0:?}")] 11 | ArgumentDecoding(parity_scale_codec::Error), 12 | } 13 | -------------------------------------------------------------------------------- /drink/src/session/mock/extension.rs: -------------------------------------------------------------------------------- 1 | // TODO: No longer used due to pallet-revive removed `type Debug` for intercepting contract calls. Need to be fixed when we figure out the alternative approach. (#144) 2 | // 3 | // use std::sync::{Arc, Mutex}; 4 | 5 | // use ink_sandbox::pallet_revive::ExecReturnValue; 6 | // use parity_scale_codec::{Decode, Encode}; 7 | 8 | // use crate::{ 9 | // errors::MessageResult, 10 | // pallet_revive::chain_extension::ReturnFlags, 11 | // session::mock::{MockRegistry, Selector}, 12 | // }; 13 | 14 | // /// Runtime extension enabling contract call interception. 15 | // pub(crate) struct MockingExtension { 16 | // /// Mock registry, shared with the sandbox. 17 | // /// 18 | // /// Potentially the runtime is executed in parallel and thus we need to wrap the registry in 19 | // /// `Arc` instead of `Rc`. 20 | // pub mock_registry: Arc>, 21 | // } 22 | 23 | // impl MockingExtension { 24 | // fn intercept_call( 25 | // &self, 26 | // contract_address: Vec, 27 | // _is_call: bool, 28 | // input_data: Vec, 29 | // ) -> Vec { 30 | // let contract_address = Decode::decode(&mut &contract_address[..]) 31 | // .expect("Contract address should be decodable"); 32 | 33 | // match self 34 | // .mock_registry 35 | // .lock() 36 | // .expect("Should be able to acquire registry") 37 | // .get(&contract_address) 38 | // { 39 | // // There is no mock registered for this address, so we return `None` to indicate that 40 | // // the call should be executed normally. 41 | // None => None::<()>.encode(), 42 | // // We intercept the call and return the result of the mock. 43 | // Some(mock) => { 44 | // let (selector, call_data) = input_data.split_at(4); 45 | // let selector: Selector = selector 46 | // .try_into() 47 | // .expect("Input data should contain at least selector bytes"); 48 | 49 | // let result = mock 50 | // .call(selector, call_data.to_vec()) 51 | // .expect("TODO: let the user define the fallback mechanism"); 52 | 53 | // // Although we don't know the exact type, thanks to the SCALE encoding we know 54 | // // that `()` will always succeed (we only care about the `Ok`/`Err` distinction). 55 | // let decoded_result: MessageResult<()> = 56 | // Decode::decode(&mut &result[..]).expect("Mock result should be decodable"); 57 | 58 | // let flags = match decoded_result { 59 | // Ok(_) => ReturnFlags::empty(), 60 | // Err(_) => ReturnFlags::REVERT, 61 | // }; 62 | 63 | // let result = ExecReturnValue { 64 | // flags, 65 | // data: result, 66 | // }; 67 | 68 | // Some(result).encode() 69 | // } 70 | // } 71 | // } 72 | // } 73 | -------------------------------------------------------------------------------- /drink/src/session/mocking_api.rs: -------------------------------------------------------------------------------- 1 | //! Mocking API for the sandbox. 2 | 3 | use std::path::Path; 4 | 5 | use frame_support::sp_runtime::traits::Bounded; 6 | use ink_primitives::DepositLimit; 7 | use ink_sandbox::{ 8 | api::prelude::*, 9 | pallet_revive::{ 10 | evm::{H160, U256}, 11 | MomentOf, 12 | }, 13 | Sandbox, H256, 14 | }; 15 | 16 | use super::{BalanceOf, Session}; 17 | use crate::{ 18 | pallet_revive::Config, 19 | session::mock::ContractMock, // DEFAULT_GAS_LIMIT, 20 | }; 21 | 22 | /// Read the contract binary file. 23 | pub fn read_contract_binary(path: &std::path::PathBuf) -> Vec { 24 | std::fs::read(path).expect("Failed to read contract file") 25 | } 26 | 27 | /// Interface for basic mocking operations. 28 | pub trait MockingApi { 29 | /// Deploy `mock` as a standard contract. Returns the address of the deployed contract. 30 | fn deploy(&mut self, mock: ContractMock) -> H160; 31 | 32 | /// Mock part of an existing contract. In particular, allows to override real behavior of 33 | /// deployed contract's messages. 34 | fn mock_existing_contract(&mut self, _mock: ContractMock, _address: H160); 35 | } 36 | 37 | impl MockingApi for Session 38 | where 39 | T::Runtime: Config, 40 | BalanceOf: Into + TryFrom + Bounded, 41 | MomentOf: Into, 42 | <::Runtime as frame_system::Config>::Hash: frame_support::traits::IsType, 43 | { 44 | fn deploy(&mut self, mock: ContractMock) -> H160 { 45 | let salt = self 46 | .mocks 47 | .lock() 48 | .expect("Should be able to acquire lock on registry") 49 | .salt(); 50 | 51 | // Construct the path to the contract file. 52 | let contract_path = Path::new(file!()) 53 | .parent() 54 | .and_then(Path::parent) 55 | .and_then(Path::parent) 56 | .expect("Failed to determine the base path") 57 | .join("test-resources") 58 | .join("dummy.polkavm"); 59 | 60 | let origin = T::convert_account_to_origin(T::default_actor()); 61 | let mock_address = self 62 | .sandbox() 63 | .deploy_contract( 64 | // Deploy a dummy contract to ensure that the pallet will treat the mock as a regular contract until it is 65 | // actually called. 66 | read_contract_binary(&contract_path), 67 | 0u32.into(), 68 | vec![], 69 | Some(salt), 70 | origin, 71 | T::default_gas_limit(), 72 | DepositLimit::Unchecked, 73 | ) 74 | .result 75 | .expect("Deployment of a dummy contract should succeed") 76 | .addr; 77 | 78 | self.mocks 79 | .lock() 80 | .expect("Should be able to acquire lock on registry") 81 | .register(mock_address, mock); 82 | 83 | mock_address 84 | } 85 | 86 | fn mock_existing_contract(&mut self, _mock: ContractMock, _address: H160) { 87 | todo!("soon") 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /drink/src/session/record.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use contract_transcode::{ContractMessageTranscoder, Value}; 4 | use frame_system::Config as SysConfig; 5 | use ink_sandbox::{ 6 | pallet_revive::{self, evm::H160}, 7 | ContractExecResultFor, ContractResultInstantiate, EventRecordOf, 8 | }; 9 | use parity_scale_codec::{Decode, Encode}; 10 | 11 | use crate::{ 12 | errors::MessageResult, 13 | minimal::{MinimalSandboxRuntime, RuntimeEvent}, 14 | session::error::SessionError, 15 | }; 16 | 17 | /// Data structure storing the results of contract interaction during a session. 18 | /// 19 | /// # Naming convention 20 | /// 21 | /// By `result` we mean the full result (enriched with some context information) of the contract 22 | /// interaction, like `ContractExecResult`. By `return` we mean the return value of the contract 23 | /// execution, like a value returned from a message or the address of a newly instantiated contract. 24 | #[derive(frame_support::DefaultNoBound)] 25 | pub struct Record { 26 | /// The results of contract instantiation. 27 | deploy_results: Vec>, 28 | /// The return values of contract instantiation (i.e. the addresses of the newly instantiated 29 | /// contracts). 30 | deploy_returns: Vec, 31 | 32 | /// The results of contract calls. 33 | call_results: Vec>, 34 | /// The return values of contract calls (in the SCALE-encoded form). 35 | call_returns: Vec>, 36 | 37 | /// The events emitted by the contracts. 38 | event_batches: Vec>, 39 | } 40 | 41 | // API for `Session` to record results and events related to contract interaction. 42 | impl Record { 43 | pub(super) fn push_deploy_result(&mut self, result: ContractResultInstantiate) { 44 | self.deploy_results.push(result); 45 | } 46 | 47 | pub(super) fn push_deploy_return(&mut self, return_value: H160) { 48 | self.deploy_returns.push(return_value); 49 | } 50 | 51 | pub(super) fn push_call_result(&mut self, result: ContractExecResultFor) { 52 | self.call_results.push(result); 53 | } 54 | 55 | pub(super) fn push_call_return(&mut self, return_value: Vec) { 56 | self.call_returns.push(return_value); 57 | } 58 | 59 | pub(super) fn push_event_batches(&mut self, events: Vec>) { 60 | self.event_batches.push(EventBatch { events }); 61 | } 62 | } 63 | 64 | // API for the end user. 65 | impl Record { 66 | /// Returns all the results of contract instantiations that happened during the session. 67 | pub fn deploy_results(&self) -> &[ContractResultInstantiate] { 68 | &self.deploy_results 69 | } 70 | 71 | /// Returns the last result of contract instantiation that happened during the session. Panics 72 | /// if there were no contract instantiations. 73 | pub fn last_deploy_result(&self) -> &ContractResultInstantiate { 74 | self.deploy_results.last().expect("No deploy results") 75 | } 76 | 77 | /// Returns all the return values of contract instantiations that happened during the session. 78 | pub fn deploy_returns(&self) -> &[H160] { 79 | &self.deploy_returns 80 | } 81 | 82 | /// Returns the last return value of contract instantiation that happened during the session. 83 | /// Panics if there were no contract instantiations. 84 | pub fn last_deploy_return(&self) -> &H160 { 85 | self.deploy_returns.last().expect("No deploy returns") 86 | } 87 | 88 | /// Returns all the results of contract calls that happened during the session. 89 | pub fn call_results(&self) -> &[ContractExecResultFor] { 90 | &self.call_results 91 | } 92 | 93 | /// Returns the last result of contract call that happened during the session. Panics if there 94 | /// were no contract calls. 95 | pub fn last_call_result(&self) -> &ContractExecResultFor { 96 | self.call_results.last().expect("No call results") 97 | } 98 | 99 | /// Returns all the (encoded) return values of contract calls that happened during the session. 100 | pub fn call_returns(&self) -> &[Vec] { 101 | &self.call_returns 102 | } 103 | 104 | /// Returns the last (encoded) return value of contract call that happened during the session. 105 | /// Panics if there were no contract calls. 106 | pub fn last_call_return(&self) -> &[u8] { 107 | self.call_returns.last().expect("No call returns") 108 | } 109 | 110 | /// Returns the last (decoded) return value of contract call that happened during the session. 111 | /// Panics if there were no contract calls. 112 | pub fn last_call_return_decoded(&self) -> Result, SessionError> { 113 | let mut raw = self.last_call_return(); 114 | MessageResult::decode(&mut raw).map_err(|err| { 115 | SessionError::Decoding(format!( 116 | "Failed to decode the result of calling a contract: {err:?}" 117 | )) 118 | }) 119 | } 120 | 121 | /// Returns all the event batches that were recorded for contract interactions during the 122 | /// session. 123 | pub fn event_batches(&self) -> &[EventBatch] { 124 | &self.event_batches 125 | } 126 | 127 | /// Returns the last event batch that was recorded for contract interactions during the session. 128 | /// Panics if there were no event batches. 129 | pub fn last_event_batch(&self) -> &EventBatch { 130 | self.event_batches.last().expect("No event batches") 131 | } 132 | } 133 | 134 | /// A batch of runtime events that were emitted during a single contract interaction. 135 | pub struct EventBatch { 136 | events: Vec>, 137 | } 138 | 139 | impl EventBatch { 140 | /// Returns all the events that were emitted during the contract interaction. 141 | pub fn all_events(&self) -> &[EventRecordOf] { 142 | &self.events 143 | } 144 | } 145 | 146 | impl EventBatch { 147 | /// Returns all the contract events that were emitted during the contract interaction. 148 | /// 149 | /// **WARNING**: This method will return all the events that were emitted by ANY contract. If your 150 | /// call triggered multiple contracts, you will have to filter the events yourself. 151 | /// 152 | /// We have to match against static enum variant, and thus (at least for now) we support only 153 | /// `MinimalSandbox`. 154 | pub fn contract_events(&self) -> Vec<&[u8]> { 155 | self.events 156 | .iter() 157 | .filter_map(|event| match &event.event { 158 | RuntimeEvent::Revive( 159 | pallet_revive::Event::::ContractEmitted { data, .. }, 160 | ) => Some(data.as_slice()), 161 | _ => None, 162 | }) 163 | .collect() 164 | } 165 | 166 | /// The same as `contract_events`, but decodes the events using the given transcoder. 167 | /// 168 | /// **WARNING**: This method will try to decode all the events that were emitted by ANY 169 | /// contract. This means that some contract events might either fail to decode or be decoded 170 | /// incorrectly (to some rubbish). In the former case, they will be skipped, but with the latter 171 | /// case, you will have to filter the events yourself. 172 | /// 173 | /// **WARNING 2**: This method will ignore anonymous events. 174 | pub fn contract_events_decoded( 175 | &self, 176 | transcoder: &Arc, 177 | ) -> Vec { 178 | let signature_topics = transcoder 179 | .metadata() 180 | .spec() 181 | .events() 182 | .iter() 183 | .filter_map(|event| event.signature_topic()) 184 | .map(|sig| sig.as_bytes().try_into().unwrap()) 185 | .collect::>(); 186 | 187 | self.contract_events() 188 | .into_iter() 189 | .filter_map(|data| { 190 | for signature_topic in &signature_topics { 191 | if let Ok(decoded) = transcoder 192 | // We have to `encode` the data because `decode_contract_event` is targeted 193 | // at decoding the data from the runtime, and not directly from the contract 194 | // events. 195 | .decode_contract_event(&signature_topic, &mut &*data.encode()) 196 | { 197 | return Some(decoded); 198 | } 199 | } 200 | None 201 | }) 202 | .collect() 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /drink/src/session/transcoding.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::BTreeMap, sync::Arc}; 2 | 3 | use contract_transcode::ContractMessageTranscoder; 4 | use ink_primitives::H160; 5 | 6 | pub struct TranscoderRegistry { 7 | transcoders: BTreeMap>, 8 | } 9 | 10 | impl TranscoderRegistry { 11 | pub fn new() -> Self { 12 | Self { 13 | transcoders: BTreeMap::new(), 14 | } 15 | } 16 | 17 | pub fn register(&mut self, contract: H160, transcoder: &Arc) { 18 | self.transcoders.insert(contract, Arc::clone(transcoder)); 19 | } 20 | 21 | pub fn get(&self, contract: &H160) -> Option> { 22 | self.transcoders.get(contract).map(Arc::clone) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /drink/test-macro/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "drink-test-macro" 3 | authors.workspace = true 4 | edition.workspace = true 5 | homepage.workspace = true 6 | license.workspace = true 7 | readme.workspace = true 8 | repository.workspace = true 9 | version.workspace = true 10 | description = "Procedural macro providing a `#[drink::test]` attribute for `drink`-based contract testing" 11 | 12 | [lib] 13 | proc-macro = true 14 | 15 | [dependencies] 16 | cargo_metadata = { workspace = true } 17 | contract-build = { workspace = true } 18 | contract-metadata = { workspace = true } 19 | convert_case = { workspace = true } 20 | darling = { workspace = true } 21 | proc-macro2 = { workspace = true } 22 | syn = { workspace = true, features = ["full"] } 23 | quote = { workspace = true } 24 | -------------------------------------------------------------------------------- /drink/test-macro/src/bundle_provision.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, path::PathBuf}; 2 | 3 | use convert_case::{Case, Casing}; 4 | use proc_macro2::{Ident, Span, TokenStream as TokenStream2}; 5 | use quote::quote; 6 | use syn::ItemEnum; 7 | 8 | pub struct BundleProviderGenerator { 9 | root_contract_name: Option, 10 | bundles: HashMap, 11 | } 12 | 13 | impl BundleProviderGenerator { 14 | pub fn new>( 15 | bundles: I, 16 | root_contract_name: Option, 17 | ) -> Self { 18 | let root_contract_name = root_contract_name.map(|name| name.to_case(Case::Pascal)); 19 | let bundles = HashMap::from_iter(bundles.map(|(name, path)| { 20 | let name = name.to_case(Case::Pascal); 21 | (name, path) 22 | })); 23 | 24 | if let Some(root_contract_name) = &root_contract_name { 25 | assert!( 26 | bundles.contains_key(root_contract_name), 27 | "Root contract must be part of the bundles" 28 | ); 29 | } 30 | 31 | Self { 32 | root_contract_name, 33 | bundles, 34 | } 35 | } 36 | 37 | pub fn generate_bundle_provision(&self, enum_item: ItemEnum) -> TokenStream2 { 38 | let enum_name = &enum_item.ident; 39 | let enum_vis = &enum_item.vis; 40 | let enum_attrs = &enum_item.attrs; 41 | 42 | let local = match &self.root_contract_name { 43 | None => quote! {}, 44 | Some(root_name) => { 45 | let local_bundle = self.bundles[root_name].to_str().expect("Invalid path"); 46 | quote! { 47 | pub fn local() -> ::drink::DrinkResult<::drink::session::ContractBundle> { 48 | ::drink::session::ContractBundle::load(#local_bundle) 49 | } 50 | } 51 | } 52 | }; 53 | 54 | let (contract_names, matches): (Vec<_>, Vec<_>) = self 55 | .bundles 56 | .keys() 57 | .map(|name| { 58 | let name_ident = Ident::new(name, Span::call_site()); 59 | let path = self.bundles[name].to_str().expect("Invalid path"); 60 | let matcher = quote! { 61 | #enum_name::#name_ident => ::drink::session::ContractBundle::load(#path), 62 | }; 63 | (name_ident, matcher) 64 | }) 65 | .unzip(); 66 | 67 | quote! { 68 | #(#enum_attrs)* 69 | #[derive(Copy, Clone, PartialEq, Eq, Debug)] 70 | #enum_vis enum #enum_name { 71 | #(#contract_names,)* 72 | } 73 | 74 | impl #enum_name { 75 | #local 76 | 77 | pub fn bundle(self) -> ::drink::DrinkResult<::drink::session::ContractBundle> { 78 | match self { 79 | #(#matches)* 80 | } 81 | } 82 | } 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /drink/test-macro/src/contract_building.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::{hash_map::Entry, HashMap}, 3 | path::PathBuf, 4 | sync::{Mutex, OnceLock}, 5 | }; 6 | 7 | use cargo_metadata::{Metadata, MetadataCommand, Package}; 8 | use contract_build::{ 9 | BuildArtifacts, BuildMode, ExecuteArgs, Features, ImageVariant, ManifestPath, 10 | MetadataArtifacts, MetadataSpec, Network, OutputType, UnstableFlags, Verbosity, 11 | }; 12 | 13 | use crate::bundle_provision::BundleProviderGenerator; 14 | 15 | /// Contract package differentiator. 16 | const INK_AS_DEPENDENCY_FEATURE: &str = "ink-as-dependency"; 17 | 18 | /// Stores the manifest paths of all contracts that have already been built. 19 | /// 20 | /// This prevents from building the same contract for every testcase separately. 21 | static CONTRACTS_BUILT: OnceLock>> = OnceLock::new(); 22 | 23 | /// Build the current package with `cargo contract build --release` (if it is a contract package), 24 | /// as well as all its contract dependencies. Return a collection of paths to corresponding 25 | /// `.contract` files. 26 | /// 27 | /// A package is considered as a contract package, if it has the `ink-as-dependency` feature. 28 | /// 29 | /// A contract dependency, is a package defined in the `Cargo.toml` file with the 30 | /// `ink-as-dependency` feature enabled. 31 | pub fn build_contracts() -> BundleProviderGenerator { 32 | let metadata = MetadataCommand::new() 33 | .exec() 34 | .expect("Error invoking `cargo metadata`"); 35 | 36 | let (maybe_root, contract_deps) = get_contract_crates(&metadata); 37 | let maybe_root = maybe_root.map(build_contract_crate); 38 | let contract_deps = contract_deps.map(build_contract_crate); 39 | 40 | BundleProviderGenerator::new( 41 | maybe_root.clone().into_iter().chain(contract_deps), 42 | maybe_root.map(|(name, _)| name), 43 | ) 44 | } 45 | 46 | /// Contract package together with the features it should be built with. 47 | struct FeaturedPackage<'metadata> { 48 | package: &'metadata Package, 49 | features_on: Vec, 50 | } 51 | 52 | fn get_contract_crates( 53 | metadata: &Metadata, 54 | ) -> ( 55 | Option, 56 | impl Iterator, 57 | ) { 58 | let pkg_lookup = |id| { 59 | metadata 60 | .packages 61 | .iter() 62 | .find(|package| package.id == id) 63 | .unwrap_or_else(|| panic!("Error resolving package {id}")) 64 | }; 65 | 66 | let dep_graph = metadata 67 | .resolve 68 | .as_ref() 69 | .expect("Error resolving dependencies"); 70 | 71 | let contract_deps = dep_graph 72 | .nodes 73 | .iter() 74 | .filter(|node| { 75 | node.features 76 | .contains(&INK_AS_DEPENDENCY_FEATURE.to_string()) 77 | }) 78 | .map(move |node| { 79 | let mut features_on = node.features.clone(); 80 | features_on.retain(|feature| feature != INK_AS_DEPENDENCY_FEATURE && feature != "std"); 81 | FeaturedPackage { 82 | package: pkg_lookup(node.id.clone()), 83 | features_on, 84 | } 85 | }); 86 | 87 | let root = dep_graph 88 | .root 89 | .as_ref() 90 | .expect("Error resolving root package"); 91 | let root = pkg_lookup(root.clone()); 92 | 93 | ( 94 | root.features 95 | .contains_key(INK_AS_DEPENDENCY_FEATURE) 96 | .then_some(FeaturedPackage { 97 | package: root, 98 | features_on: vec![], 99 | }), 100 | contract_deps, 101 | ) 102 | } 103 | 104 | fn build_contract_crate(pkg: FeaturedPackage) -> (String, PathBuf) { 105 | let manifest_path = get_manifest_path(pkg.package); 106 | let mut features = Features::default(); 107 | for feature in pkg.features_on { 108 | features.push(&feature); 109 | } 110 | 111 | match CONTRACTS_BUILT 112 | .get_or_init(|| Mutex::new(HashMap::new())) 113 | .lock() 114 | .expect("Error locking mutex") 115 | .entry(manifest_path.clone().into()) 116 | { 117 | Entry::Occupied(ready) => ready.get().clone(), 118 | Entry::Vacant(todo) => { 119 | let args = ExecuteArgs { 120 | manifest_path, 121 | verbosity: Verbosity::Default, 122 | build_mode: BuildMode::Release, 123 | features, 124 | network: Network::Online, 125 | build_artifact: BuildArtifacts::All, 126 | unstable_flags: UnstableFlags::default(), 127 | keep_debug_symbols: false, 128 | extra_lints: false, 129 | output_type: OutputType::HumanReadable, 130 | metadata_spec: MetadataSpec::Ink, 131 | image: ImageVariant::Default, 132 | }; 133 | 134 | let result = contract_build::execute(args).expect("Error building contract"); 135 | let bundle_path = match result 136 | .metadata_result 137 | .expect("Metadata should have been generated") 138 | { 139 | MetadataArtifacts::Ink(ink_metadata_artifacts) => { 140 | ink_metadata_artifacts.dest_bundle 141 | } 142 | // TODO: Support Solidity compatibility 143 | MetadataArtifacts::Solidity(_) => unimplemented!(), 144 | }; 145 | 146 | let new_entry = (pkg.package.name.clone(), bundle_path); 147 | todo.insert(new_entry.clone()); 148 | new_entry 149 | } 150 | } 151 | } 152 | 153 | fn get_manifest_path(package: &Package) -> ManifestPath { 154 | ManifestPath::new(package.manifest_path.clone().into_std_path_buf()) 155 | .unwrap_or_else(|_| panic!("Error resolving manifest path for package {}", package.name)) 156 | } 157 | -------------------------------------------------------------------------------- /drink/test-macro/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Procedural macro providing a `#[drink::test]` attribute for `drink`-based contract testing. 2 | 3 | #![warn(missing_docs)] 4 | 5 | mod bundle_provision; 6 | mod contract_building; 7 | 8 | use darling::{ast::NestedMeta, FromMeta}; 9 | use proc_macro::TokenStream; 10 | use proc_macro2::TokenStream as TokenStream2; 11 | use quote::quote; 12 | use syn::{ItemEnum, ItemFn}; 13 | 14 | use crate::contract_building::build_contracts; 15 | 16 | type SynResult = Result; 17 | 18 | /// Defines a drink!-based test. 19 | /// 20 | /// # Requirements 21 | /// 22 | /// - Your crate must have `drink` in its dependencies (and it shouldn't be renamed). 23 | /// - You mustn't import `drink::test` in the scope, where the macro is used. In other words, you 24 | /// should always use the macro only with a qualified path `#[drink::test]`. 25 | /// - Your crate cannot be part of a cargo workspace. 26 | /// 27 | /// # Impact 28 | /// 29 | /// This macro will take care of building all needed contracts for the test. The building process 30 | /// will be executed during compile time. 31 | /// 32 | /// Contracts to be built: 33 | /// - current cargo package if contains a `ink-as-dependency` feature 34 | /// - all dependencies declared in the `Cargo.toml` file with the `ink-as-dependency` feature 35 | /// enabled (works with non-local packages as well). 36 | /// 37 | /// ## Compilation features 38 | /// 39 | /// 1. The root contract package (if any) is assumed to be built without any features. 40 | /// 41 | /// 2. All contract dependencies will be built with a union of all features enabled on that package (through potentially 42 | /// different configurations or dependency paths), **excluding** `ink-as-dependency` and `std` features. 43 | /// 44 | /// # Creating a session object 45 | /// 46 | /// The macro will also create a new mutable session object and pass it to the decorated function by value. You can 47 | /// configure which sandbox should be used (by specifying a path to a type implementing 48 | /// `ink_sandbox::Sandbox` trait. Thus, your testcase function should accept a single argument: 49 | /// `mut session: Session<_>`. 50 | /// 51 | /// By default, the macro will use `drink::minimal::MinimalSandbox`. 52 | /// 53 | /// # Example 54 | /// 55 | /// ```rust, ignore 56 | /// #[drink::test] 57 | /// fn testcase(mut session: Session) { 58 | /// session 59 | /// .deploy_bundle(&get_bundle(), "new", NO_ARGS, NO_SALT, NO_ENDOWMENT) 60 | /// .unwrap(); 61 | /// } 62 | /// ``` 63 | #[proc_macro_attribute] 64 | pub fn test(attr: TokenStream, item: TokenStream) -> TokenStream { 65 | match test_internal(attr.into(), item.into()) { 66 | Ok(ts) => ts.into(), 67 | Err(e) => e.to_compile_error().into(), 68 | } 69 | } 70 | 71 | #[derive(FromMeta)] 72 | struct TestAttributes { 73 | sandbox: Option, 74 | } 75 | 76 | /// Auxiliary function to enter ?-based error propagation. 77 | fn test_internal(attr: TokenStream2, item: TokenStream2) -> SynResult { 78 | let item_fn = syn::parse2::(item)?; 79 | let macro_args = TestAttributes::from_list(&NestedMeta::parse_meta_list(attr)?)?; 80 | 81 | build_contracts(); 82 | 83 | let fn_vis = item_fn.vis; 84 | let fn_attrs = item_fn.attrs; 85 | let fn_block = item_fn.block; 86 | let fn_name = item_fn.sig.ident; 87 | let fn_async = item_fn.sig.asyncness; 88 | let fn_generics = item_fn.sig.generics; 89 | let fn_output = item_fn.sig.output; 90 | let fn_const = item_fn.sig.constness; 91 | let fn_unsafety = item_fn.sig.unsafety; 92 | 93 | let sandbox = macro_args 94 | .sandbox 95 | .unwrap_or(syn::parse2(quote! { ::drink::minimal::MinimalSandbox })?); 96 | 97 | Ok(quote! { 98 | #[test] 99 | #(#fn_attrs)* 100 | #fn_vis #fn_async #fn_const #fn_unsafety fn #fn_name #fn_generics () #fn_output { 101 | let mut session = Session::<#sandbox>::default(); 102 | #fn_block 103 | } 104 | }) 105 | } 106 | 107 | /// Defines a contract bundle provider. 108 | /// 109 | /// # Requirements 110 | /// 111 | /// - Your crate cannot be part of a cargo workspace. 112 | /// - Your crate must have `drink` in its dependencies (and it shouldn't be renamed). 113 | /// - The attributed enum must not: 114 | /// - be generic 115 | /// - have variants 116 | /// - have any attributes conflicting with `#[derive(Copy, Clone, PartialEq, Eq, Debug)]` 117 | /// 118 | /// # Impact 119 | /// 120 | /// This macro is intended to be used as an attribute of some empty enum. It will build all 121 | /// contracts crates (with rules identical to those of `#[drink::test]`), and populate the decorated 122 | /// enum with variants, one per built contract. 123 | /// 124 | /// If the current crate is a contract crate, the enum will receive a method `local()` that returns 125 | /// the contract bundle for the current crate. 126 | /// 127 | /// Besides that, the enum will receive a method `bundle(self)` that returns the contract bundle 128 | /// for corresponding contract variant. 129 | /// 130 | /// Both methods return `DrinkResult`. 131 | /// 132 | /// # Example 133 | /// 134 | /// ```rust, ignore 135 | /// #[drink::contract_bundle_provider] 136 | /// enum BundleProvider {} 137 | /// 138 | /// fn testcase() { 139 | /// Session::::default() 140 | /// .deploy_bundle_and(BundleProvider::local()?, "new", NO_ARGS, NO_SALT, NO_ENDOWMENT) 141 | /// .deploy_bundle_and(BundleProvider::AnotherContract.bundle()?, "new", NO_ARGS, NO_SALT, NO_ENDOWMENT) 142 | /// .unwrap(); 143 | /// } 144 | /// ``` 145 | #[proc_macro_attribute] 146 | pub fn contract_bundle_provider(attr: TokenStream, item: TokenStream) -> TokenStream { 147 | match contract_bundle_provider_internal(attr.into(), item.into()) { 148 | Ok(ts) => ts.into(), 149 | Err(e) => e.to_compile_error().into(), 150 | } 151 | } 152 | 153 | /// Auxiliary function to enter ?-based error propagation. 154 | fn contract_bundle_provider_internal( 155 | _attr: TokenStream2, 156 | item: TokenStream2, 157 | ) -> SynResult { 158 | let enum_item = parse_bundle_enum(item)?; 159 | let bundle_registry = build_contracts(); 160 | Ok(bundle_registry.generate_bundle_provision(enum_item)) 161 | } 162 | 163 | fn parse_bundle_enum(item: TokenStream2) -> SynResult { 164 | let enum_item = syn::parse2::(item)?; 165 | 166 | if !enum_item.generics.params.is_empty() { 167 | return Err(syn::Error::new_spanned( 168 | enum_item.generics.params, 169 | "ContractBundleProvider must not be generic", 170 | )); 171 | } 172 | if !enum_item.variants.is_empty() { 173 | return Err(syn::Error::new_spanned( 174 | enum_item.variants, 175 | "ContractBundleProvider must not have variants", 176 | )); 177 | } 178 | 179 | Ok(enum_item) 180 | } 181 | -------------------------------------------------------------------------------- /drink/test-resources/dummy.polkavm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/use-ink/drink/51beb813a4f345626ec3c1f9f9e7d5ea7a27a0e8/drink/test-resources/dummy.polkavm -------------------------------------------------------------------------------- /examples/chain-extension/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "chain-extension" 3 | authors = ["Cardinal", "Aleph Zero Foundation", "Use Ink "] 4 | edition = "2021" 5 | homepage = "https://alephzero.org" 6 | repository = "https://github.com/use-ink/drink" 7 | version = "0.1.0" 8 | 9 | [lib] 10 | path = "src/lib.rs" 11 | 12 | [dependencies] 13 | ink = { version = "6.0.0-alpha", features = ["unstable-hostfn"], default-features = false } 14 | 15 | scale = { package = "parity-scale-codec", version = "3", default-features = false, features = [ 16 | "derive", 17 | ] } 18 | scale-info = { version = "2.11.1", default-features = false, features = [ 19 | "derive", 20 | ], optional = true } 21 | 22 | [dev-dependencies] 23 | drink = { path = "../../drink" } 24 | 25 | # If you are creating a custom runtime with `drink::create_sandbox!` macro, unfortunately you need to 26 | # include these dependencies manually. Versions should match the ones in `Cargo.toml` of the `drink` crate. 27 | frame-system = { version = "40.1.0" } 28 | frame-support = { version = "40.1.0" } 29 | 30 | [features] 31 | default = ["std"] 32 | std = ["ink/std", "scale/std", "scale-info/std"] 33 | ink-as-dependency = [] 34 | -------------------------------------------------------------------------------- /examples/chain-extension/README.md: -------------------------------------------------------------------------------- 1 | # Testing a chain extension 2 | 3 | TODO: Replace with precompile example since chain extensions are deprecated in pallet-revive. 4 | 5 | This example shows how you can use `drink` to test a chain extension. 6 | 7 | From the perspective of a contract implementation or writing tests there is nothing special (i.e., more than usual) that you have to do to interact with a chain extension. 8 | The thing that `drink` makes easier for you is combining an arbitrary chain extension with `drink`'s `MinimalSandbox`. 9 | By simply calling: 10 | 11 | ```rust 12 | create_sandbox!( 13 | SandboxWithCustomChainExtension, 14 | path::to::MyCustomChainExtension 15 | ); 16 | ``` 17 | 18 | you are provided with a `Sandbox` with a runtime that contains your custom chain extension and can be used to test your contract like: 19 | 20 | ```rust 21 | Session::::default() 22 | .deploy_bundle_and(...)? 23 | .call(...)? 24 | ``` 25 | -------------------------------------------------------------------------------- /examples/chain-extension/src/chain_extension_ink_side.rs: -------------------------------------------------------------------------------- 1 | /* 2 | use ink::env::{chain_extension::FromStatusCode, DefaultEnvironment, Environment}; 3 | 4 | /// Simple chain extension that provides some staking information. 5 | #[ink::chain_extension(extension = 0)] 6 | pub trait StakingExtension { 7 | type ErrorCode = StakingExtensionErrorCode; 8 | 9 | /// Returns the number of the validators. 10 | #[ink(function = 41, handle_status = false)] 11 | fn get_num_of_validators() -> u32; 12 | } 13 | 14 | #[derive(Copy, Clone, PartialEq, Eq, Debug, scale::Encode, scale::Decode)] 15 | pub struct StakingExtensionErrorCode(u32); 16 | impl FromStatusCode for StakingExtensionErrorCode { 17 | fn from_status_code(status_code: u32) -> Result<(), Self> { 18 | match status_code { 19 | 0 => Ok(()), 20 | _ => Err(Self(status_code)), 21 | } 22 | } 23 | } 24 | 25 | /// Default ink environment with `StakingExtension` included. 26 | #[derive(Debug, Copy, Clone, PartialEq, Eq, scale::Encode, scale::Decode)] 27 | #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] 28 | pub enum StakingEnvironment {} 29 | 30 | impl Environment for StakingEnvironment { 31 | const MAX_EVENT_TOPICS: usize = ::MAX_EVENT_TOPICS; 32 | 33 | type AccountId = ::AccountId; 34 | type Balance = ::Balance; 35 | type Hash = ::Hash; 36 | type Timestamp = ::Timestamp; 37 | type BlockNumber = ::BlockNumber; 38 | type EventRecord = (); 39 | 40 | type ChainExtension = StakingExtension; 41 | } 42 | */ -------------------------------------------------------------------------------- /examples/chain-extension/src/chain_extension_runtime_side.rs: -------------------------------------------------------------------------------- 1 | /* 2 | use drink::pallet_revive::{ 3 | chain_extension::{ChainExtension, Config as ContractsConfig, Environment, Ext, RetVal}, 4 | wasm::Memory, 5 | }; 6 | use scale::Encode; 7 | 8 | use crate::CHAIN_EXTENSION_RETURN_VALUE; 9 | 10 | /// Simple chain extension that provides some mocked data. 11 | #[derive(Default)] 12 | pub struct StakingExtension; 13 | 14 | impl ChainExtension for StakingExtension { 15 | fn call, M: ?Sized + Memory>( 16 | &mut self, 17 | env: Environment, 18 | ) -> drink::pallet_revive::chain_extension::Result { 19 | // Ensure that the contract called extension method with id `41`. 20 | assert_eq!(env.func_id(), 41); 21 | 22 | // Write fixed result of the extension call into the return buffer. 23 | env.buf_in_buf_out() 24 | .write(&CHAIN_EXTENSION_RETURN_VALUE.encode(), false, None) 25 | .expect("Failed to write result"); 26 | 27 | // Return `Converging(0)` to indicate that the extension call has finished successfully. 28 | Ok(RetVal::Converging(0)) 29 | } 30 | } 31 | */ -------------------------------------------------------------------------------- /examples/chain-extension/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(not(feature = "std"), no_std, no_main)] 2 | 3 | //! TODO: Replace with precompile example since chain extensions are deprecated in pallet-revive. 4 | 5 | /* 6 | /// Fixed value returned by the example chain extension. 7 | #[cfg(test)] 8 | const CHAIN_EXTENSION_RETURN_VALUE: u32 = 100; 9 | 10 | /// Here we put ink-side part of the example chain extension. 11 | mod chain_extension_ink_side; 12 | 13 | /// Here we put runtime-side part of the example chain extension. 14 | #[cfg(test)] 15 | mod chain_extension_runtime_side; 16 | 17 | /// Simple ink! smart contract that calls a chain extension. 18 | #[ink::contract(env = StakingEnvironment)] 19 | mod contract_calling_chain_extension { 20 | use crate::chain_extension_ink_side::StakingEnvironment; 21 | 22 | #[ink(storage)] 23 | pub struct ContractCallingChainExtension {} 24 | 25 | impl ContractCallingChainExtension { 26 | #[allow(clippy::new_without_default)] 27 | #[ink(constructor)] 28 | pub fn new() -> Self { 29 | Self {} 30 | } 31 | 32 | #[ink(message)] 33 | pub fn call_ce(&self) -> u32 { 34 | self.env().extension().get_num_of_validators() 35 | } 36 | } 37 | } 38 | 39 | #[cfg(test)] 40 | mod tests { 41 | use drink::{ 42 | create_sandbox, 43 | session::{Session, NO_ARGS, NO_ENDOWMENT, NO_SALT}, 44 | }; 45 | 46 | use crate::CHAIN_EXTENSION_RETURN_VALUE; 47 | 48 | #[drink::contract_bundle_provider] 49 | enum BundleProvider {} 50 | 51 | // We can inject arbitrary chain extension into the minimal runtime as follows: 52 | create_sandbox!( 53 | SandboxWithCE, 54 | crate::chain_extension_runtime_side::StakingExtension, 55 | () 56 | ); 57 | 58 | /// Test that we can call chain extension from ink! contract and get a correct result. 59 | #[drink::test(sandbox = SandboxWithCE)] 60 | fn we_can_test_chain_extension(mut session: Session) -> Result<(), Box> { 61 | let result: u32 = session 62 | .deploy_bundle_and( 63 | BundleProvider::local()?, 64 | "new", 65 | NO_ARGS, 66 | NO_SALT, 67 | NO_ENDOWMENT, 68 | )? 69 | .call("call_ce", NO_ARGS, NO_ENDOWMENT)??; 70 | 71 | assert_eq!(result, CHAIN_EXTENSION_RETURN_VALUE); 72 | 73 | Ok(()) 74 | } 75 | } 76 | */ -------------------------------------------------------------------------------- /examples/contract-events/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "contract-events" 3 | authors = ["Cardinal"] 4 | edition = "2021" 5 | homepage = "https://alephzero.org" 6 | repository = "https://github.com/use-ink/drink" 7 | version = "0.1.0" 8 | 9 | [lib] 10 | path = "lib.rs" 11 | 12 | [dependencies] 13 | ink = { version = "6.0.0-alpha", features = ["unstable-hostfn"], default-features = false } 14 | 15 | scale = { package = "parity-scale-codec", version = "3", default-features = false, features = [ 16 | "derive", 17 | ] } 18 | scale-info = { version = "2.11.1", default-features = false, features = [ 19 | "derive", 20 | ], optional = true } 21 | 22 | [dev-dependencies] 23 | drink = { path = "../../drink" } 24 | 25 | [features] 26 | default = ["std"] 27 | std = ["ink/std", "scale/std", "scale-info/std"] 28 | ink-as-dependency = [] 29 | -------------------------------------------------------------------------------- /examples/contract-events/README.md: -------------------------------------------------------------------------------- 1 | # Contract events 2 | 3 | This example shows how we can extract events that were emitted by a contract. 4 | 5 | When you are working with a `Session` object, you can consult its `Record` - a data structure that collects all the results and events that have been produced while interacting with contracts. 6 | For example: 7 | 8 | ```rust 9 | let mut session = Session::::default(); 10 | // .. some contract interaction 11 | 12 | // `record` is a `Record` object that contains all the results and events that have been produced while interacting with contracts. 13 | let record = session.record(); 14 | ``` 15 | 16 | Given a `Record` object, we can extract the results of the contract interaction: 17 | 18 | ```rust 19 | // `deploy_returns` returns a vector of contract addresses that have been deployed during the session. 20 | let all_deployed_contracts = record.deploy_returns(); 21 | // `last_call_return_decoded` returns the decoded return value of the last contract call. 22 | let last_call_value = record.last_call_return_decoded::(); 23 | ``` 24 | 25 | as well as the events that have been emitted by contracts: 26 | 27 | ```rust 28 | // `last_event_batch` returns the batch of runtime events that have been emitted during last contract interaction. 29 | let last_event_batch = record.last_event_batch(); 30 | // We can filter out raw events emitted by contracts with `contract_events` method. 31 | let contract_events_data = last_event_batch.contract_events(); 32 | ``` 33 | -------------------------------------------------------------------------------- /examples/contract-events/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(not(feature = "std"), no_std, no_main)] 2 | 3 | #[ink::contract] 4 | mod flipper { 5 | #[ink::event] 6 | pub struct Flipped { 7 | new_value: bool, 8 | } 9 | 10 | #[ink(storage)] 11 | pub struct Flipper { 12 | value: bool, 13 | } 14 | 15 | impl Flipper { 16 | #[ink(constructor)] 17 | pub fn new(init: bool) -> Self { 18 | Self { value: init } 19 | } 20 | 21 | #[ink(message)] 22 | pub fn flip(&mut self) { 23 | self.value = !self.value; 24 | self.env().emit_event(Flipped { 25 | new_value: self.value, 26 | }); 27 | } 28 | 29 | #[ink(message)] 30 | pub fn get(&self) -> bool { 31 | self.value 32 | } 33 | } 34 | } 35 | 36 | #[cfg(test)] 37 | mod tests { 38 | use std::error::Error; 39 | 40 | use drink::session::{Session, NO_ARGS, NO_ENDOWMENT}; 41 | 42 | #[drink::contract_bundle_provider] 43 | enum BundleProvider {} 44 | 45 | #[drink::test] 46 | fn we_can_inspect_emitted_events(mut session: Session) -> Result<(), Box> { 47 | let bundle = BundleProvider::local()?; 48 | 49 | // Firstly, we deploy the contract and call its `flip` method. 50 | session.deploy_bundle(bundle.clone(), "new", &["false"], None, NO_ENDOWMENT)?; 51 | let _ = session.call::<_, ()>("flip", NO_ARGS, NO_ENDOWMENT)??; 52 | 53 | // Now we can inspect the emitted events. 54 | let record = session.record(); 55 | let contract_events = record 56 | .last_event_batch() 57 | // We can use the `contract_events_decoded` method to decode the events into 58 | // `contract_transcode::Value` objects. 59 | .contract_events_decoded(&bundle.transcoder); 60 | 61 | assert_eq!(contract_events.len(), 1); 62 | println!("flip_event: {:?}", &contract_events[0]); 63 | 64 | Ok(()) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /examples/cross-contract-call-tracing/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cross-contract-call-tracing" 3 | authors = ["Cardinal"] 4 | edition = "2021" 5 | homepage = "https://alephzero.org" 6 | repository = "https://github.com/use-ink/drink" 7 | version = "0.1.0" 8 | 9 | [dependencies] 10 | ink = { version = "6.0.0-alpha", default-features = false } 11 | 12 | scale = { package = "parity-scale-codec", version = "3", default-features = false, features = [ 13 | "derive", 14 | ] } 15 | scale-info = { version = "2.11.1", default-features = false, features = [ 16 | "derive", 17 | ], optional = true } 18 | 19 | [dev-dependencies] 20 | drink = { path = "../../drink" } 21 | 22 | [lib] 23 | path = "lib.rs" 24 | 25 | [features] 26 | default = ["std"] 27 | std = ["ink/std", "scale/std", "scale-info/std"] 28 | ink-as-dependency = [] 29 | -------------------------------------------------------------------------------- /examples/cross-contract-call-tracing/README.md: -------------------------------------------------------------------------------- 1 | # Cross contract call tracing 2 | 3 | This example shows how you can trace and debug cross contract calls. 4 | 5 | ## Scenario 6 | 7 | Here we have a single contract with 3 methods: 8 | - `call_inner(arg: u32)`: returns the result of some simple computation on `arg` 9 | - `call_middle(next_callee: AccountId, arg: u32)`: calls `call_inner(arg)` at `next_callee` and forwards the result 10 | - `call_outer(next_callee: AccountId, next_next_callee: AccountId, arg: u32)`: calls `call_middle(next_next_callee, arg)` at `next_callee` and forwards the result 11 | 12 | We deploy three instances of this contract, `inner`, `middle` and `outer`, and call `call_outer` on `outer` with `inner` and `middle` and some integer as arguments. 13 | 14 | If we were using just `cargo-contract` or some other tooling, we would be able to see only the final result of the call. 15 | However, it wouldn't be possible to trace the intermediate steps. 16 | With `drink`, we can provide handlers for (synchronous) observing every level of the call stack. 17 | 18 | ## Running 19 | 20 | ```bash 21 | cargo contract build --release 22 | cargo test --release -- --show-output 23 | ``` 24 | 25 | You should be able to see similar output: 26 | ``` 27 | Contract at address `5CmHh6aBH6YZLjHGHjVtDDU4PfvDvk9s8n5xAcZQajxikksr` has been called with data: 28 | new 29 | and returned: 30 | () 31 | 32 | Contract at address `5FNvS4rLX8Y5NotoRzyBpmeNq2cfcSRpBWbHvgNrEiY3ero7` has been called with data: 33 | new 34 | and returned: 35 | () 36 | 37 | Contract at address `5DhNNsxhPMhg8R7StY3LbHraQWTDRFEbK2C1CaAD2AGvDCAf` has been called with data: 38 | new 39 | and returned: 40 | () 41 | 42 | Contract at address `5DhNNsxhPMhg8R7StY3LbHraQWTDRFEbK2C1CaAD2AGvDCAf` has been called with data: 43 | inner_call { arg: 7 } 44 | and returned: 45 | Ok(22) 46 | 47 | Contract at address `5FNvS4rLX8Y5NotoRzyBpmeNq2cfcSRpBWbHvgNrEiY3ero7` has been called with data: 48 | middle_call { next_callee: 5DhNNsxhPMhg8R7StY3LbHraQWTDRFEbK2C1CaAD2AGvDCAf, arg: 7 } 49 | and returned: 50 | Ok(22) 51 | 52 | Contract at address `5CmHh6aBH6YZLjHGHjVtDDU4PfvDvk9s8n5xAcZQajxikksr` has been called with data: 53 | outer_call { next_callee: 5FNvS4rLX8Y5NotoRzyBpmeNq2cfcSRpBWbHvgNrEiY3ero7, next_next_callee: 5DhNNsxhPMhg8R7StY3LbHraQWTDRFEbK2C1CaAD2AGvDCAf, arg: 7 } 54 | and returned: 55 | Ok(22) 56 | 57 | 58 | successes: 59 | tests::test 60 | ``` -------------------------------------------------------------------------------- /examples/cross-contract-call-tracing/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(not(feature = "std"), no_std, no_main)] 2 | 3 | #[ink::contract] 4 | mod contract { 5 | use ink::{ 6 | env::{ 7 | call::{build_call, ExecutionInput, Selector}, 8 | DefaultEnvironment, 9 | }, 10 | H160, U256, 11 | }; 12 | 13 | #[ink(storage)] 14 | pub struct CrossCallingContract; 15 | 16 | impl CrossCallingContract { 17 | #[ink(constructor)] 18 | #[allow(clippy::new_without_default)] 19 | pub fn new() -> Self { 20 | Self {} 21 | } 22 | 23 | #[ink(message)] 24 | pub fn outer_call(&self, next_callee: H160, next_next_callee: H160, arg: u32) -> u32 { 25 | build_call::() 26 | .call(next_callee) 27 | .transferred_value(U256::zero()) 28 | .exec_input( 29 | ExecutionInput::new(Selector::new(ink::selector_bytes!("middle_call"))) 30 | .push_arg(next_next_callee) 31 | .push_arg(arg), 32 | ) 33 | .returns::() 34 | .invoke() 35 | } 36 | 37 | #[ink(message)] 38 | pub fn middle_call(&self, next_callee: H160, arg: u32) -> u32 { 39 | build_call::() 40 | .call(next_callee) 41 | .transferred_value(U256::zero()) 42 | .exec_input( 43 | ExecutionInput::new(Selector::new(ink::selector_bytes!("inner_call"))) 44 | .push_arg(arg), 45 | ) 46 | .returns::() 47 | .invoke() 48 | } 49 | 50 | #[ink(message)] 51 | pub fn inner_call(&self, arg: u32) -> u32 { 52 | match arg % 2 { 53 | 0 => arg.checked_div(2).unwrap(), 54 | _ => 3_u32.saturating_mul(arg).saturating_add(1), 55 | } 56 | } 57 | } 58 | } 59 | 60 | // TODO: Deprecated due to `pallet-revive`. There is no `debug_message`. 61 | // 62 | // #[cfg(test)] 63 | // mod tests { 64 | // use std::{cell::RefCell, error::Error}; 65 | 66 | // use drink::{ 67 | // pallet_revive_debugging::{TracingExt, TracingExtT}, 68 | // session::{contract_transcode::Value, Session, NO_ARGS, NO_ENDOWMENT}, 69 | // }; 70 | // use ink::{storage::traits::Storable, H160}; 71 | 72 | // #[drink::contract_bundle_provider] 73 | // enum BundleProvider {} 74 | 75 | // thread_local! { 76 | // static OUTER_ADDRESS: RefCell> = RefCell::new(None); 77 | // static MIDDLE_ADDRESS: RefCell> = RefCell::new(None); 78 | // static INNER_ADDRESS: RefCell> = RefCell::new(None); 79 | // } 80 | 81 | // struct TestDebugger; 82 | // impl TracingExtT for TestDebugger { 83 | // fn after_call( 84 | // &self, 85 | // contract_address: Vec, 86 | // is_call: bool, 87 | // input_data: Vec, 88 | // result: Vec, 89 | // ) { 90 | // let contract_address = H160::decode(&mut contract_address.as_slice()) 91 | // .expect("Failed to decode contract address"); 92 | // let transcoder = BundleProvider::local().unwrap().transcoder; 93 | 94 | // let data_decoded = if is_call { 95 | // transcoder.decode_contract_message(&mut input_data.as_slice()) 96 | // } else { 97 | // transcoder.decode_contract_constructor(&mut input_data.as_slice()) 98 | // } 99 | // .unwrap(); 100 | 101 | // let return_decoded = if is_call { 102 | // let call_name = if contract_address 103 | // == OUTER_ADDRESS.with(|a| a.borrow().clone().unwrap()) 104 | // { 105 | // "outer_call" 106 | // } else if contract_address == MIDDLE_ADDRESS.with(|a| a.borrow().clone().unwrap()) { 107 | // "middle_call" 108 | // } else if contract_address == INNER_ADDRESS.with(|a| a.borrow().clone().unwrap()) { 109 | // "inner_call" 110 | // } else { 111 | // panic!("Unexpected contract address") 112 | // }; 113 | 114 | // transcoder 115 | // .decode_message_return(call_name, &mut result.as_slice()) 116 | // .unwrap() 117 | // } else { 118 | // Value::Unit 119 | // }; 120 | 121 | // println!( 122 | // "Contract at address `{contract_address}` has been called with data: \ 123 | // \n {data_decoded}\nand returned:\n {return_decoded}\n" 124 | // ) 125 | // } 126 | // } 127 | 128 | // #[drink::test] 129 | // fn test(mut session: Session) -> Result<(), Box> { 130 | // session.set_storage_deposit_limit(1_000_000); 131 | 132 | // let outer_address = session.deploy_bundle( 133 | // BundleProvider::local()?, 134 | // "new", 135 | // NO_ARGS, 136 | // Some([1; 32]), 137 | // NO_ENDOWMENT, 138 | // )?; 139 | // OUTER_ADDRESS.with(|a| *a.borrow_mut() = Some(outer_address.clone())); 140 | // let middle_address = session.deploy_bundle( 141 | // BundleProvider::local()?, 142 | // "new", 143 | // NO_ARGS, 144 | // Some([2; 32]), 145 | // NO_ENDOWMENT, 146 | // )?; 147 | // MIDDLE_ADDRESS.with(|a| *a.borrow_mut() = Some(middle_address.clone())); 148 | // let inner_address = session.deploy_bundle( 149 | // BundleProvider::local()?, 150 | // "new", 151 | // NO_ARGS, 152 | // Some([3; 32]), 153 | // NO_ENDOWMENT, 154 | // )?; 155 | // INNER_ADDRESS.with(|a| *a.borrow_mut() = Some(inner_address.clone())); 156 | 157 | // let value: u32 = session.call_with_address( 158 | // outer_address, 159 | // "outer_call", 160 | // &[ 161 | // format!("{:?}", middle_address), 162 | // format!("{:?}", inner_address), 163 | // "7".to_string(), 164 | // ], 165 | // NO_ENDOWMENT, 166 | // )??; 167 | 168 | // assert_eq!(value, 22); 169 | 170 | // Ok(()) 171 | // } 172 | // } 173 | -------------------------------------------------------------------------------- /examples/dry-running/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dry-running" 3 | authors = ["Cardinal", "Aleph Zero Foundation", "Use Ink "] 4 | edition = "2021" 5 | homepage = "https://alephzero.org" 6 | repository = "https://github.com/use-ink/drink" 7 | version = "0.1.0" 8 | 9 | [dependencies] 10 | ink = { version = "6.0.0-alpha", default-features = false } 11 | 12 | [dev-dependencies] 13 | drink = { path = "../../drink" } 14 | 15 | [lib] 16 | path = "lib.rs" 17 | 18 | [features] 19 | default = ["std"] 20 | std = ["ink/std"] 21 | ink-as-dependency = [] 22 | -------------------------------------------------------------------------------- /examples/dry-running/README.md: -------------------------------------------------------------------------------- 1 | # Dry running 2 | 3 | This example shows how we can dry run both contract interactions (deployment and calls) and standard runtime transactions. 4 | 5 | ## Running 6 | 7 | ```bash 8 | cargo test --release 9 | ``` 10 | -------------------------------------------------------------------------------- /examples/dry-running/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(not(feature = "std"), no_std, no_main)] 2 | 3 | #[ink::contract] 4 | mod counter { 5 | #[ink(storage)] 6 | pub struct Counter { 7 | value: u32, 8 | } 9 | 10 | impl Counter { 11 | #[ink(constructor)] 12 | pub fn new(init: u32) -> Self { 13 | assert!(init < 10); 14 | Self { value: init } 15 | } 16 | 17 | #[ink(message)] 18 | pub fn increment(&mut self) { 19 | self.value = self.value.saturating_add(1); 20 | } 21 | 22 | #[ink(message)] 23 | pub fn get(&self) -> u32 { 24 | self.value 25 | } 26 | } 27 | } 28 | 29 | #[cfg(test)] 30 | mod tests { 31 | use drink::{ 32 | frame_support::sp_runtime::ModuleError, 33 | minimal::{MinimalSandbox, RuntimeCall}, 34 | pallet_balances, 35 | sandbox_api::prelude::*, 36 | session::{Session, NO_ARGS, NO_ENDOWMENT, NO_SALT}, 37 | AccountId32, DispatchError, Sandbox, 38 | }; 39 | 40 | #[drink::contract_bundle_provider] 41 | enum BundleProvider {} 42 | 43 | #[drink::test] 44 | fn we_can_dry_run_contract_interactions( 45 | mut session: Session, 46 | ) -> Result<(), Box> { 47 | // Firstly, let us dry-run contract instantiation with an incorrect constructor argument. 48 | let result = session.dry_run_deployment( 49 | BundleProvider::local()?, 50 | "new", 51 | &["10"], 52 | NO_SALT, 53 | NO_ENDOWMENT, 54 | )?; 55 | 56 | // Ensure that the contract was trapped. 57 | assert!(matches!( 58 | result.result, 59 | Err(DispatchError::Module(ModuleError{message: Some(error), ..})) if error == "ContractTrapped" 60 | )); 61 | // Ensure that no events were emitted. 62 | assert!(session.record().event_batches().is_empty()); 63 | 64 | // Now, let deploy the contract with a correct constructor argument. 65 | let address = session.deploy_bundle( 66 | BundleProvider::local()?, 67 | "new", 68 | &["5"], 69 | NO_SALT, 70 | NO_ENDOWMENT, 71 | )?; 72 | // Ensure that deployment triggered event emission. 73 | assert!(!session.record().event_batches().is_empty()); 74 | 75 | // Now, let us dry-run a contract call. 76 | let result = session.dry_run_call(address.clone(), "increment", NO_ARGS, NO_ENDOWMENT)?; 77 | // We can check the estimated gas consumption. 78 | let gas_estimation = result.gas_consumed; 79 | 80 | // In the end, we can execute the call and verify gas consumption. 81 | session.call_with_address::<_, ()>(address, "increment", NO_ARGS, NO_ENDOWMENT)??; 82 | let gas_consumption = session.record().last_call_result().gas_consumed; 83 | 84 | assert_eq!(gas_estimation, gas_consumption); 85 | 86 | Ok(()) 87 | } 88 | 89 | #[test] 90 | fn we_can_dry_run_normal_runtime_transaction() { 91 | let mut sandbox = MinimalSandbox::default(); 92 | 93 | // Bob will be the recipient of the transfer. 94 | let bob = AccountId32::new([2u8; 32]); 95 | 96 | // Recipient's balance before the transfer. 97 | let initial_balance = sandbox.free_balance(&bob); 98 | 99 | // Dry-run the transaction. 100 | sandbox.dry_run(|sandbox| { 101 | sandbox 102 | .runtime_call( 103 | RuntimeCall::Balances(pallet_balances::Call::transfer_allow_death { 104 | dest: bob.clone().into(), 105 | value: 100, 106 | }), 107 | Some(MinimalSandbox::default_actor()), 108 | ) 109 | .expect("Failed to execute a call") 110 | }); 111 | 112 | // At the end, the balance of the recipient should remain unchanged and no events should have been emitted. 113 | assert_eq!(sandbox.free_balance(&bob), initial_balance); 114 | assert!(sandbox.events().is_empty()); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /examples/flipper/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "flipper" 3 | authors = ["Cardinal", "Aleph Zero Foundation", "Use Ink "] 4 | edition = "2021" 5 | homepage = "https://alephzero.org" 6 | repository = "https://github.com/use-ink/drink" 7 | version = "0.1.0" 8 | 9 | [dependencies] 10 | ink = { version = "6.0.0-alpha", default-features = false } 11 | 12 | [dev-dependencies] 13 | drink = { path = "../../drink" } 14 | 15 | [lib] 16 | path = "lib.rs" 17 | 18 | [features] 19 | default = ["std"] 20 | std = ["ink/std"] 21 | ink-as-dependency = [] 22 | -------------------------------------------------------------------------------- /examples/flipper/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(not(feature = "std"), no_std, no_main)] 2 | 3 | #[ink::contract] 4 | mod flipper { 5 | #[ink(storage)] 6 | pub struct Flipper { 7 | value: bool, 8 | } 9 | 10 | impl Flipper { 11 | #[ink(constructor)] 12 | pub fn new(init: bool) -> Self { 13 | Self { value: init } 14 | } 15 | 16 | #[ink(message)] 17 | pub fn flip(&mut self) { 18 | self.value = !self.value; 19 | } 20 | 21 | #[ink(message)] 22 | pub fn get(&self) -> bool { 23 | self.value 24 | } 25 | } 26 | } 27 | 28 | #[cfg(test)] 29 | mod tests { 30 | use std::error::Error; 31 | 32 | use drink::session::{Session, NO_ARGS, NO_ENDOWMENT, NO_SALT}; 33 | 34 | #[drink::contract_bundle_provider] 35 | enum BundleProvider {} 36 | 37 | #[drink::test] 38 | fn initialization(mut session: Session) -> Result<(), Box> { 39 | let contract = BundleProvider::local()?; 40 | let init_value: bool = session 41 | .deploy_bundle_and(contract, "new", &["true"], NO_SALT, NO_ENDOWMENT)? 42 | .call_and("get", NO_ARGS, NO_ENDOWMENT)? 43 | .record() 44 | .last_call_return_decoded()? 45 | .expect("Call was successful"); 46 | 47 | assert_eq!(init_value, true); 48 | 49 | Ok(()) 50 | } 51 | 52 | #[drink::test] 53 | fn flipping(mut session: Session) -> Result<(), Box> { 54 | let contract = BundleProvider::Flipper.bundle()?; 55 | let init_value: bool = session 56 | .deploy_bundle_and(contract, "new", &["true"], NO_SALT, NO_ENDOWMENT)? 57 | .call_and("flip", NO_ARGS, NO_ENDOWMENT)? 58 | .call_and("flip", NO_ARGS, NO_ENDOWMENT)? 59 | .call_and("flip", NO_ARGS, NO_ENDOWMENT)? 60 | .call_and("get", NO_ARGS, NO_ENDOWMENT)? 61 | .record() 62 | .last_call_return_decoded()? 63 | .expect("Call was successful"); 64 | 65 | assert_eq!(init_value, false); 66 | 67 | Ok(()) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /examples/mocking/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mocking" 3 | authors = ["Cardinal", "Aleph Zero Foundation", "Use Ink "] 4 | edition = "2021" 5 | homepage = "https://alephzero.org" 6 | repository = "https://github.com/use-ink/drink" 7 | version = "0.1.0" 8 | 9 | [dependencies] 10 | ink = { version = "6.0.0-alpha", default-features = false } 11 | 12 | [dev-dependencies] 13 | drink = { path = "../../drink" } 14 | 15 | [lib] 16 | path = "lib.rs" 17 | 18 | [features] 19 | default = ["std"] 20 | std = ["ink/std"] 21 | ink-as-dependency = [] 22 | -------------------------------------------------------------------------------- /examples/mocking/README.md: -------------------------------------------------------------------------------- 1 | # Mocking contracts 2 | 3 | This example shows how we can easily mock contracts with the `drink!` library. 4 | 5 | ## Scenario 6 | 7 | Say we want to test a contract that simply forwards call to another contract (i.e. a _proxy_ pattern). 8 | Our contract has a single message `forward_call(AccountId) -> (u8, u8)`. 9 | We want to test that this proxy correctly calls the callee (with some fixed selector) and returns the unchanged result (a pair of two `u8`). 10 | 11 | Normally, we would have to implement and build a mock contract that would be deployed alongside the tested contract. 12 | With drink, we can simply mock the logic with some closures and test our contract in isolation. 13 | 14 | ## Running 15 | 16 | ```bash 17 | cargo contract build --release 18 | cargo test --release 19 | ``` 20 | -------------------------------------------------------------------------------- /examples/mocking/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(not(feature = "std"), no_std, no_main)] 2 | 3 | /// This is a fixed selector of the `callee` message. 4 | const CALLEE_SELECTOR: [u8; 4] = ink::selector_bytes!("callee"); 5 | 6 | #[ink::contract] 7 | mod proxy { 8 | use ink::{ 9 | env::{ 10 | call::{build_call, ExecutionInput}, 11 | DefaultEnvironment, 12 | }, 13 | H160, U256, 14 | }; 15 | 16 | use crate::CALLEE_SELECTOR; 17 | 18 | #[ink(storage)] 19 | pub struct Proxy {} 20 | 21 | impl Proxy { 22 | #[ink(constructor)] 23 | #[allow(clippy::new_without_default)] 24 | pub fn new() -> Self { 25 | Self {} 26 | } 27 | 28 | /// Calls `callee` with the selector `CALLEE_SELECTOR` and forwards the result. 29 | #[ink(message)] 30 | pub fn forward_call(&self, callee: H160) -> (u8, u8) { 31 | build_call::() 32 | .call(callee) 33 | .transferred_value(U256::zero()) 34 | .exec_input(ExecutionInput::new(CALLEE_SELECTOR.into())) 35 | .returns::<(u8, u8)>() 36 | .invoke() 37 | } 38 | } 39 | } 40 | 41 | #[cfg(test)] 42 | mod tests { 43 | use std::error::Error; 44 | 45 | use drink::{ 46 | mock_message, 47 | session::{mocking_api::MockingApi, Session, NO_ARGS, NO_ENDOWMENT, NO_SALT}, 48 | ContractMock, 49 | }; 50 | 51 | use crate::CALLEE_SELECTOR; 52 | 53 | #[drink::contract_bundle_provider] 54 | enum BundleProvider {} 55 | 56 | #[drink::test] 57 | fn call_mocked_message(mut session: Session) -> Result<(), Box> { 58 | // Firstly, we create the mocked contract. 59 | const RETURN_VALUE: (u8, u8) = (4, 1); 60 | let mocked_contract = 61 | ContractMock::new().with_message(CALLEE_SELECTOR, mock_message(|()| RETURN_VALUE)); 62 | 63 | // Secondly, we deploy it, similarly to a standard deployment action. 64 | let mock_address = session.mocking_api().deploy(mocked_contract); 65 | 66 | // TODO: Deprecated due to `pallet-revive`. There is no `debug_message`. 67 | // 68 | // // Now, we can deploy our proper contract and verify its behavior. 69 | // let result: (u8, u8) = session 70 | // .deploy_bundle_and(BundleProvider::local()?, "new", NO_ARGS, NO_SALT, None)? 71 | // .call_and( 72 | // "forward_call", 73 | // &[format!("{:?}", mock_address)], 74 | // NO_ENDOWMENT, 75 | // )? 76 | // .record() 77 | // .last_call_return_decoded()? 78 | // .expect("Call was successful"); 79 | // assert_eq!(result, RETURN_VALUE); 80 | 81 | Ok(()) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /examples/multiple-contracts/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "multiple-contracts" 3 | authors = ["Cardinal", "Aleph Zero Foundation", "Use Ink "] 4 | edition = "2021" 5 | homepage = "https://alephzero.org" 6 | repository = "https://github.com/use-ink/drink" 7 | version = "0.1.0" 8 | 9 | [dependencies] 10 | ink = { version = "6.0.0-alpha", default-features = false } 11 | flipper = { path = "../flipper", default-features = false, features = [ 12 | "ink-as-dependency", 13 | ] } 14 | 15 | [dev-dependencies] 16 | drink = { path = "../../drink" } 17 | 18 | [lib] 19 | path = "lib.rs" 20 | 21 | [features] 22 | default = ["std"] 23 | std = ["ink/std", "flipper/std"] 24 | ink-as-dependency = [] 25 | -------------------------------------------------------------------------------- /examples/multiple-contracts/README.md: -------------------------------------------------------------------------------- 1 | # Multiple contracts 2 | 3 | You can easily work with multiple contracts at the same time, even if they are not part of the same project. 4 | 5 | Both `#[drink::contract_bundle_provider]` and `#[drink::test]` macros take care of building all the contract crates that you declare in `Cargo.toml`. 6 | Therefore, even if you are testing a huge suite of dapps, the only thing you have to do is to run 7 | ```rust 8 | cargo test --release 9 | ``` 10 | 11 | ## Scenario 12 | 13 | We will use Flipper library as a dependency contract. 14 | Simply declare it in `Cargo.toml`: 15 | ```toml 16 | flipper = { path = "../flipper", default-features = false, features = [ 17 | "ink-as-dependency", 18 | ] } 19 | ``` 20 | 21 | As usual, we have to include `ink-as-dependency` feature to use a contract as a dependency. 22 | 23 | Locally, we have a contract that keeps two addresses: 24 | - a deployed Flipper contract's address 25 | - a user's address 26 | 27 | The contract has a single message `check() -> bool`, which queries the Flipper contract for the current flipped value. 28 | -------------------------------------------------------------------------------- /examples/multiple-contracts/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(not(feature = "std"), no_std, no_main)] 2 | 3 | #[ink::contract] 4 | mod checker { 5 | use ink::{ 6 | env::{ 7 | call::{build_call, ExecutionInput, Selector}, 8 | DefaultEnvironment, 9 | }, 10 | H160, 11 | }; 12 | 13 | #[ink(storage)] 14 | pub struct Checker { 15 | contract: H160, 16 | } 17 | 18 | impl Checker { 19 | #[ink(constructor)] 20 | pub fn new(contract: H160) -> Self { 21 | Self { contract } 22 | } 23 | 24 | #[ink(message)] 25 | pub fn check(&self) -> bool { 26 | build_call::() 27 | .call(self.contract) 28 | .exec_input(ExecutionInput::new(Selector::new(ink::selector_bytes!( 29 | "get" 30 | )))) 31 | .returns::() 32 | .invoke() 33 | } 34 | } 35 | } 36 | 37 | #[cfg(test)] 38 | mod tests { 39 | use std::error::Error; 40 | 41 | use drink::session::{Session, NO_ARGS, NO_ENDOWMENT}; 42 | 43 | #[drink::contract_bundle_provider] 44 | enum BundleProvider {} 45 | 46 | #[drink::test] 47 | fn contracts_work_correctly(mut session: Session) -> Result<(), Box> { 48 | let contract = session.deploy_bundle( 49 | BundleProvider::Flipper.bundle()?, 50 | "new", 51 | &["true"], 52 | Some([1; 32]), 53 | NO_ENDOWMENT, 54 | )?; 55 | 56 | let _checker_contract = session.deploy_bundle( 57 | BundleProvider::local()?, 58 | "new", 59 | &[format!("{:?}", contract)], 60 | Some([2; 32]), 61 | NO_ENDOWMENT, 62 | )?; 63 | 64 | let value: bool = session.call("check", NO_ARGS, NO_ENDOWMENT)??; 65 | assert!(value); 66 | 67 | Ok(()) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /examples/quick-start-with-drink/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "quick-start-with-drink" 3 | authors = ["Cardinal"] 4 | edition = "2021" 5 | homepage = "https://alephzero.org" 6 | repository = "https://github.com/use-ink/drink" 7 | version = "0.1.0" 8 | 9 | [lib] 10 | path = "lib.rs" 11 | 12 | [dependencies] 13 | # We use standard dependencies for an ink! smart-contract. 14 | 15 | # For debugging from contract, we enable the `ink-debug` feature of `ink` crate. 16 | ink = { version = "6.0.0-alpha", features = ["ink-debug"], default-features = false } 17 | 18 | [dev-dependencies] 19 | # For testing purposes we bring the `drink` library. 20 | drink = { path = "../../drink" } 21 | 22 | [features] 23 | default = ["std"] 24 | std = ["ink/std"] 25 | # If the current crate defines a smart contract that we want to test, we can't forget to have `ink-as-dependency` 26 | # feature declared. This is how `#[drink::test]` and `#[drink::contract_bundle_provider]` discovers contracts to be 27 | # built. 28 | ink-as-dependency = [] 29 | -------------------------------------------------------------------------------- /examples/quick-start-with-drink/README.md: -------------------------------------------------------------------------------- 1 | # Quick start with `drink` library 2 | 3 | This is a quick start guide introducing you to smart contract testing with `drink` library. 4 | We will see how to write tests for a simple smart contract and make use of `drink`'s features. 5 | 6 | ## Prerequisites 7 | 8 | You only need Rust installed (see [here](https://www.rust-lang.org/tools/install) for help). 9 | Drink is developed and tested with stable Rust. 10 | 11 | ## Dependencies 12 | 13 | You only need the `drink` library brought into your project: 14 | 15 | ```toml 16 | drink = { version = "0.19.0-alpha" } 17 | ``` 18 | 19 | See [Cargo.toml](Cargo.toml) for a typical cargo setup of a single-contract project. 20 | 21 | ## Writing tests 22 | 23 | ### Preparing contracts 24 | 25 | For every contract that you want to interact with from your tests, you need to create a _contract bundle_, which includes: 26 | 27 | - built contract artifact (`.polkavm` file), 28 | - contract transcoder (object based on the `.json` file, responsible for translating message arguments and results). 29 | 30 | The recommended way is to use `drink::contract_bundle_provider` macro, which will discover all the contract dependencies (including the current crate, if that is the case) and gather all contract bundles into a single registry. 31 | 32 | However, if needed, you can do it manually, by running `cargo contract build` for every such contract, and then, bring the artifacts into your tests. 33 | For this, you might want to use `drink::session::ContractBundle` API, which includes `ContractBundle::load` and `local_contract_file!` utilities. 34 | 35 | ### `drink` test macros 36 | 37 | `drink` provides a few macros to write tests for smart contracts: 38 | 39 | - `#[drink::test]` - which marks a function as a test function (similar to `#[test]`). 40 | - `#[drink::contract_bundle_provider]` - which gathers all contract artifacts into a single registry. 41 | 42 | While neither is required to write `drink` tests, they make it easier to write and maintain them. 43 | 44 | ### Writing tests 45 | 46 | Your typical test module will look like: 47 | 48 | ```rust 49 | #[cfg(test)] 50 | mod tests { 51 | use drink::session::{Session, NO_ARGS, NO_ENDOWMENT, NO_SALT}; 52 | 53 | #[drink::contract_bundle_provider] 54 | enum BundleProvider {} 55 | 56 | #[drink::test] 57 | fn deploy_and_call_a_contract(mut session: Session) -> Result<(), Box> { 58 | let result: bool = session 59 | .deploy_bundle_and(BundleProvider::local(), "new", &["true"], NO_SALT, NO_ENDOWMENT)? 60 | .call_and("flip", NO_ARGS, NO_ENDOWMENT)? 61 | .call_and("flip", NO_ARGS, NO_ENDOWMENT)? 62 | .call_and("flip", NO_ARGS, NO_ENDOWMENT)? 63 | .call("get", NO_ARGS, NO_ENDOWMENT)??; 64 | assert_eq!(result, false); 65 | } 66 | } 67 | ``` 68 | 69 | So, firstly, you declare a bundle provider like: 70 | 71 | ```rust 72 | #[drink::contract_bundle_provider] 73 | enum BundleProvider {} 74 | ``` 75 | 76 | It will take care of building all contract dependencies in the compilation phase and gather all contract bundles into a single registry. 77 | Then, you will be able to get a contract bundle by calling: 78 | 79 | ```rust 80 | let bundle = BundleProvider::local()?; // for the contract from the current crate 81 | let bundle = BundleProvider::Flipper.bundle()?; // for the contract from the `flipper` crate 82 | ``` 83 | 84 | We mark each testcase with `#[drink::test]` attribute and declare return type as `Result` so that we can use the `?` operator: 85 | 86 | ```rust 87 | #[drink::test] 88 | fn testcase() -> Result<(), Box> { 89 | // ... 90 | } 91 | ``` 92 | 93 | Then, we can use the `Session` API to interact with both contracts and the whole runtime. 94 | For details, check out testcases in [lib.rs](lib.rs). 95 | -------------------------------------------------------------------------------- /examples/quick-start-with-drink/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(not(feature = "std"), no_std, no_main)] 2 | 3 | /// This is the classical flipper contract. It stores a single `bool` value in its storage. The 4 | /// contract exposes: 5 | /// - a constructor (`new`) that initializes the `bool` value to the given value, 6 | /// - a message `flip` that flips the stored `bool` value from `true` to `false` or vice versa, 7 | /// - a getter message `get` that returns the current `bool` value. 8 | /// 9 | /// Additionally, we use the `debug_println` macro from the `ink_env` crate to produce some debug 10 | /// logs from the contract. 11 | #[ink::contract] 12 | mod flipper { 13 | // TODO: Requires `debug_println` in `ink_env`. 14 | // use ink::env::debug_println; 15 | 16 | #[ink(storage)] 17 | pub struct Flipper { 18 | value: bool, 19 | } 20 | 21 | impl Flipper { 22 | #[ink(constructor)] 23 | pub fn new(init: bool) -> Self { 24 | // TODO: Requires `debug_println` in `ink_env`. 25 | // debug_println!("Initializing contract with: `{init}`"); 26 | Self { value: init } 27 | } 28 | 29 | #[ink(message)] 30 | pub fn flip(&mut self) { 31 | // TODO: Requires `debug_println` in `ink_env`. 32 | // debug_println!("Previous value: `{}`", self.value); 33 | self.value = !self.value; 34 | // TODO: Requires `debug_println` in `ink_env`. 35 | // debug_println!("Flipped to: `{}`", self.value); 36 | } 37 | 38 | #[ink(message)] 39 | pub fn get(&self) -> bool { 40 | // TODO: Requires `debug_println` in `ink_env`. 41 | // debug_println!("Reading value from storage"); 42 | self.value 43 | } 44 | } 45 | } 46 | 47 | /// We put `drink`-based tests as usual unit tests, into a test module. 48 | #[cfg(test)] 49 | mod tests { 50 | use drink::{ 51 | sandbox_api::revive_api::decode_debug_buffer, 52 | session::{Session, NO_ARGS, NO_ENDOWMENT, NO_SALT}, 53 | }; 54 | 55 | /// `drink` automatically discovers all the contract projects that your tests will need. For 56 | /// every such dependency (including the contract from the current crate), it will generate a 57 | /// [`ContractBundle`](drink::session::ContractBundle) object that contains the compiled contract's code 58 | /// and a special transcoder, which is used to encode and decode the contract's message 59 | /// arguments. Such a bundle will be useful when deploying a contract. 60 | /// 61 | /// To get a convenient way for obtaining such bundles, we can define an empty enum and mark 62 | /// it with the [`drink::contract_bundle_provider`](drink::contract_bundle_provider) attribute. 63 | /// From now on, we can use it in all testcases in this module. 64 | #[drink::contract_bundle_provider] 65 | enum BundleProvider {} 66 | 67 | /// Now we write the simplest contract test, that will: 68 | /// 1. Deploy the contract. 69 | /// 2. Call its `flip` method. 70 | /// 3. Call its `get` method and ensure that the stored value has been flipped. 71 | /// 72 | /// We can use the [`drink::test`](drink::test) attribute to mark a function as a `drink` test. 73 | /// This way we ensure that all the required contracts are compiled and built, so that we don't 74 | /// have to run `cargo contract build` manually for every contract dependency. 75 | /// 76 | /// For convenience of using `?` operator, we mark the test function as returning a `Result`. 77 | /// 78 | /// `drink::test` will already provide us with a `Session` object. It is a wrapper around a runtime and it exposes 79 | /// a broad API for interacting with it. Session is generic over the runtime type, but usually and by default, we 80 | /// use `MinimalSandbox`, which is a minimalistic runtime that allows using smart contracts. 81 | #[drink::test] 82 | fn deploy_and_call_a_contract(mut session: Session) -> Result<(), Box> { 83 | // Now we get the contract bundle from the `BundleProvider` enum. Since the current crate 84 | // comes with a contract, we can use the `local` method to get the bundle for it. 85 | let contract_bundle = BundleProvider::local()?; 86 | 87 | // We can now deploy the contract. 88 | let _contract_address = session.deploy_bundle( 89 | // The bundle that we want to deploy. 90 | contract_bundle, 91 | // The constructor that we want to call. 92 | "new", 93 | // The constructor arguments (as stringish objects). 94 | &["true"], 95 | // Salt for the contract address derivation. 96 | NO_SALT, 97 | // Initial endowment (the amount of tokens that we want to transfer to the contract). 98 | NO_ENDOWMENT, 99 | )?; 100 | 101 | // Once the contract is instantiated, we can call the `flip` method on the contract. 102 | session.call( 103 | // The message that we want to call. 104 | "flip", 105 | // The message arguments (as stringish objects). If none, then we can use the `NO_ARGS` 106 | // constant, which spares us from typing `&[]`. 107 | NO_ARGS, 108 | // Endowment (the amount of tokens that we want to transfer to the contract). 109 | NO_ENDOWMENT, 110 | )??; 111 | 112 | // Finally, we can call the `get` method on the contract and ensure that the value has been 113 | // flipped. 114 | // 115 | // `Session::call` returns a `Result, SessionError>`, where `T` is the 116 | // type of the message result. In this case, the `get` message returns a `bool`, and we have 117 | // to explicitly hint the compiler about it. 118 | let result: bool = session.call("get", NO_ARGS, NO_ENDOWMENT)??; 119 | assert_eq!(result, false); 120 | 121 | Ok(()) 122 | } 123 | 124 | // TODO: Deprecated due to `pallet-revive`. There is no `debug_message`. 125 | // 126 | // /// In this testcase we will see how to get and read debug logs from the contract. 127 | // #[drink::test] 128 | // fn get_debug_logs(mut session: Session) -> Result<(), Box> { 129 | // session.deploy_bundle( 130 | // BundleProvider::local()?, 131 | // "new", 132 | // &["true"], 133 | // NO_SALT, 134 | // NO_ENDOWMENT, 135 | // )?; 136 | 137 | // // `deploy_bundle` returns just a contract address. If we are interested in more details 138 | // // about last operation (either deploy or call), we can get a `Record` object and use its 139 | // // `last_deploy_result` (or analogously `last_call_result`) method, which will provide us 140 | // // with a full report from the last contract interaction. 141 | // // 142 | // // In particular, we can get the decoded debug buffer from the contract. The buffer is 143 | // // just a vector of bytes, which we can decode using the `decode_debug_buffer` function. 144 | // let decoded_buffer = &session.record().last_deploy_result().debug_message; 145 | // let encoded_buffer = decode_debug_buffer(decoded_buffer); 146 | 147 | // assert_eq!(encoded_buffer, vec!["Initializing contract with: `true`"]); 148 | 149 | // Ok(()) 150 | // } 151 | 152 | /// In this testcase we will see how to work with multiple contracts. 153 | #[drink::test] 154 | fn work_with_multiple_contracts( 155 | mut session: Session, 156 | ) -> Result<(), Box> { 157 | let bundle = BundleProvider::local()?; 158 | 159 | // We can deploy the same contract multiple times. However, we have to ensure that the 160 | // derived contract addresses are different. We can do this by providing using different 161 | // arguments for the constructor or by providing a different salt. 162 | let first_address = 163 | session.deploy_bundle(bundle.clone(), "new", &["true"], NO_SALT, NO_ENDOWMENT)?; 164 | let _second_address = session.deploy_bundle( 165 | bundle.clone(), 166 | "new", 167 | &["true"], 168 | Some([0; 32]), 169 | NO_ENDOWMENT, 170 | )?; 171 | let _third_address = 172 | session.deploy_bundle(bundle, "new", &["false"], NO_SALT, NO_ENDOWMENT)?; 173 | 174 | // By default, when we run `session.call`, `drink` will interact with the last deployed 175 | // contract. 176 | let value_at_third_contract: bool = session.call("get", NO_ARGS, NO_ENDOWMENT)??; 177 | assert_eq!(value_at_third_contract, false); 178 | 179 | // However, we can also call a specific contract by providing its address. 180 | let value_at_first_contract: bool = 181 | session.call_with_address(first_address, "get", NO_ARGS, NO_ENDOWMENT)??; 182 | assert_eq!(value_at_first_contract, true); 183 | 184 | Ok(()) 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /examples/runtime-interaction/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "runtime-interaction" 3 | authors = ["Cardinal"] 4 | edition = "2021" 5 | homepage = "https://alephzero.org" 6 | repository = "https://github.com/use-ink/drink" 7 | version = "0.1.0" 8 | 9 | [lib] 10 | path = "lib.rs" 11 | 12 | [dev-dependencies] 13 | drink = { path = "../../drink" } 14 | 15 | # This is only needed to get some valid contract code to test against. 16 | wat = "1.0.81" 17 | -------------------------------------------------------------------------------- /examples/runtime-interaction/README.md: -------------------------------------------------------------------------------- 1 | # Runtime interaction 2 | 3 | This example shows how we can easily send transactions to a blockchain, just like with `subxt` or a similar client. 4 | 5 | This way we can leverage `drink!` to a full-scope runtime simulation engine. 6 | 7 | ## Running 8 | 9 | ```bash 10 | cargo test --release 11 | ``` 12 | 13 | ## `drink::Sandbox` vs `drink::Session` 14 | 15 | While in most examples and showcases for `drink` you will see `drink::Session`, here we are using the associated `Sandbox` implementation directly. 16 | `Session` is very useful when you are working with contracts, but if you are focusing only on the runtime interaction, you can simply use the underlying `Sandbox`. 17 | You can get a reference to the `Sandbox` implementation from the `Session` object using the `sandbox` method: 18 | 19 | ```rust 20 | let session = Session::::default(); 21 | ... 22 | let sandbox = session.sandbox(); // `sandbox` has type `&mut MinimalSandbox` 23 | ``` 24 | 25 | `Sandbox` is just a runtime wrapper, which enables you to interact directly with the runtime. 26 | On the other hand, `Session` is a wrapper around `Sandbox` which also provides a useful context for working with contracts. 27 | 28 | A rule of thumb is: if you are working with contracts, use `Session`, otherwise use `Sandbox`. 29 | -------------------------------------------------------------------------------- /examples/runtime-interaction/lib.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod tests { 3 | use drink::{ 4 | minimal::{MinimalSandbox, RuntimeCall}, 5 | pallet_balances, pallet_revive, 6 | sandbox_api::prelude::*, 7 | session::mocking_api::read_contract_binary, 8 | AccountId32, Sandbox, 9 | }; 10 | 11 | #[test] 12 | fn we_can_make_a_token_transfer_call() { 13 | // We create a sandbox object, which represents a blockchain runtime. 14 | let mut sandbox = MinimalSandbox::default(); 15 | 16 | // Bob will be the recipient of the transfer. 17 | const BOB: AccountId32 = AccountId32::new([2u8; 32]); 18 | 19 | // Firstly, let us check that the recipient account (`BOB`) is not the default actor, that 20 | // will be used as the caller. 21 | assert_ne!(MinimalSandbox::default_actor(), BOB); 22 | 23 | // Recipient's balance before the transfer. 24 | let initial_balance = sandbox.free_balance(&BOB); 25 | 26 | // Prepare a call object, a counterpart of a blockchain transaction. 27 | let call_object = RuntimeCall::Balances(pallet_balances::Call::transfer_allow_death { 28 | dest: BOB.into(), 29 | value: 100, 30 | }); 31 | 32 | // Submit the call to the runtime. 33 | sandbox 34 | .runtime_call(call_object, Some(MinimalSandbox::default_actor())) 35 | .expect("Failed to execute a call"); 36 | 37 | // In the end, the recipient's balance should be increased by 100. 38 | assert_eq!(sandbox.free_balance(&BOB), initial_balance + 100); 39 | } 40 | 41 | #[test] 42 | fn we_can_work_with_the_contracts_pallet_in_low_level() { 43 | let mut sandbox = MinimalSandbox::default(); 44 | 45 | // Construct the path to the contract file. 46 | let contract_path = std::path::Path::new(file!()) 47 | .parent() 48 | .expect("Failed to determine the base path") 49 | .join("test-resources") 50 | .join("dummy.polkavm"); 51 | 52 | // A few runtime calls are also available directly from the sandbox. This includes a part of 53 | // the contracts API. 54 | let actor = MinimalSandbox::default_actor(); 55 | let origin = MinimalSandbox::convert_account_to_origin(actor); 56 | let upload_result = sandbox 57 | .upload_contract(read_contract_binary(&contract_path), origin, 1_000_000) 58 | .expect("Failed to upload a contract"); 59 | 60 | // If a particular call is not available directly in the sandbox, it can always be executed 61 | // via the `runtime_call` method. 62 | let call_object = RuntimeCall::Revive(pallet_revive::Call::remove_code { 63 | code_hash: upload_result.code_hash, 64 | }); 65 | 66 | sandbox 67 | .runtime_call(call_object, Some(MinimalSandbox::default_actor())) 68 | .expect("Failed to remove a contract"); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /examples/runtime-interaction/test-resources/dummy.polkavm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/use-ink/drink/51beb813a4f345626ec3c1f9f9e7d5ea7a27a0e8/examples/runtime-interaction/test-resources/dummy.polkavm -------------------------------------------------------------------------------- /resources/blockchain-onion.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
State transition function
State transition...
Contract execution environment
Contract e...
Node / Host
Node / Host
Text is not SVG - cannot display
-------------------------------------------------------------------------------- /resources/testing-strategies.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
Node
Node
State transition function
State transition function
Contract execution environment
Contract execution environment
E2E testing
E2E testing
Unit testing
Unit testing
'quasi-E2E' testing
'quasi-E2E' tes...
Text is not SVG - cannot display
-------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition = "2021" 2 | use_field_init_shorthand = true 3 | reorder_modules = true 4 | 5 | imports_granularity = "Crate" 6 | group_imports = "StdExternalCrate" 7 | reorder_imports = true 8 | --------------------------------------------------------------------------------