Emmanuel Umeh
    • Create new note
    • Create a note from template
      • Sharing URL Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Customize slides
      • Note Permission
      • Read
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Write
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Engagement control Commenting, Suggest edit, Emoji Reply
      • Invitee
    • Publish Note

      Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

      Your note will be visible on your profile and discoverable by anyone.
      Your note is now live.
      This note is visible on your profile and discoverable online.
      Everyone on the web can find and read all notes of this public team.
      See published notes
      Unpublish note
      Please check the box to agree to the Community Guidelines.
      View profile
    • Commenting
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Suggest edit
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
    • Emoji Reply
    • Enable
    • Versions and GitHub Sync
    • Note settings
    • Engagement control
    • Transfer ownership
    • Delete this note
    • Save as template
    • Insert from template
    • Import from
      • Dropbox
      • Google Drive
      • Gist
      • Clipboard
    • Export to
      • Dropbox
      • Google Drive
      • Gist
    • Download
      • Markdown
      • HTML
      • Raw HTML
Menu Note settings Sharing URL Create Help
Create Create new note Create a note from template
Menu
Options
Versions and GitHub Sync Engagement control Transfer ownership Delete this note
Import from
Dropbox Google Drive Gist Clipboard
Export to
Dropbox Google Drive Gist
Download
Markdown HTML Raw HTML
Back
Sharing URL Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Customize slides
Note Permission
Read
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Write
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Engagement control Commenting, Suggest edit, Emoji Reply
Invitee
Publish Note

Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

Your note will be visible on your profile and discoverable by anyone.
Your note is now live.
This note is visible on your profile and discoverable online.
Everyone on the web can find and read all notes of this public team.
See published notes
Unpublish note
Please check the box to agree to the Community Guidelines.
View profile
Engagement control
Commenting
Permission
Disabled Forbidden Owners Signed-in users Everyone
Enable
Permission
  • Forbidden
  • Owners
  • Signed-in users
  • Everyone
Suggest edit
Permission
Disabled Forbidden Owners Signed-in users Everyone
Enable
Permission
  • Forbidden
  • Owners
  • Signed-in users
Emoji Reply
Enable
Import from Dropbox Google Drive Gist Clipboard
   owned this note    owned this note      
Published Linked with GitHub
Subscribed
  • Any changes
    Be notified of any changes
  • Mention me
    Be notified of mention me
  • Unsubscribe
