joseph-hurtado / IC-TypeScript-azle

TypeScript CDK for the Internet Computer

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool


Azle (Beta)

TypeScript CDK for the Internet Computer.

Disclaimer

Please exercise caution when using Azle. It is beta software that you use at your own risk and according to the terms of this MIT license.

Demergent Labs may officially recommend Azle for production use when at least the following have occurred:

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. The Azle 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.39.1/install.sh | bash

# restart your terminal

nvm install 18

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.11.0:

DFX_VERSION=0.11.0 sh -ci "$(curl -fsSL https://sdk.dfinity.org/install.sh)"

Common Installation Issues

  • Ubuntu
    • error: linker cc not found (sudo apt install build-essential)
    • is cmake not installed? (sudo apt install cmake)

Azle

Follow these steps to create an Azle project. The steps below assume a project called backend:

  1. Create a directory for your project (mkdir backend && cd backend)
  2. Create a package.json file (npm init -y)
  3. Install Azle (npm install azle)
  4. Create a dfx.json file (touch dfx.json)
  5. Create a directory and an entry TypeScript file for your canister (mkdir src && cd src && touch index.ts)

Your dfx.json file should look like this:

{
    "canisters": {
        "backend": {
            "type": "custom",
            "build": "npx azle backend",
            "root": "src",
            "ts": "src/index.ts",
            "candid": "src/index.did",
            "wasm": "target/wasm32-unknown-unknown/release/backend.wasm"
        }
    }
}

Your index.ts file should look like this:

import { Query } from 'azle';

export function hello_world(): Query<string> {
    return 'Hello world!';
}

You are now ready to deploy your application.

Deployment

Local Deployment

Start up an IC replica and deploy. The first deploy will likely take multiple minutes as it downloads and compiles many Rust dependencies. Subsequent deploys should be much quicker:

# 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 these commands 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 these commands from the root directory of your project

# first deploy
dfx canister create backend
dfx build backend
dfx canister install backend --wasm target/wasm32-unknown-unknown/release/backend.wasm.gz

# subsequent deploys
dfx build backend
dfx canister install --mode upgrade backend --wasm target/wasm32-unknown-unknown/release/backend.wasm.gz

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 canister --network ic install backend --wasm target/wasm32-unknown-unknown/release/backend.wasm.gz. 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 the TypeScript and JavaScript ecosystems have to offer.

Canister Methods

init

Examples:

import { Init } from 'azle';

export function init(): Init {
    console.log('This runs once when the canister is first initialized');
}

pre upgrade

Examples:

import { PreUpgrade } from 'azle';

export function pre_upgrade(): PreUpgrade {
    console.log('This runs before every canister upgrade');
}

post upgrade

Examples:

import { PostUpgrade } from 'azle';

export function post_upgrade(): PostUpgrade {
    console.log('This runs after every canister upgrade');
}

inspect message

Examples:

import { ic, InspectMessage, Update } from 'azle';

export function inspect_message(): InspectMessage {
    console.log('this runs before executing update calls');

    if (ic.method_name() === 'accessible') {
        ic.accept_message();
        return;
    }

    if (ic.method_name() === 'inaccessible') {
        return;
    }

    throw `Method "${ic.method_name()}" not allowed`;
}

export function accessible(): Update<boolean> {
    return true;
}

export function inaccessible(): Update<boolean> {
    return false;
}

export function also_inaccessible(): Update<boolean> {
    return false;
}

heartbeat

Examples:

import { Heartbeat } from 'azle';

export function heartbeat(): Heartbeat {
    console.log('this runs ~1 time per second');
}

update

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;
}

query

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';
}

http_request and http_request_update

Examples:

import { blob, Func, ic, nat, nat16, Opt, Query, Update, Variant } from 'azle';

type HttpRequest = {
    method: string;
    url: string;
    headers: HeaderField[];
    body: blob;
};

type HttpResponse = {
    status_code: nat16;
    headers: HeaderField[];
    body: blob;
    streaming_strategy: Opt<StreamingStrategy>;
    upgrade: Opt<boolean>;
};

type HeaderField = [string, string];

type StreamingStrategy = Variant<{
    Callback: CallbackStrategy;
}>;

type CallbackStrategy = {
    callback: Callback;
    token: Token;
};

type Callback = Func<(t: Token) => Query<StreamingCallbackHttpResponse>>;

type Token = {
    // add whatever fields you'd like
    arbitrary_data: string;
};

type StreamingCallbackHttpResponse = {
    body: blob;
    token: Opt<Token>;
};

