From 3e92ee8d0851eaf716ddb2d70ad0bf307e492c90 Mon Sep 17 00:00:00 2001 From: Sofiya <148779211+vsofiya@users.noreply.github.com> Date: Wed, 6 Mar 2024 14:25:10 +0100 Subject: [PATCH 01/15] Update _category_.json --- docs/learn/tutorials/storage/_category_.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/learn/tutorials/storage/_category_.json b/docs/learn/tutorials/storage/_category_.json index 6976b13..bf878cd 100644 --- a/docs/learn/tutorials/storage/_category_.json +++ b/docs/learn/tutorials/storage/_category_.json @@ -1,4 +1,4 @@ { - "label": "ink! Smart Contract Macros", + "label": "ink! Smart Contract Storage", "position": 7 } From b45dd98a41693cc4bf77a62f3181cc9363a9a172 Mon Sep 17 00:00:00 2001 From: Sofiya <148779211+vsofiya@users.noreply.github.com> Date: Wed, 6 Mar 2024 14:35:14 +0100 Subject: [PATCH 02/15] Create storage.md --- docs/learn/tutorials/storage/storage.md | 318 ++++++++++++++++++++++++ 1 file changed, 318 insertions(+) create mode 100644 docs/learn/tutorials/storage/storage.md diff --git a/docs/learn/tutorials/storage/storage.md b/docs/learn/tutorials/storage/storage.md new file mode 100644 index 0000000..3b91b69 --- /dev/null +++ b/docs/learn/tutorials/storage/storage.md @@ -0,0 +1,318 @@ +# How to Declare, Read and Write Storage + +## Prerequisites + +Before this tutorial, you should have already completed the [Flipper Tutorial](./flipper-contract/flipper-contract.md). This tutorial targets developers with no experience in ink! and a basic level in Rust. + +## To follow this tutorial you will need: + +- To [set up your ink! environment](https://docs.inkdevhub.io/docs/learn/build-environment/ink_environment) + +## What will we do? + +In this tutorial we will implement events, using the most basic contract [Flipper](https://github.com/paritytech/ink/blob/v4.0.0/examples/flipper/lib.rs) as an example. + +## What will we use? + +- [ink! 4.2.0](https://github.com/paritytech/ink/tree/v4.2.0) +- [cargo-contract 3.2.0](https://github.com/paritytech/cargo-contract/tree/v3.2.0) +- [substrate-contracts-node](https://github.com/paritytech/substrate-contracts-node) + +## What will you learn? + +In this tutorial you will learn how to define, read, and write storage. + +# Storage + +This is a step-by-step explanation of how to perform actions with storage in ink! smart contracts. + +## What is Storage? + +When we implement smart contracts, we always need to save some value. Sometimes we need temporary variables and we just use default variables. But in most smart contracts we need to store data that doesn’t disappear after finishing message runtime. Storage is the only way to do this. + +## Types Used in Storage + +- Rust primitives type + - `bool` + - `u{8,16,32,64,128}` + - `i{8,16,32,64,128}` + - `String` + - tuples + - arrays +- Substrate specific types: + - `AccountId` + - `Balance` + - `Hash` +- ink! provided + - Types provided in [`ink_prelude`](https://docs.rs/ink_prelude/latest/ink_prelude/index.html) + - Types provided in ****[`ink_storage`](https://docs.rs/ink_storage/4.0.0/ink_storage/index.html#) +- Enums +- Custom data structure [details](https://use.ink/datastructures/custom-datastructure) + +### Example + +The simplest example of using Storage is in a standard [Flipper](https://github.com/paritytech/ink/blob/v4.0.0/examples/flipper/lib.rs) contract. It implements this way: + +```rust +#[ink(storage)] +pub struct Flipper { + value: bool, +} +``` + +It just stores `bool` value on-chain and gives access to the smart contract to read and write this field. + +## How to define Storage? + +Defining storage is the first step to developing a smart contract. It must include fields to store data on-cain. Ink! makes this process very simple for developers. Owing to the ink! macros, smart contract implementation looks like a simple rust struct that has some fields and implements some methods. So defining Storage is just defining struct with `#[ink::storage]` macro. + +```rust +#[ink(storage)] +pub struct Contract { + ... +} +``` + +Every field in the `Contract` struct will be stored in Storage. + +Let’s add some different fields to our `Contract` to show how it works. + +```rust +#[ink(storage)] +pub struct Contract { + bool_value: bool, + value: i32, + vec: ink::prelude::vec::Vec, + string: ink::prelude::string::String, + last_account: AccountId, + hash: Hash, + balance: Balance, + map: ink::storage::Mapping +} +``` + +But it’s not enough to define Storage yet. As a next step, we need to initialize all fields in the constructor. Let’s define one (a smart contract may have multiple constructors): + +```rust +impl Contract { + #[ink(constructor)] + pub fn new() -> Self { + Self { + bool_value: false, + value: 0, + vec: ink::prelude::vec::Vec::new(), + string: ink::prelude::string::String::from(""), + account: Self::env().caller(), + hash: Hash::from([0x0; 32]), + balance: 0, + map: ink::storage::Mapping::new() + } + } +... +} +``` + +## How to read Storage values? + +It’s very simple to read from the Storage. You just need to use `self.` syntax. For example: + +```rust +#[ink(message)] +pub fn get_value(&self) -> i32 { + self.value +} +``` + +## How to write Storage values? + +As you know, every message takes `self` as the first argument. If you need to only read values from Storage you may use `&self` syntax, but if you want to mutate values you should use `&mut self`. In this case you can use `self. = ` syntax: + +```rust +#[ink(message)] +pub fn get_value(&self) -> i32 { + self.value +} + +#[ink(message)] +pub fn set_value(&mut self, value: i32) { + self.value = value; +} +``` + +# How to work with different non-primitive types and structures? + +## Enums + +We can store Enums in the storage field. Let’s define it: + +```rust +enum Enum { + A, + B, + C +} + +#[ink(storage)] +pub struct Contract { + ... + enum_value: Enum, +} +``` + +The values of an enum should be referenced as `Enum::A`, `Enum::B`, `Enum::C`. + +## Lazy<> + +To understand how `Lazy` works, let’s dive deeper into the concepts behind ink! storage. + +### Storage organization + +The following image illustrates how ink! storage store values: + +![https://use.ink/img/kv.svg](https://prod-files-secure.s3.us-west-2.amazonaws.com/95ea02ce-fc8a-4084-86b7-9a7994a8a074/1fe5d716-1ee8-434a-9331-c95fedd84a48/Untitled.png) + +https://use.ink/img/kv.svg + +Storage data is always encoded with the `[SCALE](https://docs.substrate.io/reference/scale-codec/)` codec. The storage API works by saving and retrieving entries in a single storage cell. Each storage cell has its own exclusive storage key for accessing the stored data. In many ways, the storage API operates similarly to a traditional key-value database. + +### Packed vs Non-Packed layout + +Types that can be stored entirely under a single storage cell are considered `[Packed](https://docs.rs/ink_storage_traits/4.0.0/ink_storage_traits/trait.Packed.html)`. By default, ink! tries to store all storage struct fields under a single storage cell. Consequentially, with a `Packed` storage layout, any message interacting with the contract storage will always need to operate on the entire contract storage structure. + +If we have a few tiny fields in storage it’s not a problem to load all of them in every message, but if we have a large field with `Packed` type and if it’s not used in every message, it’s not rational to load this field every time. + +In this situation `Lazy<>` comes into the game. `Lazy<>` wrapper makes a field to store in another storage cell. And it will be loaded from storage only if you will interact with that field. + +![https://use.ink/img/storage-layout.svg](https://prod-files-secure.s3.us-west-2.amazonaws.com/95ea02ce-fc8a-4084-86b7-9a7994a8a074/dd86be8b-ff04-476a-8de6-20b42494790f/storage-layout.svg) + +https://use.ink/img/storage-layout.svg + +For example, we have a `Vec` with a huge amount of entries. We want to load it from storage only in one method. So we can wrap it in `Lazy<>`. + +```rust +#[ink::contract] +mod contract { + use ink::storage::Mapping; + use ink::storage::Lazy; + use ink::prelude::vec::Vec; + + #[derive(Default)] + #[ink(storage)] + pub struct Contract { + vec: Lazy>, + } + + impl Contract { + #[ink(constructor)] + pub fn new() -> Self { + Self::default() + } + + #[ink(message)] + pub fn get_vec_value(&self) -> Vec { + self.vec.get_or_default() + } + + #[ink(message)] + pub fn push_value(&mut self, value: i32) { + let mut vec = self.vec.get_or_default(); + vec.push(value); + self.vec.set(&vec); + } + } +} +``` + +To read `Lazy<>` wrapped fields we should use `Lazy::get()` method and `Lazy::set()` to write. Also, you can specify the storage key. + +```rust +#[ink(storage)] +pub struct Contract { + vec: Lazy, ManualKey<0x123123>>, +} +``` + +By default, it calculates automatically by `[AutoKey](https://docs.rs/ink_storage_traits/4.0.0/ink_storage_traits/struct.AutoKey.html)` primitive. But this possibility might be useful to make all your storage keys always stay the same regardless of the version of your contract or ink! itself (note that the key calculation algorithm may change with future ink! versions). Using `ManualKey` instead of `AutoKey` might be especially desirable for upgradable contracts, as using `AutoKey` might result in a different storage key for the same field in a newer version of the contract. This may break your contract after an upgrade. + +## Mapping + +`Mapping` is a data structure that is very similar to Map(HashMap, BTreeMap…). The main difference and advantage of `Mapping` use in smart contracts is the way how key-value pairs are stored in the Storage. Every key-value pair of `Mapping` is stored in different storage cells and because of that when we want to read or write one value `Mapping` will load only this key-value pair from the storage. It is called lazy loading. It saves a lot of gas when you use it in your smart contract. However, it is not possible to iterate over the contents of a `Mapping`. For example, you can use `Mapping` to save some value for every account that has ever called this smart contract. + +```rust +#[ink::contract] +mod contract { +use ink::storage::Mapping; + +#[ink(storage)] +pub struct Contract { + values: Mapping, +} + +impl Contract { + #[ink(constructor)] + pub fn new() -> Self { + Self { + values: Mapping::new(), + } + } + + #[ink(message)] + pub fn get_value(&self) -> i32 { + self.values.get(self.env().caller()).unwrap_or(0) + } + + #[ink(message)] + pub fn set_value(&mut self, value: i32) { + self.values.insert(self.env().caller(), &value); + } +} +``` + +As you see, to get a value from `Mapping` you need to use `Mapping::get()` function, which returns `Option`. If there is no value for the requested key it will return `None`, otherwise `Some(value)`, so we need to `unwrap()` to get value. To store value in `Mapping`, just use `Mapping::insert(key, value)` function. + +## Custom Structs + +We can define structs that use any of the types that were used before in this guide and store it in the field. Any custom type wanting to be compatible with ink! storage must implement the `[Storable](https://docs.rs/ink_storage_traits/4.0.0/ink_storage_traits/trait.Storable.html)` trait, so it can be SCALE `[encoded](https://docs.rs/parity-scale-codec/3.2.2/parity_scale_codec/trait.Encode.html)` and `[decoded](https://docs.rs/parity-scale-codec/3.2.2/parity_scale_codec/trait.Decode.html)`. Additionally, the traits `[StorageLayout](https://docs.rs/ink_storage_traits/4.0.0/ink_storage_traits/trait.StorageLayout.html)` and `[TypeInfo](https://docs.rs/scale-info/2.3.1/scale_info/trait.TypeInfo.html)` are required as well. But don't worry, usually these traits can just be derived: + +```rust +enum Enum { + A, + B, + C +} + +#[derive(scale::Decode, scale::Encode)] +#[cfg_attr( + feature = "std", + derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout) +)] +struct Struct { + bool_value: bool, + value: i32, + vec: ink::prelude::vec::Vec, + string: ink::prelude::string::String, + last_account: crate::contract::AccountId, + hash: Hash, + balance: Balance, + map: ink::storage::Mapping, + enum_value: Enum +} + +#[ink(storage)] +pub struct Contract { + struct_value: Struct, +} +``` + +Also, you can simply use `#[ink::storage_item]` macro: + +```rust +#[ink::storage_item] +struct Struct { + ... +} +``` + +# Conclusion + +Now you know how to use storage in ink! smart contracts. From 3adf2530e4acfe8cc1aae3e6e026a57509d9bcf0 Mon Sep 17 00:00:00 2001 From: Sofiya <148779211+vsofiya@users.noreply.github.com> Date: Wed, 6 Mar 2024 15:46:39 +0100 Subject: [PATCH 03/15] Create _category_.json --- docs/learn/tutorials/upgrade-contracts/_category_.json | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 docs/learn/tutorials/upgrade-contracts/_category_.json diff --git a/docs/learn/tutorials/upgrade-contracts/_category_.json b/docs/learn/tutorials/upgrade-contracts/_category_.json new file mode 100644 index 0000000..3430541 --- /dev/null +++ b/docs/learn/tutorials/upgrade-contracts/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "ink! Smart Contract Storage", + "position": 8 +} From 5ec17b8109dfa5e3830e893ec495755982fb6481 Mon Sep 17 00:00:00 2001 From: Sofiya <148779211+vsofiya@users.noreply.github.com> Date: Wed, 6 Mar 2024 15:50:03 +0100 Subject: [PATCH 04/15] Create upgrade-contracts.md --- .../upgrade-contracts/upgrade-contracts.md | 199 ++++++++++++++++++ 1 file changed, 199 insertions(+) create mode 100644 docs/learn/tutorials/upgrade-contracts/upgrade-contracts.md diff --git a/docs/learn/tutorials/upgrade-contracts/upgrade-contracts.md b/docs/learn/tutorials/upgrade-contracts/upgrade-contracts.md new file mode 100644 index 0000000..33da73d --- /dev/null +++ b/docs/learn/tutorials/upgrade-contracts/upgrade-contracts.md @@ -0,0 +1,199 @@ +# How to upgrade ink! smart contracts + +## Prerequisites + +Before this tutorial, it’s highly recommended to go through our tutorials on how to declare, read and write storage, and how to do cross-contract calls. This tutorial targets developers with a basic understanding of ink! and a basic level in Rust. + +### To follow this tutorial you will need: + +- To [set up your ink! environment](https://docs.inkdevhub.io/docs/learn/build-environment/ink_environment) + +### What will we do? + +In this tutorial, we will learn upgradeable contract concepts and how to implement them in different ways. + +### What will we use? + +- [ink! 4.2.0](https://github.com/paritytech/ink/tree/v4.2.0) +- [cargo-contract 3.2.0](https://github.com/paritytech/cargo-contract/tree/v3.2.0) +- [substrate-contracts-node](https://github.com/paritytech/substrate-contracts-node) + +# Upgradeable contracts + +Let’s talk about upgradeable contracts and understand what they are. + +## What are Upgradeable smart contracts? + +As you know all contracts are immutable on chain. Immutability is an essential requirement for achieving [decentralization](https://101blockchains.com/decentralization-in-blockchain/) and [security of smart contracts](https://101blockchains.com/smart-contract-security-guide/). However, it is also important to understand that the immutability of smart contracts creates some prominent limitations if there is a bug in the smart contract, or the contracts is outdated and needs an upgrade. For this reason, upgradeable contracts exist. + +## How to implement it? + +In ink! there are different ways to make an upgradeable contract: + +- Replacing Contract Code with `[set_code_hash()](https://paritytech.github.io/ink/ink_env/fn.set_code_hash.html)`; +- Proxy Forwarding; +- Delegating execution to foreign Contract Code with `[delegate_call](https://paritytech.github.io/ink/ink_env/call/fn.build_call.html#example-3-delegate-call)`. + +## set_code_hash() + +With `set_code_hash()` you can fully change your contract. The only limit you have is storage compatibility. You may feel free to change the functions’ signatures and logic. It is the easiest way to make a contract upgradeable, you just need to add a message with the `set_code_hash()` function in your contract. + +```rust +#[ink(message)] +pub fn set_code(&mut self, new_code_hash: [u8; 32]) { + ink::env::set_code_hash(&new_code_hash).unwrap_or_else(|err| { + panic!("Failed to set new code hash: {:?}", err) + }); +} +``` + +To use this function you need to upload or instantiate another contract code to the network, get the code_hash of it, and call the `set_code(code_hash)` message. After that, your contract will change code_hash, but keep its address. Now you have a completely different contract on the initial address. Note that storage fields that were in the initial contract will keep their values from the previous contract. + +## Proxy Forwarding + +This concept uses cross-contract calls to upgrade its functionality. In `Proxy` contract you can only pick the address of the contract to which you will forward all calls. `Proxy` is just a re-translator of calls. You can use it to achieve upgradeability, by changing address to a newly deployed upgraded contract. Here is an example of how `Proxy` contract can be implemented: + +```rust +#[ink::contract] +mod proxy { + #[ink(storage)] + pub struct Proxy { + /// The `AccountId` of a contract where any call that does not match a + /// selector of this contract is forwarded to. + forward_address: AccountId, + /// The `AccountId` of a privileged account that can update the + /// forwarding address. This address is set to the account that + /// instantiated this contract. + admin: AccountId, + } + + impl Proxy { + #[ink(constructor)] + pub fn new(forward_address: AccountId) -> Self { + let caller = Self::env().caller(); + Self { + forward_address, + admin: caller, + } + } + + #[ink(message)] + pub fn change_forward_address(&mut self, new_address: AccountId) { + assert_eq!( + self.env().caller(), + self.admin, + "caller {:?} does not have sufficient permissions, only {:?} does", + self.env().caller(), + self.admin, + ); + self.forward_address = new_address; + } + + #[ink(message, payable, selector = _)] + pub fn forward(&self) -> u32 { + ink::env::call::build_call::() + .call(self.forward_to) + .transferred_value(self.env().transferred_value()) + .call_flags( + ink::env::CallFlags::default() + .set_forward_input(true) + .set_tail_call(true), + ) + .invoke(); + unreachable!( + "the forwarded call will never return since `tail_call` was set" + ); + } + } +} +``` + +As you see, this version of `Proxy` has an admin field. It is used for the security of the contract. Only the address that has deployed the contract can change `forward_address`. As you see `forward` function is payable, and has `_` selector. It means that the forward function will be called when any unknown selector is called to this contract. In the `forward` function, we call the contract with `contract_address` address and pass to it all of the args and transferred value. + +To understand how it works, you can deploy `Proxy` contract and call any message from another deployed contract from `Proxy`, and it will forward your call to this contract. + +## Delegating execution to foreign Contract Code with `[delegate_call](https://paritytech.github.io/ink/ink_env/call/fn.build_call.html#example-3-delegate-call)` + +This is the most controllable method to implement upgradeable contracts. The idea is to build delegate calls. The main advantage of this method is that you can keep signatures of messages and keep storage compatible for every upgrade. Also, you can use different contract implementations when you call any method because you choose `code_hash` every time. + +Here is an example of an upgradeable contract with `delegate_call`: + +```rust +#![cfg_attr(not(feature = "std"), no_std, no_main)] + +#[ink::contract] +mod upgradeable { + use ink::env::call::{build_call, ExecutionInput, Selector}; + use ink::env::{CallFlags, DefaultEnvironment}; + + #[ink(storage)] + pub struct Upgradeable { + value: i32, + } + + impl Upgradeable { + #[ink(constructor)] + pub fn new(init_value: i32) -> Self { + Self { value: init_value } + } + + #[ink(message)] + pub fn get(&self, code_hash: Hash) -> i32 { + build_call::() + .delegate(code_hash) + .call_flags(CallFlags::default().set_tail_call(true)) + .exec_input( + ExecutionInput::new(Selector::new(ink::selector_bytes!("get"))) + ) + .returns::() + .invoke() + } + + #[ink(message)] + pub fn set(&mut self, code_hash: Hash, value: i32) { + build_call::() + .delegate(code_hash) + .call_flags(CallFlags::default().set_tail_call(true)) + .exec_input( + ExecutionInput::new(Selector::new(ink::selector_bytes!("set"))) + .push_arg(value) + ) + .returns::<()>() + .invoke() + } + } +} +``` + +Now when you deploy the contract, you can use the logic of any contract that has compatible storage and message signatures. For example, you can upload this contract to network and use its `code_hash`. + +```rust +#[ink::contract] +mod contract { + #[ink(storage)] + pub struct Contract { + value: i32, + } + + impl Contract { + #[ink(constructor)] + pub fn new(init_value: i32) -> Self { + Self { value: init_value } + } + + #[ink(message)] + pub fn set(&mut self, value: i32) { + self.value = value; + } + + #[ink(message)] + pub fn get(&self) -> i32 { + self.value + } + } +} +``` + +# Conclusion + +Now you know what upgradeable contracts are and how to implement them. From 0dd7dbbf6bab2dee7edb79f7c16a4025e33670d5 Mon Sep 17 00:00:00 2001 From: Sofiya <148779211+vsofiya@users.noreply.github.com> Date: Wed, 6 Mar 2024 15:50:16 +0100 Subject: [PATCH 05/15] Update _category_.json --- docs/learn/tutorials/upgrade-contracts/_category_.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/learn/tutorials/upgrade-contracts/_category_.json b/docs/learn/tutorials/upgrade-contracts/_category_.json index 3430541..5aa8400 100644 --- a/docs/learn/tutorials/upgrade-contracts/_category_.json +++ b/docs/learn/tutorials/upgrade-contracts/_category_.json @@ -1,4 +1,4 @@ { - "label": "ink! Smart Contract Storage", + "label": "How to Upgrade ink! Smart Contracts", "position": 8 } From 6c609281fd3f15ddf4bd99f865ecbbbdb4a6f7d8 Mon Sep 17 00:00:00 2001 From: Sofiya <148779211+vsofiya@users.noreply.github.com> Date: Wed, 6 Mar 2024 15:51:07 +0100 Subject: [PATCH 06/15] Update define-events.md --- docs/learn/tutorials/define-events/define-events.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/learn/tutorials/define-events/define-events.md b/docs/learn/tutorials/define-events/define-events.md index bacced5..a1d0bf1 100644 --- a/docs/learn/tutorials/define-events/define-events.md +++ b/docs/learn/tutorials/define-events/define-events.md @@ -2,7 +2,7 @@ ## Prerequisites -Before this tutorial, you should have already completed the [Flipper Tutorial](https://docs.astar.network/docs/tutorials/flipper_tutorial). This tutorial targets developers with no experience in ink! and a basic level in Rust. +Before this tutorial, you should have already completed the [Flipper Tutorial](./flipper-contract/flipper-contract.md). This tutorial targets developers with no experience in ink! and a basic level in Rust. ## To follow this tutorial you will need: From 11acf7ba655e016b510786288e168396fa3b3c2a Mon Sep 17 00:00:00 2001 From: Sofiya <148779211+vsofiya@users.noreply.github.com> Date: Wed, 6 Mar 2024 15:51:44 +0100 Subject: [PATCH 07/15] Update macros.md --- docs/learn/tutorials/macros/macros.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/learn/tutorials/macros/macros.md b/docs/learn/tutorials/macros/macros.md index ca29983..551a685 100644 --- a/docs/learn/tutorials/macros/macros.md +++ b/docs/learn/tutorials/macros/macros.md @@ -2,7 +2,7 @@ ## Prerequisites -Before this tutorial, you should have already completed the [Flipper Tutorial](https://docs.astar.network/docs/tutorials/from-zero-to-ink-hero/flipper-contract/). This tutorial targets developers with no experience in ink! and a basic level in Rust. +Before this tutorial, you should have already completed the [Flipper Tutorial](./flipper-contract/flipper-contract.md). This tutorial targets developers with no experience in ink! and a basic level in Rust. ### To follow this tutorial you will need: From 9b4260f751267a6cfa0b16437d43ec55acdfc688 Mon Sep 17 00:00:00 2001 From: Sofiya <148779211+vsofiya@users.noreply.github.com> Date: Wed, 6 Mar 2024 15:53:36 +0100 Subject: [PATCH 08/15] Create _category_.json --- docs/learn/tutorials/cross-calls/_category_.json | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 docs/learn/tutorials/cross-calls/_category_.json diff --git a/docs/learn/tutorials/cross-calls/_category_.json b/docs/learn/tutorials/cross-calls/_category_.json new file mode 100644 index 0000000..18be368 --- /dev/null +++ b/docs/learn/tutorials/cross-calls/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "How To Do Cross-Contract Calls", + "position": 9 +} From 91106082f55bd48e9397e1eabb194001a1adac13 Mon Sep 17 00:00:00 2001 From: Sofiya <148779211+vsofiya@users.noreply.github.com> Date: Wed, 6 Mar 2024 16:08:43 +0100 Subject: [PATCH 09/15] Create cross-calls.md --- .../tutorials/cross-calls/cross-calls.md | 571 ++++++++++++++++++ 1 file changed, 571 insertions(+) create mode 100644 docs/learn/tutorials/cross-calls/cross-calls.md diff --git a/docs/learn/tutorials/cross-calls/cross-calls.md b/docs/learn/tutorials/cross-calls/cross-calls.md new file mode 100644 index 0000000..c50d791 --- /dev/null +++ b/docs/learn/tutorials/cross-calls/cross-calls.md @@ -0,0 +1,571 @@ +# How to do cross-contract calls + +## Prerequisites + +This tutorial targets developers with basic knowledge of ink! and a basic level in Rust. + +### To follow this tutorial you will need: + +- To [set up your ink! environment](https://docs.inkdevhub.io/docs/learn/build-environment/ink_environment) + +### What will we do? + +In this tutorial, we will learn how to do cross-contract calls. + +### What will we use? + +- [ink! 4.2.0](https://github.com/paritytech/ink/tree/v4.2.0) +- [cargo-contract 3.2.0](https://github.com/paritytech/cargo-contract/tree/v3.2.0) +- [substrate-contracts-node](https://github.com/paritytech/substrate-contracts-node) + +# Cross-contract calls + +This is a step-by-step explanation of how to implement cross-contract calls in ink! smart contracts. + +## What are cross-contract calls? + +In ink! contracts it is possible to call messages and constructors of other contracts. This process is called *cross-contract calls*. + +# How to define and use cross-contract calls? + +Let’s create learning contracts to understand how events work. There are a few methods how to do cross-contract calls: + +- `ContractRef` — refers to structs that are generated by the ink! code generation for cross-contract calls. They give developers a type-safe way of interacting with a contract. You need to import the contract you want to call as a dependency of your contract. You can’t use it to call a contract that is already deployed. +- `CreateBuilder` — an easy way for you to instantiate a contract. Same as in `ContractRef`, you will still need a reference for your contract. It’s similar to the previous method, but it provides a little bit more flexibility. +- `CallBuilder` — gives you a couple of ways to call messages from other contracts. With this method, you can make `Call`s and `DelegateCall`s. + +## 1) Define initial contracts + +In a new project folder, execute the following: + +```bash +cargo contract new flipper # flipper is introduced from the beginning. +``` + +And create `MainContract` and make it clear for now. We will call `Flipper` from it. + +```bash +cargo contract new main_contract +``` + +Filename: +main_contract/lib.rs + +```rust +#![cfg_attr(not(feature = "std"), no_std, no_main)] + +#[ink::contract] +mod main_contract { + #[ink(storage)] + pub struct MainContract {} + + impl MainContract { + #[ink(constructor)] + pub fn new(init_value: bool) -> Self { + Self + } + } +} +``` + +## 2) ContractRef + +As mentioned above, to use `ContractRef` we need to import a reference to your contract that you want to call to our `MainContract`. + +First, give `FlipperRef` public access. + +Filename: +flipper/lib.rs + +```rust +#![cfg_attr(not(feature = "std"), no_std, no_main)] + +//Give `FlipperRef` public access +pub use self::flipper::FlipperRef; + +#[ink::contract] +mod flipper { +... +} +``` + +Secondly, add `Flipper` as dependency to `MainContract` in `Cargo.toml` + +Filename: +flipper/lib.rs + +```toml +[dependencies] +... +flipper = { path = "../flipper", default-features = false, features = ["ink-as-dependency"] } +... +[features] +... +std = [ +... + "flipper/std", +] +``` + +Thirdly, import `FlipperRef` in `MainContract`. + +Filename: +main_contract/ + +lib.rs + +```rust +#[ink::contract] +mod main_contract { + use flipper::FlipperRef; +... +} +``` + +To save its instance we will create a storage field that stores `FliperRef`. + +Filename: +main_contract/ + +lib.rs + +```rust +#[ink(storage)] +pub struct MainContract { + flipper: FlipperRef, +} +``` + +And finally, we can instantiate `Flipper` contract. We will do it in the constructor and save in `flipper` field in storage. + +Filename: +main_contract/ + +lib.rs + +```rust +impl MainContract { + #[ink(constructor)] + pub fn new(code_hash: Hash, init_value: bool) -> Self { + let flipper = FlipperRef::new(init_value) + .code_hash(code_hash) + .endowment(0) + .salt_bytes([0xDE, 0xAD, 0xBE, 0xEF]) + .instantiate(); + Self { flipper } + } +} +``` + +Now we have an instance of `Flipper` and we can call it now. Let’s crate functions `flip` and `get` that simply call `Flipper`’s methods with same names. + +Filename: +main_contract/ + +lib.rs + +```rust +impl MainContract { + ... + #[ink(message)] + pub fn flip(&mut self) { + self.flipper.flip(); + } + + #[ink(message)] + pub fn get(&self) -> bool { + self.flipper.get() + } +} +``` + +Let’s deploy contracts and test how it works. As mentioned before, we can’t call other contracts if it’s not instantiated in the network. So first we need to build `Flipper` and upload it. + +```bash +cargo contract build --manifest-path flipper/Cargo.toml +cargo contract upload --manifest-path flipper/Cargo.toml --suri //Alice -x +``` + +Now we have the `code_hash` of the `Flipper` contract uploaded to the network. + +```bash +Code hash 0xc264a93fef7117d04fad5a4bc4925c962d9ab1d9a565cadb419f93e559ebeb71 +``` + +And we can use it to deploy `MainContract`. + +```bash +cargo contract build --manifest-path main_contract/Cargo.toml +cargo contract instantiate \ + --manifest-path main_contract/Cargo.toml \ + --constructor new \ + --args 0xc264a93fef7117d04fad5a4bc4925c962d9ab1d9a565cadb419f93e559ebeb71 true \ + --suri //Alice \ + -x -y +``` + +If everything is successful, you will see the address of the contract. + +```bash +Contract 5E1qsGrY7p2M5fCVeGiE6UPrw59xMpwmGbErMUA7X3WL9KqT +``` + +That’s all! Now we have deployed a contract that can do cross-contract calls to the `Flipper`. Let’s test how it works. + +```bash +cargo contract call --manifest-path main_contract/Cargo.toml \ +--contract 5E1qsGrY7p2M5fCVeGiE6UPrw59xMpwmGbErMUA7X3WL9KqT \ +--suri //Alice \ +--message get +``` + +We should get something similar, because initial value of `Flipper` is true. + +```bash +Result Ok(true) +Reverted false +``` + +Let’s try to `flip` it and see what we get: + +```bash +cargo contract call --manifest-path main_contract/Cargo.toml \ +--contract 5E1qsGrY7p2M5fCVeGiE6UPrw59xMpwmGbErMUA7X3WL9KqT \ +--suri //Alice \ +--message flip \ +-x \ +-y + +cargo contract call --manifest-path main_contract/Cargo.toml \ +--contract 5E1qsGrY7p2M5fCVeGiE6UPrw59xMpwmGbErMUA7X3WL9KqT \ +--suri //Alice \ +--message get +``` + +We see that the contract flips value in `Flipper`. + +```bash +Result Ok(false) +Reverted false +``` + +## 3) **CreateBuilder** + +`CreateBuilder` is pretty similar to `ContractRef`. All difference is in the syntax of instantiating the contract by `code_hash`. To use `CreateBuilder` follow all steps from the `[ContractRef` section](https://www.notion.so/How-to-do-cross-contract-calls-57990f5dbf634dc48922a7108c177a5a?pvs=21), but replace the instantiating contract with: + +Filename: +main_contract/ + +lib.rs + +```rust +impl MainContract { + #[ink(constructor)] + pub fn new(code_hash: Hash, init_value: bool) -> Self { + let flipper : FlipperRef = build_create::() + .code_hash(code_hash) + .gas_limit(0) + .endowment(0) + .exec_input( + ExecutionInput::new(Selector::new(ink::selector_bytes!("new"))) + .push_arg(init_value) + ) + .salt_bytes(&[0xDE, 0xAD, 0xBE, 0xEF]) + .returns::() + .instantiate(); + + Self { flipper } + } +... +} +``` + +Now we can test it and make sure that it works like the previous method. + +## 3) **CallBuilder** + +With `CallBuilder` you can call messages from other contracts that are already instantiated in the network. There are two ways to use `CallBuilder` : + +- `Call` +- `DelegateCall` + +### Call + +Let’s build a call in a new message `flip_call` and `get_call`. + +Filename: +main_contract/ + +lib.rs + +```rust +impl MainContract { + #[ink(message)] + pub fn flip_call(&mut self, contract_address: AccountId) { + build_call::() + .call(contract_address) + .gas_limit(0) + .transferred_value(0) + .exec_input( + ExecutionInput::new(Selector::new(ink::selector_bytes!("flip"))) + ) + .returns::<()>() + .invoke(); + } + + #[ink(message)] + pub fn get_call(&self, contract_address: AccountId) -> bool{ + build_call::() + .call(contract_address) + .gas_limit(0) + .transferred_value(0) + .exec_input( + ExecutionInput::new(Selector::new(ink::selector_bytes!("get"))) + ) + .returns::() + .invoke() + } +} +``` + +As you can see, these use the address of the contract that has already been deployed to the network. So we should deploy our `Flipper` first. + +```bash +cargo contract instantiate \ + --manifest-path flipper/Cargo.toml \ + --constructor default \ + --suri //Alice \ + -x -y +``` + +```bash +Contract 5DaiKstjdJxqr87xdp2e4tWNJXghySJ1X7mRbPBeNFpQ6puz +``` + +Now we have the address of `Flipper` and we can call its methods from `MainContract`. Let’s deploy build and deploy `MainContract`. + +```bash +cargo contract build --manifest-path main_contract/Cargo.toml &&\ +cargo contract instantiate \ + --manifest-path main_contract/Cargo.toml \ + --constructor new \ + --args 0xc264a93fef7117d04fad5a4bc4925c962d9ab1d9a565cadb419f93e559ebeb71 true \ + --suri //Alice \ + -x -y +``` + +```bash +Contract 5EhV43qsuLae3GRSy6Qp9Zh8rgnzQV7tYFHuDECSTZZBcNYR +``` + +Let’s test it and make sure it works well. + +```bash +//Returns `Ok(false)` +cargo contract call \ + --manifest-path main_contract/Cargo.toml \ + --contract 5EhV43qsuLae3GRSy6Qp9Zh8rgnzQV7tYFHuDECSTZZBcNYR \ + --suri //Alice \ + --message get_call \ + --args 5DaiKstjdJxqr87xdp2e4tWNJXghySJ1X7mRbPBeNFpQ6puz + +//Flips +cargo contract call \ + --manifest-path main_contract/Cargo.toml \ + --contract 5EhV43qsuLae3GRSy6Qp9Zh8rgnzQV7tYFHuDECSTZZBcNYR \ + --suri //Alice \ + --message flip_call \ + --args 5DaiKstjdJxqr87xdp2e4tWNJXghySJ1X7mRbPBeNFpQ6puz \ + -x -y + +//Returns `Ok(true)` +cargo contract call \ + --manifest-path main_contract/Cargo.toml \ + --contract 5EhV43qsuLae3GRSy6Qp9Zh8rgnzQV7tYFHuDECSTZZBcNYR \ + --suri //Alice \ + --message get_call \ + --args 5DaiKstjdJxqr87xdp2e4tWNJXghySJ1X7mRbPBeNFpQ6puz +``` + +### DelegateCall + +Let’s build a delegate call in a new message `flip_delegate` and `get_delegate`. The difference from other methods is that you use `MainContract`’s storage instead of `Flipper`'s, but we use `Flipper`'s logic. Because of this feature, we must take storage compatibility into account. It means that we need to have all fields from `Flipper`'s storage in `MainContract`'s storage. Another big difference is that you call `Flipper` from your address, not from `MainContract`'s like in `Call`. With this method, you can create upgradeable contracts. + +> There we can add a link for upgradeable contracts guide that will be created in future +> + +Let's update our storage to be compatible with Flipper's storage. Also, we will add an `addresses` field to storage. We will need it in the next steps. Don’t forget to import `ink::prelude::vec::Vec`. + +Filename: +flipper/ + +lib.rs + +```rust +#[ink(storage)] +pub struct Flipper { + value: bool, + addresses: Vec, +} +... +#[ink(constructor)] +pub fn new(init_value: bool) -> Self { + Self { value: init_value, addresses: Vec::new() } +} +``` + +Filename: +main_contract/ + +lib.rs + +```rust +#[ink(storage)] +pub struct MainContract { +... + value: bool, + addresses: Vec, +} +... +#[ink(constructor)] +pub fn new(init_value: bool) -> Self { + Self { value: init_value, addresses: Vec::new() } +} +``` + +Now we have compatible storages. Let’s use our `addresses` to demonstrate the difference between `Call` and `DelegateCall`. We will push the account address of callee whenever a `flip` message is called. Also, we will add `get_addresses` message to check our `addresses`. + +Filename: +flipper/ + +lib.rs + +```rust +... +#[ink(message)] +pub fn flip(&mut self) { + self.value = !self.value; + self.addresses.push(self.env().caller()); +} +... +#[ink(message)] +pub fn get_addresses(&self) -> Vec { + self.addresses.clone() +} +``` + +Same in `MainContract`. We will have `get_addesses_call`, `flip_delegate`, `get_delegate` and `get_addresses_delegate`. + +Filename: +main_contract/ + +lib.rs + +```rust +... +#[ink(message)] +pub fn get_addresses_call(&self, contract_address: AccountId) -> Vec { + build_call::() + .call(contract_address) + .gas_limit(0) + .transferred_value(0) + .exec_input( + ExecutionInput::new(Selector::new(ink::selector_bytes!("get_addresses"))) + ) + .returns::>() + .invoke() +} + +#[ink(message)] +pub fn flip_delegate(&mut self, code_hash: Hash) { + build_call::() + .delegate(code_hash) + .call_flags(CallFlags::default().set_tail_call(true)) + .exec_input( + ExecutionInput::new(Selector::new(ink::selector_bytes!("flip"))) + ) + .returns::<()>() + .invoke() +} + +#[ink(message)] +pub fn get_delegate(&self, code_hash: Hash) -> bool { + build_call::() + .delegate(code_hash) + .call_flags(CallFlags::default().set_tail_call(true)) + .exec_input( + ExecutionInput::new(Selector::new(ink::selector_bytes!("get"))) + ) + .returns::() + .invoke() +} + +#[ink(message)] +pub fn get_addresses_delegate(&self, code_hash: Hash) -> Vec { + build_call::() + .delegate(code_hash) + .call_flags(CallFlags::default().set_tail_call(true)) + .exec_input( + ExecutionInput::new(Selector::new(ink::selector_bytes!("get_addresses"))) + ) + .returns::>() + .invoke() +} +``` + +Notice that you don’t need to instantiate another contract when you use `DelegateCall` , you may only upload it to the network. But in our example, we will instantiate `Flipper` to show the differences between `Call` and `DelegateCall`. So now we build and deploy both contracts. + +```bash +//Flipper +Code hash 0xdac256919525471242bd9195414ed298288f3b6c6a7a22f27d3e3c85c6d7e028 +Contract 5CY1z1Mwam3hqpTf8PhUGgHMKvAtJ8z6B5EThQuFfse29ZMW + +//MainContract +Contract 5HpmRAxgKTAR7Vzekis917eR6HR782zF7F1ritiVZp854VYu +``` + +Let’s call `flip_call` and then check `addresses` by `get_addresses_call`. + +```bash +cargo contract call --manifest-path main_contract/Cargo.toml \ +--contract 5HpmRAxgKTAR7Vzekis917eR6HR782zF7F1ritiVZp854VYu \ +--message flip_call \ +--args 5CY1z1Mwam3hqpTf8PhUGgHMKvAtJ8z6B5EThQuFfse29ZMW \ +--suri //Alice \ +-x + +cargo contract call --manifest-path main_contract/Cargo.toml \ +--contract 5HpmRAxgKTAR7Vzekis917eR6HR782zF7F1ritiVZp854VYu \ +--message get_addresses_call \ +--args 5CY1z1Mwam3hqpTf8PhUGgHMKvAtJ8z6B5EThQuFfse29ZMW \ +--suri //Alice + +//Output +['5HpmRAxgKTAR7Vzekis917eR6HR782zF7F1ritiVZp854VYu'] // MainContract's address +``` + +As we can see, the callee in this situation is `MainContract`. Let’s see what happens if we call `flip_delegate` and `get_addresses_delegate`. + +```bash +cargo contract call --manifest-path main_contract/Cargo.toml \ +--contract 5HpmRAxgKTAR7Vzekis917eR6HR782zF7F1ritiVZp854VYu \ +--message flip_delegate \ +--args 0xdac256919525471242bd9195414ed298288f3b6c6a7a22f27d3e3c85c6d7e028 \ +--suri //Alice \ +-x + +cargo contract call --manifest-path main_contract/Cargo.toml \ +--contract 5HpmRAxgKTAR7Vzekis917eR6HR782zF7F1ritiVZp854VYu \ +--message get_addresses_delegate \ +--args 0xdac256919525471242bd9195414ed298288f3b6c6a7a22f27d3e3c85c6d7e028 \ +--suri //Alice + +//Output +['5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY'] // Alice's address +``` + +As you can see in this situation callee is Alice's account, that is, the account that is called the `MainContract`. Also, as mentioned before `delegate` use `MainContract`'s storage. + +# Conclusion + +In this guide, we have become familiar with various methods of doing cross-contract calls and learned the differences between them. From 8adc3d94d9d248f68142efa9198ad2387afda7e4 Mon Sep 17 00:00:00 2001 From: Sofiya <148779211+vsofiya@users.noreply.github.com> Date: Wed, 6 Mar 2024 16:12:58 +0100 Subject: [PATCH 10/15] Update storage.md --- docs/learn/tutorials/storage/storage.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/learn/tutorials/storage/storage.md b/docs/learn/tutorials/storage/storage.md index 3b91b69..1d5d8dd 100644 --- a/docs/learn/tutorials/storage/storage.md +++ b/docs/learn/tutorials/storage/storage.md @@ -2,7 +2,7 @@ ## Prerequisites -Before this tutorial, you should have already completed the [Flipper Tutorial](./flipper-contract/flipper-contract.md). This tutorial targets developers with no experience in ink! and a basic level in Rust. +Before this tutorial, you should have already completed the [Flipper Tutorial](https://docs.inkdevhub.io/docs/learn/tutorials/flipper-contract/flipper). This tutorial targets developers with no experience in ink! and a basic level in Rust. ## To follow this tutorial you will need: From 00cf56c2d1bd6e1e41a51c31006ef1551ff2c060 Mon Sep 17 00:00:00 2001 From: Sofiya <148779211+vsofiya@users.noreply.github.com> Date: Wed, 6 Mar 2024 16:13:16 +0100 Subject: [PATCH 11/15] Update macros.md --- docs/learn/tutorials/macros/macros.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/learn/tutorials/macros/macros.md b/docs/learn/tutorials/macros/macros.md index 551a685..d21a51c 100644 --- a/docs/learn/tutorials/macros/macros.md +++ b/docs/learn/tutorials/macros/macros.md @@ -2,7 +2,7 @@ ## Prerequisites -Before this tutorial, you should have already completed the [Flipper Tutorial](./flipper-contract/flipper-contract.md). This tutorial targets developers with no experience in ink! and a basic level in Rust. +Before this tutorial, you should have already completed the [Flipper Tutorial](https://docs.inkdevhub.io/docs/learn/tutorials/flipper-contract/flipper). This tutorial targets developers with no experience in ink! and a basic level in Rust. ### To follow this tutorial you will need: From 562071e0cfd6d13934b89aae0d636e3706c0db85 Mon Sep 17 00:00:00 2001 From: Sofiya <148779211+vsofiya@users.noreply.github.com> Date: Wed, 6 Mar 2024 16:13:36 +0100 Subject: [PATCH 12/15] Update define-events.md --- docs/learn/tutorials/define-events/define-events.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/learn/tutorials/define-events/define-events.md b/docs/learn/tutorials/define-events/define-events.md index a1d0bf1..b995b18 100644 --- a/docs/learn/tutorials/define-events/define-events.md +++ b/docs/learn/tutorials/define-events/define-events.md @@ -2,7 +2,7 @@ ## Prerequisites -Before this tutorial, you should have already completed the [Flipper Tutorial](./flipper-contract/flipper-contract.md). This tutorial targets developers with no experience in ink! and a basic level in Rust. +Before this tutorial, you should have already completed the [Flipper Tutorial](https://docs.inkdevhub.io/docs/learn/tutorials/flipper-contract/flipper). This tutorial targets developers with no experience in ink! and a basic level in Rust. ## To follow this tutorial you will need: From 4099dc7388eef6d9f2a3932bf7f8b99ace55d439 Mon Sep 17 00:00:00 2001 From: Sofiya <148779211+vsofiya@users.noreply.github.com> Date: Wed, 6 Mar 2024 16:18:32 +0100 Subject: [PATCH 13/15] Update storage.md --- docs/learn/tutorials/storage/storage.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/learn/tutorials/storage/storage.md b/docs/learn/tutorials/storage/storage.md index 1d5d8dd..dbe2b53 100644 --- a/docs/learn/tutorials/storage/storage.md +++ b/docs/learn/tutorials/storage/storage.md @@ -161,7 +161,7 @@ pub struct Contract { The values of an enum should be referenced as `Enum::A`, `Enum::B`, `Enum::C`. -## Lazy<> +## Lazy To understand how `Lazy` works, let’s dive deeper into the concepts behind ink! storage. From 63d11e57de62ff672f7fa13282b1a1bade877702 Mon Sep 17 00:00:00 2001 From: Sofiya <148779211+vsofiya@users.noreply.github.com> Date: Wed, 6 Mar 2024 16:26:26 +0100 Subject: [PATCH 14/15] Update docusaurus.config.js --- docusaurus.config.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docusaurus.config.js b/docusaurus.config.js index 233a744..646e83e 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -73,7 +73,7 @@ presets: [ type: 'doc', docId: 'getting-started', position: 'left', - label: 'Site Index', + label: 'Go to the Docs', }, { href: 'https://github.com/inkdevhub/inkdevhub-docs', @@ -89,7 +89,7 @@ presets: [ title: 'Docs', items: [ { - label: 'Site Index', + label: 'Go to the Docs', to: '/docs/getting-started', }, ], From b0ce7b03d898f9676684b6df083262ae8f1835a9 Mon Sep 17 00:00:00 2001 From: Sofiya <148779211+vsofiya@users.noreply.github.com> Date: Wed, 6 Mar 2024 16:27:24 +0100 Subject: [PATCH 15/15] Update docusaurus.config.js --- docusaurus.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docusaurus.config.js b/docusaurus.config.js index 646e83e..4cdfd2e 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -73,7 +73,7 @@ presets: [ type: 'doc', docId: 'getting-started', position: 'left', - label: 'Go to the Docs', + label: 'About ink! Dev Hub', }, { href: 'https://github.com/inkdevhub/inkdevhub-docs',