Subscribe
# Build and Interact With a Message Board ICP Canister Using Rust Welcome to this ICP Rust Development 101 tutorial! This tutorial is designed to provide an introduction to developing on the Internet Computer Protocol (ICP) platform. In this guide, you will learn the basics of building and interacting with decentralized canisters powered by Rust. By the end of this guide, you will have a solid understanding of developing for the ICP platform and be able to create the foundation for decentralized applications. In this tutorial, we will be building a Message-board web3 canister in rust. This canister will allow users to create messages, update messages, delete messages, and list messages. ### What you'll learn #### Developing a Message Board Canister in Rust: - Understand the core concepts of Rust-based canister development on the Internet Computer Protocol (ICP) platform. - Dive into Rust language features, including ownership, lifetimes, and memory safety, and apply them to create a message board canister. - Implement CRUD (Create, Read, Update, Delete) functionality to manage messages efficiently within the canister. - How to deploy and interact with a web3 canister #### Deploying and Interacting with a Web3 Canister: - Explore the deployment process of a web3 canister on the ICP platform, including canister IDs and smart contract deployment techniques. - Learn how to interact with the deployed web3 canister using both local and web-based tools. #### Optimizing Development Workflow: - Set up your development environment, including installing essential tools like Node.js, Node Version Manager (nvm), and DFX. - Familiarize yourself with the necessary configuration files (e.g., dfx.json) and project structure for ICP canister development. - Leverage the power of WebAssembly (Wasm) and Motoko language to streamline your development process and enhance canister performance. ### What is a Canister? A canister is a fundamental building block and execution environment for deploying and running software applications. Canisters are similar to smart contracts on other blockchain platforms, but they offer more powerful features and capabilities. Canisters on the Internet Computer enable the development of scalable and decentralized applications. They offer a secure and efficient execution environment, enabling developers to build a wide range of applications, including decentralized finance (DeFi) platforms, social media applications, decentralized autonomous organizations (DAOs), and more. ### Prerequisites While having some prior coding experience is beneficial, it is not a hard requirement for this tutorial. However, we do recommend that you have the following: - **Basic knowledge of Rust** - Although this isn't strictly necessary, it could prove beneficial. - **Knowledge of how to use your terminal** - This tutorial will require you to use your terminal. If you're unfamiliar with the terminal or need a refresher, watch this beginner tutorial on how to use the terminal: [Learn the Command Line: Basic Commands](https://www.youtube.com/watch?v=MBBWVgE0ewk). - **Familiarity with IDEs** - This tutorial utilizes the web version of VSCode. If you're unfamiliar with IDEs or need a refresher, watch this beginner tutorial on how to use VSCode: [Learn Visual Studio Code in 7min (Official Beginner Tutorial)](https://www.youtube.com/watch?v=B-s71n0dHUk). - **A GitHub account** - This tutorial utilizes GitHub Codespaces. If you don't have a GitHub account yet, create one at [github.com](https://github.com/) ### Tech Stack - **DFX - Internet Computer CLI:** DFX is the command-line interface for the Internet Computer, used to create, build, deploy, and manage canisters on the platform. It also offers a local development environment for testing. - **Rust - Systems Programming Language:** Rust, a powerful systems programming language, combines high-level language expressiveness with low-level performance. Its ownership system and concurrency model ensure reliable, efficient software development. - **GitHub Codespaces (Optional):** Codespaces, a GitHub feature, enables cloud-based development without the need for local installations. It offers a convenient way to start projects quickly. - **Node.js - JavaScript Runtime (v18):** Node.js, version 18, serves as the JavaScript runtime for executing code outside web browsers. - **Cargo - Rust Package Manager:** Cargo is the official build tool and package manager for Rust. It simplifies dependency management and project building, making it an essential component in Rust development. - **Candid - Interface Description Language (IDL):** Candid is used to define and describe the public interfaces of services, including methods and input/output types. It plays a crucial role in our project's canister interface descriptions. ### Overview 1. [Setup](#1-setup) (10 min) - This section will guide you through the necessary steps to set up your project. 2. [Building the canister](#2-building-the-canister) (20 min) - In this section, we will build out our message board ICP Canister. 3. [Deploy and Interact With our Canister](#3-build-and-deploy-our-canister) (5 min) - In this section, we will interact with the Todo canister via our command line and the Candid web interface. 4. [Conclusion](#4-conclusion) (2 min) - Finally, we will conclude this tutorial and give you some ideas on how to continue. If you want to skip to the complete code, you can find the GitHub repository [here](https://github.com/). ## 1. Setup In this initial section, we are going to set up our boilerplate code and run it in a Codespace or in your local machine. ### 1.1 Preparing Your Development Environment You have two options for setting up your development environment: using GitHub Codespaces or setting up locally on your machine. #### 1.1.1 Option 1: Using GitHub Codespaces GitHub Codespaces provides a powerful, browser-based development environment that eliminates the need for local setup. This allows you to focus on learning and building. To create a new Codespace with the boilerplate, follow these steps: 1. **Access the ICP Rust Boilerplate Repository:** - Go to the [ICP Rust boilerplate repository](https://github.com/dacadeorg/icp-101-rust-boilerplate). 2. **Create a Codespace:** - Click on the "Code" button located at the top right of the repository page. - From the dropdown menu, select "Create codespace on main." This action will generate a new Codespace pre-configured with everything you need to start working on this project. 3. **Wait for Dependencies to Install:** - The first time you open the Codespace, the necessary dependencies for this project will be installed automatically. Please be patient, as this process may take a few minutes. You can monitor the installation progress in the terminal. #### 1.1.2 Option 2: Setting Up Locally #### Initializing the boilerplate code To clone the boilerplate code, follow the steps: 1. **Access the ICP message board Repository:** - Go to the [ICP message board repository](https://github.com/dacadeorg/icp-101-rust-boilerplate). 2. **Clone the Repository:** - Click on the "Code" button, then switch to the "Local" tab. Copy the repository's URL. - In your terminal, navigate to the directory where you want to store your project and clone the repository with the following command: ```bash git clone https://github.com/dacadeorg/icp-101-rust-boilerplate ``` 3. **Enter the Project Directory:** - Move into the cloned repository's directory using the following command: ```bash cd icp-101-rust-boilerplate ``` Now that you have successfully set up your boilerplte environment, you can open up the codebase in your favorite editor. ### 1.2. Building the Canister In this section, we will set up our Rust development environment and build our ICP Canister. #### 1.2.1 Setting Up Rust Development Environment Before diving into creating ICP Canisters with Rust, let's set up your Rust development environment to ensure a smooth workflow. Rust offers a powerful toolset for system-level programming and web development alike. We'll install Rust and configure it for the project. Rust comes with a dedicated package manager called "Cargo" that makes managing dependencies and building projects a breeze. If you already have Rust set up, you can skip this step. #### 1.2.2 Installing Rust To get started, install Rust by following these steps: - **Install Rust via rustup:** Firstly let us run the following command to install Rust using rustup, the official Rust toolchain installer: ```bash curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh ``` After running the command, you should see a similar prompt in your terminal ```bash info: downloading installer Welcome to Rust! This will download and install the official compiler for the Rust programming language, and its package manager, Cargo. Rustup metadata and toolchains will be installed into the Rustup home directory, located at: /home/codespace/.rustup This can be modified with the RUSTUP_HOME environment variable. The Cargo home directory is located at: /home/codespace/.cargo This can be modified with the CARGO_HOME environment variable. The cargo, rustc, rustup and other commands will be added to Cargo's bin directory, located at: /home/codespace/.cargo/bin This path will then be added to your PATH environment variable by modifying the profile files located at: /home/codespace/.profile /home/codespace/.bashrc /home/codespace/.zshenv You can uninstall at any time with rustup self uninstall and these changes will be reverted. Current installation options: default host triple: x86_64-unknown-linux-gnu default toolchain: stable (default) profile: default modify PATH variable: yes 1) Proceed with installation (default) 2) Customize installation 3) Cancel installation > ``` Press 1 and hit enter or just directly press enter to proceed with the installation since 1 is selected as default. After installation is complete, you should see a similar output in your terminal ```bash Rust is installed now. Great! To get started you may need to restart your current shell. This would reload your PATH environment variable to include Cargo's bin directory ($HOME/.cargo/bin). To configure your current shell, run: source "$HOME/.cargo/env" ``` Next run the source command to ensure that the necessary environment variables are loaded: ``` source "$HOME/.cargo/env" ``` #### 1.2.3 Installing wasm32-unknown-unknown target Next, we need to install the wasm32-unknown-unknown target, which is a WebAssembly target for the Rust programming language. Run the following command to install the wasm32-unknown-unknown target: ``` rustup target add wasm32-unknown-unknown ``` #### 1.2.4 Installing Candid Extractor Next, we need to install Candid Extractor, a tool for extracting Candid interface definitions from WebAssembly (Wasm) modules. Run the following command to install Candid Extractor: ``` cargo install candid-extractor ``` #### 1.2.5 Installing DFX We'll also need DFX, a command-line interface for the Internet Computer, to create our Rust ICP project. Install DFX by running: ``` DFX_VERSION=0.14.3 sh -ci "$(curl -fsSL https://sdk.dfinity.org/install.sh)" ``` Now that we have DFX installed, we need to add it to your path. Run the following command: ```echo 'export PATH="$PATH:$HOME/bin"' >> "$HOME/.bashrc"``` #### 1.2.6 Setting Up Wasmtime To complete our setup, we need Wasmtime, an open-source WebAssembly runtime that allows you to execute WebAssembly modules outside of web browsers. It's designed to be fast, efficient, and flexible, making it suitable for various use cases beyond traditional web development. Run the following command to set up Wasmtime: ``` curl https://wasmtime.dev/install.sh -sSf | bash ``` If you are using GitHub Codespaces, you will need to reload your terminal to ensure all changes are properly applied. Click on the "Reload" button in the top-right corner of your terminal. #### 1.2.7 Ensuring Everything is Installed Correctly - **Close and Reopen Your Terminal:** Close and reopen your terminal to ensure the new environment variables are loaded - **Check the Installed Rust Version:** Run the following command to check the installed Rust version: ```bash cargo --version ``` You should see something like this ``` cargo 1.72.0 (103a7ff2e 2023-08-15) ``` - **Check the Installed DFX Version:** - Run the following command to check the installed DFX version: ```bash dfx --version ``` You should see something like this ```bash dfx 0.14.3 ``` - **Check the Installed Wasmtime Version:** - Run the following command to check the installed Wasmtime version: ```bash wasmtime --version ``` You should see something like this ```bash wasmtime-cli 13.0.0 ``` With everything installed, we're ready to start building our ICP Canister with Rust. In the next section, we'll set up our boilerplate code and start building our ICP Canister. ### 1.3 Boilerplate Code Overview Now that we have our terminal set up, we can begin creating our canister. We can make use of DFX on our terminal to initialize a Rust boilerplate code to speed up our development. When creating our boilerplate, we should see a structure similar to the following: ``` |-- src | |-- icp_rust_boilerplate_backend | |-- src | | |-- lib.rs | | | |-- Cargo.toml | | | |-- icp_rust_boilerplate_backend.did | | |-- .gitignore | |-- Cargo.lock | |-- Cargo.toml | |-- dfx.json ``` Now, let's break down each component: #### Src Directory Inside the `src` directory, we have a subdirectory named `icp_rust_boilerplate_backend`. This is where the core of our Canister resides. #### icp_rust_boilerplate_backend Subdirectory This subdirectory holds our Canister's Rust code. Here's what you'll find inside: - `src/lib.rs`: The main Rust source file where we'll define our Canister's logic. - `Cargo.toml`: The package configuration file for our Canister. It specifies dependencies and other project-related information. - `icp_rust_boilerplate_backend.did`: A file that contains the Candid interface definitions for our Canister. This defines how external callers will interact with our Canister. #### .gitignore This file specifies patterns of files and directories that Git should ignore when tracking changes. It's particularly useful for excluding build artifacts and temporary files from version control. #### Cargo.lock and Cargo.toml These files are part of Rust's package management system. `Cargo.toml` defines the package's metadata and dependencies, while `Cargo.lock` is automatically generated and records the exact versions of dependencies used in the last successful build. #### dfx.json This JSON configuration file contains settings related to the Internet Computer development environment, including project configuration, build settings, and more. Now, let's go ahead and create a new file in our root directory called `did.sh`. This Bash script will allow us to automate the generation of Candid interface definitions (DID files) for a set of Canister projects using the Rust programming language. Candid is a serialization format used in the Internet Computer Protocol for defining the interface of Canisters. Our directory structure should now look like this: ``` |-- src | |-- icp_rust_boilerplate_backend |-- .gitignore | |-- Cargo.lock | |-- Cargo.toml | |-- dfx.json | |-- did.sh ``` ### 1.4 Explaining the DID Generation Script To automate the generation of Candid interface definitions (DID files) for your Canister projects, we have a `did.sh` Bash script in your root directory which in this case is under `icp_rust_boilerplate`. This script will simplify the process and ensure that your interfaces are always up-to-date. Below is the content of the `did.sh` script: ``` #!/usr/bin/env bash function generate_did() { local canister=$1 canister_root="src/$canister" cargo build --manifest-path="$canister_root/Cargo.toml" \ --target wasm32-unknown-unknown \ --release --package "$canister" \ candid-extractor "target/wasm32-unknown-unknown/release/$canister.wasm" > "$canister_root/$canister.did" } CANISTERS=icp_rust_boilerplate_backend for canister in $(echo $CANISTERS | sed "s/,/ /g") do generate_did "$canister" done ``` In the provided script, you'll notice that we've replaced the original CANISTERS line to match the name of your own canister, which in this case is icp_rust_boilerplate_backend. For example, if your canister is named test_rust_boilerplate, you should change the line ``` CANISTERS=icp_rust_boilerplate_backend ``` to the name of your own canister which in the case of this tutorial will be ``` CANISTERS=test_rust_boilerplate_backend ``` Additionally, we have a package.json file in our root directory to help automate the process. The package.json file defines two scripts: - generate: This script runs did.sh and then dfx generate to generate Candid interface definitions and other necessary files. - gen-deploy: This script does the same as generate but also deploys the canister with dfx deploy -y. You should run the generate script each time you modify, add, or remove exported functions of the canister. Otherwise, you may need to modify the Candid file manually. With this initial setup, you're ready to dive into the development process. In the next sections, we'll delve into the specific details of each file, beginning with src/icp_rust_boilerplate_backend/src/lib.rs. Let's start building our ICP Canister step by step! ## 2. Building the Canister In this section, we'll delve into writing our smart contract for our Rust project. Our goal is to build a simple message board application within our canister. This application will enable users to create messages, update messages, delete messages, and list messages. ### 2.1 Starting with lib.rs Let's navigate to the lib.rs file inside the src folder of our Canister project. This file serves as the entry point for our Canister, and it's where we'll write our custom code. Feel free to clear the contents of the lib.rs file to start with a clean slate for writing our custom Canister code. In the upcoming sections, we'll explore how to implement various functionalities within our Canister. ### 2.2 Importing Dependencies Let's begin by importing the necessary dependencies for our canister. We'll be using the following dependencies to facilitate our development: ``` #[macro_use] extern crate serde; use candid::{Decode, Encode}; use ic_cdk::api::time; use ic_stable_structures::memory_manager::{MemoryId, MemoryManager, VirtualMemory}; use ic_stable_structures::{BoundedStorable, Cell, DefaultMemoryImpl, StableBTreeMap, Storable}; use std::{borrow::Cow, cell::RefCell}; ``` - `candid`: Candid is a serialization format used in the Internet Computer Protocol for defining the interface of Canisters. - `ic_cdk`: The Internet Computer SDK (IC SDK) provides libraries that enable developers to build software for the Internet Computer. - `ic_stable_structures`: This library offers a set of data structures that remain stable across upgrades. - `std`: The Rust Standard Library provides essential runtime functionality for building portable Rust software. - `serde`: Serde is a framework for serializing and deserializing Rust data structures efficiently and generically. ### 2.3 Defining Canister's Struct We'll define our canister's struct, which will hold our canister's state. To store our canister's state, we'll use a VirtualMemory struct, which is a wrapper around a MemoryManager. This allows us to store data in a stable memory location, ensuring persistence across upgrades. ``` type Memory = VirtualMemory<DefaultMemoryImpl>; type IdCell = Cell<u64, Memory>; ``` The idCell is a cell responsible for holding the current ID of the message. We'll utilize this to generate unique IDs for each message. ### 2.4 Defining Message Struct Next, we'll define our Message struct, which resembles a regular JavaScript object. We'll use this struct to store our messages. ``` #[derive(candid::CandidType, Clone, Serialize, Deserialize, Default)] struct Message { id: u64, title: String, body: String, attachment_url: String, created_at: u64, updated_at: Option<u64>, } ``` This struct will represent the messages in our message board application, and it includes fields for ID, title, body, attachment URL, creation timestamp, and an optional update timestamp. With these initial definitions, we're ready to start building our smart contract logic for our Canister project. In the following sections, we'll delve deeper into implementing various functionalities within our Canister. ### 2.5 Setting up Thread Local Variables Now, let's set up our thread-local variables that will hold our canister's state. We will use a `RefCell` to manage our canister's state, allowing us to access it from anywhere in our code. A `RefCell` is a mutable memory location with dynamically checked borrow rules. It is useful when you're confident that your code adheres to the borrowing rules, but the compiler cannot guarantee that. ``` thread_local! { static MEMORY_MANAGER: RefCell<MemoryManager<DefaultMemoryImpl>> = RefCell::new( MemoryManager::init(DefaultMemoryImpl::default()) ); static ID_COUNTER: RefCell<IdCell> = RefCell::new( IdCell::init(MEMORY_MANAGER.with(|m| m.borrow().get(MemoryId::new(0))), 0) .expect("Cannot create a counter") ); static STORAGE: RefCell<StableBTreeMap<u64, Message, Memory>> = RefCell::new(StableBTreeMap::init( MEMORY_MANAGER.with(|m| m.borrow().get(MemoryId::new(1))) )); } ``` The three thread-local variables we've defined are: - `MEMORY_MANAGER` - This thread-local variable holds our canister's virtual memory, enabling us to access the memory manager from any part of our code. - `ID_COUNTER` - It holds our canister's ID counter, allowing us to access it from anywhere in our code. - `STORAGE` - This variable holds our canister's storage, enabling access from anywhere in our code. ### 2.6 Setting Up MessagePayload With our state variables configured, we can proceed to set up our message payload. The MessagePayload struct is used when adding or updating messages and includes fields for the title, body, and attachment URL. ``` #[derive(candid::CandidType, Serialize, Deserialize, Default)] struct MessagePayload { title: String, body: String, attachment_url: String, } ``` The MessagePayload struct defines the structure for the data that will be used when creating or updating messages within our canister. With our thread-local variables and message payload structure in place, we are ready to start implementing the core logic for our message board application within the smart contract. In the upcoming sections, we will dive deeper into handling message creation, updates, deletions, and listing. At this junction, your code should look like this: ``` #[macro_use] extern crate serde; use candid::{Decode, Encode}; use ic_cdk::api::time; use ic_stable_structures::memory_manager::{MemoryId, MemoryManager, VirtualMemory}; use ic_stable_structures::{BoundedStorable, Cell, DefaultMemoryImpl, StableBTreeMap, Storable}; use std::{borrow::Cow, cell::RefCell}; type Memory = VirtualMemory<DefaultMemoryImpl>; type IdCell = Cell<u64, Memory>; #[derive(candid::CandidType, Clone, Serialize, Deserialize, Default)] struct Message { id: u64, title: String, body: String, attachment_url: String, created_at: u64, updated_at: Option<u64>, } // a trait that must be implemented for a struct that is stored in a stable struct impl Storable for Message { fn to_bytes(&self) -> std::borrow::Cow<[u8]> { Cow::Owned(Encode!(self).unwrap()) } fn from_bytes(bytes: std::borrow::Cow<[u8]>) -> Self { Decode!(bytes.as_ref(), Self).unwrap() } } // another trait that must be implemented for a struct that is stored in a stable struct impl BoundedStorable for Message { const MAX_SIZE: u32 = 1024; const IS_FIXED_SIZE: bool = false; } thread_local! { static MEMORY_MANAGER: RefCell<MemoryManager<DefaultMemoryImpl>> = RefCell::new( MemoryManager::init(DefaultMemoryImpl::default()) ); static ID_COUNTER: RefCell<IdCell> = RefCell::new( IdCell::init(MEMORY_MANAGER.with(|m| m.borrow().get(MemoryId::new(0))), 0) .expect("Cannot create a counter") ); static STORAGE: RefCell<StableBTreeMap<u64, Message, Memory>> = RefCell::new(StableBTreeMap::init( MEMORY_MANAGER.with(|m| m.borrow().get(MemoryId::new(1))) )); } #[derive(candid::CandidType, Serialize, Deserialize, Default)] struct MessagePayload { title: String, body: String, attachment_url: String, } ``` ### 2.7 Managing Messages Now, let's explore the functions responsible for managing messages, starting with the `get_message` function, which retrieves a message from our canister's storage. ``` #[ic_cdk::query] fn get_message(id: u64) -> Result<Message, Error> { match _get_message(&id) { Some(message) => Ok(message), None => Err(Error::NotFound { msg: format!("a message with id={} not found", id), }), } } ``` - `get_message Function` : The get_message function takes an id as input and returns a Result containing a Message or an Error. It is marked with the #[ic_cdk::query] attribute, indicating that it is a query function that does not modify the canister's state. It uses the _get_message helper function to retrieve the message from the canister's storage. Next, we explore the _get_message function, responsible for fetching a message from our canister's storage. ``` fn _get_message(id: &u64) -> Option<Message> { STORAGE.with(|s| s.borrow().get(id)) } ``` - `_get_message Function` : The _get_message is a helper function used inside the get_message function. It accepts an id as a reference and returns an Option<Message>. It retrieves the message from the canister's storage using the STORAGE thread-local variable. Now, let's create the add_message function, responsible for adding a new message to our canister's storage. ``` #[ic_cdk::update] fn add_message(message: MessagePayload) -> Option<Message> { let id = ID_COUNTER .with(|counter| { let current_value = *counter.borrow().get(); counter.borrow_mut().set(current_value + 1) }) .expect("cannot increment id counter"); let message = Message { id, title: message.title, body: message.body, attachment_url: message.attachment_url, created_at: time(), updated_at: None, }; do_insert(&message); Some(message) } ``` - `add_message` Function: The add_message function takes a message of type MessagePayload as input and returns an Option<Message>. It generates a unique id for the message, creates a new Message struct, and adds it to the canister's storage. It uses the do_insert helper function to perform the storage operation. In the next sections, we will cover additional functions for updating, deleting, and listing messages as part of our message board application's smart contract logic. Next, let's explore the `do_insert` function, which is responsible for adding a message to our canister's storage. ``` // helper method to perform insert. fn do_insert(message: &Message) { STORAGE.with(|service| service.borrow_mut().insert(message.id, message.clone())); } ``` The `do_insert` function takes in a message and adds the message to our canister's storage. It uses the `STORAGE` thread local variable to add the message to our canister's storage. ### 3.1.7 Updating and Deleting Messages Now, let's create the `update_message` function, which is responsible for updating a message in our canister's storage. ``` #[ic_cdk::update] fn update_message(id: u64, payload: MessagePayload) -> Result<Message, Error> { match STORAGE.with(|service| service.borrow().get(&id)) { Some(mut message) => { message.attachment_url = payload.attachment_url; message.body = payload.body; message.title = payload.title; message.updated_at = Some(time()); do_insert(&message); Ok(message) } None => Err(Error::NotFound { msg: format!( "couldn't update a message with id={}. message not found", id ), }), } } ``` Just like the `add_message` function, the `update_message` function takes an id and a payload of type MessagePayload as input and returns a Result containing a Message or an Error. It updates an existing message in the canister's storage based on the provided id. If the message is successfully updated, it returns the updated message. Otherwise, it returns an error. Next, let's create the delete_message function, responsible for deleting a message from our canister's storage. ``` #[ic_cdk::update] fn delete_message(id: u64) -> Result<Message, Error> { match STORAGE.with(|service| service.borrow_mut().remove(&id)) { Some(message) => Ok(message), None => Err(Error::NotFound { msg: format!( "couldn't delete a message with id={}. message not found.", id ), }), } } ``` - `delete_message` Function: The delete_message function takes an id as input and returns a Result containing a Message or an Error. It deletes an existing message from the canister's storage based on the provided id. If the message is successfully deleted, it returns the deleted message. Otherwise, it returns an error. Finally, we create the Error enum, responsible for handling errors. ``` #[derive(candid::CandidType, Deserialize, Serialize)] enum Error { NotFound { msg: String }, } ``` To generate the Candid interface definitions for our canister, add the following code to the bottom of the file: ``` // need this to generate candid ic_cdk::export_candid!(); ``` This completes the explanation of the functions for managing messages within the smart contract of the message board application. With these functions in place, you have the fundamental building blocks to create, update, delete, and retrieve messages from the canister's storage. At the end of this section, your code should look like this: ``` #[macro_use] extern crate serde; use candid::{Decode, Encode}; use ic_cdk::api::time; use ic_stable_structures::memory_manager::{MemoryId, MemoryManager, VirtualMemory}; use ic_stable_structures::{BoundedStorable, Cell, DefaultMemoryImpl, StableBTreeMap, Storable}; use std::{borrow::Cow, cell::RefCell}; type Memory = VirtualMemory<DefaultMemoryImpl>; type IdCell = Cell<u64, Memory>; #[derive(candid::CandidType, Clone, Serialize, Deserialize, Default)] struct Message { id: u64, title: String, body: String, attachment_url: String, created_at: u64, updated_at: Option<u64>, } // a trait that must be implemented for a struct that is stored in a stable struct impl Storable for Message { fn to_bytes(&self) -> std::borrow::Cow<[u8]> { Cow::Owned(Encode!(self).unwrap()) } fn from_bytes(bytes: std::borrow::Cow<[u8]>) -> Self { Decode!(bytes.as_ref(), Self).unwrap() } } // another trait that must be implemented for a struct that is stored in a stable struct impl BoundedStorable for Message { const MAX_SIZE: u32 = 1024; const IS_FIXED_SIZE: bool = false; } thread_local! { static MEMORY_MANAGER: RefCell<MemoryManager<DefaultMemoryImpl>> = RefCell::new( MemoryManager::init(DefaultMemoryImpl::default()) ); static ID_COUNTER: RefCell<IdCell> = RefCell::new( IdCell::init(MEMORY_MANAGER.with(|m| m.borrow().get(MemoryId::new(0))), 0) .expect("Cannot create a counter") ); static STORAGE: RefCell<StableBTreeMap<u64, Message, Memory>> = RefCell::new(StableBTreeMap::init( MEMORY_MANAGER.with(|m| m.borrow().get(MemoryId::new(1))) )); } #[derive(candid::CandidType, Serialize, Deserialize, Default)] struct MessagePayload { title: String, body: String, attachment_url: String, } #[ic_cdk::query] fn get_message(id: u64) -> Result<Message, Error> { match _get_message(&id) { Some(message) => Ok(message), None => Err(Error::NotFound { msg: format!("a message with id={} not found", id), }), } } #[ic_cdk::update] fn add_message(message: MessagePayload) -> Option<Message> { let id = ID_COUNTER .with(|counter| { let current_value = *counter.borrow().get(); counter.borrow_mut().set(current_value + 1) }) .expect("cannot increment id counter"); let message = Message { id, title: message.title, body: message.body, attachment_url: message.attachment_url, created_at: time(), updated_at: None, }; do_insert(&message); Some(message) } #[ic_cdk::update] fn update_message(id: u64, payload: MessagePayload) -> Result<Message, Error> { match STORAGE.with(|service| service.borrow().get(&id)) { Some(mut message) => { message.attachment_url = payload.attachment_url; message.body = payload.body; message.title = payload.title; message.updated_at = Some(time()); do_insert(&message); Ok(message) } None => Err(Error::NotFound { msg: format!( "couldn't update a message with id={}. message not found", id ), }), } } // helper method to perform insert. fn do_insert(message: &Message) { STORAGE.with(|service| service.borrow_mut().insert(message.id, message.clone())); } #[ic_cdk::update] fn delete_message(id: u64) -> Result<Message, Error> { match STORAGE.with(|service| service.borrow_mut().remove(&id)) { Some(message) => Ok(message), None => Err(Error::NotFound { msg: format!( "couldn't delete a message with id={}. message not found.", id ), }), } } #[derive(candid::CandidType, Deserialize, Serialize)] enum Error { NotFound { msg: String }, } // a helper method to get a message by id. used in get_message/update_message fn _get_message(id: &u64) -> Option<Message> { STORAGE.with(|service| service.borrow().get(id)) } // need this to generate candid ic_cdk::export_candid!(); ``` ## 3. Deploy and Interact With Our Canister With our canister code ready, it's time to build and deploy our canister. ### 3.1 Starting the Local Canister Replica First, let's start our local canister replica in the background. Run the following command in your terminal: ``` dfx start --background ``` When you run this command, you should see a similar output: ``` Running dfx start for version 0.14.0 Using the default definition for the 'local' shared network because /Users/<username>/.config/dfx/networks.json does not exist. Dashboard: http://localhost:49846/_/dashboard ``` Finally, go ahead and deploy your canister code with the command ``` npm run gen-deploy ``` This command will initiate the deployment process for your canister. Once the deployment is successful, your canister will be available on the Internet Computer, and you can start interacting with it using various methods and APIs. **Note** : If you are getting a permission error when running this command, you can apply the following steps to resolve: First run the following command in your terminal ``` chmod +x did.sh ``` Finally rerun the command ``` npm run gen-deploy ``` ### 3.2 Interacting With Our Canister Now that we have our canister deployed, There are two ways to interact with our canister: #### 3.2.1 Option 1: Using the terminal To interact with our canister using the terminal, we can make use of the dfx canister call command. This command allows us to call functions on our canister from the terminal. 1. **Adding a message**: Let's start by calling the add_message function to add a new message to our canister. Run the following command in your terminal: ``` dfx canister call icp_rust_boilerplate_backend add_message '( record { title = "Hello World"; body = "This is my first message"; attachment_url = "https://www.dacade.org"; } )' ``` This command will add a new message to our canister. You should see a similar output: ``` ( opt record { id = 0 : nat64; title = "Hello World"; updated_at = null; body = "This is my first message"; created_at = 1_697_100_483_214_101_991 : nat64; attachment_url = "https://www.dacade.org"; } ) ``` 2. **Retrieving a message**: Next, let's call the get_message function to retrieve the message we just added. Run the following command in your terminal: ``` dfx canister call icp_rust_boilerplate_backend get_message '(0)' ``` This command will retrieve the message we just added. You should see a similar output: ``` ( variant { Ok = record { id = 0 : nat64; title = "Hello World"; updated_at = null; body = "This is my first message"; created_at = 1_697_100_483_214_101_991 : nat64; attachment_url = "https://www.dacade.org"; } }, ) ``` 3. **Updating a message**: Next, let's call the update_message function to update the message we just added. Run the following command in your terminal: ``` dfx canister call icp_rust_boilerplate_backend update_message '(0, record { title = "Hello World Updated"; body = "This is my first message"; attachment_url = "https://www.dacade.org"; })' ``` This command will update the message we just added. You should see a similar output: ``` ( variant { Ok = record { id = 0 : nat64; title = "Hello World updated"; updated_at = opt (1_697_100_678_394_806_548 : nat64); body = "This is my first message"; created_at = 1_697_100_483_214_101_991 : nat64; attachment_url = "https://www.dacade.org"; } }, ``` 4. **Deleting a message**: Finally, let's call the delete_message function to delete the message we just added. Run the following command in your terminal: ``` dfx canister call icp_rust_boilerplate_backend delete_message '(0)' ``` This command will delete the message we just added. You should see a similar output: ``` ( variant { Ok = record { id = 0 : nat64; title = "Hello World updated"; updated_at = opt (1_697_100_678_394_806_548 : nat64); body = "This is my first message"; created_at = 1_697_100_483_214_101_991 : nat64; attachment_url = "https://www.dacade.org"; } }, ) ``` #### 3.2.2 Option 2: Using the Candid UI **Note : If you are using Codespaces, this option will not work, so you have to go with the Option 1.** First, let's start our local canister replica in the background. Run the following command in your terminal: ``` dfx start --background ``` When you run this command, you should see a similar output: ``` Running dfx start for version 0.14.0 Using the default definition for the 'local' shared network because /Users/<username>/.config/dfx/networks.json does not exist. Dashboard: http://localhost:49846/_/dashboard ``` Finally, go ahead and deploy your canister code with the command ``` npm run gen-deploy ``` When it is done you would see a similar output: ``` Installing canisters... Creating UI canister on the local network. The UI canister on the "local" network is "bd3sg-teaaa-aaaaa-qaaba-cai" Installing code for canister icp_rust_message_board_contract_backend, with canister ID bkyz2-fmaaa-aaaaa-qaaaq-cai Deployed canisters. URLs: Backend canister via Candid interface: icp_rust_boilerplate_backend: http://127.0.0.1:4943/?canisterId=bd3sg-teaaa-aaaaa-qaaba-cai&id=bkyz2-fmaaa-aaaaa-qaaaq-cai ``` You can then copy the URL for the backend canister via Candid interface and paste it in your browser. This will open the Candid UI for your canister which looks like this: ![](https://hackmd.io/_uploads/Hy66WtSba.gif) ## 4. Conclusion In this tutorial, you've embarked on a journey to explore the development of a decentralized Rust-based canister on the Internet Computer Protocol (ICP) platform. We introduced you to key concepts essential for working with Rust and canisters, guiding you through the process of setting up your development environment and creating a web3 canister with fundamental CRUD operations. Throughout this tutorial, you've accomplished the following: - **Understanding Rust's Role:** You've gained an understanding of the pivotal role that Rust plays as a systems programming language in developing canisters. You've explored Rust's ownership system, advanced type system, and concurrency model, equipping you to build secure and efficient software on the ICP platform. - **Deployment and Interaction:** You've learned how to deploy your Rust canister using the `dfx deploy` command, and you've delved into the methods for interacting with your canister through both the terminal and web interfaces. You've executed essential functions, including adding, retrieving, updating, and deleting messages, and grasped the nuances of these commands. - **Empowering Your Journey:** By completing this tutorial, you've gained practical experience in Rust-based canister development, setting the stage for creating more intricate and robust decentralized applications (dApps) on the Internet Computer. As you continue to explore the vast landscape of the Internet Computer and its ecosystem, remember that you have a multitude of possibilities at your fingertips. Whether you're drawn to decentralized finance (DeFi), social media applications, decentralized autonomous organizations (DAOs), or any other innovative use case, the knowledge and skills you've acquired here provide a solid foundation for your journey. It's important to note that this course is open source and falls under the MIT license. We encourage you to contribute to enhancing the course content by making pull requests if you have suggestions for improvement. You can do so by visiting the course repository [here](https://github.com/yourrepository) and sharing your insights with the community. For further learning and engaging with like-minded individuals, consider exploring the following resources within the ICP community: - **Discord:** Join the vibrant ICP community on Discord, where you can partake in discussions, seek solutions to challenges, and collaborate with fellow developers. [Join the Conversation](https://discord.com/invite/cA7y6ezyE2). - **Forum:** The [ICP Forum](https://forum.dfinity.org/) is a valuable platform for asking questions, sharing your projects, and gaining insights into the work of other community members. - **Rust in the ICP Ecosystem:** To extend your knowledge of Rust within the ICP ecosystem, explore the various resources available. These platforms offer a wealth of knowledge and the support of a passionate community, facilitating your journey into the decentralized web. We anticipate and look forward to witnessing the innovative creations that will emerge from your endeavors.

