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.
A canister is a fundamental building block and execution environment for deploying and running software applications on the Internet Computer Protocol (ICP) platform. Canisters bundle together code and state to create a secure and efficient execution environment. They are similar to smart contracts on other blockchain platforms. Canisters enable the development of scalable and decentralized applications, including DeFi platforms, social media applications, DAOs, and more.
While having prior coding experience is necessary, you do not need to have any prior blockchain experience to follow this tutorial. However, we do recommend that you have the following:
If you want to skip to the complete code, you can find the GitHub repository here.
In this section, we will help you set up the boilerplate code for our project. By the end of this section, you'll have a development environment pre-configured with all the necessary tools and dependencies, and you'll be ready to start building your canisters.
You have two options for setting up your development environment: you can set up your development environment either locally on your machine or in the cloud using GitHub Codespaces.
GitHub Codespaces provides a complete, ready-to-use dev environment in your browser. It saves you from the need for local setup, allowing you to concentrate on learning and building.
To create a new Codespace with the boilerplate, follow these steps:
Access the ICP Rust Boilerplate Repository
Go to the ICP Rust boilerplate repository.
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.
If you prefer to set up your development environment locally, follow these steps:
Access the ICP message board Repository
Go to the ICP message board repository.
Clone the Repository:
Now that you have successfully set up your boilerplte environment, you can open up the codebase in your favorite editor.
In this section, we'll install the necessary dependencies for our project. We'll install Rust, Wasmtime, Candid Extractor and DFX. We'll also set up a script to automate the generation of Candid interface definitions for our canister.
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.
To install Rust, we'll use rustup, the official Rust toolchain installer. Run the following command in your terminal to install Rust:
After running the command, you should see a welcome message, that will ask you how to proceed with the installation:
Press 1 and hit enter or just directly press enter to proceed with the installation since 1 is selected as default.
After the installation is complete, you should see a similar output in your terminal:
Next run the source command to ensure that the necessary environment variables are loaded:
This completes the installation of Rust. In the next section, we'll install the necessary dependencies for our project.
Next, we need to install the wasm32-unknown-unknown target, which is a WebAssembly target for the Rust programming language. So that we can compile Rust code to WebAssembly and run it on the Internet Computer.
Run the following command to install the wasm32-unknown-unknown target:
Next, we need to install Candid Extractor, a tool that allows you to extract Candid interface definitions from WebAssembly modules. Which enables us to generate Candid interface definitions for our canister, to interact with it.
Run the following command to install Candid Extractor:
We'll also need DFX, a command-line interface for the Internet Computer. DFX allows you to create, build, deploy, and manage canisters on the platform. It also offers a local development environment for testing.
Install DFX by running:
Now that we have DFX installed, we need to add it to your path. Run the following command:
With all the dependencies installed, we're ready to start building our canister.
In this section, we'll explore the boilerplate code that was generated for you. It is designed to help you get started quickly by providing the necessary configuration files. This code also includes a simple canister that serves as a reference for constructing your own canisters.
The boilerplate code is organized into the following directories and files:
Let's explore each of these files and directories in detail.
Inside the src
directory, we have a subdirectory named icp_rust_boilerplate_backend
. This is where the core of our Canister resides.
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.You will know this file if you've used Git before. It specifies files and directories that should be ignored by Git.
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.
This JSON configuration file contains settings related to the Internet Computer development environment, including project configuration, build settings, and more.
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.
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
to the name of your own canister which in the case of this tutorial will be
Additionally, we have a package.json file in our root directory that contains scripts for generating Candid interface definitions and deploying our canister.
The package.json file defines two scripts:
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!
In this section, we'll build out our message board ICP Canister. We'll implement CRUD (Create, Read, Update, Delete) functionality to manage messages within a Canister.
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 Canister's logic.
Feel free to clear the contents of the lib.rs file and follow along with the code snippets in this tutorial.
Let's begin by importing the necessary dependencies for our canister. We'll be using the following dependencies to facilitate our development:
candid
: Candid is a serialization format used in the Internet Computer Protocol for defining the interface of Canisters.ic_cdk
: The core crate (package/module) of the Rust CDK (Canister Development Kit) for the Internet Computer. It provides the core methods that enable Rust programs to interact with the Internet Computer blockchain system API.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.Next, we'll define our Memory and IdCell types. We'll use these types to store our canister's state and generate unique IDs for each message.
To store our canister's state, we'll use a MemoryManager to manage our canister's virtual 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.
Next, we'll define our Message struct, which will represent the messages in our message board application.
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 implementing the core logic for our message board application within the smart contract.
Next, we'll implement the Storable and BoundedStorable traits for our Message struct. These traits are required for a struct to be stored in a stable struct. Traits are a way to group methods into a common interface that can be implemented by multiple types. They are similar to interfaces in other programming languages.
The Storable trait is used to convert a struct to bytes and vice versa. The BoundedStorable trait is used to define the maximum size of a struct and whether it is a fixed size or not.
Now, let's set up our thread-local variables that will hold our canister's state. Thread-local variables are variables that are local to the current thread (sequence of instructions). They are useful when you need to share data between multiple threads.
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.
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.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.
The MessagePayload struct defines the structure for the data that will be used when creating or updating messages within our canister.
At this point, your code should look like this:
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.
In this section, we'll implement the core logic for managing messages within our canister.
get_message Function
:Let's start by implementing the get_message
function, which retrieves a message from our canister's storage.
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.
_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.
add_message Function
:Now, let's create the add_message function, responsible for adding a new message to our canister's storage.
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.
do_insert Function
:As we saw in the previous section, the do_insert function is a helper function used inside the add_message function.
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.
update_message Function
:Now, let's create the update_message
function, which is responsible for updating a message in our canister's storage.
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.
delete_message Function
:Next, let's create the delete_message function, responsible for deleting a message from our canister's storage.
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.
enum Error
:Finally, we create the Error enum, which is used to represent errors that may occur when interacting with our canister.
To generate the Candid interface definitions for our canister, add the following code to the bottom of the file:
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:
With our canister code ready, it's time to build and deploy our canister.
First, let's start our local canister replica in the background. Run the following command in your terminal:
When you run this command, you should see a similar output:
Finally, go ahead and deploy your canister code with the command:
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:
Rerun the following command:
Now that we have our canister deployed, There are two ways to interact with our canister:
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:
This command will add a new message to our canister. You should see a similar output:
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:
This command will retrieve the message we just added. You should see a similar output:
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:
This command will update the message we just added. You should see a similar output:
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:
This command will delete the message we just added. You should see a similar output:
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:
When you run this command, you should see a similar output:
Finally, go ahead and deploy your canister code with the command:
When it is done you would see a similar output:
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:
In this tutorial, you've explored 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:
Written a smart contract (Canister) in Rust: You've learned how to write a smart contract in Rust, including the necessary dependencies and functions for managing messages within the canister's storage.
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 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:
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.