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:

  1. Create a directory for your project
  2. Create a package.json file
  3. Install Azle
  4. Create a dfx.json file
  5. Create a directory and entry TypeScript file for your canister
  6. 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:

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, and PostUpgrade method parameters and return types
  • Canister method declaration parameters and return types
  • Stable 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

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:

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

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.

GitHub

View Github