Import from clipboard

Paste your markdown or webpage here...

Advanced permission required

Your current role can only read. Ask the system administrator to acquire write and comment permission.

This team is disabled

Sorry, this team is disabled. You can't edit this note.

This note is locked

Sorry, only owner can edit this note.

Reach the limit

Sorry, you've reached the max length this note can be.
Please reduce the content or divide it to more notes, thank you!

Import from Gist

Import from Snippet

or

Export to Snippet

Are you sure?

Do you really want to delete this note?
All users will lose their connection.

Create a note from template

Create a note from template

Oops...
This template has been removed or transferred.
Upgrade
All
  • All
  • Team
No template.

Create a template

Upgrade

Delete template

Do you really want to delete this template?
Turn this template into a regular note and keep its content, versions, and comments.

This page need refresh

You have an incompatible client version.
Refresh to update.
New version available!
See releases notes here
Refresh to enjoy new features.
Your user state has changed.
Refresh to load new user state.

Sign in

Forgot password

or

By clicking below, you agree to our terms of service.

Sign in via Facebook Sign in via Twitter Sign in via GitHub Sign in via Dropbox Sign in with Wallet
Wallet ( )
Connect another wallet

New to HackMD? Sign up

Help

  • English
  • 中文
  • Français
  • Deutsch
  • 日本語
  • Español
  • Català
  • Ελληνικά
  • Português
  • italiano
  • Türkçe
  • Русский
  • Nederlands
  • hrvatski jezik
  • język polski
  • Українська
  • हिन्दी
  • svenska
  • Esperanto
  • dansk

