Skip to content

Commit

Permalink
Tutorial for ink! cross contract call and e2e test (#351)
Browse files Browse the repository at this point in the history
* initial

* added doc

* review updates

* fit the code change

* removed wizard

* Update docs/build/wasm/from-zero-to-ink-hero/manic-minter/manic-e2e.md

* Update docs/build/wasm/from-zero-to-ink-hero/manic-minter/manic-e2e.md

* Update docs/build/wasm/from-zero-to-ink-hero/manic-minter/manic-e2e.md

* Update docs/build/wasm/from-zero-to-ink-hero/manic-minter/manic-contract.md

* Update docs/build/wasm/from-zero-to-ink-hero/manic-minter/manic-contract.md

---------

Co-authored-by: midegdugarova <[email protected]>
  • Loading branch information
Maar-io and midegdugarova authored May 30, 2023
1 parent 10f6f4b commit 2c751c4
Show file tree
Hide file tree
Showing 6 changed files with 502 additions and 1 deletion.
2 changes: 1 addition & 1 deletion docs/build/wasm/from-zero-to-ink-hero/dex/_category_.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"label": "Build Uniswap V2 core DEX",
"position": 3
"position": 4
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"label": "Manic Minter",
"position": 3
}
156 changes: 156 additions & 0 deletions docs/build/wasm/from-zero-to-ink-hero/manic-minter/manic-contract.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
---
sidebar_position: 3
---

