From ba0407c91d0332d50a0d1fae5deb3859017ad5e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Miko=C5=82ajczyk?= Date: Mon, 30 Oct 2023 10:32:28 +0100 Subject: [PATCH 1/2] Quick start guide --- README.md | 4 +- drink/src/bundle.rs | 1 + drink/src/contract_api.rs | 9 +- examples/quick-start-with-drink/Cargo.toml | 34 ++++ examples/quick-start-with-drink/README.md | 85 ++++++++++ examples/quick-start-with-drink/lib.rs | 175 +++++++++++++++++++++ 6 files changed, 305 insertions(+), 3 deletions(-) create mode 100755 examples/quick-start-with-drink/Cargo.toml create mode 100644 examples/quick-start-with-drink/README.md create mode 100644 examples/quick-start-with-drink/lib.rs diff --git a/README.md b/README.md index 625e5ff..5ecbece 100644 --- a/README.md +++ b/README.md @@ -75,11 +75,13 @@ Check our helpful and verbose examples in the [examples](examples) directory. `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`: ```toml -drink = { version = "0.5" } +drink = { version = "0.6" } ``` Full library documentation is available at: https://docs.rs/drink. +**Quick start guide** is available [here](examples/quick-start-with-drink/README.md). + ## As an alternative backend to ink!'s E2E testing framework DRink! is already integrated with ink! and can be used as a drop-in replacement for the standard E2E testing environment. diff --git a/drink/src/bundle.rs b/drink/src/bundle.rs index 35413a2..9509b03 100644 --- a/drink/src/bundle.rs +++ b/drink/src/bundle.rs @@ -14,6 +14,7 @@ use crate::{DrinkResult, Error}; /// - `deploy_bundle_and` /// - `upload_bundle` /// - `upload_bundle_and` +#[derive(Clone)] pub struct ContractBundle { /// WASM blob of the contract pub wasm: Vec, diff --git a/drink/src/contract_api.rs b/drink/src/contract_api.rs index 0d5d9d6..eb9ec97 100644 --- a/drink/src/contract_api.rs +++ b/drink/src/contract_api.rs @@ -1,5 +1,7 @@ //! Contracts API. +use std::ops::Not; + use frame_support::weights::Weight; use frame_system::Config; use pallet_contracts::{CollectEvents, DebugInfo, Determinism}; @@ -191,10 +193,13 @@ impl ContractApi for Sandbox { } } -/// Converts bytes to a '\n'-split string. +/// Converts bytes to a '\n'-split string, ignoring empty lines. pub fn decode_debug_buffer(buffer: &[u8]) -> Vec { let decoded = buffer.iter().map(|b| *b as char).collect::(); - decoded.split('\n').map(|s| s.to_string()).collect() + decoded + .split('\n') + .filter_map(|s| s.is_empty().not().then_some(s.to_string())) + .collect() } #[cfg(test)] diff --git a/examples/quick-start-with-drink/Cargo.toml b/examples/quick-start-with-drink/Cargo.toml new file mode 100755 index 0000000..0220d39 --- /dev/null +++ b/examples/quick-start-with-drink/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "quick-start-with-drink" +authors = ["Cardinal"] +edition = "2021" +homepage = "https://alephzero.org" +repository = "https://github.com/Cardinal-Cryptography/drink" +version = "0.1.0" + +[lib] +path = "lib.rs" + +[dependencies] +# We use standard dependencies for an ink! smart-contract. + +# For debugging from contract, we enable the `ink-debug` feature of `ink` crate. +ink = { version = "=4.2.1", default-features = false, features = ["ink-debug"] } +scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] } +scale-info = { version = "2.6", default-features = false, features = ["derive"], optional = true } + +[dev-dependencies] +# For testing purposes we bring the `drink` library. +drink = { path = "../../drink" } + +[features] +default = ["std"] +std = [ + "ink/std", + "scale/std", + "scale-info/std", +] +# If the current crate defines a smart contract that we want to test, we can't forget to have `ink-as-dependency` +# feature declared. This is how `#[drink::test]` and `#[drink::contract_bundle_provider]` discovers contracts to be +# built. +ink-as-dependency = [] diff --git a/examples/quick-start-with-drink/README.md b/examples/quick-start-with-drink/README.md new file mode 100644 index 0000000..465a190 --- /dev/null +++ b/examples/quick-start-with-drink/README.md @@ -0,0 +1,85 @@ +# Quick start with `drink` library + +This is a quick start guide introducing you to smart contract testing with `drink` library. +We will see how to write tests for a simple smart contract and make use of `drink`'s features. + +## Prerequisites + +You only need a Rust installed (see [here](https://www.rust-lang.org/tools/install) for help). +Drink is developed and tested with stable Rust 1.70 (see [toolchain file](../../rust-toolchain.toml)). + +## Dependencies + +You only need `drink` library brought into your project: +```toml +drink = { version = "0.6" } +``` + +See [Cargo.toml](Cargo.toml) for a typical cargo setup of a single-contract projects. + +## Writing tests + +### Preparing contracts + +For every contract that you want to interact with from your tests, you need to create a _contract bundle_, which includes: + - built contract artifact (`.wasm` file), + - contract transcoder (object based on the `.json` file, responsible for translating message arguments and results). + +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. + +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. +For this, you might want to use `drink::ContractBundle` API, which includes `ContractBundle::load` and `local_contract_file!` utilities. + +### `drink` test macros + +`drink` provides a few macros to write tests for smart contracts: + - `#[drink::test]` - which marks a function as a test function (similar to `#[test]`). + - `#[drink::contract_bundle_provider]` - which gathers all contract artifacts into a single registry. + +While neither is required to write `drink` tests, they make it easier to write and maintain them. + +### Writing tests + +Your typical test module will look like: +```rust +#[cfg(test)] +mod tests { + #[drink::contract_bundle_provider] + enum BundleProvider {} + + #[drink::test] + fn deploy_and_call_a_contract() -> Result<(), Box> { + let result: bool = Session::::new()? + .deploy_bundle_and(BundleProvider::local(), "new", &["true"], vec![], None)? + .call_and("flip", NO_ARGS, None)? + .call_and("flip", NO_ARGS, None)? + .call_and("flip", NO_ARGS, None)? + .call("get", NO_ARGS, None)??; + assert_eq!(result, false); + } +} +``` + +So, firstly you declare a bundle provider like: +```rust +#[drink::contract_bundle_provider] +enum BundleProvider {} +``` + +It will take care of building all contract dependencies in the compilation phase and gather all contract bundles into a single registry. +Then, you will be able to get a contract bundle by calling: +```rust +let bundle = BundleProvider::local()?; // for the contract from the current crate +let bundle = BundleProvider::Flipper.bundle()?; // for the contract from the `flipper` crate +``` + +We mark testcase with `#[drink::test]` attribute and declare return type as `Result` so that we can use `?` operator: +```rust +#[drink::test] +fn testcase() -> Result<(), Box> { + // ... +} +``` + +Then, we can use `Session` API to interact with both contracts and the whole runtime. +For details, check out testcases in [lib.rs](lib.rs). diff --git a/examples/quick-start-with-drink/lib.rs b/examples/quick-start-with-drink/lib.rs new file mode 100644 index 0000000..4cc1a00 --- /dev/null +++ b/examples/quick-start-with-drink/lib.rs @@ -0,0 +1,175 @@ +#![cfg_attr(not(feature = "std"), no_std, no_main)] + +/// This is the classical flipper contract. It stores a single `bool` value in its storage. The +/// contract exposes: +/// - a constructor (`new`) that initializes the `bool` value to the given value, +/// - a message `flip` that flips the stored `bool` value from `true` to `false` or vice versa, +/// - a getter message `get` that returns the current `bool` value. +/// +/// Additionally, we use the `debug_println` macro from the `ink_env` crate to produce some debug +/// logs from the contract. +#[ink::contract] +mod flipper { + use ink::env::debug_println; + + #[ink(storage)] + pub struct Flipper { + value: bool, + } + + impl Flipper { + #[ink(constructor)] + pub fn new(init: bool) -> Self { + debug_println!("Initializing contract with: `{init}`"); + Self { value: init } + } + + #[ink(message)] + pub fn flip(&mut self) { + debug_println!("Previous value: `{}`", self.value); + self.value = !self.value; + debug_println!("Flipped to: `{}`", self.value); + } + + #[ink(message)] + pub fn get(&self) -> bool { + debug_println!("Reading value from storage"); + self.value + } + } +} + +/// We put `drink`-based tests as usual unit tests, into a test module. +#[cfg(test)] +mod tests { + use drink::{ + contract_api::decode_debug_buffer, + runtime::MinimalRuntime, + session::{Session, NO_ARGS}, + }; + + /// `drink` automatically discovers all the contract projects that your tests will need. For + /// every such dependency (including the contract from the current crate), it will generate a + /// [`ContractBundle`](drink::ContractBundle) object that contains the compiled contract's code + /// and a special transcoder, which is used to encode and decode the contract's message + /// arguments. Such a bundle will be useful when deploying a contract. + /// + /// To get a convenient way for obtaining such bundles, we can define an empty enum and mark + /// it with the [`drink::contract_bundle_provider`](drink::contract_bundle_provider) attribute. + /// From now on, we can use it in all testcases in this module. + #[drink::contract_bundle_provider] + enum BundleProvider {} + + /// Now we write the simplest contract test, that will: + /// 1. Deploy the contract. + /// 2. Call its `flip` method. + /// 3. Call its `get` method and ensure that the stored value has been flipped. + /// + /// We can use the [`drink::test`](drink::test) attribute to mark a function as a `drink` test. + /// This way we ensure that all the required contracts are compiled and built, so that we don't + /// have to run `cargo contract build` manually for every contract dependency. + /// + /// For convenience of using `?` operator, we mark the test function as returning a `Result`. + #[drink::test] + fn deploy_and_call_a_contract() -> Result<(), Box> { + // Firstly, we create a `Session` object. It is a wrapper around a runtime and it exposes a + // broad API for interacting with it. + // + // It is generic over the runtime type, but usually, it is sufficient to use + // `MinimalRuntime`, which is a minimalistic runtime that allows using smart contracts. + let mut session = Session::::new()?; + + // Now we get the contract bundle from the `BundleProvider` enum. Since the current crate + // comes with a contract, we can use the `local` method to get the bundle for it. + let contract_bundle = BundleProvider::local()?; + + // We can now deploy the contract. + let _contract_address = session.deploy_bundle( + // The bundle that we want to deploy. + contract_bundle, + // The constructor that we want to call. + "new", + // The constructor arguments (as stringish objects). + &["true"], + // Salt for the contract address derivation. + vec![], + // Initial endowment (the amount of tokens that we want to transfer to the contract). + None, + )?; + + // Once the contract is instantiated, we can call the `flip` method on the contract. + session.call( + // The message that we want to call. + "flip", + // The message arguments (as stringish objects). If none, then we can use the `NO_ARGS` + // constant, which spares us from typing `&[]`. + NO_ARGS, + // Endowment (the amount of tokens that we want to transfer to the contract). + None, + )??; + + // Finally, we can call the `get` method on the contract and ensure that the value has been + // flipped. + // + // `Session::call` returns a `Result, SessionError>`, where `T` is the + // type of the message result. In this case, the `get` message returns a `bool`, and we have + // to explicitly hint the compiler about it. + let result: bool = session.call("get", NO_ARGS, None)??; + assert_eq!(result, false); + + Ok(()) + } + + /// In this testcase we will see how to get and read debug logs from the contract. + #[drink::test] + fn get_debug_logs() -> Result<(), Box> { + // We create a session object as usual and deploy the contract bundle. + let mut session = Session::::new()?; + session.deploy_bundle(BundleProvider::local()?, "new", &["true"], vec![], None)?; + + // `deploy_bundle` returns just a contract address. If we are interested in more details + // about last operation (either deploy or call), we can use the `last_deploy_result` + // (or analogously `last_call_result`) method, which will provide us with a full report + // from the last contract interaction. + // + // In particular, we can get the decoded debug buffer from the contract. The buffer is + // just a vector of bytes, which we can decode using the `decode_debug_buffer` function. + let decoded_buffer = &session + .last_deploy_result() + .expect("The deployment succeeded, so there should be a result available") + .debug_message; + let encoded_buffer = decode_debug_buffer(decoded_buffer); + + assert_eq!(encoded_buffer, vec!["Initializing contract with: `true`"]); + + Ok(()) + } + + /// In this testcase we will see how to work with multiple contracts. + #[drink::test] + fn work_with_multiple_contracts() -> Result<(), Box> { + let mut session = Session::::new()?; + let bundle = BundleProvider::local()?; + + // We can deploy the same contract multiple times. However, we have to ensure that the + // derived contract addresses are different. We can do this by providing using different + // arguments for the constructor or by providing a different salt. + let first_address = + session.deploy_bundle(bundle.clone(), "new", &["true"], vec![], None)?; + let _second_address = + session.deploy_bundle(bundle.clone(), "new", &["true"], vec![0], None)?; + let _third_address = session.deploy_bundle(bundle, "new", &["false"], vec![], None)?; + + // By default, when we run `session.call`, `drink` will interact with the last deployed + // contract. + let value_at_third_contract: bool = session.call("get", NO_ARGS, None)??; + assert_eq!(value_at_third_contract, false); + + // However, we can also call a specific contract by providing its address. + let value_at_first_contract: bool = + session.call_with_address(first_address, "get", NO_ARGS, None)??; + assert_eq!(value_at_first_contract, true); + + Ok(()) + } +} From 769893cc67092418e763df3f6056f267ea0263a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Miko=C5=82ajczyk?= Date: Tue, 31 Oct 2023 08:08:01 +0100 Subject: [PATCH 2/2] I'm good at articles --- examples/quick-start-with-drink/README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/examples/quick-start-with-drink/README.md b/examples/quick-start-with-drink/README.md index 465a190..a646f72 100644 --- a/examples/quick-start-with-drink/README.md +++ b/examples/quick-start-with-drink/README.md @@ -5,17 +5,17 @@ We will see how to write tests for a simple smart contract and make use of `drin ## Prerequisites -You only need a Rust installed (see [here](https://www.rust-lang.org/tools/install) for help). +You only need Rust installed (see [here](https://www.rust-lang.org/tools/install) for help). Drink is developed and tested with stable Rust 1.70 (see [toolchain file](../../rust-toolchain.toml)). ## Dependencies -You only need `drink` library brought into your project: +You only need the `drink` library brought into your project: ```toml drink = { version = "0.6" } ``` -See [Cargo.toml](Cargo.toml) for a typical cargo setup of a single-contract projects. +See [Cargo.toml](Cargo.toml) for a typical cargo setup of a single-contract project. ## Writing tests @@ -48,7 +48,7 @@ mod tests { enum BundleProvider {} #[drink::test] - fn deploy_and_call_a_contract() -> Result<(), Box> { + fn deploy_and_call_a_contract() -> Result<(), Box> { let result: bool = Session::::new()? .deploy_bundle_and(BundleProvider::local(), "new", &["true"], vec![], None)? .call_and("flip", NO_ARGS, None)? @@ -60,7 +60,7 @@ mod tests { } ``` -So, firstly you declare a bundle provider like: +So, firstly, you declare a bundle provider like: ```rust #[drink::contract_bundle_provider] enum BundleProvider {} @@ -73,13 +73,13 @@ let bundle = BundleProvider::local()?; // for the contract from the current crat let bundle = BundleProvider::Flipper.bundle()?; // for the contract from the `flipper` crate ``` -We mark testcase with `#[drink::test]` attribute and declare return type as `Result` so that we can use `?` operator: +We mark each testcase with `#[drink::test]` attribute and declare return type as `Result` so that we can use the `?` operator: ```rust #[drink::test] -fn testcase() -> Result<(), Box> { +fn testcase() -> Result<(), Box> { // ... } ``` -Then, we can use `Session` API to interact with both contracts and the whole runtime. +Then, we can use the `Session` API to interact with both contracts and the whole runtime. For details, check out testcases in [lib.rs](lib.rs).