Welcome to this ICP Azle 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 Azle canisters. 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.
tsconfig.json
, dfx.json
, and package.json
filesA 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.
Azle is a TypeScript Canister Development Kit (CDK) for the Internet Computer (IC). It provides a set of libraries and tools that make it easy to build and deploy canisters on the IC platform. Azle allows web developers to bring their TypeScript/JavaScript skills to the IC and use various npm packages and VS Code intellisense. In this tutorial, you will use Azle to create and deploy your canisters.
Azle is still in development and does not yet have many live, successful, continuously operating applications deployed to the IC.
If you want to learn more about Azle, check out the Azle documentation.
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:
Here are the key technologies and tools we'll be using:
tsconfig.json
) for setting up the TypeScript compiler options.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 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, go to the ICP-azle-boilerplate repository.
Next, click on the "Code" button, then select "Create codespace on main". This action will generate a new Codespace, pre-configured with everything you need to start building this project.
Please note that the first time you open the Codespace, the dependencies for this project will be installed automatically. This process may take a few minutes, but you can monitor the installation progress in the terminal.
If you prefer to set up your development environment locally, start by navigating to the ICP-azle-boilerplate repository. Select the "Code" button, then the "Local" tab, and copy the repository's URL.
In your terminal, navigate to the directory where you want to store your project, then clone the repository to your local machine by running:
git clone https://github.com/dacadeorg/ICP-azle-boilerplate.git
Next, move into the cloned repository's directory with:
cd ICP-azle-boilerplate
Finally, install the project's dependencies by running:
npm install
This command will install all the necessary dependencies for the project. Once the installation is complete, you're ready to start building your canisters!
In this section, we will prepare our terminal environment by installing key tools: Node Version Manager (nvm) and DFX. Please note that the following instructions are specifically for Unix-like systems such as Linux and macOS. If you're on a Windows system, you would need to set up the Windows Subsystem for Linux (WSL) to follow along, or alternatively, you could use GitHub Codespaces. Let's get started.
Install Node Version Manager (nvm): Nvm is a useful tool that allows for the management of multiple active Node.js versions. With nvm, switching between different Node.js versions is a breeze. For this tutorial, we'll utilize Node.js version 18. To install nvm, execute the following command in your terminal:
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh | bash
Switch to Node.js version 18: Node.js is a JavaScript runtime that enables the execution of JavaScript outside of a browser environment, and it's necessary for running our Azle project. To switch to Node.js version 18 using nvm, use the following command:
nvm use 18
Install DFX: DFX is the command-line interface for the Internet Computer, and we'll use it to create our Azle project. To install DFX, execute this command:
DFX_VERSION=0.14.1 sh -ci "$(curl -fsSL https://sdk.dfinity.org/install.sh)"
Add DFX to your path: Add DFX to your PATH: Now that DFX is installed, we need to add it to our system's PATH. This allows us to execute DFX commands from any location within the terminal. Run this command to add DFX to your PATH:
echo 'export PATH="$PATH:$HOME/bin"' >> "$HOME/.bashrc"
Reload your terminal (if using GitHub Codespaces): Reload your terminal (if using GitHub Codespaces): If you're using GitHub Codespaces for this tutorial, you'll need to reload your terminal to ensure all changes are properly applied. You can do this by clicking on the "Reload" button located in the top-right corner of your terminal.
The boilerplate code we've prepared serves as a basic Azle project. It is designed to help you get started quickly by providing the necessary configuration files and dependencies. This code also includes a simple canister that serves as a reference for constructing your own canisters. Let's explore its key components:
1. TypeScript Configuration File (tsconfig.json
): Located in the root directory of your project, this file sets up the TypeScript compiler options. Here is what it looks like:
You can learn more about these options in the TypeScript documentation.
2. DFX Configuration File (dfx.json
): Also in the root directory, this file configures DFX and includes the following:
This configuration file communicates vital aspects of your canister to the DFINITY SDK (dfx). Here, we're creating a message_board
canister using the Azle framework. Let's break down the properties:
message_board
in this case.src/index.ts
), where we write our TypeScript code.message_board
canister.src
folder and src/index.ts
file respectively for our code.src/index.did
), an interface description language (IDL) used by Internet Computer..azle/message_board/message_board.wasm.gz
), a fast, efficient, and secure binary instruction format.3. Package.json File: The package.json
file in the root directory manages the project's metadata and dependencies.
This file is crucial for managing the project's dependencies and scripts. It contains information about the project such as its name, version, and main file. It also lists the dependencies and devDependencies needed for the project, specifying their versions:
"@dfinity/agent"
: This is the JavaScript client library for the Internet Computer. It enables your application to communicate with canisters running on the Internet Computer. It is responsible for tasks such as sending requests to canisters, handling responses, and managing security and identity.
"@dfinity/candid"
: Candid is an interface description language (IDL) for the Internet Computer. It's used to define and describe the public interface of a service, namely its methods and their input/output types. This library provides tools for working with Candid in JavaScript.
"azle"
: Azle is a framework for building decentralized applications on the Internet Computer. It provides tools and abstractions that make it easier to write, deploy, and interact with canisters.
"uuid"
: The uuid package is a popular JavaScript library for creating unique identifiers. This could be used in your application for any purpose where you need a unique ID, such as creating unique identifiers for users, orders, or other entities.
The scripts
section includes commands that can be run from the terminal, while the engines
section specifies the versions of Node.js that the project is compatible with.
In this section, we're going to write our messaging canister. This canister is designed to handle the fundamental CRUD (Create, Read, Update, and Delete) operations, which are key to the functioning of any data-driven application. This functionality enables efficient data management within the canister. More specifically, we're going to use Azle to build a simple message board application, which will allow users to create, update, delete, and view messages.
If you're familiar with TypeScript, you'll find the Azle syntax quite similar. But even if you're new to TypeScript, there's no need to worry - we'll be guiding you through the syntax as we proceed with the development.
First, we need to set up the directory where we'll be writing our code. To do this, create a folder named src
by using the command mkdir src
in your terminal or by right-clicking on the left-hand side panel of your terminal and selecting "New Folder".
Once the src
directory is created, we need to establish an entry point for our canister. This will be done by creating a file named index.ts
inside the src
folder. You can do this by executing the command touch src/index.ts
in your terminal or by right-clicking inside the src
folder in your terminal and selecting "New File". This index.ts
file will serve as the entry point for our canister - it's where we'll be writing our application code.
To start, we need to incorporate several dependencies which our smart contract will make use of. Add the following lines of code at the top of your index.ts
file:
Here's a brief rundown of what each of these imported items does:
$query
: Function enabling us to retrieve information from our canister.$update
: Function facilitating updates to our canister.Record
: Type used for creating a record data structure.StableBTreeMap
: Type used for creating a map data structure.Vec
: Type used for creating a vector data structure.match
: Function enabling us to perform pattern matching on a result.Result
: Type used for creating a result data structure.nat64
: Type used for creating a 64-bit unsigned integer.ic
: Type used for creating an Internet Computer data structure.Opt
: Type used for creating an optional data structure.uuidv4
: Function generating a unique identifier for each new message.Before we start writing the logic of our canister, it's important to define the structure of the data we'll be working with. In our case, this is the 'Message' that will be posted on the board. This definition will help us ensure consistency and clarity when dealing with messages in our smart contract.
This code block defines the 'Message' type, where each message is characterized by a unique identifier, a title, a body, an attachment URL, and timestamps indicating when the message was created and last updated.
After defining the structure of a Message, we need to specify what kind of data will be sent to our smart contract. This is called the payload. In our context, the payload will contain the basic information needed to create a new message.
Incorporate the following code into your index.ts
file:
This 'MessagePayload' type outlines the structure of the data that will be sent to our smart contract when a new message is created. Each payload consists of a title, a body, and an attachment URL.
Now that we've defined our message types, we need a place to store these messages. For this, we'll be creating a storage variable in our index.ts
file:
This line of code establishes a storage variable named messageStorage
, which is a map associating strings (our keys) with messages (our values). This storage will allow us to store and retrieve messages within our canister.
Let's break down the new StableBTreeMap
constructor:
0
signifies the memory id, where to instantiate the map.44
sets the maximum size of the key (in bytes) in this map, it's 44 bytes because uuid_v4 generates identifiers which are exactly 44 bytes each.1024
defines the maximum size of each value within the map, ensuring our messages don't exceed a certain size.The next step is to create a function that retrieves all messages stored within our canister. To accomplish this, add the following code to your index.ts file:
This getMessages
function gives us access to all messages on our message board. The $query
decorator preceding the function tells Azle that getMessages
is a query function, meaning it reads from but doesn't alter the state of our canister.
The function returns a Result
type, which can hold either a value or an error. In this case, we're returning a vector of messages (Vec<Message>
) on successful execution, or a string error message if something goes wrong."
The next step involves creating a function to retrieve a specific message using its unique identifier (ID). Add the following code to your index.ts
file:
Here's an in-depth look at what the code does:
$query
decorator to indicate that this function is a query function. A query function is one that does not alter the state of our canister.getMessage
function is defined, which takes a string parameter id
. This id
is the unique identifier for the message we wish to retrieve. The function's return type is Result<Message, string>
. This means the function either returns a Message
object if successful or a string error message if unsuccessful.match
function from Azle. This function is used to handle possible options from a function that may or may not return a result, in our case, messageStorage.get(id)
.messageStorage.get(id)
attempts to retrieve a message with the given id
from our messageStorage
.id
is found, the Some
function is triggered, passing the found message as a parameter. We then return the found message wrapped in Result.Ok
.id
is found, the None
function is triggered. We return an error message wrapped in Result.Err
stating that no message with the given id
was found.This function, therefore, allows us to specifically query a message by its unique ID. If no message is found for the provided ID, we clearly communicate this by returning an informative error message."
Following on, we will create a function to add new messages. Input the following code into your index.ts file:
Here's a detailed exploration of the key components:
The $update
decorator is utilized to signify to Azle that this function is an update function. It is labelled as such because it modifies the state of our canister.
The function addMessage
is defined, which accepts a parameter payload
of type MessagePayload
. This payload will contain the data for the new message to be created.
Inside the function, we generate a new Message
object. The id
field of the message is assigned a unique identifier generated by the uuidv4
function. The created_at
field is assigned the current time retrieved using ic.time()
. The updated_at
field is set to Opt.None
since the message has not been updated at the point of creation. Finally, the remaining fields are spread from the payload
using the spread operator (...payload
).
The newly created message is then inserted into the messageStorage
using the insert
method. The id
of the message is used as the key.
The function concludes by returning the newly created message, wrapped in a Result.Ok
. If any errors occurred during the process, the function would return a string error message.
This function thus facilitates the creation of new messages within our canister, providing each with a unique identifier and timestamp."
Our next step is to create a function that allows us to update an existing message. Insert the following code into your index.ts
file:
This function, denoted by the $update
decorator, will change the state of our canister. Here's a breakdown of the new elements:
updateMessage
function takes two parameters: id
, which represents the unique identifier of the message to be updated, and payload
, which contains the new data for the message.match
function to handle the outcome of retrieving a message from messageStorage
by its id
. The match
function takes two cases: Some
and None
.Some
case, it implies that a message with the provided id
exists. We create an updated message by spreading the existing message and the payload into a new object, and set the updated_at
field with the current time using ic.time()
. This updated message is then inserted back into messageStorage
using the same id
.None
case, it indicates that no message with the provided id
could be found. In this situation, the function returns an error message stating that the update operation couldn't be performed as the message was not found.This updateMessage
function thus enables us to update the contents of an existing message within our canister.
The final step in our canister development is to create a function that allows for message deletion. Insert the following code into your index.ts
file:
Here, we're using the messageStorage.remove(id)
method to attempt to remove a message by its ID from our storage. If the operation is successful, it returns the deleted message, which we wrap in a Result.Ok
and return from the function. If no message with the given ID exists, the removal operation returns None
, and we return an error message wrapped in a Result.Err
, notifying that no message could be found with the provided ID to delete.
This function, marked by the $update
decorator, further extends our canister's capabilities, now including message deletion alongside creation, retrieval, and update.
A notable point is that the uuidV4 package may not function correctly within our canister. To address this, we need to apply a workaround that ensures compatibility with Azle. Insert the following code at the end of your index.ts file:
In this block of code, we're extending the globalThis
object by adding a crypto
property to it. This property is an object with a method called getRandomValues
. This method generates an array of random values, which is required by the uuidV4
function to generate unique IDs. Here's how it works:
We create a new Uint8Array
with 32 elements. Each element is an 8-bit unsigned integer, meaning it can hold a value between 0 and 255.
We then iterate over this array, assigning each element a random value between 0 and 255. This is achieved by using Math.random()
to generate a random float between 0 and 1, then multiplying it by 256 and rounding down to the nearest whole number with Math.floor()
.
Finally, we return this array of random values. This array is used by the uuidV4
function to create unique IDs for our messages.
By adding this block of code, we ensure that the uuidV4
package works smoothly with the Azle framework within our canister."
At the end of this step, your index.ts
file should look like this:
Having completed the coding of our canister, it's now time to deploy and interact with it.
The first step is to initialize our local Internet Computer replica, which is essentially an instance of the Internet Computer blockchain where our canister will run. We'll start this replica in the background to allow for other operations. This can be done by executing the following command in your terminal:
Upon successful execution, your terminal will display an output similar to the one below. This output confirms that a local instance of the Internet Computer is running, and it also provides a link to a dashboard where you can monitor the status of your local instance.
In this output, the URL for the dashboard (http://localhost:49846/_/dashboard) will be particularly helpful for debugging and observing the activity of your local replica.
IMPORTANT NOTE
StableBTreeMap, which is the data structure we use for messageStorage
, has certain constraints that you need to be aware of. Specifically, once a StableBTreeMap is initialized, its configuration becomes immutable. This means that you cannot make changes to aspects such as the data types or sizes of the keys or values.
If you need to make any changes to these elements of the StableBTreeMap, you will need to restart your local replica with the --clean
flag. The --clean
flag ensures that the replica is started afresh, allowing for the changes in configuration to take effect.
Here's how you can do it:
Remember, only use the --clean
flag if you have made changes to the configuration of your StableBTreeMap. If no changes have been made, a regular start of the local replica (i.e., without the --clean
flag) will suffice.
Next, we will compile our canister code and install it on the local network using the dfx deploy
command:
The dfx deploy
command is a convenient way to register, build, and deploy a canister on the Internet Computer network. By default, it targets all canisters defined in the project's dfx.json
configuration file. This command combines the following steps into one:
dfx canister create --all
)dfx build
)dfx canister install --all
)Executing the dfx deploy
command should result in an output similar to:
Note: If this is your first time running the dfx deploy
command, it may take a moment to register, build, and deploy your application. Take this time to relax as the system does its work.
Once the command completes, you should see a message indicating the successful deployment of your canisters. The output will include URLs for interacting with your backend canister through the Candid interface. For example:
The provided URL (in this case: http://127.0.0.1:4943/?canisterId=bd3sg-teaaa-aaaaa-qaaba-cai&id=bkyz2-fmaaa-aaaaa-qaaaq-cai) is the endpoint for your message_board canister. This URL links to a Candid interface, which provides a web-based interface for interacting with your canister's methods.
You can view a GIF illustrating this process:
There are two primary ways to interact with our canister: through the command line interface (CLI) or the web interface. We'll begin with the CLI.
To interact with our canister through the CLI, we'll be using the dfx canister call
command. This command allows us to invoke functions on our canister from the terminal.
1. Adding a message
First, let's invoke the addMessage function from our canister file, which we created earlier. This function will add a message to our canister. Execute the following command in your terminal:
If the function call is successful, you should receive a response similar to this:
This output indicates that the addMessage
function has successfully added a message to your canister. The message includes a unique identifier, attachment URL, title, body, and creation timestamp. The updated_at
field remains null
because the message has not been updated since it was created.
2. Retrieving a single message
To retrieve a single message, invoke the getMessage
function. Replace 79daba82-18ce-4f69-afa1-7b3389368d1f
with the unique ID of the message you wish to retrieve. Here's the command:
dfx canister call message_board getMessage '("79daba82-18ce-4f69-afa1-7b3389368d1f")'
3. Updating a message
To update a message, use the updateMessage
function. Replace 79daba82-18ce-4f69-afa1-7b3389368d1f
with the unique ID of the message you wish to update. Here's the command:
dfx canister call message_board updateMessage '("79daba82-18ce-4f69-afa1-7b3389368d1f", record {"title"= "new title"; "body"= "new message"; "attachmentURL"= "url/path/to/some/photo/attachment"})'
4. Retrieving messages
To retrieve all messages, invoke the getMessages
function. In this case, we're not passing any argument to the function. Here's the command:
dfx canister call message_board getMessages '()'
5. Deleting a message
To delete a message, use the deleteMessage
function. Replace 79daba82-18ce-4f69-afa1-7b3389368d1f
with the unique ID of the message you wish to delete. Here's the command:
dfx canister call message_board deleteMessage '("79daba82-18ce-4f69-afa1-7b3389368d1f")'
Try for yourself, to add, retrieve, update, and delete messages using the CLI.
Now that we've covered the CLI, let's move on to the web interface.
getMessage
function from our canister file.To view the message we just added, we can make use of the candid interface that was generated to us when we ran the "dfx deploy" command.
It should look something like this:
Note: In Codespaces, the web interface might sometimes not be displayed correctly. In that case, you will need to use the CLI to interact with your canister.
In the interface, click on the getMessage
function. Then, enter the ID of the message you wish to retrieve. In this instance, we'll be retrieving the message we just created, hence we'll need to input the ID that we received from the addMessage
function response. Please note, your message ID will differ from the example given here.
After entering the ID, click on the Call
button. If done correctly, you should receive a response similar to this:
You can view a GIF illustrating this process of interacting with the web interface:
Now you can use the web interface to interact with the same functions we used in the CLI.
To conclude your work session, you can stop your local Azle replica by executing the following command in your terminal:
dfx stop
This command will shut down your local replica. Remember to always stop your local replica when you're done working to free up system resources."
In this tutorial, we've walked you through the process of building and interacting with a decentralized Azle canister. We introduced you to key concepts related to the Internet Computer and canisters and then guided you through the steps to set up your project and construct a message web3 canister with basic CRUD functionality.
You learned how to deploy your canister using dfx deploy
and how to interact with it both through the terminal and the Candid web interface. We explored how to execute various functions like addMessage
, getMessages
, updateMessage
, and deleteMessage
, and discussed the structure and usage of the commands required.
By working through this tutorial, you have gained hands-on experience with the Azle framework, one of the many tools available for building applications on the Internet Computer. This knowledge is a stepping stone for building more complex and robust decentralized applications (dApps) on this emerging platform.
As you continue to explore and experiment, remember that the Internet Computer and its associated technologies offer a wide array of possibilities. Whether you're interested in decentralized finance (DeFi) platforms, social media applications, decentralized autonomous organizations (DAOs), or something else entirely, the tools and techniques you've learned here will serve as a valuable foundation.
For additional learning and connecting with like-minded individuals, consider visiting the following resources:
These platforms are filled with enthusiastic individuals and helpful resources that can further facilitate your journey into the decentralized web. We look forward to seeing what you'll create!