-
Notifications
You must be signed in to change notification settings - Fork 96
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Tutorial for ink! cross contract call and e2e test (#351)
* 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
1 parent
10f6f4b
commit 2c751c4
Showing
6 changed files
with
502 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
{ | ||
"label": "Build Uniswap V2 core DEX", | ||
"position": 3 | ||
"position": 4 | ||
} |
4 changes: 4 additions & 0 deletions
4
docs/build/wasm/from-zero-to-ink-hero/manic-minter/_category_.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
156
docs/build/wasm/from-zero-to-ink-hero/manic-minter/manic-contract.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
147
docs/build/wasm/from-zero-to-ink-hero/manic-minter/manic-e2e.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
35
docs/build/wasm/from-zero-to-ink-hero/manic-minter/manic-minter.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
Oops, something went wrong.