TypeScript CDK for the Internet Computer
Azle (Beta)
TypeScript CDK for the Internet Computer.
Disclaimer
Azle is beta software. It has not been thoroughly tested by Demergent Labs or the community. There have been no extensive security reviews. There are very few live applications built with Azle.
The safest way to use Azle is to assume that your canister could get hacked, frozen, broken, or erased at any moment. Remember that you use Azle at your own risk and according to the terms of the MIT license found here.
Discussion
Feel free to open issues or join us in the DFINITY DEV TypeScript Discord channel.
Documentation
Most of Azle’s documentation is currently found in this README. A more detailed mdBook-style book similar to Sudograph’s will later be hosted on the Internet Computer.
Installation
You should have the following installed on your system:
After installing the prerequisites, you can make a project and install Azle.
Node.js
Run the following commands to install Node.js and npm. nvm is highly recommended and its use is shown below:
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh | bash
# restart your terminal
nvm install 14
Rust
Run the following command to install Rust:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
dfx
Run the following command to install dfx 0.9.3:
# Azle has been tested against version 0.9.3, so it is safest to install that specific version for now
DFX_VERSION=0.9.3 sh -ci "$(curl -fsSL https://sdk.dfinity.org/install.sh)"
Azle
Follow these steps to create an Azle project:
- Create a directory for your project
- Create a
package.json
file - Install Azle
- Create a
dfx.json
file - Create a directory and entry TypeScript file for your canister
- Fill out your
dfx.json
file
Here are the commands you might run from a terminal to setup your project:
mkdir backend
cd backend
npm init -y
npm install azle
touch dfx.json
mkdir src
cd src
touch backend.ts
Your dfx.json
should look like this:
{
"canisters": {
"backend": {
"type": "custom",
"build": "npx azle backend",
"root": "src",
"ts": "src/backend.ts",
"candid": "src/backend.did",
"wasm": "target/wasm32-unknown-unknown/release/backend.wasm"
}
}
}
Common Installation Issues
- Ubuntu
- error: linker cc not found (sudo apt install build-essential)
- is cmake not installed? (sudo apt install cmake)
Deployment
Local deployment
Start up an IC replica and deploy:
# Open a terminal and navigate to your project's root directory, then run the following command to start a local IC replica
dfx start
# Alternatively to the above command, you can run the replica in the background
dfx start --background
# If you are running the replica in the background, you can run this command within the same terminal as the dfx start --background command
# If you are not running the replica in the background, then open another terminal and run this command from the root directory of your project
dfx deploy
You can then interact with your canister like any other canister written in Motoko or Rust. For more information about calling your canister using dfx
, see here.
dfx commands for the query example:
dfx canister call query query
# The result is: ("This is a query function")
dfx commands for the update example:
dfx canister call update update '("Why hello there")'
# The result is: ()
dfx canister call update query
# The result is: ("Why hello there")
dfx commands for the simple_erc20 example:
dfx canister call simple_erc20 initializeSupply '("TOKEN", "Token", 1_000_000, "0")'
# The result is: (true)
dfx canister call simple_erc20 name
# The result is: ("Token")
dfx canister call simple_erc20 ticker
# The result is: ("TOKEN")
dfx canister call simple_erc20 totalSupply
# The result is: (1_000_000 : nat64)
dfx canister call simple_erc20 balance '("0")'
# The result is: (1_000_000 : nat64)
dfx canister call simple_erc20 transfer '("0", "1", 100)'
# The result is: (true)
Live deployment
Deploying to the live Internet Computer generally only requires adding the --network ic
option to the deploy command: dfx deploy --network ic
. This assumes you already have converted ICP into cycles appropriately. See here for more information on getting ready to deploy to production.
Canisters
More information:
- https://smartcontracts.org/docs/developers-guide/concepts/canisters-code.html
- https://wiki.internetcomputer.org/wiki/Canisters_(dapps/smart_contracts)
- https://smartcontracts.org/docs/developers-guide/design-apps.html
In many ways developing canisters with Azle is similar to any other TypeScript/JavaScript project. To see what canister source code looks like, see the examples.
A canister is the fundamental application unit on the Internet Computer. It contains the code and state of your application. When deployed to the Internet Computer, your canister becomes an everlasting process. Its global variables automatically persist.
Users of your canister interact with it through RPC calls performed using HTTP requests. These calls will hit your canister’s Query
and Update
methods. These methods, with their parameter and return types, are the interface to your canister.
Azle allows you to write canisters while embracing much of what that the TypeScript and JavaScript ecosystems have to offer.
Candid data types
Examples:
Candid is an interface description language created by DFINITY. It defines interfaces between services (in our context canisters), allowing canisters and clients written in various languages to easily interact with each other.
Much of what Azle is doing under-the-hood is translating TypeScript code into various formats that Candid understands (for example Azle will generate a Candid .did
file from your TypeScript code). To do this your TypeScript code must use various Azle-provided types.
Please note that these types are only needed in the following locations in your code:
Query
,Update
,Init
, andPostUpgrade
method parameters and return typesCanister
method declaration parameters and return typesStable
variable declaration types
You do not need to use these types, and you do not need to use TypeScript, anywhere else. You could write the rest of your application in JavaScript if that’s what makes you happy.
Data types:
- int
- int64
- int32
- int16
- int8
- nat
- nat64
- nat32
- nat16
- nat8
- float64
- float32
- Principal
- string
- boolean
- Record
- Variant
- Array
- Opt
int
The Azle type int
corresponds to the Candid type int and will become a JavaScript BigInt at runtime.
TypeScript:
import { int, Query, ic } from 'azle';
export function getInt(): Query<int> {
return 170141183460469231731687303715884105727n;
}
export function printInt(int: int): Query<int> {
ic.print(typeof int);
return int;
}
Candid:
service: {
"getInt": () -> (int) query;
"printInt": (int) -> (int) query;
}
int64
The Azle type int64
corresponds to the Candid type int64 and will become a JavaScript BigInt at runtime.
TypeScript:
import { int64, Query, ic } from 'azle';
export function getInt64(): Query<int64> {
return 9223372036854775807n;
}
export function printInt64(int64: int64): Query<int64> {
ic.print(typeof int64);
return int64;
}
Candid:
service: {
"getInt64": () -> (int64) query;
"printInt64": (int64) -> (int64) query;
}
int32
The Azle type int32
corresponds to the Candid type int32 and will become a JavaScript Number at runtime.
TypeScript:
import { int32, Query, ic } from 'azle';
export function getInt32(): Query<int32> {
return 2147483647;
}
export function printInt32(int32: int32): Query<int32> {
ic.print(typeof int32);
return int32;
}
Candid:
service: {
"getInt32": () -> (int32) query;
"printInt32": (int32) -> (int32) query;
}
int16
The Azle type int16
corresponds to the Candid type int16 and will become a JavaScript Number at runtime.
TypeScript:
import { int16, Query, ic } from 'azle';
export function getInt16(): Query<int16> {
return 32767;
}
export function printInt16(int16: int16): Query<int16> {
ic.print(typeof int16);
return int16;
}
Candid:
service: {
"getInt16": () -> (int16) query;
"printInt16": (int16) -> (int16) query;
}
int8
The Azle type int8
corresponds to the Candid type int8 and will become a JavaScript Number at runtime.
TypeScript:
import { int8, Query, ic } from 'azle';
export function getInt8(): Query<int8> {
return 127;
}
export function printInt8(int8: int8): Query<int8> {
ic.print(typeof int8);
return int8;
}
Candid:
service: {
"getInt8": () -> (int8) query;
"printInt8": (int8) -> (int8) query;
}
nat
The Azle type nat
corresponds to the Candid type nat and will become a JavaScript BigInt at runtime.
TypeScript:
import { nat, Query, ic } from 'azle';
export function getNat(): Query<nat> {
return 340282366920938463463374607431768211455n;
}
export function printNat(nat: nat): Query<nat> {
ic.print(typeof nat);
return nat;
}
Candid:
service: {
"getNat": () -> (nat) query;
"printNat": (nat) -> (nat) query;
}
nat64
The Azle type nat64
corresponds to the Candid type nat64 and will become a JavaScript BigInt at runtime.
TypeScript:
import { nat64, Query, ic } from 'azle';
export function getNat64(): Query<nat64> {
return 18446744073709551615n;
}
export function printNat64(nat64: nat64): Query<nat64> {
ic.print(typeof nat64);
return nat64;
}
Candid:
service: {
"getNat64": () -> (nat64) query;
"printNat64": (nat64) -> (nat64) query;
}
nat32
The Azle type nat32
corresponds to the Candid type nat32 and will become a JavaScript Number at runtime.
TypeScript:
import { nat32, Query, ic } from 'azle';
export function getNat32(): Query<nat32> {
return 4294967295;
}
export function printNat32(nat32: nat32): Query<nat32> {
ic.print(typeof nat32);
return nat32;
}
Candid:
service: {
"getNat32": () -> (nat32) query;
"printNat32": (nat32) -> (nat32) query;
}
nat16
The Azle type nat16
corresponds to the Candid type nat16 and will become a JavaScript Number at runtime.
TypeScript:
import { nat16, Query, ic } from 'azle';
export function getNat16(): Query<nat16> {
return 65535;
}
export function printNat16(nat16: nat16): Query<nat16> {
ic.print(typeof nat16);
return nat16;
}
Candid:
service: {
"getNat16": () -> (nat16) query;
"printNat16": (nat16) -> (nat16) query;
}
nat8
The Azle type nat8
corresponds to the Candid type nat8 and will become a JavaScript Number at runtime.
TypeScript:
import { nat8, Query, ic } from 'azle';
export function getNat8(): Query<nat8> {
return 255;
}
export function printNat8(nat8: nat8): Query<nat8> {
ic.print(typeof nat8);
return nat8;
}
Candid:
service: {
"getNat8": () -> (nat8) query;
"printNat8": (nat8) -> (nat8) query;
}
float64
The Azle type float64
corresponds to the Candid type float64 and will become a JavaScript Number at runtime.
TypeScript:
import { float64, Query, ic } from 'azle';
export function getFloat64(): Query<float64> {
return Math.E;
}
export function printFloat64(float64: float64): Query<float64> {
ic.print(typeof float64);
return float64;
}
Candid:
service: {
"getFloat64": () -> (float64) query;
"printFloat64": (float64) -> (float64) query;
}
float32
The Azle type float32
corresponds to the Candid type float32 and will become a JavaScript Number at runtime.
TypeScript:
import { float32, Query, ic } from 'azle';
export function getFloat32(): Query<float32> {
return Math.PI;
}
export function printFloat32(float32: float32): Query<float32> {
ic.print(typeof float32);
return float32;
}
Candid:
service: {
"getFloat32": () -> (float32) query;
"printFloat32": (float32) -> (float32) query;
}
Principal
The Azle type Principal
corresponds to the Candid type principal and will become a JavaScript String at runtime.
TypeScript:
import { Principal, Query, ic } from 'azle';
export function getPrincipal(): Query<Principal> {
return 'rrkah-fqaaa-aaaaa-aaaaq-cai';
}
export function printPrincipal(principal: Principal): Query<Principal> {
ic.print(typeof principal);
return principal;
}
Candid:
service: {
"getPrincipal": () -> (principal) query;
"printPrincipal": (principal) -> (principal) query;
}
string
The TypeScript type string
corresponds to the Candid type text and will become a JavaScript String at runtime.
TypeScript:
import { Query, ic } from 'azle';
export function getString(): Query<string> {
return 'Hello world!';
}
export function printString(string: string): Query<string> {
ic.print(typeof string);
return string;
}
Candid:
service: {
"getString": () -> (text) query;
"printString": (text) -> (text) query;
}
boolean
The TypeScript type boolean
corresponds to the Candid type bool and will become a JavaScript Boolean at runtime.
TypeScript:
import { Query, ic } from 'azle';
export function getBoolean(): Query<boolean> {
return true;
}
export function printBoolean(boolean: boolean): Query<boolean> {
ic.print(typeof boolean);
return boolean;
}
Candid:
service: {
"getBoolean": () -> (bool) query;
"printBoolean": (bool) -> (bool) query;
}
Record
TypeScript type aliases referring to object literals correspond to the Candid record type and will become JavaScript Objects at runtime.
TypeScript:
import { Variant } from 'azle';
type Post = {
id: string;
author: User;
reactions: Reaction[];
text: string;
thread: Thread;
};
type Reaction = {
id: string;
author: User;
post: Post;
reactionType: ReactionType;
};
type ReactionType = Variant<{
fire?: null;
thumbsUp?: null;
thumbsDown?: null;
}>;
type Thread = {
id: string;
author: User;
posts: Post[];
title: string;
};
type User = {
id: string;
posts: Post[];
reactions: Reaction[];
threads: Thread[];
username: string;
};
Candid:
type Thread = record {
"id": text;
"author": User;
"posts": vec Post;
"title": text;
};
type User = record {
"id": text;
"posts": vec Post;
"reactions": vec Reaction;
"threads": vec Thread;
"username": text;
};
type Reaction = record {
"id": text;
"author": User;
"post": Post;
"reactionType": ReactionType;
};
type Post = record {
"id": text;
"author": User;
"reactions": vec Reaction;
"text": text;
"thread": Thread;
};
type ReactionType = variant {
"fire": null;
"thumbsUp": null;
"thumbsDown": null
};
Variant
TypeScript type aliases referring to object literals wrapped in the Variant
Azle type correspond to the Candid variant type and will become JavaScript Objects at runtime.
TypeScript:
import { Variant, nat32 } from 'azle';
type ReactionType = Variant<{
fire?: null;
thumbsUp?: null;
thumbsDown?: null;
emotion?: Emotion;
firework?: Firework;
}>;
type Emotion = Variant<{
happy?: null;
sad?: null;
}>
type Firework = {
color: string;
numStreaks: nat32;
};
Candid:
type ReactionType = variant {
"fire": null;
"thumbsUp": null;
"thumbsDown": null;
"emotion": Emotion;
"firework": Firework
};
type Emotion = variant {
"happy": null;
"sad": null
};
type Firework = record {
"color": text;
"numStreaks": nat32;
};
Array
TypeScript []
array syntax corresponds to the Candid type vec and will become an array of the enclosed type at runtime. Only the []
array syntax is supported at this time (i.e. not Array
or ReadonlyArray
etc).
TypeScript:
import { Query, int32 } from 'azle';
export function getNumbers(): Query<int32[]> {
return [0, 1, 2, 3];
}
Candid:
service: {
"getNumbers": () -> (vec int32) query;
}
Opt
The Azle type Opt
corresponds to the Candid type opt and will become the enclosed JavaScript type or null at runtime.
TypeScript:
import { Opt, Query } from 'azle';
export function getOptSome(): Query<Opt<boolean>> {
return true;
}
export function getOptNone(): Query<Opt<boolean>> {
return null;
}
Candid:
service: {
"getOptSome": () -> (opt bool) query;
"getOptNone": () -> (opt bool) query;
}
Query methods
Examples:
More information:
- https://smartcontracts.org/docs/developers-guide/concepts/canisters-code.html#query-update
- https://smartcontracts.org/docs/developers-guide/design-apps.html
Query methods expose public callable functions that are read-only. All state changes will be discarded after the function call completes.
Query calls do not go through consensus and thus return very quickly relative to update calls. This also means they are less secure than update calls unless certified data is used in conjunction with the query call.
To create a query method, simply wrap the return type of your function in the Azle Query
type.
import { Query } from 'azle';
export function query(): Query<string> {
return 'This is a query function';
}
Update methods
Examples:
More information:
Update methods expose public callable functions that are writable. All state changes will be persisted after the function call completes.
Update calls go through consensus and thus return very slowly (a few seconds) relative to query calls. This also means they are more secure than query calls unless certified data is used in conjunction with the query call.
To create an update method, simply wrap the return type of your function in the azle Update
type.
import {
Query,
Update
} from 'azle';
let currentMessage: string = '';
export function query(): Query<string> {
return currentMessage;
}
export function update(message: string): Update<void> {
currentMessage = message;
}
IC API
Examples:
Azle exports the ic
object which contains access to certain IC APIs.
import {
Query,
nat64,
ic,
Principal
} from 'azle';
// returns the principal of the identity that called this function
export function caller(): Query<string> {
return ic.caller();
}
// returns the amount of cycles available in the canister
export function canisterBalance(): Query<nat64> {
return ic.canisterBalance();
}
// returns this canister's id
export function id(): Query<Principal> {
return ic.id();
}
// prints a message through the local replica's output
export function print(message: string): Query<boolean> {
ic.print(message);
return true;
}
// returns the current timestamp
export function time(): Query<nat64> {
return ic.time();
}
// traps with a message, stopping execution and discarding all state within the call
export function trap(message: string): Query<boolean> {
ic.trap(message);
return true;
}
Cross-canister calls
Examples:
DFINITY documentation:
More documentation to come, see the examples and the DFINITY documentation for the time being.
Init method
Examples:
DFINITY documentation:
More documentation to come, see the examples and the DFINITY documentation for the time being.
PreUpgrade method
Examples:
DFINITY documentation:
More documentation to come, see the examples and the DFINITY documentation for the time being.
PostUpgrade method
Examples:
DFINITY documentation:
More documentation to come, see the examples and the DFINITY documentation for the time being.
Stable storage
Examples:
More information:
More documentation to come, see the examples and the DFINITY documentation for the time being.
Heartbeat method
Examples:
DFINITY documentation:
More documentation to come, see the examples and the DFINITY documentation for the time being.
Roadmap
- 1.0
- Feature parity with Rust and Motoko CDKs
- Core set of Azle-specific npm packages
- Sudograph integration
- Official dfx integration with
"type": "typescript"
or"type": "azle"
- Live robust examples
- Video series
- Comprehensive benchmarks
- Robust property-based tests
- Optimized compilation
- Security audits
- 2.0
- Azle VS Code plugin
- Inter-Canister Query Calls
Limitations
- Varied missing TypeScript syntax or JavaScript features
- Really bad compiler errors (you will probably not enjoy them)
- Limited asynchronous TypeScript/JavaScript (generators only for now, no promises or async/await)
- Imported npm packages may use unsupported syntax or APIs
- Unknown security vulnerabilities
- Unknown cycle efficiency relative to canisters written in Rust or Motoko
- And much much more
Gotchas and caveats
- Because Azle is built on Rust, to ensure the best compatibility use underscores to separate words in directory, file, and canister names
- You must use type names directly when importing them (TODO do an example)
Decentralization
Please note that the following plan is very subject to change, especially in response to compliance with government regulations. Please carefully read the Azle License Extension to understand Azle’s copyright and the AZLE token in more detail.
Azle’s tentative path towards decentralization is focused on traditional open source governance paired with a new token concept known as Open Source tokens (aka OS tokens or OSTs). The goal for OS tokens is to legally control the copyright and to fully control the repository for open source projects. In other words, OS tokens are governance tokens for open source projects.
Azle’s OS token is called AZLE. Currently it only controls Azle’s copyright and not the Azle repository. Demergent Labs controls its own Azle repository. Once a decentralized git repository is implemented on the Internet Computer, the plan is to move Demergent Labs’ Azle repository there and give full control of that repository to the AZLE token holders.
Demergent Labs currently owns the majority of AZLE tokens, and thus has ultimate control over Azle’s copyright and AZLE token allocations. Demergent Labs will use its own discretion to distribute AZLE tokens over time to contributors and other parties, eventually owning much less than 50% of the tokens.
Contributing
All contributors must agree to and sign the Azle License Extension.
Please consider working on the good first issues and help wanted issues before suggesting other work to be done.
Before beginning work on a contribution, please create or comment on the issue you want to work on and wait for clearance from Demergent Labs.
See Demergent Labs’ Coding Guidelines for what to expect during code reviews.
Local testing
If you want to ensure running the examples with a fresh clone works, run npm link
from the Azle root directory and then npm link azle
inside of the example’s root directory. Not all of the examples are currently kept up-to-date with the correct Azle npm package.
License
Azle’s copyright is governed by the LICENSE and LICENSE_EXTENSION.