export function http_request(req: HttpRequest): Query<HttpResponse> {
    return {
        status_code: 200,
        headers: [['content-type', 'text/plain']],
        body: Uint8Array.from([]),
        streaming_strategy: null,
        upgrade: true
    };
}

export function http_request_update(req: HttpRequest): Update<HttpResponse> {
    return {
        status_code: 200,
        headers: [['content-type', 'text/plain']],
        body: Uint8Array.from([]),
        streaming_strategy: null,
        upgrade: null
    };
}

Candid 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 specific locations in your code, including but not limited to the following areas:

  • Query, Update, Init, and PostUpgrade method parameters and return types
  • Canister method declaration parameters and return types
  • Stable variable declaration types

Basically, you only need to write in TypeScript and use the Azle types when Candid serialization or deserialization is necessary. You could write the rest of your application in plain JavaScript if you'd like.

Data types:

text

The TypeScript type string corresponds to the Candid type text and will become a JavaScript String at runtime.

TypeScript:

import { Query } from 'azle';

export function get_string(): Query<string> {
    return 'Hello world!';
}

export function print_string(string: string): Query<string> {
    console.log(typeof string);
    return string;
}

Candid:

service: {
    "get_string": () -> (text) query;
    "print_string": (text) -> (text) query;
}

blob

The Azle type blob corresponds to the Candid type blob and will become a JavaScript Uint8Array at runtime.

TypeScript:

import { blob, Query } from 'azle';

export function get_blob(): Query<blob> {
    return Uint8Array.from([68, 73, 68, 76, 0, 0]);
}

export function print_blob(blob: blob): Query<blob> {
    console.log(typeof blob);
    return blob;
}

Candid:

service: {
    "get_blob": () -> (blob) query;
    "print_blob": (blob) -> (blob) query;
}

nat

The Azle type nat corresponds to the Candid type nat and will become a JavaScript BigInt at runtime.

TypeScript:

import { nat, Query } from 'azle';

export function get_nat(): Query<nat> {
    return 340_282_366_920_938_463_463_374_607_431_768_211_455n;
}

export function print_nat(nat: nat): Query<nat> {
    console.log(typeof nat);
    return nat;
}

Candid:

service: {
    "get_nat": () -> (nat) query;
    "print_nat": (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 } from 'azle';

export function get_nat64(): Query<nat64> {
    return 18_446_744_073_709_551_615n;
}

export function print_nat64(nat64: nat64): Query<nat64> {
    console.log(typeof nat64);
    return nat64;
}

Candid:

service: {
    "get_nat64": () -> (nat64) query;
    "print_nat64": (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 } from 'azle';

export function get_nat32(): Query<nat32> {
    return 4_294_967_295;
}

export function print_nat32(nat32: nat32): Query<nat32> {
    console.log(typeof nat32);
    return nat32;
}

Candid:

service: {
    "get_nat32": () -> (nat32) query;
    "print_nat32": (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 } from 'azle';

export function get_nat16(): Query<nat16> {
    return 65_535;
}

export function print_nat16(nat16: nat16): Query<nat16> {
    console.log(typeof nat16);
    return nat16;
}

Candid:

service: {
    "get_nat16": () -> (nat16) query;
    "print_nat16": (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 } from 'azle';

export function get_nat8(): Query<nat8> {
    return 255;
}

export function print_nat8(nat8: nat8): Query<nat8> {
    console.log(typeof nat8);
    return nat8;
}

Candid:

service: {
    "get_nat8": () -> (nat8) query;
    "print_nat8": (nat8) -> (nat8) query;
}

int

The Azle type int corresponds to the Candid type int and will become a JavaScript BigInt at runtime.

TypeScript:

import { int, Query } from 'azle';

export function get_int(): Query<int> {
    return 170_141_183_460_469_231_731_687_303_715_884_105_727n;
}

export function print_int(int: int): Query<int> {
    console.log(typeof int);
    return int;
}

Candid:

service: {
    "get_int": () -> (int) query;
    "print_int": (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 } from 'azle';

export function get_int64(): Query<int64> {
    return 9_223_372_036_854_775_807n;
}

export function print_int64(int64: int64): Query<int64> {
    console.log(typeof int64);
    return int64;
}

Candid:

service: {
    "get_int64": () -> (int64) query;
    "print_int64": (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 } from 'azle';

export function get_int32(): Query<int32> {
    return 2_147_483_647;
}

export function print_int32(int32: int32): Query<int32> {
    console.log(typeof int32);
    return int32;
}

Candid:

service: {
    "get_int32": () -> (int32) query;
    "print_int32": (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 } from 'azle';

export function get_int16(): Query<int16> {
    return 32_767;
}

export function print_int16(int16: int16): Query<int16> {
    console.log(typeof int16);
    return int16;
}

Candid:

service: {
    "get_int16": () -> (int16) query;
    "print_int16": (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 } from 'azle';

export function get_int8(): Query<int8> {
    return 127;
}

export function print_int8(int8: int8): Query<int8> {
    console.log(typeof int8);
    return int8;
}

Candid:

service: {
    "get_int8": () -> (int8) query;
    "print_int8": (int8) -> (int8) query;
}

float64

The Azle type float64 corresponds to the Candid type float64 and will become a JavaScript Number at runtime.

TypeScript:

import { float64, Query } from 'azle';

export function get_float64(): Query<float64> {
    return Math.E;
}

export function print_float64(float64: float64): Query<float64> {
    console.log(typeof float64);
    return float64;
}

Candid:

service: {
    "get_float64": () -> (float64) query;
    "print_float64": (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 } from 'azle';

export function get_float32(): Query<float32> {
    return Math.PI;
}

export function print_float32(float32: float32): Query<float32> {
    console.log(typeof float32);
    return float32;
}

Candid:

service: {
    "get_float32": () -> (float32) query;
    "print_float32": (float32) -> (float32) query;
}

bool

The TypeScript type boolean corresponds to the Candid type bool and will become a JavaScript Boolean at runtime.

TypeScript:

import { Query } from 'azle';

export function get_bool(): Query<boolean> {
    return true;
}

export function print_bool(bool: boolean): Query<boolean> {
    console.log(typeof bool);
    return bool;
}

Candid:

service: {
    "get_bool": () -> (bool) query;
    "print_bool": (bool) -> (bool) query;
}

null

The TypeScript type null corresponds to the Candid type null and will become a JavaScript null at runtime.

TypeScript:

import { Query } from 'azle';

export function get_null(): Query<null> {
    return null;
}

export function print_null(_null: null): Query<null> {
    console.log(typeof _null);
    return _null;
}

Candid:

service: {
    "get_null": () -> (null) query;
    "print_null": (null) -> (null) query;
}

vec

TypeScript [] array syntax corresponds to the Candid type vec and will become an array of the specified type at runtime (except for nat8[] which will become a Uint8Array, thus it is recommended to use the blob type instead of nat8[]). Only the [] array syntax is supported at this time (i.e. not Array or ReadonlyArray etc).

TypeScript:

import { int32, Query } from 'azle';

export function get_numbers(): Query<int32[]> {
    return [0, 1, 2, 3];
}

Candid:

service: {
    "get_numbers": () -> (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 get_opt_some(): Query<Opt<boolean>> {
    return true;
}

export function get_opt_none(): Query<Opt<boolean>> {
    return null;
}

Candid:

service: {
    "get_opt_some": () -> (opt bool) query;
    "get_opt_none": () -> (opt bool) query;
}

record

TypeScript type aliases referring to object literals correspond to the Candid record type and will become JavaScript Objects at runtime.

TypeScript:

type Post = {
    id: string;
    author: User;
    text: string;
    thread: Thread;
};

type Thread = {
    id: string;
    author: User;
    posts: Post[];
    title: string;
};

type User = {
    id: string;
    posts: Post[];
    threads: Thread[];
    username: string;
};

Candid:

type Post = record {
    "id": text;
    "author": User;
    "text": text;
    "thread": Thread;
};

type Thread = record {
    "id": text;
    "author": User;
    "posts": vec Post;
    "title": text;
};

type User = record {
    "id": text;
    "posts": vec Post;
    "threads": vec Thread;
    "username": text;
};

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 { nat32, Variant } 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;
};

func

The Azle type func corresponds to the Candid type func and at runtime will become a JavaScript array with two elements, the first being an @dfinity/principal and the second being a JavaScript string. The @dfinity/principal represents the principal of the canister/service where the function exists, and the string represents the function's name.

TypeScript:

import { Func, nat64, Principal, Query, Update, Variant } from 'azle';

type User = {
    id: string;
    basic_func: BasicFunc;
    complex_func: ComplexFunc;
};

type Reaction = Variant<{
    Good: null;
    Bad: null;
    BasicFunc: BasicFunc;
    ComplexFunc: ComplexFunc;
}>;

type BasicFunc = Func<(param1: string) => Query<string>>;
type ComplexFunc = Func<(user: User, reaction: Reaction) => Update<nat64>>;

export function get_basic_func(): Query<BasicFunc> {
    return [
        Principal.fromText('rrkah-fqaaa-aaaaa-aaaaq-cai'),
        'simple_function_name'
    ];
}

export function get_complex_func(): Query<ComplexFunc> {
    return [
        Principal.fromText('ryjl3-tyaaa-aaaaa-aaaba-cai'),
        'complex_function_name'
    ];
}

Candid:

type User = record {
    "id": text;
    "basic_func": BasicFunc;
    "complex_func": ComplexFunc;
};
type Reaction = variant { "Good": null; "Bad": null; "BasicFunc": BasicFunc; "ComplexFunc": ComplexFunc };

type BasicFunc = func (text) -> (text) query;
type ComplexFunc = func (User, Reaction) -> (nat64);

service: () -> {
    "get_basic_func": () -> (BasicFunc) query;
    "get_complex_func": () -> (ComplexFunc) query;
}

service

Not yet implemented.

principal

The Azle type Principal corresponds to the Candid type principal and will become an @dfinity/principal at runtime.

TypeScript:

import { Principal, Query } from 'azle';

export function get_principal(): Query<Principal> {
    return Principal.fromText('rrkah-fqaaa-aaaaa-aaaaq-cai');
}

export function print_principal(principal: Principal): Query<Principal> {
    console.log(typeof principal);
    return principal;
}

Candid:

service: {
    "get_principal": () -> (principal) query;
    "print_principal": (principal) -> (principal) query;
}

reserved

The Azle type reserved corresponds to the Candid type reserved and will become a JavaScript null at runtime.

TypeScript:

import { Query, reserved } from 'azle';

export function get_reserved(): Query<reserved> {
    return 'anything';
}

export function print_reserved(reserved: reserved): Query<reserved> {
    console.log(typeof reserved);
    return reserved;
}

Candid:

service: {
    "get_reserved": () -> (reserved) query;
    "print_reserved": (reserved) -> (reserved) query;
}

empty

The Azle type empty corresponds to the Candid type empty and has no JavaScript value at runtime.

TypeScript:

import { empty, Query } from 'azle';

export function get_empty(): Query<empty> {
    throw 'Anything you want';
}

// Note: It is impossible to call this function because it requires an argument
// but there is no way to pass an "empty" value as an argument.
export function print_empty(empty: empty): Query<empty> {
    console.log(typeof empty);
    throw 'Anything you want';
}

Candid:

service: {
    "get_empty": () -> (empty) query;
    "print_empty": (empty) -> (empty) query;
}

Canister APIs

canister balance

Examples:

import { ic, nat, Query } from 'azle';

// returns the amount of cycles available in the canister
export function canister_balance(): Query<nat64> {
    return ic.canister_balance();
}

canister balance 128

Examples:

import { ic, nat, Query } from 'azle';

// returns the amount of cycles available in the canister
export function canister_balance128(): Query<nat> {
    return ic.canister_balance128();
}

data certificate

Examples:

import { blob, ic, Opt, Query } from 'azle';

// When called from a query call, returns the data certificate authenticating certified_data set by this canister. Returns None if called not from a query call.
export function data_certificate(): Query<Opt<blob>> {
    return ic.data_certificate();
}

canister id

Examples:

import { ic, Principal, Query } from 'azle';

// returns this canister's id
export function id(): Query<Principal> {
    return ic.id();
}

print

Examples:

import { ic, Query } from 'azle';

// prints a message through the local replica's output
export function print(message: string): Query<boolean> {
    ic.print(message);

    return true;
}

set certified data

Examples:

import { blob, ic, Update } from 'azle';

// sets up to 32 bytes of certified data
export function set_certified_data(data: blob): Update<void> {
    ic.set_certified_data(data);
}

time

Examples:

import { ic, nat64, Query } from 'azle';

// returns the current timestamp
export function time(): Query<nat64> {
    return ic.time();
}

trap

Examples:

import { ic, Query } from 'azle';

// 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;
}

Call APIs

caller

Examples:

import { ic, Principal, Query } from 'azle';

// returns the principal of the identity that called this function
export function caller(): Query<Principal> {
    return ic.caller();
}

accept message

Examples:

import { ic, InspectMessage, Update } from 'azle';

export function inspect_message(): InspectMessage {
    console.log('this runs before executing update calls');

    if (ic.method_name() === 'accessible') {
        ic.accept_message();
        return;
    }

    if (ic.method_name() === 'inaccessible') {
        return;
    }

    throw `Method "${ic.method_name()}" not allowed`;
}

export function accessible(): Update<boolean> {
    return true;
}

export function inaccessible(): Update<boolean> {
    return false;
}

export function also_inaccessible(): Update<boolean> {
    return false;
}

arg data

Not yet implemented.

arg data raw

Examples:

// returns the argument data as bytes.
export function arg_data_raw(
    arg1: blob,
    arg2: int8,
    arg3: boolean,
    arg4: string
): Query<blob> {
    return ic.arg_data_raw();
}

arg data raw size

Examples:

// returns the length of the argument data in bytes
export function arg_data_raw_size(
    arg1: blob,
    arg2: int8,
    arg3: boolean,
    arg4: string
): Query<nat32> {
    return ic.arg_data_raw_size();
}

call

Examples:

import { Canister, CanisterResult, ic, ok, Update, Variant } from 'azle';

type Canister1 = Canister<{
    method(): CanisterResult<boolean>;
}>;

const canister1 = ic.canisters.Canister1<Canister1>(
    Principal.fromText('rkp4c-7iaaa-aaaaa-aaaca-cai')
);

type CallCanister1MethodResult = Variant<{
    ok: boolean;
    err: string;
}>;

export function* call_canister1_method(): Update<CallCanister1MethodResult> {
    const canister_result: CanisterResult<boolean> = yield canister1.method();

    if (!ok(canister_result)) {
        return {
            err: canister_result.err
        };
    }

    return {
        ok: canister_result.ok
    };
}

call raw

Examples:

import { blob, ic, ok, Principal, Update } from 'azle';

export function* get_randomness(): Update<blob> {
    const canister_result: CanisterResult<blob> = yield ic.call_raw(
        Principal.fromText('aaaaa-aa'),
        'raw_rand',
        Uint8Array.from([68, 73, 68, 76, 0, 0]),
        0n // this is a nat64
    );

    if (!ok(canister_result)) {
        return Uint8Array.from([]);
    }

    return canister_result.ok;
}

call raw 128

Examples:

import { blob, ic, ok, Principal, Update } from 'azle';

export function* get_randomness(): Update<blob> {
    const canister_result: CanisterResult<blob> = yield ic.call_raw128(
        Principal.fromText('aaaaa-aa'),
        'raw_rand',
        Uint8Array.from([68, 73, 68, 76, 0, 0]),
        0n // this is a nat
    );

    if (!ok(canister_result)) {
        return Uint8Array.from([]);
    }

    return canister_result.ok;
}

call with payment

Examples:

import { Canister, CanisterResult, ic, ok, Update, Variant } from 'azle';

type Canister1 = Canister<{
    method(): CanisterResult<boolean>;
}>;

const canister1 = ic.canisters.Canister1<Canister1>(
    Principal.fromText('rkp4c-7iaaa-aaaaa-aaaca-cai')
);

type CallCanister1MethodResult = Variant<{
    ok: boolean;
    err: string;
}>;

export function* call_canister1_method(): Update<CallCanister1MethodResult> {
    const canister_result: CanisterResult<boolean> = yield canister1
        .method()
        .with_cycles(100_000_000_000n);

    if (!ok(canister_result)) {
        return {
            err: canister_result.err
        };
    }

    return {
        ok: canister_result.ok
    };
}

call with payment 128

Examples:

import { Canister, CanisterResult, ic, ok, Update, Variant } from 'azle';

type Canister1 = Canister<{
    method(): CanisterResult<boolean>;
}>;

const canister1 = ic.canisters.Canister1<Canister1>(
    Principal.fromText('rkp4c-7iaaa-aaaaa-aaaca-cai')
);

type CallCanister1MethodResult = Variant<{
    ok: boolean;
    err: string;
}>;

export function* call_canister1_method(): Update<CallCanister1MethodResult> {
    const canister_result: CanisterResult<boolean> = yield canister1
        .method()
        .with_cycles128(100_000_000_000n);

    if (!ok(canister_result)) {
        return {
            err: canister_result.err
        };
    }

    return {
        ok: canister_result.ok
    };
}

method name

Examples:

import { ic, InspectMessage, Update } from 'azle';

export function inspect_message(): InspectMessage {
    console.log('this runs before executing update calls');

    if (ic.method_name() === 'accessible') {
        ic.accept_message();
        return;
    }

    if (ic.method_name() === 'inaccessible') {
        return;
    }

    throw `Method "${ic.method_name()}" not allowed`;
}

export function accessible(): Update<boolean> {
    return true;
}

export function inaccessible(): Update<boolean> {
    return false;
}

export function also_inaccessible(): Update<boolean> {
    return false;
}

msg cycles accept

Examples:

import { ic, nat64, Update } from 'azle';

// Moves all transferred cycles to the canister
export function receive_cycles(): Update<nat64> {
    return ic.msg_cycles_accept(ic.msg_cycles_available());
}

msg cycles accept 128

Examples:

import { ic, nat, Update } from 'azle';

// Moves all transferred cycles to the canister
export function receive_cycles128(): Update<nat> {
    return ic.msg_cycles_accept128(ic.msg_cycles_available128());
}

msg cycles available

Examples:

import { ic, nat64, Update } from 'azle';

// Moves all transferred cycles to the canister
export function receive_cycles(): Update<nat64> {
    return ic.msg_cycles_accept(ic.msg_cycles_available());
}

msg cycles available 128

Examples:

import { ic, nat64, Update } from 'azle';

// Moves all transferred cycles to the canister
export function receive_cycles128(): Update<nat64> {
    return ic.msg_cycles_accept128(ic.msg_cycles_available128());
}

msg cycles refunded

Examples:

import { Canister, CanisterResult, ic, nat64, ok, Update, Variant } from 'azle';

type Canister1 = Canister<{
    method(): CanisterResult<boolean>;
}>;

const canister1 = ic.canisters.Canister1<Canister1>(
    Principal.fromText('rkp4c-7iaaa-aaaaa-aaaca-cai')
);

type CallCanister1MethodResult = Variant<{
    ok: nat64;
    err: string;
}>;

export function* call_canister1_method(): Update<CallCanister1MethodResult> {
    const canister_result: CanisterResult<boolean> = yield canister1
        .method()
        .with_cycles(100_000_000_000n);

    if (!ok(canister_result)) {
        return {
            err: canister_result.err
        };
    }

    return {
        ok: ic.msg_cycles_refunded()
    };
}

msg cycles refunded 128

Examples:

import { Canister, CanisterResult, ic, nat, ok, Update, Variant } from 'azle';

type Canister1 = Canister<{
    method(): CanisterResult<boolean>;
}>;

const canister1 = ic.canisters.Canister1<Canister1>(
    Principal.fromText('rkp4c-7iaaa-aaaaa-aaaca-cai')
);

type CallCanister1MethodResult = Variant<{
    ok: nat;
    err: string;
}>;

export function* call_canister1_method(): Update<CallCanister1MethodResult> {
    const canister_result: CanisterResult<boolean> = yield canister1
        .method()
        .with_cycles128(100_000_000_000n);

    if (!ok(canister_result)) {
        return {
            err: canister_result.err
        };
    }

    return {
        ok: ic.msg_cycles_refunded128()
    };
}

notify

Examples:

import { Canister, CanisterResult, ic, ok, Update } from 'azle';

type Canister1 = Canister<{
    method(): CanisterResult<boolean>;
}>;

const canister1 = ic.canisters.Canister1<Canister1>(
    Principal.fromText('rkp4c-7iaaa-aaaaa-aaaca-cai')
);

export function call_canister1_method(): Update<boolean> {
    const canister_result: CanisterResult<null> = canister1.method().notify();

    if (!ok(canister_result)) {
        return false;
    }

    return true;
}

notify raw

Examples:

import { ic, ok, Principal, Update } from 'azle';

export function send_notification(): Update<boolean> {
    const result = ic.notify_raw(
        Principal.fromText('ryjl3-tyaaa-aaaaa-aaaba-cai'),
        'receive_notification',
        Uint8Array.from([68, 73, 68, 76, 0, 0]),
        0n
    );

    if (!ok(result)) {
        return false;
    }

    return true;
}

notify with payment 128

Examples:

import { Canister, CanisterResult, ic, ok, Update } from 'azle';

type Canister1 = Canister<{
    method(): CanisterResult<boolean>;
}>;

const canister1 = ic.canisters.Canister1<Canister1>(
    Principal.fromText('rkp4c-7iaaa-aaaaa-aaaca-cai')
);

export function call_canister1_method(): Update<boolean> {
    const canister_result: CanisterResult<null> = canister1
        .method()
        .notify()
        .with_cycles128(100_000_000_000n);

    if (!ok(canister_result)) {
        return false;
    }

    return true;
}

performance counter

Examples:

export function performance_counter(): Query<nat64> {
    return ic.performance_counter(0);
}

reject

Examples:

import { empty, ic, QueryManual } from 'azle';

export function reject(message: string): QueryManual<empty> {
    ic.reject(message);
}

reject code

Examples:

import { Canister, CanisterResult, ic, RejectionCode, Update } from 'azle';

type Canister1 = Canister<{
    method(): CanisterResult<boolean>;
}>;

const canister1 = ic.canisters.Canister1<Canister1>(
    Principal.fromText('rkp4c-7iaaa-aaaaa-aaaca-cai')
);

export function* get_rejection_code(): Update<RejectionCode> {
    yield canister1.method();
    return ic.reject_code();
}

reject message

Examples:

import { Canister, CanisterResult, ic, Update } from 'azle';

type Canister1 = Canister<{
    method(): CanisterResult<boolean>;
}>;

const canister1 = ic.canisters.Canister1<Canister1>(
    Principal.fromText('rkp4c-7iaaa-aaaaa-aaaca-cai')
);

export function* get_rejection_message(): Update<string> {
    yield canister1.method();
    return ic.reject_message();
}

reply

Examples:

import { ic, UpdateManual } from 'azle';

export function manual_update(message: string): UpdateManual<string> {
    if (message === 'reject') {
        ic.reject(message);
        return;
    }

    ic.reply(message);
}

reply raw

Examples:

import { blob, ic, int, UpdateManual, Variant } from 'azle';

type RawReply = {
    int: int;
    text: string;
    bool: boolean;
    blob: blob;
    variant: Options;
};

type Options = Variant<{
    Small: null;
    Medium: null;
    Large: null;
}>;

export function reply_raw(): UpdateManual<RawReply> {
    // const response = '(record { "int" = 42; "text" = "text"; "bool" = true; "blob" = blob "Surprise!"; "variant" = variant {Medium} })';
    // const hex = execSync(`didc encode '${response}'`).toString().trim();
    // TODO expose candid encoding/decoding in azle.
    // See https://github.com/demergent-labs/azle/issues/400

    const candidEncodedArgumentsHexString =
        '4449444c036c05ef99c0027cddfae4880401aa88ee88047ead99e7e70471858189e70d026d7b6b019591f39a037f01002a0953757270726973652101047465787400';
    const candidEncodedArgumentsByteArray =
        candidEncodedArgumentsHexString
            .match(/.{1,2}/g)
            ?.map((byte) => parseInt(byte, 16)) ?? [];
    ic.reply_raw(new Uint8Array(candidEncodedArgumentsByteArray));
}

result

Not yet implemented.

Stable Memory

stable storage

Examples:

import { Init, nat, Stable, Update } from 'azle';

type StableStorage = Stable<{
    stable_nat: nat;
}>;

let stable_storage: StableStorage = ic.stable_storage();

export function init(_stable_nat: nat): Init {
    stable_storage.stable_nat = _stable_nat;
}

stable64 grow

Examples:

import { ic, nat64, Stable64GrowResult, Update } from 'azle';

export function stable64_grow(new_pages: nat64): Update<Stable64GrowResult> {
    return ic.stable64_grow(new_pages);
}

stable64 read

Examples:

import { blob, ic, nat64, Query } from 'azle';

export function stable64_read(offset: nat64, length: nat64): Query<blob> {
    return ic.stable64_read(offset, length);
}

stable64 size

Examples:

import { ic, nat64, Query } from 'azle';

export function stable64_size(): Query<nat64> {
    return ic.stable64_size();
}

stable64 write

Examples:

import { blob, ic, nat64, Update } from 'azle';

export function stable64_write(offset: nat64, buf: blob): Update<void> {
    ic.stable64_write(offset, buf);
}

stable bytes

Examples:

import { blob, ic, Query } from 'azle';

export function stable_bytes(): Query<blob> {
    return ic.stable_bytes();
}

stable grow

Examples:

import { ic, nat32, StableGrowResult, Update } from 'azle';

export function stable_grow(new_pages: nat32): Update<StableGrowResult> {
    return ic.stable_grow(new_pages);
}

stable read

Examples:

import { blob, ic, nat32, Query } from 'azle';

export function stable_read(offset: nat32, length: nat32): Query<blob> {
    return ic.stable_read(offset, length);
}

stable size

Examples:

import { ic, nat32, Query } from 'azle';

export function stable_size(): Query<nat32> {
    return ic.stable_size();
}

stable write

Examples:

import { blob, ic, nat32, Update } from 'azle';

export function stable_write(offset: nat32, buf: blob): Update<void> {
    ic.stable_write(offset, buf);
}

Feature Parity

The following is a comparison of all of the major features of the Rust CDK, Motoko, and Azle.

  • ✔️ = supported
  • ❌ = not supported
  • ❔ = unknown
  • ✅ = not applicable

Canister Methods

Feature Rust CDK Motoko Azle
init ✔️ ✔️ ✔️
pre upgrade ✔️ ✔️ ✔️
post upgrade ✔️ ✔️ ✔️
inspect message ✔️ ✔️ ✔️
heartbeat ✔️ ✔️ ✔️
update ✔️ ✔️ ✔️
query ✔️ ✔️ ✔️
http_request ✔️ ✔️ ✔️
http_request_update ✔️ ✔️ ✔️

Candid Types

Feature Rust CDK Motoko Azle
text ✔️ ✔️ ✔️
blob ✔️ ✔️ ✔️
nat ✔️ ✔️ ✔️
nat64 ✔️ ✔️ ✔️
nat32 ✔️ ✔️ ✔️
nat16 ✔️ ✔️ ✔️
nat8 ✔️ ✔️ ✔️
int ✔️ ✔️ ✔️
int64 ✔️ ✔️ ✔️
int32 ✔️ ✔️ ✔️
int16 ✔️ ✔️ ✔️
int8 ✔️ ✔️ ✔️
float64 ✔️ ✔️ ✔️
float32 ✔️ ✔️ ✔️
bool ✔️ ✔️ ✔️
null ✔️ ✔️ ✔️
vec ✔️ ✔️ ✔️
opt ✔️ ✔️ ✔️
record ✔️ ✔️ ✔️
variant ✔️ ✔️ ✔️
func ✔️ ✔️ ✔️
service ✔️ ✔️
principal ✔️ ✔️ ✔️
reserved ✔️ ✔️ ✔️
empty ✔️ ✔️ ✔️

Canister APIs

Feature Rust CDK Motoko Azle
canister balance ✔️ ✔️
canister balance 128 ✔️ ✔️ ✔️
data certificate ✔️ ✔️ ✔️
canister id ✔️ ✔️ ✔️
print ✔️ ✔️ ✔️
set certified data ✔️ ✔️ ✔️
time ✔️ ✔️ ✔️
trap ✔️ ✔️ ✔️

Call APIs

Feature Rust CDK Motoko Azle
caller ✔️ ✔️ ✔️
accept message ✔️ ✔️ ✔️
arg data ✔️
arg data raw ✔️ ✔️
arg data raw size ✔️ ✔️
call ✔️ ✔️ ✔️
call raw ✔️ ✔️
call raw 128 ✔️ ✔️ ✔️
call with payment ✔️ ✔️
call with payment 128 ✔️ ✔️ ✔️
method name ✔️ ✔️ ✔️
msg cycles accept ✔️ ✔️
msg cycles accept 128 ✔️ ✔️ ✔️
msg cycles available ✔️ ✔️
msg cycles available 128 ✔️ ✔️ ✔️
msg cycles refunded ✔️ ✔️
msg cycles refunded 128 ✔️ ✔️ ✔️
notify ✔️ ✔️
notify raw ✔️ ✔️
notify with payment 128 ✔️ ✔️
performance counter ✔️ ✔️ ✔️
reject ✔️ ✔️
reject code ✔️ ✔️
reject message ✔️ ✔️
reply ✔️ ✔️
reply raw ✔️ ✔️
result ✔️

Stable Memory

Feature Rust CDK Motoko Azle
stable storage ✔️ ✔️ ✔️
stable64 grow ✔️ ✔️ ✔️
stable64 read ✔️ ✔️ ✔️
stable64 size ✔️ ✔️ ✔️
stable64 write ✔️ ✔️ ✔️
stable bytes ✔️ ✔️
stable grow ✔️ ✔️
stable read ✔️ ✔️
stable size ✔️ ✔️
stable write ✔️ ✔️
stable read nat64 ✔️
stable write nat64 ✔️
stable read nat32 ✔️
stable write nat32 ✔️
stable read nat16 ✔️
stable write nat16 ✔️
stable read nat8 ✔️
stable write nat8 ✔️
stable read int64 ✔️
stable write int64 ✔️
stable read int32 ✔️
stable write int32 ✔️
stable read int16 ✔️
stable write int16 ✔️
stable read int8 ✔️
stable write int8 ✔️
stable read float64 ✔️
stable write float64 ✔️

Roadmap

  • July 2022
    • Extensive automated benchmarking
  • August 2022
    • Compiler error DX revamp
    • Rust rewrite
  • September 2022
    • Extensive automated property testing
    • Multiple independent security reviews/audits

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)
  • 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
  • Avoid inline types and use type aliases instead
  • import * syntax is not supported for any type that will undergo Candid serialization or deserialization
  • Inefficient Wasm instruction usage relative to Rust and Motoko, especially with arrays
  • Unknown security vulnerabilities
  • Many small inconveniences

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 reach out before working on anything that is not in the good first issues or help wanted issues. 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.

About

TypeScript CDK for the Internet Computer

License:MIT License


Languages

Language:TypeScript 91.2%Language:Rust 8.5%Language:Shell 0.2%Language:JavaScript 0.1%