Documents

Help & Tutorial

How to use Book mode

Slide Example

API Docs

Edit in VSCode

Install browser extension

Contacts

Feedback

Discord

Send us email

Resources

Releases

Pricing

Blog

Policy

Terms

Privacy

Cheatsheet

Syntax Example Reference
# Header Header 基本排版
- Unordered List
  • Unordered List
1. Ordered List
  1. Ordered List
- [ ] Todo List
  • Todo List
> Blockquote
Blockquote
**Bold font** Bold font
*Italics font* Italics font
~~Strikethrough~~ Strikethrough
19^th^ 19th
H~2~O H2O
++Inserted text++ Inserted text
==Marked text== Marked text
[link text](https:// "title") Link
![image alt](https:// "title") Image
`Code` Code 在筆記中貼入程式碼
```javascript
var i = 0;
```
var i = 0;
:smile: :smile: Emoji list
{%youtube youtube_id %} Externals
$L^aT_eX$ LaTeX
:::info
This is a alert area.
:::

This is a alert area.

Versions and GitHub Sync
Get Full History Access

  • Edit version name
  • Delete

revision author avatar     named on  

More Less

Note content is identical to the latest version.
Compare
    Choose a version
    No search result
    Version not found
Sign in to link this note to GitHub
Learn more
This note is not linked with GitHub
 

Feedback

Submission failed, please try again

Thanks for your support.

On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

Please give us some advice and help us improve HackMD.

 

Thanks for your feedback

Remove version name

Do you want to remove this version name and description?

Transfer ownership

Transfer to
    Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

      Link with GitHub

      Please authorize HackMD on GitHub
      • Please sign in to GitHub and install the HackMD app on your GitHub repo.
      • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
      Learn more  Sign in to GitHub

      Push the note to GitHub Push to GitHub Pull a file from GitHub

        Authorize again
       

      Choose which file to push to

      Select repo
      Refresh Authorize more repos
      Select branch
      Select file
      Select branch
      Choose version(s) to push
      • Save a new version and push
      • Choose from existing versions
      Include title and tags
      Available push count

      Pull from GitHub

       
      File from GitHub
      File from HackMD

      GitHub Link Settings

      File linked

      Linked by
      File path
      Last synced branch
      Available push count

      Danger Zone

      Unlink
      You will no longer receive notification when GitHub file changes after unlink.

      Syncing

      Push failed

      Push successfully