Writing smart contracts
Canisters are an enhanced type of smart contract. Before a canister is deployed to ICP, the canister's code is compiled into a WebAssembly (Wasm) program, enabling it to store persistent data, be managed by entities such as DAOs, host entire applications, and more. Each canister has the following components:
Learn more about canister components.
Canister development kits (CDKs)
A canister development kit (CDK) can be used to write the code for a canister. CDKs provide build scripts that compile the canister code into Wasm programs that are compatible with ICP.
Available CDKs include:
Motoko: ICP's native programming language. By default, Motoko is installed with the IC SDK.
Rust CDK: By default, the Rust CDK is included as a dependency of the IC SDK. Alternatively, you can install it yourself.
Kybra: Python CDK : Learn more about how to install and use Azle.
Azle: TypeScript CDK : Learn more about how to install and use Kybra.
Bitfinity EVM for Solidity : Learn more about how to use the Bitfinity EVM.
C++ CDK: Supported through the ICPP-pro CDK.
Creating a project
Before writing canister code, you need to create a project.
- Create a new project
- Use an ICP Ninja project
If you did not download an ICP Ninja project, or if you would like to create a new project, run:
dfx new PROJECT_NAME --type=motoko
cd PROJECT_NAME
Options for the --type
flag are motoko
, rust
, azle
, and kybra
.
Navigate into the directory where you downloaded your ICP Ninja project using a command like:
cd /path/to/icp/ninja/project
You should be in a directory that contains a file called dfx.json
. This file is used to configure your project's settings. It includes the project's canister definitions, such as the canister's type, source code file, and dependencies.
{
"canisters": {
"PROJECT_NAME_backend": { // Backend canister name
"main": "src/PROJECT_NAME_backend/main.mo", // Backend canister source code
"type": "motoko" // Canister language
},
"PROJECT_NAME_frontend": { // Frontend canister name
"dependencies": [
"PROJECT_NAME_backend"
],
"source": [
"src/PROJECT_NAME_frontend/dist" // Frontend canister source code
],
"type": "assets", // All frontend canisters will have type 'assets' regardless of the frontend framework used
"workspace": "PROJECT_NAME_frontend"
}
},
"defaults": {
"build": {
"args": "",
"packtool": ""
}
},
"output_env_file": ".env",
"version": 1
}
Default project architecture
The default project architecture used by dfx new
and ICP Ninja projects contains two canisters, a backend
and a frontend
.
The backend
canister stores the dapp's functions and core logic.
The frontend
canister stores the app's frontend assets, including files such as HTML, CSS, JavaScript, React, images, and videos.
Writing canister code
This section will focus on writing backend canister code. If you want to explore some examples with application frontends, view the ICP Ninja projects.
Prerequisites
- Download and install an IDE or code editor. VS Code is recommended.
For writing Motoko code, the Motoko VS Code extension is highly recommended for syntax highlighting.
1. Open the backend canister source code file in your code editor.
For Motoko projects, this file will be src/PROJECT_NAME_backend/main.mo
.
For Rust projects, this file will be src/PROJECT_NAME_backend/src/lib.rs
.
For Azle or Kybra projects, please refer to their respective documentation for details about their default code files.
If you are using the default template created by dfx
, you will see the following default "Hello, world!" code:
- Motoko
- Rust
- TypeScript
- Python
actor {
public query func greet(name : Text) : async Text {
return "Hello, " # name # "!";
};
};
#[ic_cdk::query]
fn greet(name: String) -> String {
format!("Hello, {}!", name)
}
import { Canister, query, text } from 'azle';
export default Canister({
greet: query([text], text, (name) => {
return `Hello, ${name}!`;
})
})
from kybra import query
@query
def greet(name: str) -> str:
return f"Hello, {name}!"
If you are using an ICP Ninja project, this code will vary based on the example you downloaded.
2. Add randomness.
A key feature of ICP is its ability to generate onchain randomness using a verifiable random function (VRF) and chain-key cryptography. In each round of consensus on a subnet, the VRF is evaluated using the number of the round as input, producing a fresh set of random bytes. Then, the bytes are used as the seed for a pseudorandom number generator (PRNG), called random tape, which uses chain-key cryptography to create a random, unique value for each canister that requests it. Through this process, it is impossible to predict future outputs.
To use randomness in your canister, you need to make a call to the raw_rand
method of the management canister.
To demonstrate how to use randomness, you will create a simple app that generates a random number.
Remove the existing code in your backend source code file. Then, insert the following:
- Motoko
- Rust
- TypeScript
- Python
import Nat8 "mo:base/Nat8";
import Debug "mo:base/Debug";
import Blob "mo:base/Blob";
import Nat "mo:base/Nat";
// The management canister's principal ID is "aaaaa-aa".
import IC "ic:aaaaa-aa";
actor {
// Create a stable variable to store the current random number.
// Stable variables persist across canister upgrades.
stable var currentRandomNumber : Nat = 0;
// Create a function to generate a new random number.
// This function calls the raw_rand method of the management canister.
// Then it uses the random bytes returned to generate a random number.
private func generateNewNumber() : async () {
let randomBytes = await IC.raw_rand();
if (randomBytes.size() > 0) {
// Use the first byte to generate a number between 0 and 255
let bytes : [Nat8] = Blob.toArray(randomBytes);
currentRandomNumber := Nat8.toNat(bytes[0]);
Debug.print("Generated new random number: " # Nat.toText(currentRandomNumber));
};
};
// Use a query call to get current random number.
public query func getCurrentNumber() : async Nat {
currentRandomNumber
};
}
use candid::Nat;
// Import the management canister's raw_rand method
use ic_cdk::api::management_canister::main::raw_rand;
use std::time::Duration;
// Define a variable to hold the current random number.
thread_local! {
static CURRENT_RANDOM_NUMBER: std::cell::RefCell<Nat> = std::cell::RefCell::new(Nat::from(0u8));
}
// Define an init function to generate a new random number.
#[ic_cdk::init]
fn generate_new_number() {
let _ = ic_cdk::spawn(async {
let random_bytes = raw_rand().await.unwrap().0;
if !random_bytes.is_empty() {
CURRENT_RANDOM_NUMBER.with_borrow_mut(|n| {
*n = Nat::from(random_bytes[0]);
});
// Print the random number
ic_cdk::println!(
"New random number generated: {}",
CURRENT_RANDOM_NUMBER.with_borrow(|n| n.clone())
);
}
});
}
// Define a query function to get the random number.
#[ic_cdk::query]
fn get_current_number() -> Nat {
CURRENT_RANDOM_NUMBER.with_borrow(|n| n.clone())
}
3. Add a timer.
At this point, the canister code generates a random number and stores it in a variable. You can retrieve that value with a query function. To demonstrate another feature of ICP, let's add a timer that generates a new random number every 5 seconds.
Timers are used to automatically execute actions after a specified interval or delay, enabling canisters to perform autonomous tasks.
Add the following highlighted line to your backend code:
- Motoko
- Rust
- TypeScript
- Python
import Nat8 "mo:base/Nat8";
import Debug "mo:base/Debug";
import Blob "mo:base/Blob";
import Nat "mo:base/Nat";
// The management canister's principal ID is "aaaaa-aa".
import IC "ic:aaaaa-aa";
import Timer "mo:base/Timer";
actor {
// Create a stable variable to store the current random number.
// Stable variables persist across canister upgrades.
stable var currentRandomNumber : Nat = 0;
// Create a function to generate a new random number.
// This function calls the raw_rand method of the management canister.
// Then it uses the random bytes returned to generate a random number.
private func generateNewNumber() : async () {
let randomBytes = await IC.raw_rand();
if (randomBytes.size() > 0) {
// Use the first byte to generate a number between 0 and 255
let bytes : [Nat8] = Blob.toArray(randomBytes);
currentRandomNumber := Nat8.toNat(bytes[0]);
Debug.print("Generated new random number: " # Nat.toText(currentRandomNumber));
};
};
// Use a query call to get the current random number.
public query func getCurrentNumber() : async Nat {
currentRandomNumber
};
// Initialize timer to generate a new number every 5 seconds
let timer = Timer.recurringTimer(#seconds 5, generateNewNumber);
}
use candid::Nat;
// Import the management canister's raw_rand method
use ic_cdk::api::management_canister::main::raw_rand;
// Import time duration for timers.
use std::time::Duration;
thread_local! {
static CURRENT_RANDOM_NUMBER: std::cell::RefCell<Nat> = std::cell::RefCell::new(Nat::from(0u8));
}
const N: Duration = Duration::from_secs(5);
// Create a timer that executes the generate_new_number function every 5 seconds.
#[ic_cdk::init]
async fn init() {
let _timer_id = ic_cdk_timers::set_timer_interval(N, generate_new_number);
}
// Define a method to generate a new random number.
#[ic_cdk::update]
fn generate_new_number() {
let _ = ic_cdk::spawn(async {
let random_bytes = raw_rand().await.unwrap().0;
if !random_bytes.is_empty() {
CURRENT_RANDOM_NUMBER.with_borrow_mut(|n| {
*n = Nat::from(random_bytes[0]);
});
ic_cdk::println!(
"New random number generated: {}",
CURRENT_RANDOM_NUMBER.with_borrow(|n| n.clone())
);
}
});
}
// Define a query method to get the random number.
#[ic_cdk::query]
fn get_current_number() -> Nat {
CURRENT_RANDOM_NUMBER.with_borrow(|n| n.clone())
}
4. Retrieve external data.
HTTPS outcalls can obtain data from any external source, including other blockchains, traditional Web2 APIs, and other web services. When HTTPS outcalls are used locally, the returned result is not validated since the local replica is only a single node. HTTPS outcalls used on the mainnet validate the returned result through the subnet's consensus, providing security that the data obtained has not been maliciously tampered with during transport.
HTTPS outcalls currently support GET
and POST
HTTPS methods.
To demonstrate how to create and send HTTPS outcalls, let's add an HTTPS outcall GET request that returns information about ICP's daily stats from the ICP API. Insert the following highlighted code into your backend source code file:
- Motoko
- Rust
- TypeScript
- Python
import Nat8 "mo:base/Nat8";
import Debug "mo:base/Debug";
import Blob "mo:base/Blob";
import Nat "mo:base/Nat";
// The management canister's principal ID is "aaaaa-aa".
import IC "ic:aaaaa-aa";
import Timer "mo:base/Timer";
import Cycles "mo:base/ExperimentalCycles";
import Text "mo:base/Text";
import Types "Types";
actor {
// Create a stable variable to store the current random number.
// Stable variables persist across canister upgrades.
stable var currentRandomNumber : Nat = 0;
// Create a function to generate a new random number.
// This function calls the raw_rand method of the management canister.
// Then it uses the random bytes returned to generate a random number.
private func generateNewNumber() : async () {
let randomBytes = await IC.raw_rand();
if (randomBytes.size() > 0) {
// Use the first 2 bytes to generate a number between 0 and 255
let bytes : [Nat8] = Blob.toArray(randomBytes);
currentRandomNumber := Nat8.toNat(bytes[0]);
Debug.print("Generated new random number: " # Nat.toText(currentRandomNumber));
};
};
// Use a query call to get the current random number.
public query func getCurrentNumber() : async Nat {
currentRandomNumber
};
// Initialize a timer to generate a new number every 5 seconds.
let timer = Timer.recurringTimer<system>(#seconds 5, generateNewNumber);
// Define a public function to get the daily stats about ICP from the ICP API.
public func getIcpInfo() : async Text {
// Define the API endpoints to obtain data from.
// This example uses the IC API, but any API endpoint can be used.
let url = "https://ic-api.internetcomputer.org/api/v3/daily-stats?format=json";
let transform_context : Types.TransformContext = {
function = transform;
context = Blob.fromArray([]);
};
// Define the http_request components.
let http_request = {
url = url;
max_response_bytes = null; // Optional.
headers = [];
body = null; // Optional.
method = #get;
transform = ?transform_context;
};
// HTTP outcalls require cycles to be attached to the call.
Cycles.add<system>(20_949_972_000);
// Execute the HTTP outcall.
let http_response = await IC.http_request(http_request);
let response_body: Blob = http_response.body;
switch (Text.decodeUtf8(response_body)) {
case null { "No value returned" };
case (?y) { y };
};
};
// Define a transform function to return the status response and body.
public query func transform(raw : Types.TransformArgs) : async IC.http_request_result {
{
status = raw.response.status;
body = raw.response.body;
headers = [ ];
};
};
}
This HTTP outcalls code references custom types, as indicated by the import statement import Types "Types";
and code statements like Types.TransformContext
. Custom type definitions can be created from within the main.mo
file, but for types that may be used in several different source code files, it can be beneficial to define them in a single Types.mo
file, and then import them as needed.
Create a new file called src/PROJECT_NAME_backend/Types.mo
that contains the following:
import IC "ic:aaaaa-aa";
module Types {
// HTTPS outcalls have an optional "transform" key. These two types help describe it.
// The transform function may transform the body in any way, add or remove headers, modify headers, etc.
public type TransformArgs = {
response : IC.http_request_result;
context : Blob;
};
public type TransformContext = {
function : shared query TransformArgs -> async IC.http_request_result;
context : Blob;
};
}
use candid::Nat;
// Import the management canister's raw_rand method
use ic_cdk::api::management_canister::main::raw_rand;
// Import time duration for timers.
use std::time::Duration;
// Import the management canister's http_request method
use ic_cdk::api::management_canister::http_request::{
http_request, CanisterHttpRequestArgument, HttpMethod, HttpResponse, TransformArgs,
TransformContext, TransformFunc,
};
thread_local! {
static CURRENT_RANDOM_NUMBER: std::cell::RefCell<Nat> = std::cell::RefCell::new(Nat::from(0u8));
}
const N: Duration = Duration::from_secs(5);
// Create a timer that executes the generate_new_number function every 5 seconds.
#[ic_cdk::init]
async fn init() {
let _timer_id = ic_cdk_timers::set_timer_interval(N, generate_new_number);
}
// Define a method to generate a new random number.
#[ic_cdk::update]
fn generate_new_number() {
let _ = ic_cdk::spawn(async {
let random_bytes = raw_rand().await.unwrap().0;
if !random_bytes.is_empty() {
CURRENT_RANDOM_NUMBER.with_borrow_mut(|n| {
*n = Nat::from(random_bytes[0]);
});
ic_cdk::println!(
"New random number generated: {}",
CURRENT_RANDOM_NUMBER.with_borrow(|n| n.clone())
);
}
});
}
// Define a query method to get the random number.
#[ic_cdk::query]
fn get_current_number() -> Nat {
CURRENT_RANDOM_NUMBER.with_borrow(|n| n.clone())
}
// Define an update method using the HTTPS outcalls feature.
#[ic_cdk::update]
async fn get_icp_info() -> String {
let url = format!(
"https://ic-api.internetcomputer.org/api/v3/daily-stats?format=json",
);
// Define the HTTP request arguments.
let request = CanisterHttpRequestArgument {
url: url.to_string(),
method: HttpMethod::GET,
body: None, // Optional.
max_response_bytes: None, // Optional.
transform: Some(TransformContext {
// The "method" parameter must have the same name as the function name of your transform function.
function: TransformFunc(candid::Func {
principal: ic_cdk::api::id(),
method: "transform".to_string(),
}),
context: vec![],
}),
headers: vec![],
};
// HTTPS outcalls require cycles.
let cycles = 230_949_972_000;
match http_request(request, cycles).await {
// Decode and return the response.
// Learn more: https://docs.rs/ic-cdk/latest/ic_cdk/api/management_canister/http_request/struct.HttpResponse.html
Ok((response,)) => {
// Return the body as a string and end the method.
String::from_utf8(response.body).expect("Transformed response is not UTF-8 encoded.")
}
Err((r, m)) => {
let message =
format!("The http_request resulted into error. RejectionCode: {r:?}, Error: {m}");
// Return the error as a string and end the method.
message
}
}
}
// Define a transform method.
// Strips all data that is not needed from the original response.
#[ic_cdk::query]
fn transform(raw: TransformArgs) -> HttpResponse {
let headers = vec![];
let mut res = HttpResponse {
status: raw.response.status.clone(),
body: raw.response.body.clone(),
headers,
};
if res.status == 200u64 {
res.body = raw.response.body;
} else {
ic_cdk::api::print(format!("Received an error = {:?}", raw));
}
res
}
5. Putting it all together.
At this point, we have three functions in this code that demonstrate three different ICP features: onchain randomness, timers for autonomous execution, and HTTPS outcalls to obtain data. Let's edit the code to use them in conjunction with one another rather than separately:
- Onchain randomness will be used to generate a number between 133000 and 133255. This number will be used to select a recent NNS proposal at random.
The Network Nervous System (NNS) is the decentralized organization that governs the ICP network. To make changes to the network, a proposal must be submitted and voted on.
The random number will be passed into the HTTPS outcall. The HTTPS outcall will query the IC API endpoint for details about that specific proposal number.
Timers will be used to generate a new number every 30 seconds and send a new HTTPS outcall every 32 seconds.
Replace the backend canister source code with the following revised code:
- Motoko
- Rust
- TypeScript
- Python
import Timer "mo:base/Timer";
import Nat8 "mo:base/Nat8";
import Debug "mo:base/Debug";
import Blob "mo:base/Blob";
import Nat "mo:base/Nat";
import Cycles "mo:base/ExperimentalCycles";
import Text "mo:base/Text";
import Types "Types";
// The management canister's principal ID is "aaaaa-aa".
import IC "ic:aaaaa-aa";
actor {
// Create a stable variable to store the current random number.
// Stable variables persist across canister upgrades.
stable var currentRandomNumber : Nat = 0;
stable var proposalId : Text = "1";
// Create a function to generate a new random number.
// This function calls the raw_rand method of the management canister.
// Then it uses the random bytes returned to generate a random number.
private func generateNewNumber() : async () {
let randomBytes = await IC.raw_rand();
if (randomBytes.size() > 0) {
// Use the first byte to generate a number between 133000 and 133255 (recent proposal numbers)
let bytes : [Nat8] = Blob.toArray(randomBytes);
currentRandomNumber := Nat8.toNat(bytes[0]) + 133000;
proposalId := Nat.toText(currentRandomNumber);
};
};
// Define a public function to get proposal information.
public func getIcpInfo() : async Text {
// Define the API endpoints to obtain data from.
// This example uses the IC API, but any API endpoint can be used.
let url = "https://ic-api.internetcomputer.org/api/v3/proposals/" # proposalId ;
let transform_context = {
function = transform;
context = Blob.fromArray([]);
};
// Define the http_request components.
let http_request = {
url = url;
max_response_bytes = null; //optional for request
headers = [];
body = null; //optional for request
method = #get;
transform = ?transform_context;
};
// HTTP outcalls require cycles are attached to the call.
Cycles.add<system>(20_949_972_000);
// Execute the HTTPS outcall.
let http_response = await IC.http_request(http_request);
let response_body: Blob = http_response.body;
switch (Text.decodeUtf8(response_body)) {
case null { "No value returned" };
case (?y) { y };
};
};
// Define a transform function to return the status response and body.
public query func transform(raw : Types.TransformArgs) : async IC.http_request_result {
{
status = raw.response.status;
body = raw.response.body;
headers = [ ];
};
};
private func printResults() : async () {
Debug.print("Generated new random proposal number: " # Nat.toText(currentRandomNumber));
let result : Text = await getIcpInfo();
Debug.print("Proposal info obtained through HTTPS outcall: " # result);
};
// Initialize timer to generate new number every 30 seconds
let timer1 = Timer.recurringTimer<system>(#seconds 30, generateNewNumber);
// Initialize timer to send an HTTPS outcall and print the results every 32 seconds
let timer2 = Timer.recurringTimer<system>(#seconds 32, printResults);
}
You will still need the Types.mo
file created in step 4. You do not need to make any edits to the Types.mo
file.
use candid::Nat;
use ic_cdk::api::management_canister::http_request::{
http_request, CanisterHttpRequestArgument, HttpMethod, HttpResponse, TransformArgs,
TransformContext, TransformFunc,
};
use ic_cdk::api::management_canister::main::raw_rand;
use std::time::Duration;
// NOTE: These thread_local! variables are not stored in the stable memory and are not persisted across canister upgrades.
thread_local! {
static CURRENT_RANDOM_NUMBER: std::cell::RefCell<Nat> = std::cell::RefCell::new(Nat::from(0u8));
static PROPOSAL_ID: std::cell::RefCell<String> = std::cell::RefCell::new("1".to_string());
}
#[ic_cdk::init]
async fn init() {
// Initialize timer to generate new number every 30 seconds
ic_cdk_timers::set_timer_interval(Duration::from_secs(30), generate_new_number);
// Initialize timer to send an HTTPS outcall and print the results every 32 seconds
ic_cdk_timers::set_timer_interval(Duration::from_secs(32), print_results);
}
// Create a function to generate a new random number.
// This function calls the raw_rand method of the management canister.
// Then it uses the random bytes returned to generate a random number.
fn generate_new_number() {
ic_cdk::spawn(async {
let random_bytes = raw_rand().await.unwrap().0;
if !random_bytes.is_empty() {
// Use the first byte to generate a number between 133000 and 133255 (recent proposal numbers)
let current_random_number = random_bytes[0] as u32 + 133000u32;
CURRENT_RANDOM_NUMBER.with_borrow_mut(|n| {
*n = Nat::from(current_random_number).clone();
});
PROPOSAL_ID.with_borrow_mut(|p| {
*p = current_random_number.to_string();
});
}
});
}
// Define a public function to get proposal information.
#[ic_cdk::update]
async fn get_icp_info() -> String {
// Define the API endpoints to obtain data from.
// This example uses the IC API, but any API endpoint can be used.
let url = format!(
"https://ic-api.internetcomputer.org/api/v3/proposals/{}",
PROPOSAL_ID.with_borrow(|p| p.clone())
);
let arg = CanisterHttpRequestArgument {
url,
max_response_bytes: None,
method: HttpMethod::GET,
headers: vec![],
body: None,
transform: Some(TransformContext {
function: TransformFunc(candid::Func {
method: "transform".to_string(),
principal: ic_cdk::id(),
}),
context: vec![],
}),
};
// HTTP outcalls require cycles are attached to the call.
let http_response = http_request(arg, 20_949_972_000).await.unwrap().0;
String::from_utf8(http_response.body).unwrap()
}
#[ic_cdk::query]
fn transform(raw: TransformArgs) -> HttpResponse {
HttpResponse {
status: raw.response.status,
headers: vec![],
body: raw.response.body,
}
}
fn print_results() {
ic_cdk::println!(
"Generated new random proposal number: {}",
CURRENT_RANDOM_NUMBER.with_borrow(|n| n.clone())
);
ic_cdk::spawn(async {
let result = get_icp_info().await;
ic_cdk::println!("Proposal info obtained through HTTPS outcall: {}", result);
});
}
Next step
Now that you have written the canister's code, the canister needs to be deployed.