# ManicMinter Contract
This tutorial will not include complete code from the contract to keep it short and focused on the cross contract calls and e2e tests. The full code for this example is available [here](https://github.com/swanky-dapps/manic-minter)

## Storage
To start with delete the boilerplate code from the `lib.rs` file.
The storage will be defined as follows:
```rust
pub struct ManicMinter {
/// Contract owner
owner: AccountId,
/// Oxygen contract address
token_contract: AccountId,
/// Minting price. Caller must pay this price to mint one new token from Oxygen contract
price: Balance,
}
```

## Error and Types
We will define the error and types as follows:
```rust
/// The ManicMinter error types.
pub enum Error {
/// Returned if not enough balance to fulfill a request is available.
BadMintValue,
/// Returned if the token contract account is not set during the contract creation.
ContractNotSet,
/// The call is not allowed if the caller is not the owner of the contract
NotOwner,
/// Returned if multiplication of price and amount overflows
OverFlow,
/// Returned if the cross contract transaction failed
TransactionFailed,
}

pub type Result<T> = core::result::Result<T, Error>;
```
## Contract Trait
The following trait will be used to define the contract interface:
```rust
pub trait Minting {
/// Mint new tokens from Oxygen contract
#[ink(message, payable)]
fn manic_mint(&mut self, amount: Balance) -> Result<()>;

/// Set minting price for one Oxygen token
#[ink(message)]
fn set_price(&mut self, price: Balance) -> Result<()>;

/// Get minting price for one Oxygen token
#[ink(message)]
fn get_price(&self) -> Balance;
}
```

## Constructor
The constructor will be defined as follows:
```rust
impl ManicMinter {
#[ink(constructor)]
pub fn new(contract_acc: AccountId) -> Self {
Self {
owner: Self::env().caller(),
token_contract: contract_acc,
price: 0,
}
}
}
```
## Cross Contract Call
The `manic_mint` method will execute cross contract call to Oxygen contract using `Call Builder`. The method will be defined as follows:
```rust
impl Minting for ManicMinter {
#[ink(message, payable)]
fn manic_mint(&mut self, amount: Balance) -> Result<()> {
//---- snip ----

let mint_result = build_call::<DefaultEnvironment>()
.call(self.token_contract)
.gas_limit(5000000000)
.exec_input(
ExecutionInput::new(Selector::new(ink::selector_bytes!("PSP22Mintable::mint")))
.push_arg(caller)
.push_arg(amount),
)
.returns::<()>()
.try_invoke();

//---- snip ----
}
}
```
Let's go over the code line by line:
* The `build_call().call()` method is called passing Oxygen contract address as an argument.
* The `gas_limit` method is invoked with a value of 5000000000 to set the gas limit for the contract execution.
* The `exec_input` method is used to specify the execution input for the contract call.
* An `ExecutionInput` instance is created with the selector `PSP22Mintable::mint` with the arguments of `caller` address and `amount`.
* The `returns` method is called to specify the expected return type of the contract call, which in this case is ().
* The `try_invoke` method is called to execute the contract call and capture any potential errors.

:::note

To learn more about the `Call Builder` and other methods for cross contract call please refer to the [ink! documentation](https://use.ink/basics/cross-contract-calling).

:::


## Cargo Update
Update `Cargo.toml` dependency with the following content:
```toml
[dependencies]
ink = { version = "4.1.0", default-features = false }
```

Since we will use PSP22 by Openbrush in our e2e test we need to add it under `dev-dependencies`
```toml
[dev-dependencies]
ink_e2e = "4.1.0"
openbrush = { tag = "3.1.0", git = "https://github.com/727-Ventures/openbrush-contracts", default-features = false, features = ["psp34", "ownable"] }
oxygen = { path = "../oxygen", default-features = false, features = ["ink-as-dependency"] }
```

## Oxygen Contract Update
Since we are using Oxygen contract for our testing we need to update it to be able to use it as a dependency. The code is already provided in the previous chapter, but please note that
1. `Cargo.toml` needs to be updated to become a library:
```toml
crate-type = [
"rlib",
]
```
2. At the top of the `lib.rs` file for Oxygen contract add `ref`

```rust
pub use self::oxygen::OxygenRef;
```
3. Under the `features` section of the `Cargo.toml` file add the following:
```toml
ink-as-dependency = []
```
:::note
* This is a prerequisite for ManicMinter contract to import the Oxygen library in the `Cargo.toml` file with feature `ink-as-dependency`:
```rust
oxygen = { path = "../oxygen", default-features = false, features = ["ink-as-dependency"] }
```
:::

## Summary of the ManicMinter Contract Chapter
* The ManicMinter contract mints new fungible tokens.
* The ManicMinter contract mints Oxygen tokens by invoking cross contract call to Oxygen contract.
* The Oxygen contract needs to be set as library with `ink-as-dependency` feature to be used as a dependency in the ManicMinter contract.


The full code for this example is available [here](https://github.com/swanky-dapps/manic-minter).
147 changes: 147 additions & 0 deletions docs/build/wasm/from-zero-to-ink-hero/manic-minter/manic-e2e.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
---
sidebar_position: 4
---

# ManicMinter e2e Test
In this chapter we will write e2e tests for the ManicMinter contract. The e2e tests will be written in Rust using the ink! e2e framework. The e2e tests will be executed on a local substrate node.
Just like in previous chapter we will not include complete code from the contract to keep it short and focused on the e2e tests.
## Import Crates
Let's create a new module `e2e_tests` within the body of the `mod manicminter` and import the following crates:
```rust
#[cfg(all(test, feature = "e2e-tests"))]
mod e2e_tests {
use super::*;
use crate::manicminter::ManicMinterRef;
use ink::primitives::AccountId;
use ink_e2e::build_message;
use openbrush::contracts::ownable::ownable_external::Ownable;
use openbrush::contracts::psp22::psp22_external::PSP22;
use oxygen::oxygen::OxygenRef;

type E2EResult<T> = std::result::Result<T, Box<dyn std::error::Error>>;

const AMOUNT: Balance = 100;
const PRICE: Balance = 10;
}
```
You will notice that we import Openbrush traits to invoke methods from the Oxygen contract, which is implemented using Openbrush's version of PSP22.

## Instantiate Contracts
We will use the `ink_e2e::Client` to instantiate the contracts. The `ink_e2e::Client` is a wrapper around the `ink_env::test` environment. The `ink_e2e::Client` provides a convenient way to instantiate contracts and invoke contract methods.

In the declarative macro add our contracts as `additional contracts`:
```rust
#[ink_e2e::test(additional_contracts = "manicminter/Cargo.toml oxygen/Cargo.toml")]
async fn e2e_minting_works(mut client: ink_e2e::Client<C, E>) -> E2EResult<()> {
let initial_balance: Balance = 1_000_000;

// Instantiate Oxygen contract
let token_constructor = OxygenRef::new(initial_balance);
let oxygen_account_id = client
.instantiate("oxygen", &ink_e2e::alice(), token_constructor, 0, None)
.await
.expect("token instantiate failed")
.account_id;

// Instantiate ManicMinter contract
let manic_minter_constructor = ManicMinterRef::new(oxygen_account_id);
let manic_minter_account_id = client
.instantiate(
"manic-minter",
&ink_e2e::alice(),
manic_minter_constructor,
0,
None,
)
.await
.expect("ManicMinter instantiate failed")
.account_id;
}
```

## Set ManicMinter as Owner of Oxygen
We will use the `build_message` macro to compose the `transfer_ownership` method of the Oxygen contract. The `client.call()` executes the contract call. The `call_dry_run` method with `owner()` message verifies the result of the contract call.

```rust
// Set ManicMinter contract to be the owner of Oxygen contract
let change_owner = build_message::<OxygenRef>(oxygen_account_id.clone())
.call(|p| p.transfer_ownership(manic_minter_account_id));
client
.call(&ink_e2e::alice(), change_owner, 0, None)
.await
.expect("calling `transfer_ownership` failed");

// Verify that ManicMinter is the Oxygen contract owner
let owner = build_message::<OxygenRef>(oxygen_account_id.clone()).call(|p| p.owner());
let owner_result = client
.call_dry_run(&ink_e2e::alice(), &owner, 0, None)
.await
.return_value();
assert_eq!(owner_result, manic_minter_account_id);
```

## Set Price for Oxygen Tokens

We use the `build_message` macro to compose the `set_price` method of the ManicMinter contract. The `client.call()` executes the contract call.

```rust
// Contract owner sets price
let price_message = build_message::<ManicMinterRef>(manic_minter_account_id.clone())
.call(|manicminter| manicminter.set_price(PRICE));
client
.call(&ink_e2e::alice(), price_message, 0, None)
.await
.expect("calling `set_price` failed");

```
## Mint Oxygen Tokens
We are now ready to execute `manic_mint` method of the ManicMinter contract. We use the `build_message` macro to compose the `manic_mint` method of the ManicMinter contract. The `client.call()` executes the contract call. The `call_dry_run` method with `balance_of()` message verifies the result of the contract call on the Oxygen contract.

```rust
// Bob mints AMOUNT of Oxygen tokens by calling ManicMinter contract
let mint_message = build_message::<ManicMinterRef>(manic_minter_account_id.clone())
.call(|manicminter| manicminter.manic_mint(AMOUNT));
client
.call(&ink_e2e::bob(), mint_message, PRICE * AMOUNT, None)
.await
.expect("calling `pink_mint` failed");

// Verify that tokens were minted on Oxygen contract
let bob_account_id = get_bob_account_id();
let balance_message = build_message::<OxygenRef>(oxygen_account_id.clone())
.call(|p| p.balance_of(bob_account_id));
let token_balance = client
.call_dry_run(&ink_e2e::bob(), &balance_message, 0, None)
.await
.return_value();
assert_eq!(token_balance, AMOUNT);
```

## Execute e2e Test
The e2e tests are invoking the node which is running on the local machine.
Before running the test we need to setup the environment variable `CONTRACT_NODE` to the executable local node. The node can be Swanky-node or any other node that implements pallet-contracts.
```bash
export CONTRACTS_NODE="YOUR_CONTRACTS_NODE_PATH"
```
As an example it can be set to the following value:
```bash
export CONTRACTS_NODE="/home/p/Documents/astar/target/release/astar-collator"
```
After setting your node path, run the following command to execute the e2e tests:
```bash
cargo test --features e2e-tests
```
## Debugging e2e Test
If you want to print some variables and messages during the e2e test execution you can use the `println!` macro. The output will be printed in the terminal where the test is executed. To be able to see the printed output you need to run the test with `--nocapture` flag:
```bash
cargo test --features e2e-tests -- --nocapture
```

## Summary of the e2e Test Chapter:
* We imported the required crates for e2e tests.
* We instantiated the ManicMinter and Oxygen contracts.
* We set the ManicMinter contract to be the owner of the Oxygen contract.
* We set the price for Oxygen tokens.
* We minted Oxygen tokens using the ManicMinter contract.

The full code for this example is available [here](https://github.com/swanky-dapps/manic-minter).
35 changes: 35 additions & 0 deletions docs/build/wasm/from-zero-to-ink-hero/manic-minter/manic-minter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
sidebar_position: 1
---

# Prerequisites
This tutorial is suitable for developers with **intermediate** knowledge of ink! and basic understanding of Rust. Previous experience compiling and deploying an ink! smart contract will be beneficial, such as from following the previous Flipper and NFT contract tutorials:

| Tutorial | Difficulty |
|----------------------------------------------------------------------------|--------------------------------|
| [Your First Flipper Contract](../flipper-contract/flipper-contract.md) | Basic ink! - Basic Rust |
| [NFT contract with PSP34](../nft/nft.md) | Intermediate ink! - Basic Rust |


## How to Start
To follow this tutorial you will need:
- To [set up your ink! environment](/docs/build/environment/ink_environment.md).
- Basic Rust knowledge. [Learn Rust](https://www.rust-lang.org/learn)
- Prior knowledge about ERC20 is helpful but not mandatory.

## What will be used?
- [ink! v4.1.0](https://github.com/paritytech/ink/tree/v4.0.0)
- [Openbrush 3.1.0](https://github.com/727-Ventures/openbrush-contracts/tree/3.0.0)
- cargo-contract 2.1.0

## What will you learn?
- Creation of a fungible token with PSP22 standard.
- Use Openbrush wizard to create PSP22 smart contract.
- Use Rust trait and implement it in same file.
- Calling cross contract method with Builder.
- ink! e2e test for cross contract calls.

## Summary
[I. Contract Setup](./manic-setup.md)
[II. ManicMinter Contract](./manic-contract.md)
[III ManicMinter e2e Test](./manic-e2e.md)
Loading

0 comments on commit 2c751c4

Please sign in to comment.