Azle (Beta)
-Azle is a TypeScript Canister Development Kit (CDK) for the Internet Computer (IC). In other words, it's a TypeScript/JavaScript runtime for building applications (canisters) on the IC.
+Azle is a TypeScript and JavaScript Canister Development Kit (CDK) for the Internet Computer (IC). In other words, it's a TypeScript/JavaScript runtime for building applications (canisters) on the IC.
Disclaimer
Azle may have unknown security vulnerabilities due to the following:
-
-
- Azle does not yet have many live, successful, continuously operating applications deployed to the IC
- Azle does not yet have extensive automated property tests
- Azle does not yet have multiple independent security reviews/audits -
- Azle heavily relies on Boa which is self-proclaimed to be experimental +
- Azle does not yet have many live, successful, continuously operating applications deployed to the IC
Roadmap
-We hope to get to a production-ready 1.0 in 2024. The following are the major blockers to 1.0:
+We hope to move Azle from beta to release candidates by the end of 2023, and to move from release candidates to 1.0 in early 2024.
+Blockers for release candidates
-
-
- QuickJS/SpiderMonkey integration for performance, security, and stability -
- Broad npm package support -
- Extensive automated property testing +
- Good automated property test coverage +
- Settling of API/syntax +
- Good npm package support +
Blockers for 1.0
+-
+
- Extensive automated property test coverage
- Multiple independent security reviews/audits +
- Broad npm package support
Demergent Labs
Azle is currently developed by Demergent Labs, a for-profit company with a grant from DFINITY.
@@ -174,8 +179,8 @@Azle and the IC provide unique benefits and drawbacks, and both are not currently suitable for all application use-cases.
The following information will help you to determine when Azle and the IC might be beneficial for your use-case.
Benefits
-Azle intends to be a full TypeScript/JavaScript environment for the IC (a decentralized cloud platform), with support for all of the TypeScript/JavaScript language and as many relevant host APIs as possible. These host APIs will be similar to those available in the Node.js and web browser environments.
-One of the core benefits of Azle is that it allows web developers to bring their TypeScript/JavaScript skills to the IC. For example, Azle allows the use of various npm packages and VS Code intellisense.
+Azle intends to be a full TypeScript and JavaScript environment for the IC (a decentralized cloud platform), with support for all of the TypeScript and JavaScript language and as many relevant environment APIs as possible. These environment APIs will be similar to those available in the Node.js and web browser environments.
+One of the core benefits of Azle is that it allows web developers to bring their TypeScript or JavaScript skills to the IC. For example, Azle allows the use of various npm packages and VS Code intellisense.
As for the IC, we believe its main benefits can be broken down into the following categories:
- Ownership
@@ -195,7 +200,7 @@ DAOs. As opposed to DAOs built on most blockchains, the IC allows full-stack applications to be controlled by groups. This means that the group fully controls the running instances of the frontend and the backend code.
Autonomous ownership
-
In addition to allowing applications to be owned by groups of people, the IC also allows applications to be owned by no one. This essentially creates autonomous applications or everlasting processes that execute indefinitely. The IC will allow such an application to run until it depletes its balance of cycles, or until the NNS votes to shut it down.
+In addition to allowing applications to be owned by groups of people, the IC also allows applications to be owned by no one. This essentially creates autonomous applications or everlasting processes that execute indefinitely. The IC will essentially allow such an application to run indefinitely, unless it depletes its balance of cycles, or the NNS votes to shut it down, neither of which is inevitable.
Permanent APIs
Because most web APIs are owned and operated by individual entities, their fate is tied to that of their owners. If their owners go out of business, then those APIs may cease to exist. If their owners decide that they do not like or agree with certain users, they may restrict their access. In the end, they may decide to shut down or restrict access for arbitrary reasons.
Because the IC allows for group and autonomous ownership of cloud software, the IC is able to produce potentially permanent web APIs. A decentralized group of independent entities will find it difficult to censor API consumers or shut down an API. An autonomous API would take those difficulties to the extreme, as it would continue operating as long as consumers were willing to pay for it.
@@ -251,33 +256,24 @@ Azle
Beta
Azle reached beta in April of 2022. It's an immature project that may have unforeseen bugs and other issues. We're working constantly to improve it. We hope to get to a production-ready 1.0 in 2024. The following are the major blockers to 1.0:
-- QuickJS/SpiderMonkey integration for performance, security, and stability
-- Broad npm package support
-- Extensive automated property testing
+- Extensive automated property test coverage
- Multiple independent security reviews/audits
+- Broad npm package support
Security risks
As discussed earlier, these are some things to keep in mind:
-- Azle does not yet have many live, successful, continuously operating applications deployed to the IC
- Azle does not yet have extensive automated property tests
- Azle does not yet have multiple independent security reviews/audits
-- Azle heavily relies on Boa which is self-proclaimed to be experimental
+- Azle does not yet have many live, successful, continuously operating applications deployed to the IC
-High cycle usage
-We have done some preliminary benchmarking, and based on that our rough heuristic is that Azle will cost 2-4x more cycles than the equivalent project in Motoko or Rust. The performance of your application depends on many factors, and this should just be a rough estimate.
-There is evidence to suggest that a 30x improvement in performance is possible in our underlying JS engine.
Missing APIs
-Azle is not Node.js nor is it V8 running in a web browser. It is using a new JavaScript interpreter running in a very new and very different environment. APIs from the Node.js and web browser ecosystems may not be present in Azle. Our goal is to support as many of these APIs as possible over time.
-Missing JavaScript features
-There may be some missing JavaScript language features. You can track our language feature support based on Boa's conformance with the ECMAScript test suite. We also use multiple underlying compilers and bundlers before we execute your JavaScript, thus we may support more language features than the test suite conformance shows.
+Azle is not Node.js nor is it V8 running in a web browser. It is using a JavaScript interpreter running in a very new and very different environment. APIs from the Node.js and web browser ecosystems may not be present in Azle. Our goal is to support as many of these APIs as possible over time.
IC
Some of the IC's main drawbacks can be summarized as follows:
@@ -299,7 +295,7 @@ Limit
Lack of privacy
You should assume that all of your application data (unless it is end-to-end encrypted) is accessible to multiple third-parties with no direct relationship and limited commitment to you. Currently all canister state sits unencrypted on node operator's machines. Application-layer access controls for data are possible, but motivated node operators will have an easy time getting access to your data.
NNS risk
-The NNS has the ability to uninstall any canister and can generally change anything about the IC. As of the time of this writing, DFINITY effectively controls much of the NNS through its follower relationships. The NNS must mature and decentralize to provide practical and realistic guarantees to canisters and their users.
+The NNS has the ability to uninstall any canister and can generally change anything about the IC protocol. The NNS uses a simple liquid democracy based on coin/token voting and follower relationships. At the time of this writing most of the voting power on the NNS follows DFINITY for protocol changes, effectively giving DFINITY write control to the protocol while those follower relationships remain in place. The NNS must mature and decentralize to provide practical and realistic protections to canisters and their users.
DAOs. As opposed to DAOs built on most blockchains, the IC allows full-stack applications to be controlled by groups. This means that the group fully controls the running instances of the frontend and the backend code.
Autonomous ownership
-
In addition to allowing applications to be owned by groups of people, the IC also allows applications to be owned by no one. This essentially creates autonomous applications or everlasting processes that execute indefinitely. The IC will allow such an application to run until it depletes its balance of cycles, or until the NNS votes to shut it down.
+In addition to allowing applications to be owned by groups of people, the IC also allows applications to be owned by no one. This essentially creates autonomous applications or everlasting processes that execute indefinitely. The IC will essentially allow such an application to run indefinitely, unless it depletes its balance of cycles, or the NNS votes to shut it down, neither of which is inevitable.
Permanent APIs
Because most web APIs are owned and operated by individual entities, their fate is tied to that of their owners. If their owners go out of business, then those APIs may cease to exist. If their owners decide that they do not like or agree with certain users, they may restrict their access. In the end, they may decide to shut down or restrict access for arbitrary reasons.
Because the IC allows for group and autonomous ownership of cloud software, the IC is able to produce potentially permanent web APIs. A decentralized group of independent entities will find it difficult to censor API consumers or shut down an API. An autonomous API would take those difficulties to the extreme, as it would continue operating as long as consumers were willing to pay for it.
@@ -251,33 +256,24 @@Azle
Beta
Azle reached beta in April of 2022. It's an immature project that may have unforeseen bugs and other issues. We're working constantly to improve it. We hope to get to a production-ready 1.0 in 2024. The following are the major blockers to 1.0:
-
-
- QuickJS/SpiderMonkey integration for performance, security, and stability -
- Broad npm package support -
- Extensive automated property testing +
- Extensive automated property test coverage
- Multiple independent security reviews/audits +
- Broad npm package support
Security risks
As discussed earlier, these are some things to keep in mind:
-
-
- Azle does not yet have many live, successful, continuously operating applications deployed to the IC
- Azle does not yet have extensive automated property tests
- Azle does not yet have multiple independent security reviews/audits -
- Azle heavily relies on Boa which is self-proclaimed to be experimental +
- Azle does not yet have many live, successful, continuously operating applications deployed to the IC
High cycle usage
-We have done some preliminary benchmarking, and based on that our rough heuristic is that Azle will cost 2-4x more cycles than the equivalent project in Motoko or Rust. The performance of your application depends on many factors, and this should just be a rough estimate.
-There is evidence to suggest that a 30x improvement in performance is possible in our underlying JS engine.
Missing APIs
-Azle is not Node.js nor is it V8 running in a web browser. It is using a new JavaScript interpreter running in a very new and very different environment. APIs from the Node.js and web browser ecosystems may not be present in Azle. Our goal is to support as many of these APIs as possible over time.
-Missing JavaScript features
-There may be some missing JavaScript language features. You can track our language feature support based on Boa's conformance with the ECMAScript test suite. We also use multiple underlying compilers and bundlers before we execute your JavaScript, thus we may support more language features than the test suite conformance shows.
+Azle is not Node.js nor is it V8 running in a web browser. It is using a JavaScript interpreter running in a very new and very different environment. APIs from the Node.js and web browser ecosystems may not be present in Azle. Our goal is to support as many of these APIs as possible over time.
IC
Some of the IC's main drawbacks can be summarized as follows:
-
@@ -299,7 +295,7 @@
Limit
Lack of privacy
You should assume that all of your application data (unless it is end-to-end encrypted) is accessible to multiple third-parties with no direct relationship and limited commitment to you. Currently all canister state sits unencrypted on node operator's machines. Application-layer access controls for data are possible, but motivated node operators will have an easy time getting access to your data.
NNS risk
-The NNS has the ability to uninstall any canister and can generally change anything about the IC. As of the time of this writing, DFINITY effectively controls much of the NNS through its follower relationships. The NNS must mature and decentralize to provide practical and realistic guarantees to canisters and their users.
+The NNS has the ability to uninstall any canister and can generally change anything about the IC protocol. The NNS uses a simple liquid democracy based on coin/token voting and follower relationships. At the time of this writing most of the voting power on the NNS follows DFINITY for protocol changes, effectively giving DFINITY write control to the protocol while those follower relationships remain in place. The NNS must mature and decentralize to provide practical and realistic protections to canisters and their users.
NNS risk
- + @@ -323,7 +319,7 @@NNS risk
- + diff --git a/the_azle_book/book/candid.html b/the_azle_book/book/candid.html index ed39030410..46fa0d47bd 100644 --- a/the_azle_book/book/candid.html +++ b/the_azle_book/book/candid.html @@ -83,7 +83,7 @@ @@ -146,887 +146,177 @@The Azle Book
Candid
-
-
- 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 +
- text +
- blob +
- nat +
- nat8 +
- nat16 +
- nat32 +
- nat64 +
- int +
- int8 +
- int16 +
- int32 +
- int64 +
- float32 +
- float64 +
- bool +
- null +
- vec +
- opt +
- record +
- variant +
- func +
- service +
- principal +
- reserved +
- empty
Candid is an interface description language created by DFINITY. It can be used to define interfaces between services (canisters), allowing canisters and clients written in various languages to easily interact with each other.
-Azle allows you to express Candid types through a combination of native and Azle-provided TypeScript types. These types will be necessary in various places as you define your canister. For example, Candid types must be used when defining the parameters and return types of your query and update methods.
-It's important to note that the Candid types are represented at runtime using specific JavaScript data structures that may differ in behavior from the description of the actual Candid type. For example, a float32
Candid type is a JavaScript Number, a nat64
is a JavaScript BigInt, and an int
is also a JavaScript BigInt.
Keep this in mind as it may result in unexpected behavior. Each Candid type and its equivalent JavaScript runtime value is explained in more detail in this chapter.
-A reference of all Candid types available on the Internet Computer (IC) can be found here.
-The following is a simple example showing how to import and use most of the Candid types available in Azle:
+Candid is an interface description language created by DFINITY. It can be used to define interfaces between services (canisters), allowing canisters and clients written in various languages to easily interact with each other. This interaction occurs through the serialization/encoding and deserialization/decoding of runtime values to and from Candid bytes.
+Azle performs automatic encoding and decoding of JavaScript values to and from Candid bytes through the use of various CandidType
objects. For example, CandidType
objects are used when defining the parameter and return types of your query and update methods. They are also used to define the keys and values of a StableBTreeMap
.
It's important to note that the CandidType
objects decode Candid bytes into specific JavaScript runtime data structures that may differ in behavior from the description of the actual Candid type. For example, a float32
Candid type is a JavaScript Number, a nat64
is a JavaScript BigInt, and an int
is also a JavaScript BigInt.
Keep this in mind as it may result in unexpected behavior. Each CandidType
object and its equivalent JavaScript runtime value is explained in more detail in The Azle Book Candid reference.
A more canonical reference of all Candid types available on the Internet Computer (IC) can be found here.
+The following is a simple example showing how to import and use many of the CandidType
objects available in Azle:
import {
blob,
- CallResult,
- float64,
+ bool,
+ Canister,
float32,
+ float64,
Func,
int,
- int8,
int16,
int32,
int64,
+ int8,
nat,
- nat8,
nat16,
nat32,
nat64,
+ nat8,
+ None,
+ Null,
Opt,
Principal,
- $query,
- Query,
+ query,
Record,
- Service,
- serviceQuery,
- serviceUpdate,
+ Recursive,
+ text,
+ update,
Variant,
Vec
} from 'azle';
-type Candid = Record<{
- text: string;
- blob: blob;
- nat: nat;
- nat64: nat64;
- nat32: nat32;
- nat16: nat16;
- nat8: nat8;
- int: int;
- int64: int64;
- int32: int32;
- int16: int16;
- int8: int8;
- float64: float64;
- float32: float32;
- bool: boolean;
- null: null;
- vec: Vec<string>;
- opt: Opt<nat>;
- record: Record<{
- firstName: string;
- lastName: string;
- age: nat8;
- }>;
- variant: Variant<{
- Tag1: null;
- Tag2: null;
- Tag3: int;
- }>;
- func: Func<Query<() => Candid>>;
- service: MyService;
- principal: Principal;
-}>;
-
-class MyService extends Service {
- @serviceQuery
- query1: () => CallResult<boolean>;
-
- @serviceUpdate
- update1: () => CallResult<string>;
-}
-
-$query;
-export function candidTypes(): Candid {
- return {
- text: 'text',
- blob: Uint8Array.from([]),
- nat: 340_282_366_920_938_463_463_374_607_431_768_211_455n,
- nat64: 18_446_744_073_709_551_615n,
- nat32: 4_294_967_295,
- nat16: 65_535,
- nat8: 255,
- int: 170_141_183_460_469_231_731_687_303_715_884_105_727n,
- int64: 9_223_372_036_854_775_807n,
- int32: 2_147_483_647,
- int16: 32_767,
- int8: 127,
- float64: Math.E,
- float32: Math.PI,
- bool: true,
- null: null,
- vec: ['has one element'],
- opt: Opt.None,
- record: {
- firstName: 'John',
- lastName: 'Doe',
- age: 35
- },
- variant: {
- Tag1: null
- },
- func: [
- Principal.fromText('rrkah-fqaaa-aaaaa-aaaaq-cai'),
- 'candidTypes'
- ],
- service: new MyService(Principal.fromText('aaaaa-aa')),
- principal: Principal.fromText('ryjl3-tyaaa-aaaaa-aaaba-cai')
- };
-}
+const MyCanister = Canister({
+ query: query([], bool),
+ update: update([], text)
+});
+
+const Candid = Record({
+ text: text,
+ blob: blob,
+ nat: nat,
+ nat64: nat64,
+ nat32: nat32,
+ nat16: nat16,
+ nat8: nat8,
+ int: int,
+ int64: int64,
+ int32: int32,
+ int16: int16,
+ int8: int8,
+ float64: float64,
+ float32: float32,
+ bool: bool,
+ null: Null,
+ vec: Vec(text),
+ opt: Opt(nat),
+ record: Record({
+ firstName: text,
+ lastName: text,
+ age: nat8
+ }),
+ variant: Variant({
+ Tag1: Null,
+ Tag2: Null,
+ Tag3: int
+ }),
+ func: Recursive(() => Func([], Candid, 'query')),
+ canister: Canister({
+ query: query([], bool),
+ update: update([], text)
+ }),
+ principal: Principal
+});
+
+export default Canister({
+ candidTypes: query([], Candid, () => {
+ return {
+ text: 'text',
+ blob: Uint8Array.from([]),
+ nat: 340_282_366_920_938_463_463_374_607_431_768_211_455n,
+ nat64: 18_446_744_073_709_551_615n,
+ nat32: 4_294_967_295,
+ nat16: 65_535,
+ nat8: 255,
+ int: 170_141_183_460_469_231_731_687_303_715_884_105_727n,
+ int64: 9_223_372_036_854_775_807n,
+ int32: 2_147_483_647,
+ int16: 32_767,
+ int8: 127,
+ float64: Math.E,
+ float32: Math.PI,
+ bool: true,
+ null: null,
+ vec: ['has one element'],
+ opt: None,
+ record: {
+ firstName: 'John',
+ lastName: 'Doe',
+ age: 35
+ },
+ variant: {
+ Tag1: null
+ },
+ func: [
+ Principal.fromText('rrkah-fqaaa-aaaaa-aaaaq-cai'),
+ 'candidTypes'
+ ],
+ canister: MyCanister(Principal.fromText('aaaaa-aa')),
+ principal: Principal.fromText('ryjl3-tyaaa-aaaaa-aaaba-cai')
+ };
+ })
+});
Calling candidTypes
with dfx
will return:
(
record {
- "int" = 170_141_183_460_469_231_731_687_303_715_884_105_727 : int;
- "nat" = 340_282_366_920_938_463_463_374_607_431_768_211_455 : nat;
- "opt" = null;
- "vec" = vec { "has one element" };
- "service" = service "aaaaa-aa";
- "principal" = principal "ryjl3-tyaaa-aaaaa-aaaba-cai";
- "blob" = vec {};
- "bool" = true;
- "func" = func "rrkah-fqaaa-aaaaa-aaaaq-cai".candidTypes;
- "int8" = 127 : int8;
- "nat8" = 255 : nat8;
- "null" = null : null;
- "text" = "text";
- "nat16" = 65_535 : nat16;
- "nat32" = 4_294_967_295 : nat32;
- "nat64" = 18_446_744_073_709_551_615 : nat64;
- "int16" = 32_767 : int16;
- "int32" = 2_147_483_647 : int32;
- "int64" = 9_223_372_036_854_775_807 : int64;
- "variant" = variant { Tag1 };
- "float32" = 3.1415927 : float32;
- "float64" = 2.718281828459045 : float64;
- "record" = record { age = 35 : nat8; lastName = "Doe"; firstName = "John" };
+ func = func "rrkah-fqaaa-aaaaa-aaaaq-cai".candidTypes;
+ text = "text";
+ nat16 = 65_535 : nat16;
+ nat32 = 4_294_967_295 : nat32;
+ nat64 = 18_446_744_073_709_551_615 : nat64;
+ record = record { age = 35 : nat8; lastName = "Doe"; firstName = "John" };
+ int = 170_141_183_460_469_231_731_687_303_715_884_105_727 : int;
+ nat = 340_282_366_920_938_463_463_374_607_431_768_211_455 : nat;
+ opt = null;
+ vec = vec { "has one element" };
+ variant = variant { Tag1 };
+ nat8 = 255 : nat8;
+ canister = service "aaaaa-aa";
+ int16 = 32_767 : int16;
+ int32 = 2_147_483_647 : int32;
+ int64 = 9_223_372_036_854_775_807 : int64;
+ null = null : null;
+ blob = vec {};
+ bool = true;
+ principal = principal "ryjl3-tyaaa-aaaaa-aaaba-cai";
+ int8 = 127 : int8;
+ float32 = 3.1415927 : float32;
+ float64 = 2.718281828459045 : float64;
},
)
-
-text
-The TypeScript type string
and the Azle type text
both correspond to the Candid type text and will become a JavaScript String at runtime.
TypeScript:
-import { $query } from 'azle';
-
-$query;
-export function getString(): string {
- return 'Hello world!';
-}
-
-$query;
-export function printString(string: string): string {
- console.log(typeof string);
- return string;
-}
-
-Candid:
-service : () -> {
- getString : () -> (text) query;
- printString : (text) -> (text) query;
-}
-
-dfx:
-dfx canister call candid_canister printString '("Hello world!")'
-("Hello world!")
-
-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';
-
-$query;
-export function getBlob(): blob {
- return Uint8Array.from([68, 73, 68, 76, 0, 0]);
-}
-
-$query;
-export function printBlob(blob: blob): blob {
- console.log(typeof blob);
- return blob;
-}
-
-Candid:
-service : () -> {
- getBlob : () -> (vec nat8) query;
- printBlob : (vec nat8) -> (vec nat8) query;
-}
-
-dfx:
-dfx canister call candid_canister printBlob '(vec { 68; 73; 68; 76; 0; 0; })'
-(blob "DIDL\00\00")
-
-dfx canister call candid_canister printBlob '(blob "DIDL\00\00")'
-(blob "DIDL\00\00")
-
-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';
-
-$query;
-export function getNat(): nat {
- return 340_282_366_920_938_463_463_374_607_431_768_211_455n;
-}
-
-$query;
-export function printNat(nat: nat): nat {
- console.log(typeof nat);
- return nat;
-}
-
-Candid:
-service : () -> {
- getNat : () -> (nat) query;
- printNat : (nat) -> (nat) query;
-}
-
-dfx:
-dfx canister call candid_canister printNat '(340_282_366_920_938_463_463_374_607_431_768_211_455 : nat)'
-(340_282_366_920_938_463_463_374_607_431_768_211_455 : nat)
-
-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';
-
-$query;
-export function getNat64(): nat64 {
- return 18_446_744_073_709_551_615n;
-}
-
-$query;
-export function printNat64(nat64: nat64): nat64 {
- console.log(typeof nat64);
- return nat64;
-}
-
-Candid:
-service : () -> {
- getNat64 : () -> (nat64) query;
- printNat64 : (nat64) -> (nat64) query;
-}
-
-dfx:
-dfx canister call candid_canister printNat64 '(18_446_744_073_709_551_615 : nat64)'
-(18_446_744_073_709_551_615 : nat64)
-
-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';
-
-$query;
-export function getNat32(): nat32 {
- return 4_294_967_295;
-}
-
-$query;
-export function printNat32(nat32: nat32): nat32 {
- console.log(typeof nat32);
- return nat32;
-}
-
-Candid:
-service : () -> {
- getNat32 : () -> (nat32) query;
- printNat32 : (nat32) -> (nat32) query;
-}
-
-dfx:
-dfx canister call candid_canister printNat32 '(4_294_967_295 : nat32)'
-(4_294_967_295 : nat32)
-
-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';
-
-$query;
-export function getNat16(): nat16 {
- return 65_535;
-}
-
-$query;
-export function printNat16(nat16: nat16): nat16 {
- console.log(typeof nat16);
- return nat16;
-}
-
-Candid:
-service : () -> {
- getNat16 : () -> (nat16) query;
- printNat16 : (nat16) -> (nat16) query;
-}
-
-dfx:
-dfx canister call candid_canister printNat16 '(65_535 : nat16)'
-(65_535 : nat16)
-
-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';
-
-$query;
-export function getNat8(): nat8 {
- return 255;
-}
-
-$query;
-export function printNat8(nat8: nat8): nat8 {
- console.log(typeof nat8);
- return nat8;
-}
-
-Candid:
-service : () -> {
- getNat8 : () -> (nat8) query;
- printNat8 : (nat8) -> (nat8) query;
-}
-
-dfx:
-dfx canister call candid_canister printNat8 '(255 : nat8)'
-(255 : nat8)
-
-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';
-
-$query;
-export function getInt(): int {
- return 170_141_183_460_469_231_731_687_303_715_884_105_727n;
-}
-
-$query;
-export function printInt(int: int): int {
- console.log(typeof int);
- return int;
-}
-
-Candid:
-service : () -> {
- getInt : () -> (int) query;
- printInt : (int) -> (int) query;
-}
-
-dfx:
-dfx canister call candid_canister printInt '(170_141_183_460_469_231_731_687_303_715_884_105_727 : int)'
-(170_141_183_460_469_231_731_687_303_715_884_105_727 : int)
-
-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';
-
-$query;
-export function getInt64(): int64 {
- return 9_223_372_036_854_775_807n;
-}
-
-$query;
-export function printInt64(int64: int64): int64 {
- console.log(typeof int64);
- return int64;
-}
-
-Candid:
-service : () -> {
- getInt64 : () -> (int64) query;
- printInt64 : (int64) -> (int64) query;
-}
-
-dfx:
-dfx canister call candid_canister printInt64 '(9_223_372_036_854_775_807 : int64)'
-(9_223_372_036_854_775_807 : int64)
-
-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';
-
-$query;
-export function getInt32(): int32 {
- return 2_147_483_647;
-}
-
-$query;
-export function printInt32(int32: int32): int32 {
- console.log(typeof int32);
- return int32;
-}
-
-Candid:
-service : () -> {
- getInt32 : () -> (int32) query;
- printInt32 : (int32) -> (int32) query;
-}
-
-dfx:
-dfx canister call candid_canister printInt32 '(2_147_483_647 : int32)'
-(2_147_483_647 : int32)
-
-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';
-
-$query;
-export function getInt16(): int16 {
- return 32_767;
-}
-
-$query;
-export function printInt16(int16: int16): int16 {
- console.log(typeof int16);
- return int16;
-}
-
-Candid:
-service : () -> {
- getInt16 : () -> (int16) query;
- printInt16 : (int16) -> (int16) query;
-}
-
-dfx:
-dfx canister call candid_canister printInt16 '(32_767 : int16)'
-(32_767 : int16)
-
-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';
-
-$query;
-export function getInt8(): int8 {
- return 127;
-}
-
-$query;
-export function printInt8(int8: int8): int8 {
- console.log(typeof int8);
- return int8;
-}
-
-Candid:
-service : () -> {
- getInt8 : () -> (int8) query;
- printInt8 : (int8) -> (int8) query;
-}
-
-dfx:
-dfx canister call candid_canister printInt8 '(127 : int8)'
-(127 : int8)
-
-float64
-The Azle type float64
and the TypeScript type number
both correspond to the Candid type float64 and will become a JavaScript Number at runtime.
TypeScript:
-import { float64, $query } from 'azle';
-
-$query;
-export function getFloat64(): float64 {
- return Math.E;
-}
-
-$query;
-export function printFloat64(float64: float64): float64 {
- console.log(typeof float64);
- return float64;
-}
-
-Candid:
-service : () -> {
- getFloat64 : () -> (float64) query;
- printFloat64 : (float64) -> (float64) query;
-}
-
-dfx:
-dfx canister call candid_canister printFloat64 '(2.718281828459045 : float64)'
-(2.718281828459045 : float64)
-
-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';
-
-$query;
-export function getFloat32(): float32 {
- return Math.PI;
-}
-
-$query;
-export function printFloat32(float32: float32): float32 {
- console.log(typeof float32);
- return float32;
-}
-
-Candid:
-service : () -> {
- getFloat32 : () -> (float32) query;
- printFloat32 : (float32) -> (float32) query;
-}
-
-dfx:
-dfx canister call candid_canister printFloat32 '(3.1415927 : float32)'
-(3.1415927 : float32)
-
-bool
-The TypeScript type boolean
corresponds to the Candid type bool and will become a JavaScript Boolean at runtime.
TypeScript:
-import { $query } from 'azle';
-
-$query;
-export function getBool(): boolean {
- return true;
-}
-
-$query;
-export function printBool(bool: boolean): boolean {
- console.log(typeof bool);
- return bool;
-}
-
-Candid:
-service : () -> {
- getBool : () -> (bool) query;
- printBool : (bool) -> (bool) query;
-}
-
-dfx:
-dfx canister call candid_canister printBool '(true)'
-(true)
-
-null
-The TypeScript type null
corresponds to the Candid type null and will become a JavaScript null at runtime.
TypeScript:
-import { $query } from 'azle';
-
-$query;
-export function getNull(): null {
- return null;
-}
-
-$query;
-export function printNull(null_: null): null {
- console.log(typeof null_);
- return null_;
-}
-
-Candid:
-service : () -> {
- getNull : () -> (null) query;
- printNull : (null) -> (null) query;
-}
-
-dfx:
-dfx canister call candid_canister printNull '(null)'
-(null : null)
-
-vec
-The Azle type Vec
corresponds to the Candid type vec and will become a JavaScript array of the specified type at runtime (except for Vec<nat8>
which will become a Uint8Array
, thus it is recommended to use the blob
type instead of Vec<nat8>
).
TypeScript:
-import { int32, $query, Vec } from 'azle';
-
-$query;
-export function getNumbers(): Vec<int32> {
- return [0, 1, 2, 3];
-}
-
-$query;
-export function printNumbers(numbers: Vec<int32>): Vec<int32> {
- console.log(typeof numbers);
- return numbers;
-}
-
-Candid:
-service : () -> {
- getNumbers : () -> (vec int32) query;
- printNumbers : (vec int32) -> (vec int32) query;
-}
-
-dfx:
-dfx canister call candid_canister printNumbers '(vec { 0 : int32; 1 : int32; 2 : int32; 3 : int32 })'
-(vec { 0 : int32; 1 : int32; 2 : int32; 3 : int32 })
-
-opt
-The Azle type Opt
corresponds to the Candid type opt. It is a variant with Some
and None
cases. At runtime if the value of the variant is Some
, the Some
property of the variant object will have a value of the enclosed Opt
type at runtime.
TypeScript:
-import { Opt, $query } from 'azle';
-
-$query;
-export function getOptSome(): Opt<boolean> {
- return Opt.Some(true);
-}
-
-$query;
-export function getOptNone(): Opt<boolean> {
- return Opt.None;
-}
-
-Candid:
-service : () -> {
- getOptNone : () -> (opt bool) query;
- getOptSome : () -> (opt bool) query;
-}
-
-dfx:
-dfx canister call candid_canister getOptSome
-(opt true)
-
-dfx canister call candid_canister getOptNone
-(null)
-
-record
-TypeScript type aliases referring to object literals wrapped in the Record
Azle type correspond to the Candid record type and will become JavaScript Objects at runtime.
TypeScript:
-import { Principal, $query, Record } from 'azle';
-
-type User = Record<{
- id: Principal;
- username: string;
-}>;
-
-$query;
-export function getUser(): User {
- return {
- id: Principal.fromUint8Array(Uint8Array.from([0])),
- username: 'lastmjs'
- };
-}
-
-$query;
-export function printUser(user: User): User {
- console.log(typeof user);
- return user;
-}
-
-Candid:
-type User = record { id : principal; username : text };
-service : () -> {
- getUser : () -> (User) query;
- printUser : (User) -> (User) query;
-}
-
-dfx:
-dfx canister call candid_canister printUser '(record { id = principal "2ibo7-dia"; username = "lastmjs" })'
-(record { id = principal "2ibo7-dia"; username = "lastmjs" })
-
-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 { $query, Variant } from 'azle';
-
-type Reaction = Variant<{
- Fire: null;
- ThumbsUp: null;
- Emotion: Emotion;
-}>;
-
-type Emotion = Variant<{
- Happy: null;
- Indifferent: null;
- Sad: null;
-}>;
-
-$query;
-export function getReaction(): Reaction {
- return {
- Fire: null
- };
-}
-
-$query;
-export function printReaction(reaction: Reaction): Reaction {
- console.log(typeof reaction);
- return reaction;
-}
-
-Candid:
-type Emotion = variant { Sad; Indifferent; Happy };
-type Reaction = variant { Emotion : Emotion; Fire; ThumbsUp };
-service : () -> {
- getReaction : () -> (Reaction) query;
- printReaction : (Reaction) -> (Reaction) query;
-}
-
-dfx:
-dfx canister call candid_canister printReaction '(variant { Fire })'
-(variant { Fire })
-
-func
-The Azle type Func
corresponds to the Candid type func. It is a TypeScript Tuple and will become a JavaScript array with two elements at runtime.
The first element is an @dfinity/principal and the second is 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.
A func
acts as a callback, allowing the func
receiver to know which canister instance and method must be used to call back.
TypeScript:
-import { Func, Principal, $query, Query } from 'azle';
-
-type BasicFunc = Func<Query<(param1: string) => string>>;
-
-$query;
-export function getBasicFunc(): BasicFunc {
- return [Principal.fromText('rrkah-fqaaa-aaaaa-aaaaq-cai'), 'getBasicFunc'];
-}
-
-$query;
-export function printBasicFunc(basicFunc: BasicFunc): BasicFunc {
- console.log(typeof basicFunc);
- return basicFunc;
-}
-
-Candid:
-service : () -> {
- getBasicFunc : () -> (func (text) -> (text) query) query;
- printBasicFunc : (func (text) -> (text) query) -> (
- func (text) -> (text) query,
- ) query;
-}
-
-dfx:
-dfx canister call candid_canister printBasicFunc '(func "r7inp-6aaaa-aaaaa-aaabq-cai".getBasicFunc)'
-(func "r7inp-6aaaa-aaaaa-aaabq-cai".getBasicFunc)
-
-service
-JavaScript classes that inherit from the Azle type Service
correspond to the Candid service type and will become child classes capable of creating instances that can perform cross-canister calls at runtime.
TypeScript:
-import {
- CallResult,
- Principal,
- $query,
- Result,
- Service,
- serviceQuery,
- serviceUpdate,
- $update
-} from 'azle';
-
-class SomeService extends Service {
- @serviceQuery
- query1: () => CallResult<boolean>;
-
- @serviceUpdate
- update1: () => CallResult<string>;
-}
-
-$query;
-export function getService(): SomeService {
- return new SomeService(Principal.fromText('aaaaa-aa'));
-}
-
-$update;
-export async function callService(
- service: SomeService
-): Promise<Result<string, string>> {
- return await service.update1().call();
-}
-
-Candid:
-type ManualReply = variant { Ok : text; Err : text };
-service : () -> {
- callService : (
- service { query1 : () -> (bool) query; update1 : () -> (text) },
- ) -> (ManualReply);
- getService : () -> (
- service { query1 : () -> (bool) query; update1 : () -> (text) },
- ) query;
-}
-
-dfx:
-dfx canister call candid_canister getService
-(service "aaaaa-aa")
-
-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';
-
-$query;
-export function getPrincipal(): Principal {
- return Principal.fromText('rrkah-fqaaa-aaaaa-aaaaq-cai');
-}
-
-$query;
-export function printPrincipal(principal: Principal): Principal {
- console.log(typeof principal);
- return principal;
-}
-
-Candid:
-service : () -> {
- getPrincipal : () -> (principal) query;
- printPrincipal : (principal) -> (principal) query;
-}
-
-dfx:
-dfx canister call candid_canister printPrincipal '(principal "rrkah-fqaaa-aaaaa-aaaaq-cai")'
-(principal "rrkah-fqaaa-aaaaa-aaaaq-cai")
-
-reserved
-The Azle type reserved
corresponds to the Candid type reserved, is the TypeScript type any
, and will become a JavaScript null at runtime.
TypeScript:
-import { $query, reserved } from 'azle';
-
-$query;
-export function getReserved(): reserved {
- return 'anything';
-}
-
-$query;
-export function printReserved(reserved: reserved): reserved {
- console.log(typeof reserved);
- return reserved;
-}
-
-Candid:
-service : () -> {
- getReserved : () -> (reserved) query;
- printReserved : (reserved) -> (reserved) query;
-}
-
-dfx:
-dfx canister call candid_canister printReserved '(null)'
-(null : reserved)
-
-empty
-The Azle type empty
corresponds to the Candid type empty and has no JavaScript value at runtime.
TypeScript:
-import { empty, $query } from 'azle';
-
-$query;
-export function getEmpty(): 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.
-$query;
-export function printEmpty(empty: empty): empty {
- console.log(typeof empty);
- throw 'Anything you want';
-}
-
-Candid:
-service : () -> {
- getEmpty : () -> (empty) query;
- printEmpty : (empty) -> (empty) query;
-}
-
-dfx:
-dfx canister call candid_canister printEmpty '("You can put anything here")'
-Error: Failed to create argument blob.
-Caused by: Failed to create argument blob.
- Invalid data: Unable to serialize Candid values: type mismatch: "You can put anything here" cannot be of type empty
Caniste
-
+
@@ -172,7 +172,7 @@ Caniste
-
+
diff --git a/the_azle_book/book/cross_canister.html b/the_azle_book/book/cross_canister.html
index d3d542bea0..b419dc5360 100644
--- a/the_azle_book/book/cross_canister.html
+++ b/the_azle_book/book/cross_canister.html
@@ -83,7 +83,7 @@
@@ -166,256 +166,197 @@ Cross-canister<
whoami
Canisters are generally able to call the query or update methods of other canisters in any subnet. We refer to these types of calls as cross-canister calls.
-A cross-canister call begins with a definition of the canister to be called, referred to as a service.
-Imagine a simple service called token_canister
:
-import { ic, match, nat64, Principal, StableBTreeMap, $update } from 'azle';
+A cross-canister call begins with a definition of the canister to be called.
+Imagine a simple canister called token_canister
:
+import {
+ Canister,
+ ic,
+ nat64,
+ Opt,
+ Principal,
+ StableBTreeMap,
+ update
+} from 'azle';
-let accounts = new StableBTreeMap<Principal, nat64>(0, 38, 15);
+let accounts = StableBTreeMap(Principal, nat64, 0);
-$update;
-export function transfer(to: Principal, amount: nat64): nat64 {
- const from = ic.caller();
+export default Canister({
+ transfer: update([Principal, nat64], nat64, (to, amount) => {
+ const from = ic.caller();
- const fromBalance = match(accounts.get(from), {
- Some: (some) => some,
- None: () => 0n
- });
- const toBalance = match(accounts.get(to), {
- Some: (some) => some,
- None: () => 0n
- });
+ const fromBalance = getBalance(accounts.get(from));
+ const toBalance = getBalance(accounts.get(to));
- accounts.insert(from, fromBalance - amount);
- accounts.insert(to, toBalance + amount);
+ accounts.insert(from, fromBalance - amount);
+ accounts.insert(to, toBalance + amount);
- return amount;
-}
-
-Here's how you would create its service definition:
-import { CallResult, Principal, nat64, Service, serviceUpdate } from 'azle';
+ return amount;
+ })
+});
-class TokenCanister extends Service {
- @serviceUpdate
- transfer: (to: Principal, amount: nat64) => CallResult<nat64>;
+function getBalance(accountOpt: Opt<nat64>): nat64 {
+ if ('None' in accountOpt) {
+ return 0n;
+ } else {
+ return accountOpt.Some;
+ }
}
-Once you have a service definition you can instantiate it with the canister's Principal
and then invoke its methods.
-Here's how to instantiate TokenCanister
:
-const tokenCanister = new TokenCanister(
+Now that you have the canister definition, you can import and instantiate it in another canister:
+import { Canister, ic, nat64, Principal, update } from 'azle';
+import TokenCanister from './token_canister';
+
+const tokenCanister = TokenCanister(
Principal.fromText('r7inp-6aaaa-aaaaa-aaabq-cai')
);
+
+export default Canister({
+ payout: update([Principal, nat64], nat64, async (to, amount) => {
+ return await ic.call(tokenCanister.transfer, {
+ args: [to, amount]
+ });
+ })
+});
-And here's a more complete example of a canister called payout_canister
that performs a cross-canister call to token_canister
:
-import {
- CallResult,
- nat64,
- Principal,
- Result,
- Service,
- serviceUpdate,
- $update
-} from 'azle';
+If you don't have the actual definition of the token canister with the canister method implementations, you can always create your own canister definition without method implementations:
+import { Canister, ic, nat64, Principal, update } from 'azle';
-class TokenCanister extends Service {
- @serviceUpdate
- transfer: (to: Principal, amount: nat64) => CallResult<nat64>;
-}
+const TokenCanister = Canister({
+ transfer: update([Principal, nat64], nat64)
+});
-const tokenCanister = new TokenCanister(
+const tokenCanister = TokenCanister(
Principal.fromText('r7inp-6aaaa-aaaaa-aaabq-cai')
);
-$update;
-export async function payout(
- to: Principal,
- amount: nat64
-): Promise<Result<nat64, string>> {
- return await tokenCanister.transfer(to, amount).call();
-}
+export default Canister({
+ payout: update([Principal, nat64], nat64, async (to, amount) => {
+ return await ic.call(tokenCanister.transfer, {
+ args: [to, amount]
+ });
+ })
+});
-Notice that the tokenCanister.transfer
method, because it is a cross-canister method, returns a CallResult
. All cross-canister calls return CallResult
, which has an Ok
or Err
property depending on if the cross-canister call was successful or not.
-The IC guarantees that cross-canister calls will return. This means that, generally speaking, you will always receive a CallResult
. Azle does not throw on cross-canister calls. Wrapping your cross-canister call in a try...catch
most likely won't do anything useful.
-Let's add to our example code and explore adding some practical result-based error-handling to stop people from stealing tokens.
+The IC guarantees that cross-canister calls will return. This means that, generally speaking, you will always receive a response from ic.call
. If there are errors during the call, ic.call
will throw. Wrapping your cross-canister call in a try...catch
allows you to handle these errors.
+Let's add to our example code and explore adding some practical error-handling to stop people from stealing tokens.
token_canister
:
import {
+ Canister,
ic,
- match,
nat64,
+ Opt,
Principal,
- Result,
StableBTreeMap,
- $update,
- Variant
+ update
} from 'azle';
-let accounts = new StableBTreeMap<Principal, nat64>(0, 38, 15);
-
-$update;
-export function transfer(
- to: Principal,
- amount: nat64
-): Variant<
- Result<
- nat64,
- Variant<{
- InsufficientBalance: nat64;
- }>
- >
-> {
- const from = ic.caller();
-
- const fromBalance = match(accounts.get(from), {
- Some: (some) => some,
- None: () => 0n
- });
-
- if (fromBalance < amount) {
- return {
- Err: {
- InsufficientBalance: fromBalance
- }
- };
- }
+let accounts = StableBTreeMap(Principal, nat64, 0);
+
+export default Canister({
+ transfer: update([Principal, nat64], nat64, (to, amount) => {
+ const from = ic.caller();
+
+ const fromBalance = getBalance(accounts.get(from));
- const toBalance = match(accounts.get(to), {
- Some: (some) => some,
- None: () => 0n
- });
+ if (amount > fromBalance) {
+ throw new Error(`${from} has an insufficient balance`);
+ }
- accounts.insert(from, fromBalance - amount);
- accounts.insert(to, toBalance + amount);
+ const toBalance = getBalance(accounts.get(to));
- return {
- Ok: amount
- };
+ accounts.insert(from, fromBalance - amount);
+ accounts.insert(to, toBalance + amount);
+
+ return amount;
+ })
+});
+
+function getBalance(accountOpt: Opt<nat64>): nat64 {
+ if ('None' in accountOpt) {
+ return 0n;
+ } else {
+ return accountOpt.Some;
+ }
}
payout_canister
:
-import {
- CallResult,
- match,
- nat64,
- Principal,
- Result,
- Service,
- serviceUpdate,
- $update,
- Variant
-} from 'azle';
-
-class TokenCanister extends Service {
- @serviceUpdate
- transfer: (
- to: Principal,
- amount: nat64
- ) => CallResult<
- Result<
- nat64,
- Variant<{
- InsufficientBalance: nat64;
- }>
- >
- >;
-}
+import { Canister, ic, nat64, Principal, update } from 'azle';
+import TokenCanister from './index';
-const tokenCanister = new TokenCanister(
- Principal.fromText('r7inp-6aaaa-aaaaa-aaabq-cai')
+const tokenCanister = TokenCanister(
+ Principal.fromText('bkyz2-fmaaa-aaaaa-qaaaq-cai')
);
-$update;
-export async function payout(
- to: Principal,
- amount: nat64
-): Promise<Result<nat64, string>> {
- const callResult = await tokenCanister.transfer(to, amount).call();
-
- return match(callResult, {
- Ok: (transferResult) =>
- match(transferResult, {
- Ok: (ok) => ({ Ok: ok }),
- Err: (err) => ({ Err: JSON.stringify(err) })
- }),
- Err: (err) => ({ Err: err })
- });
-}
+export default Canister({
+ payout: update([Principal, nat64], nat64, async (to, amount) => {
+ try {
+ return await ic.call(tokenCanister.transfer, {
+ args: [to, amount]
+ });
+ } catch (error) {
+ console.log(error);
+ }
+
+ return 0n;
+ })
+});
-Azle provides a match
function that will help you handle variant branches. This provides some benefits over using in
, such as if ('Err' in result)
or if ('Ok' in result)
. There are other ways to check for the Ok
or Err
properties as well, feel free to experiment with the way that you prefer. They all have trade-offs.
+Throwing will allow you to express error conditions and halt execution, but you may find embracing the Result
variant as a better solution for error handling because of its composability and predictability.
So far we have only shown a cross-canister call from an update method. Update methods can call other update methods or query methods (but not composite query methods as discussed below). If an update method calls a query method, that query method will be called in replicated mode. Replicated mode engages the consensus process, but for queries the state will still be discarded.
Cross-canister calls can also be initiated from query methods. These are known as composite queries, and in Azle they are simply async
query methods. Composite queries can call other composite query methods and regular query methods. Composite queries cannot call update methods.
Here's an example of a composite query method:
-import {
- CallResult,
- Principal,
- $query,
- Result,
- Service,
- serviceQuery
-} from 'azle';
+import { bool, Canister, ic, Principal, query } from 'azle';
-class SomeCanister extends Service {
- @serviceQuery
- queryForBoolean: () => CallResult<boolean>;
-}
+const SomeCanister = Canister({
+ queryForBoolean: query([], bool)
+});
-const someCanister = new SomeCanister(
+const someCanister = SomeCanister(
Principal.fromText('ryjl3-tyaaa-aaaaa-aaaba-cai')
);
-$query;
-export async function querySomeCanister(): Promise<Result<boolean, string>> {
- return await someCanister.queryForBoolean().call();
-}
+export default Canister({
+ querySomeCanister: query([], bool, async () => {
+ return await ic.call(someCanister.queryForBoolean);
+ })
+});
-You can expect cross-canister calls within the same subnet to take up to a few seconds to complete, and cross-canister calls across subnets take about double that time.
+You can expect cross-canister calls within the same subnet to take up to a few seconds to complete, and cross-canister calls across subnets take about double that time. Composite queries should be much faster, similar to query calls in latency.
If you don't need to wait for your cross-canister call to return, you can use notify
:
-import {
- CallResult,
- Principal,
- RejectionCode,
- Result,
- Service,
- serviceUpdate,
- $update
-} from 'azle';
+import { Canister, ic, Principal, update, Void } from 'azle';
-class SomeCanister extends Service {
- @serviceUpdate
- receiveNotification: () => CallResult<void>;
-}
+const SomeCanister = Canister({
+ receiveNotification: update([], Void)
+});
-const someCanister = new SomeCanister(
+const someCanister = SomeCanister(
Principal.fromText('ryjl3-tyaaa-aaaaa-aaaba-cai')
);
-$update;
-export function sendNotification(): Result<null, RejectionCode> {
- return someCanister.receiveNotification().notify();
-}
+export default Canister({
+ sendNotification: update([], Void, () => {
+ return ic.notify(someCanister.receiveNotification);
+ })
+});
-If you need to send cycles with your cross-canister call, you can call cycles
before calling call
or notify
:
-import {
- CallResult,
- Principal,
- RejectionCode,
- Result,
- Service,
- serviceUpdate,
- $update
-} from 'azle';
+If you need to send cycles with your cross-canister call, you can add cycles
to the config
object of ic.notify
:
+import { Canister, ic, Principal, update, Void } from 'azle';
-class SomeCanister extends Service {
- @serviceUpdate
- receiveNotification: () => CallResult<void>;
-}
+const SomeCanister = Canister({
+ receiveNotification: update([], Void)
+});
-const someCanister = new SomeCanister(
+const someCanister = SomeCanister(
Principal.fromText('ryjl3-tyaaa-aaaaa-aaaba-cai')
);
-$update;
-export function sendNotification(): Result<null, RejectionCode> {
- return someCanister.receiveNotification().cycles(1_000_000n).notify();
-}
+export default Canister({
+ sendNotification: update([], Void, () => {
+ return ic.notify(someCanister.receiveNotification, {
+ cycles: 1_000_000n
+ });
+ })
+});
diff --git a/the_azle_book/book/cycles.html b/the_azle_book/book/cycles.html
index 30109de2ee..f865726fab 100644
--- a/the_azle_book/book/cycles.html
+++ b/the_azle_book/book/cycles.html
@@ -83,7 +83,7 @@
diff --git a/the_azle_book/book/deployment.html b/the_azle_book/book/deployment.html
index 4eaf38ce80..31d7ae5042 100644
--- a/the_azle_book/book/deployment.html
+++ b/the_azle_book/book/deployment.html
@@ -83,7 +83,7 @@
@@ -236,6 +236,13 @@ Dep
To deploy an individual canister:
dfx deploy --network ic canister_name
+Common deployment issues
+If you run into an error during deployment, try the following:
+
+- Add the
--verbose
flag to the build
command in your dfx.json
file like so: "build": "npx azle build hello_world --verbose
+- Ensure that you have followed the instructions correctly in the installation chapter, especially noting the build dependencies
+- Reach out on the Discord channel
+
diff --git a/the_azle_book/book/examples.html b/the_azle_book/book/examples.html
index 99d4e25f8c..b058129049 100644
--- a/the_azle_book/book/examples.html
+++ b/the_azle_book/book/examples.html
@@ -83,7 +83,7 @@
@@ -174,7 +174,7 @@ Examples
-
+
@@ -188,7 +188,7 @@ Examples
-
+
diff --git a/the_azle_book/book/http.html b/the_azle_book/book/http.html
index 77a440f3e5..ee6b2d68d5 100644
--- a/the_azle_book/book/http.html
+++ b/the_azle_book/book/http.html
@@ -83,7 +83,7 @@
@@ -153,64 +153,71 @@
import {
blob,
+ bool,
+ Canister,
Func,
nat16,
+ None,
Opt,
- $query,
- Query,
+ query,
Record,
+ text,
Tuple,
Variant,
Vec
} from 'azle';
-type HttpRequest = Record<{
- method: string;
- url: string;
- headers: Vec<Header>;
- body: blob;
-}>;
-
-type HttpResponse = Record<{
- status_code: nat16;
- headers: Vec<Header>;
- body: blob;
- streaming_strategy: Opt<StreamingStrategy>;
- upgrade: Opt<boolean>;
-}>;
-
-type Header = Tuple<[string, string]>;
-
-type StreamingStrategy = Variant<{
- Callback: CallbackStrategy;
-}>;
-
-type CallbackStrategy = Record<{
- callback: Callback;
- token: Token;
-}>;
-
-type Callback = Func<Query<(t: Token) => StreamingCallbackHttpResponse>>;
-
-type StreamingCallbackHttpResponse = Record<{
- body: blob;
- token: Opt<Token>;
-}>;
-
-type Token = Record<{
- arbitrary_data: string;
-}>;
-
-$query;
-export function http_request(req: HttpRequest): HttpResponse {
- return {
- status_code: 200,
- headers: [],
- body: Uint8Array.from([]),
- streaming_strategy: Opt.None,
- upgrade: Opt.Some(false)
- };
-}
+const Token = Record({
+ // add whatever fields you'd like
+ arbitrary_data: text
+});
+
+const StreamingCallbackHttpResponse = Record({
+ body: blob,
+ token: Opt(Token)
+});
+
+export const Callback = Func([text], StreamingCallbackHttpResponse, 'query');
+
+const CallbackStrategy = Record({
+ callback: Callback,
+ token: Token
+});
+
+const StreamingStrategy = Variant({
+ Callback: CallbackStrategy
+});
+
+type HeaderField = [text, text];
+const HeaderField = Tuple(text, text);
+
+const HttpResponse = Record({
+ status_code: nat16,
+ headers: Vec(HeaderField),
+ body: blob,
+ streaming_strategy: Opt(StreamingStrategy),
+ upgrade: Opt(bool)
+});
+
+const HttpRequest = Record({
+ method: text,
+ url: text,
+ headers: Vec(HeaderField),
+ body: blob,
+ certificate_version: Opt(nat16)
+});
+
+export default Canister({
+ http_request: query([HttpRequest], HttpResponse, (req) => {
+ return {
+ status_code: 200,
+ headers: [],
+ body: Buffer.from('hello'),
+ streaming_strategy: None,
+ upgrade: None
+ };
+ })
+});
Outgoing HTTP requests
Examples:
@@ -219,118 +226,122 @@ outgoing_http_requests
import {
- Alias,
+ Canister,
ic,
- $init,
- match,
+ init,
nat32,
- $query,
+ Principal,
+ query,
+ Some,
StableBTreeMap,
- $update,
- Opt
+ text,
+ update
} from 'azle';
import {
HttpResponse,
HttpTransformArgs,
managementCanister
} from 'azle/canisters/management';
-import decodeUtf8 from 'decode-utf8';
-import encodeUtf8 from 'encode-utf8';
-
-type JSON = Alias<string>;
-
-let stableStorage = new StableBTreeMap<string, string>(0, 25, 1_000);
-
-$init;
-export function init(ethereumUrl: string): void {
- stableStorage.insert('ethereumUrl', ethereumUrl);
-}
-
-$update;
-export async function ethGetBalance(ethereumAddress: string): Promise<JSON> {
- const httpResult = await managementCanister
- .http_request({
- url: match(stableStorage.get('ethereumUrl'), {
- Some: (url) => url,
- None: () => ''
- }),
- max_response_bytes: Opt.Some(2_000n),
- method: {
- post: null
- },
- headers: [],
- body: Opt.Some(
- new Uint8Array(
- encodeUtf8(
- JSON.stringify({
- jsonrpc: '2.0',
- method: 'eth_getBalance',
- params: [ethereumAddress, 'earliest'],
- id: 1
- })
- )
- )
- ),
- transform: Opt.Some({
- function: [ic.id(), 'ethTransform'],
- context: Uint8Array.from([])
- })
- })
- .cycles(50_000_000n)
- .call();
-
- return match(httpResult, {
- Ok: (httpResponse) => decodeUtf8(Uint8Array.from(httpResponse.body)),
- Err: (err) => ic.trap(err)
- });
-}
-
-$update;
-export async function ethGetBlockByNumber(number: nat32): Promise<JSON> {
- const httpResult = await managementCanister
- .http_request({
- url: match(stableStorage.get('ethereumUrl'), {
- Some: (url) => url,
- None: () => ''
- }),
- max_response_bytes: Opt.Some(2_000n),
- method: {
- post: null
- },
- headers: [],
- body: Opt.Some(
- new Uint8Array(
- encodeUtf8(
- JSON.stringify({
- jsonrpc: '2.0',
- method: 'eth_getBlockByNumber',
- params: [`0x${number.toString(16)}`, false],
- id: 1
- })
- )
- )
- ),
- transform: Opt.Some({
- function: [ic.id(), 'ethTransform'],
- context: Uint8Array.from([])
- })
- })
- .cycles(50_000_000n)
- .call();
-
- return match(httpResult, {
- Ok: (httpResponse) => decodeUtf8(Uint8Array.from(httpResponse.body)),
- Err: (err) => ic.trap(err)
- });
-}
-
-$query;
-export function ethTransform(args: HttpTransformArgs): HttpResponse {
- return {
- ...args.response,
- headers: []
- };
-}
+
+let stableStorage = StableBTreeMap(text, text, 0);
+
+export default Canister({
+ init: init([text], (ethereumUrl) => {
+ stableStorage.insert('ethereumUrl', ethereumUrl);
+ }),
+ ethGetBalance: update([text], text, async (ethereumAddress) => {
+ const urlOpt = stableStorage.get('ethereumUrl');
+
+ if ('None' in urlOpt) {
+ throw new Error('ethereumUrl is not defined');
+ }
+
+ const url = urlOpt.Some;
+
+ const httpResponse = await ic.call(managementCanister.http_request, {
+ args: [
+ {
+ url,
+ max_response_bytes: Some(2_000n),
+ method: {
+ post: null
+ },
+ headers: [],
+ body: Some(
+ Buffer.from(
+ JSON.stringify({
+ jsonrpc: '2.0',
+ method: 'eth_getBalance',
+ params: [ethereumAddress, 'earliest'],
+ id: 1
+ }),
+ 'utf-8'
+ )
+ ),
+ transform: Some({
+ function: [ic.id(), 'ethTransform'] as [
+ Principal,
+ string
+ ],
+ context: Uint8Array.from([])
+ })
+ }
+ ],
+ cycles: 50_000_000n
+ });
+
+ return Buffer.from(httpResponse.body.buffer).toString('utf-8');
+ }),
+ ethGetBlockByNumber: update([nat32], text, async (number) => {
+ const urlOpt = stableStorage.get('ethereumUrl');
+
+ if ('None' in urlOpt) {
+ throw new Error('ethereumUrl is not defined');
+ }
+
+ const url = urlOpt.Some;
+
+ const httpResponse = await ic.call(managementCanister.http_request, {
+ args: [
+ {
+ url,
+ max_response_bytes: Some(2_000n),
+ method: {
+ post: null
+ },
+ headers: [],
+ body: Some(
+ Buffer.from(
+ JSON.stringify({
+ jsonrpc: '2.0',
+ method: 'eth_getBlockByNumber',
+ params: [`0x${number.toString(16)}`, false],
+ id: 1
+ }),
+ 'utf-8'
+ )
+ ),
+ transform: Some({
+ function: [ic.id(), 'ethTransform'] as [
+ Principal,
+ string
+ ],
+ context: Uint8Array.from([])
+ })
+ }
+ ],
+ cycles: 50_000_000n
+ });
+
+ return Buffer.from(httpResponse.body.buffer).toString('utf-8');
+ }),
+ ethTransform: query([HttpTransformArgs], HttpResponse, (args) => {
+ return {
+ ...args.response,
+ headers: []
+ };
+ })
+});
diff --git a/the_azle_book/book/index.html b/the_azle_book/book/index.html
index 3df4ff18a9..843dae0009 100644
--- a/the_azle_book/book/index.html
+++ b/the_azle_book/book/index.html
@@ -83,7 +83,7 @@
diff --git a/the_azle_book/book/installation.html b/the_azle_book/book/installation.html
index 7adec042f9..d20c17bc0b 100644
--- a/the_azle_book/book/installation.html
+++ b/the_azle_book/book/installation.html
@@ -83,7 +83,7 @@
@@ -148,8 +148,9 @@ InstallationFollow the instructions exactly as stated below to avoid issues.
You should be using a *nix environment (Linux, Mac OS, WSL if using Windows) with bash and have the following installed on your system:
-- Node.js 18
-- dfx 0.14.2
+- Node.js 18
+- dfx 0.15.0
+- Build dependencies
Node.js
We highly recommend using nvm to install Node.js (and npm, which is included with Node.js). Run the following commands to install Node.js and npm with nvm:
@@ -158,19 +159,31 @@ Node.js
Now restart your terminal and run the following command:
nvm install 18
-dfx
-Run the following command to install dfx 0.14.2:
-DFX_VERSION=0.14.2 sh -ci "$(curl -fsSL https://sdk.dfinity.org/install.sh)"
+dfx 0.15.0
+Run the following command to install dfx 0.15.0:
+DFX_VERSION=0.15.0 sh -ci "$(curl -fsSL https://sdk.dfinity.org/install.sh)"
If after trying to run dfx
commands you encounter an error such as dfx: command not found
, you might need to add $HOME/bin
to your path. Here's an example of doing this in your .bashrc
:
echo 'export PATH="$PATH:$HOME/bin"' >> "$HOME/.bashrc"
+
+Build dependencies
+You may or may not need these dependencies based on your OS. If you run into errors while deploying, try the following:
+Ubuntu/WSL
+sudo apt install clang
+sudo apt install build-essential
+sudo apt install libssl-dev
+sudo apt install pkg-config
+
+Mac
+# Install the Xcode Command Line Tools
+xcode-select --install
Cross-canister<
whoami
Canisters are generally able to call the query or update methods of other canisters in any subnet. We refer to these types of calls as cross-canister calls.
-A cross-canister call begins with a definition of the canister to be called, referred to as a service.
-Imagine a simple service called token_canister
:
-import { ic, match, nat64, Principal, StableBTreeMap, $update } from 'azle';
+A cross-canister call begins with a definition of the canister to be called.
+Imagine a simple canister called token_canister
:
+import {
+ Canister,
+ ic,
+ nat64,
+ Opt,
+ Principal,
+ StableBTreeMap,
+ update
+} from 'azle';
-let accounts = new StableBTreeMap<Principal, nat64>(0, 38, 15);
+let accounts = StableBTreeMap(Principal, nat64, 0);
-$update;
-export function transfer(to: Principal, amount: nat64): nat64 {
- const from = ic.caller();
+export default Canister({
+ transfer: update([Principal, nat64], nat64, (to, amount) => {
+ const from = ic.caller();
- const fromBalance = match(accounts.get(from), {
- Some: (some) => some,
- None: () => 0n
- });
- const toBalance = match(accounts.get(to), {
- Some: (some) => some,
- None: () => 0n
- });
+ const fromBalance = getBalance(accounts.get(from));
+ const toBalance = getBalance(accounts.get(to));
- accounts.insert(from, fromBalance - amount);
- accounts.insert(to, toBalance + amount);
+ accounts.insert(from, fromBalance - amount);
+ accounts.insert(to, toBalance + amount);
- return amount;
-}
-
-Here's how you would create its service definition:
-import { CallResult, Principal, nat64, Service, serviceUpdate } from 'azle';
+ return amount;
+ })
+});
-class TokenCanister extends Service {
- @serviceUpdate
- transfer: (to: Principal, amount: nat64) => CallResult<nat64>;
+function getBalance(accountOpt: Opt<nat64>): nat64 {
+ if ('None' in accountOpt) {
+ return 0n;
+ } else {
+ return accountOpt.Some;
+ }
}
-Once you have a service definition you can instantiate it with the canister's Principal
and then invoke its methods.
-Here's how to instantiate TokenCanister
:
-const tokenCanister = new TokenCanister(
+Now that you have the canister definition, you can import and instantiate it in another canister:
+import { Canister, ic, nat64, Principal, update } from 'azle';
+import TokenCanister from './token_canister';
+
+const tokenCanister = TokenCanister(
Principal.fromText('r7inp-6aaaa-aaaaa-aaabq-cai')
);
+
+export default Canister({
+ payout: update([Principal, nat64], nat64, async (to, amount) => {
+ return await ic.call(tokenCanister.transfer, {
+ args: [to, amount]
+ });
+ })
+});
-And here's a more complete example of a canister called payout_canister
that performs a cross-canister call to token_canister
:
-import {
- CallResult,
- nat64,
- Principal,
- Result,
- Service,
- serviceUpdate,
- $update
-} from 'azle';
+If you don't have the actual definition of the token canister with the canister method implementations, you can always create your own canister definition without method implementations:
+import { Canister, ic, nat64, Principal, update } from 'azle';
-class TokenCanister extends Service {
- @serviceUpdate
- transfer: (to: Principal, amount: nat64) => CallResult<nat64>;
-}
+const TokenCanister = Canister({
+ transfer: update([Principal, nat64], nat64)
+});
-const tokenCanister = new TokenCanister(
+const tokenCanister = TokenCanister(
Principal.fromText('r7inp-6aaaa-aaaaa-aaabq-cai')
);
-$update;
-export async function payout(
- to: Principal,
- amount: nat64
-): Promise<Result<nat64, string>> {
- return await tokenCanister.transfer(to, amount).call();
-}
+export default Canister({
+ payout: update([Principal, nat64], nat64, async (to, amount) => {
+ return await ic.call(tokenCanister.transfer, {
+ args: [to, amount]
+ });
+ })
+});
-Notice that the tokenCanister.transfer
method, because it is a cross-canister method, returns a CallResult
. All cross-canister calls return CallResult
, which has an Ok
or Err
property depending on if the cross-canister call was successful or not.
-The IC guarantees that cross-canister calls will return. This means that, generally speaking, you will always receive a CallResult
. Azle does not throw on cross-canister calls. Wrapping your cross-canister call in a try...catch
most likely won't do anything useful.
-Let's add to our example code and explore adding some practical result-based error-handling to stop people from stealing tokens.
+The IC guarantees that cross-canister calls will return. This means that, generally speaking, you will always receive a response from ic.call
. If there are errors during the call, ic.call
will throw. Wrapping your cross-canister call in a try...catch
allows you to handle these errors.
+Let's add to our example code and explore adding some practical error-handling to stop people from stealing tokens.
token_canister
:
import {
+ Canister,
ic,
- match,
nat64,
+ Opt,
Principal,
- Result,
StableBTreeMap,
- $update,
- Variant
+ update
} from 'azle';
-let accounts = new StableBTreeMap<Principal, nat64>(0, 38, 15);
-
-$update;
-export function transfer(
- to: Principal,
- amount: nat64
-): Variant<
- Result<
- nat64,
- Variant<{
- InsufficientBalance: nat64;
- }>
- >
-> {
- const from = ic.caller();
-
- const fromBalance = match(accounts.get(from), {
- Some: (some) => some,
- None: () => 0n
- });
-
- if (fromBalance < amount) {
- return {
- Err: {
- InsufficientBalance: fromBalance
- }
- };
- }
+let accounts = StableBTreeMap(Principal, nat64, 0);
+
+export default Canister({
+ transfer: update([Principal, nat64], nat64, (to, amount) => {
+ const from = ic.caller();
+
+ const fromBalance = getBalance(accounts.get(from));
- const toBalance = match(accounts.get(to), {
- Some: (some) => some,
- None: () => 0n
- });
+ if (amount > fromBalance) {
+ throw new Error(`${from} has an insufficient balance`);
+ }
- accounts.insert(from, fromBalance - amount);
- accounts.insert(to, toBalance + amount);
+ const toBalance = getBalance(accounts.get(to));
- return {
- Ok: amount
- };
+ accounts.insert(from, fromBalance - amount);
+ accounts.insert(to, toBalance + amount);
+
+ return amount;
+ })
+});
+
+function getBalance(accountOpt: Opt<nat64>): nat64 {
+ if ('None' in accountOpt) {
+ return 0n;
+ } else {
+ return accountOpt.Some;
+ }
}
payout_canister
:
-import {
- CallResult,
- match,
- nat64,
- Principal,
- Result,
- Service,
- serviceUpdate,
- $update,
- Variant
-} from 'azle';
-
-class TokenCanister extends Service {
- @serviceUpdate
- transfer: (
- to: Principal,
- amount: nat64
- ) => CallResult<
- Result<
- nat64,
- Variant<{
- InsufficientBalance: nat64;
- }>
- >
- >;
-}
+import { Canister, ic, nat64, Principal, update } from 'azle';
+import TokenCanister from './index';
-const tokenCanister = new TokenCanister(
- Principal.fromText('r7inp-6aaaa-aaaaa-aaabq-cai')
+const tokenCanister = TokenCanister(
+ Principal.fromText('bkyz2-fmaaa-aaaaa-qaaaq-cai')
);
-$update;
-export async function payout(
- to: Principal,
- amount: nat64
-): Promise<Result<nat64, string>> {
- const callResult = await tokenCanister.transfer(to, amount).call();
-
- return match(callResult, {
- Ok: (transferResult) =>
- match(transferResult, {
- Ok: (ok) => ({ Ok: ok }),
- Err: (err) => ({ Err: JSON.stringify(err) })
- }),
- Err: (err) => ({ Err: err })
- });
-}
+export default Canister({
+ payout: update([Principal, nat64], nat64, async (to, amount) => {
+ try {
+ return await ic.call(tokenCanister.transfer, {
+ args: [to, amount]
+ });
+ } catch (error) {
+ console.log(error);
+ }
+
+ return 0n;
+ })
+});
-Azle provides a match
function that will help you handle variant branches. This provides some benefits over using in
, such as if ('Err' in result)
or if ('Ok' in result)
. There are other ways to check for the Ok
or Err
properties as well, feel free to experiment with the way that you prefer. They all have trade-offs.
+Throwing will allow you to express error conditions and halt execution, but you may find embracing the Result
variant as a better solution for error handling because of its composability and predictability.
So far we have only shown a cross-canister call from an update method. Update methods can call other update methods or query methods (but not composite query methods as discussed below). If an update method calls a query method, that query method will be called in replicated mode. Replicated mode engages the consensus process, but for queries the state will still be discarded.
Cross-canister calls can also be initiated from query methods. These are known as composite queries, and in Azle they are simply async
query methods. Composite queries can call other composite query methods and regular query methods. Composite queries cannot call update methods.
Here's an example of a composite query method:
-import {
- CallResult,
- Principal,
- $query,
- Result,
- Service,
- serviceQuery
-} from 'azle';
+import { bool, Canister, ic, Principal, query } from 'azle';
-class SomeCanister extends Service {
- @serviceQuery
- queryForBoolean: () => CallResult<boolean>;
-}
+const SomeCanister = Canister({
+ queryForBoolean: query([], bool)
+});
-const someCanister = new SomeCanister(
+const someCanister = SomeCanister(
Principal.fromText('ryjl3-tyaaa-aaaaa-aaaba-cai')
);
-$query;
-export async function querySomeCanister(): Promise<Result<boolean, string>> {
- return await someCanister.queryForBoolean().call();
-}
+export default Canister({
+ querySomeCanister: query([], bool, async () => {
+ return await ic.call(someCanister.queryForBoolean);
+ })
+});
-You can expect cross-canister calls within the same subnet to take up to a few seconds to complete, and cross-canister calls across subnets take about double that time.
+You can expect cross-canister calls within the same subnet to take up to a few seconds to complete, and cross-canister calls across subnets take about double that time. Composite queries should be much faster, similar to query calls in latency.
If you don't need to wait for your cross-canister call to return, you can use notify
:
-import {
- CallResult,
- Principal,
- RejectionCode,
- Result,
- Service,
- serviceUpdate,
- $update
-} from 'azle';
+import { Canister, ic, Principal, update, Void } from 'azle';
-class SomeCanister extends Service {
- @serviceUpdate
- receiveNotification: () => CallResult<void>;
-}
+const SomeCanister = Canister({
+ receiveNotification: update([], Void)
+});
-const someCanister = new SomeCanister(
+const someCanister = SomeCanister(
Principal.fromText('ryjl3-tyaaa-aaaaa-aaaba-cai')
);
-$update;
-export function sendNotification(): Result<null, RejectionCode> {
- return someCanister.receiveNotification().notify();
-}
+export default Canister({
+ sendNotification: update([], Void, () => {
+ return ic.notify(someCanister.receiveNotification);
+ })
+});
-If you need to send cycles with your cross-canister call, you can call cycles
before calling call
or notify
:
-import {
- CallResult,
- Principal,
- RejectionCode,
- Result,
- Service,
- serviceUpdate,
- $update
-} from 'azle';
+If you need to send cycles with your cross-canister call, you can add cycles
to the config
object of ic.notify
:
+import { Canister, ic, Principal, update, Void } from 'azle';
-class SomeCanister extends Service {
- @serviceUpdate
- receiveNotification: () => CallResult<void>;
-}
+const SomeCanister = Canister({
+ receiveNotification: update([], Void)
+});
-const someCanister = new SomeCanister(
+const someCanister = SomeCanister(
Principal.fromText('ryjl3-tyaaa-aaaaa-aaaba-cai')
);
-$update;
-export function sendNotification(): Result<null, RejectionCode> {
- return someCanister.receiveNotification().cycles(1_000_000n).notify();
-}
+export default Canister({
+ sendNotification: update([], Void, () => {
+ return ic.notify(someCanister.receiveNotification, {
+ cycles: 1_000_000n
+ });
+ })
+});
diff --git a/the_azle_book/book/cycles.html b/the_azle_book/book/cycles.html
index 30109de2ee..f865726fab 100644
--- a/the_azle_book/book/cycles.html
+++ b/the_azle_book/book/cycles.html
@@ -83,7 +83,7 @@
diff --git a/the_azle_book/book/deployment.html b/the_azle_book/book/deployment.html
index 4eaf38ce80..31d7ae5042 100644
--- a/the_azle_book/book/deployment.html
+++ b/the_azle_book/book/deployment.html
@@ -83,7 +83,7 @@
@@ -236,6 +236,13 @@ Dep
To deploy an individual canister:
dfx deploy --network ic canister_name
+Common deployment issues
+If you run into an error during deployment, try the following:
+
+- Add the
--verbose
flag to the build
command in your dfx.json
file like so: "build": "npx azle build hello_world --verbose
+- Ensure that you have followed the instructions correctly in the installation chapter, especially noting the build dependencies
+- Reach out on the Discord channel
+
diff --git a/the_azle_book/book/examples.html b/the_azle_book/book/examples.html
index 99d4e25f8c..b058129049 100644
--- a/the_azle_book/book/examples.html
+++ b/the_azle_book/book/examples.html
@@ -83,7 +83,7 @@
@@ -174,7 +174,7 @@ Examples
-
+
@@ -188,7 +188,7 @@ Examples
-
+
diff --git a/the_azle_book/book/http.html b/the_azle_book/book/http.html
index 77a440f3e5..ee6b2d68d5 100644
--- a/the_azle_book/book/http.html
+++ b/the_azle_book/book/http.html
@@ -83,7 +83,7 @@
@@ -153,64 +153,71 @@
import {
blob,
+ bool,
+ Canister,
Func,
nat16,
+ None,
Opt,
- $query,
- Query,
+ query,
Record,
+ text,
Tuple,
Variant,
Vec
} from 'azle';
-type HttpRequest = Record<{
- method: string;
- url: string;
- headers: Vec<Header>;
- body: blob;
-}>;
-
-type HttpResponse = Record<{
- status_code: nat16;
- headers: Vec<Header>;
- body: blob;
- streaming_strategy: Opt<StreamingStrategy>;
- upgrade: Opt<boolean>;
-}>;
-
-type Header = Tuple<[string, string]>;
-
-type StreamingStrategy = Variant<{
- Callback: CallbackStrategy;
-}>;
-
-type CallbackStrategy = Record<{
- callback: Callback;
- token: Token;
-}>;
-
-type Callback = Func<Query<(t: Token) => StreamingCallbackHttpResponse>>;
-
-type StreamingCallbackHttpResponse = Record<{
- body: blob;
- token: Opt<Token>;
-}>;
-
-type Token = Record<{
- arbitrary_data: string;
-}>;
-
-$query;
-export function http_request(req: HttpRequest): HttpResponse {
- return {
- status_code: 200,
- headers: [],
- body: Uint8Array.from([]),
- streaming_strategy: Opt.None,
- upgrade: Opt.Some(false)
- };
-}
+const Token = Record({
+ // add whatever fields you'd like
+ arbitrary_data: text
+});
+
+const StreamingCallbackHttpResponse = Record({
+ body: blob,
+ token: Opt(Token)
+});
+
+export const Callback = Func([text], StreamingCallbackHttpResponse, 'query');
+
+const CallbackStrategy = Record({
+ callback: Callback,
+ token: Token
+});
+
+const StreamingStrategy = Variant({
+ Callback: CallbackStrategy
+});
+
+type HeaderField = [text, text];
+const HeaderField = Tuple(text, text);
+
+const HttpResponse = Record({
+ status_code: nat16,
+ headers: Vec(HeaderField),
+ body: blob,
+ streaming_strategy: Opt(StreamingStrategy),
+ upgrade: Opt(bool)
+});
+
+const HttpRequest = Record({
+ method: text,
+ url: text,
+ headers: Vec(HeaderField),
+ body: blob,
+ certificate_version: Opt(nat16)
+});
+
+export default Canister({
+ http_request: query([HttpRequest], HttpResponse, (req) => {
+ return {
+ status_code: 200,
+ headers: [],
+ body: Buffer.from('hello'),
+ streaming_strategy: None,
+ upgrade: None
+ };
+ })
+});
Outgoing HTTP requests
Examples:
@@ -219,118 +226,122 @@ outgoing_http_requests
import {
- Alias,
+ Canister,
ic,
- $init,
- match,
+ init,
nat32,
- $query,
+ Principal,
+ query,
+ Some,
StableBTreeMap,
- $update,
- Opt
+ text,
+ update
} from 'azle';
import {
HttpResponse,
HttpTransformArgs,
managementCanister
} from 'azle/canisters/management';
-import decodeUtf8 from 'decode-utf8';
-import encodeUtf8 from 'encode-utf8';
-
-type JSON = Alias<string>;
-
-let stableStorage = new StableBTreeMap<string, string>(0, 25, 1_000);
-
-$init;
-export function init(ethereumUrl: string): void {
- stableStorage.insert('ethereumUrl', ethereumUrl);
-}
-
-$update;
-export async function ethGetBalance(ethereumAddress: string): Promise<JSON> {
- const httpResult = await managementCanister
- .http_request({
- url: match(stableStorage.get('ethereumUrl'), {
- Some: (url) => url,
- None: () => ''
- }),
- max_response_bytes: Opt.Some(2_000n),
- method: {
- post: null
- },
- headers: [],
- body: Opt.Some(
- new Uint8Array(
- encodeUtf8(
- JSON.stringify({
- jsonrpc: '2.0',
- method: 'eth_getBalance',
- params: [ethereumAddress, 'earliest'],
- id: 1
- })
- )
- )
- ),
- transform: Opt.Some({
- function: [ic.id(), 'ethTransform'],
- context: Uint8Array.from([])
- })
- })
- .cycles(50_000_000n)
- .call();
-
- return match(httpResult, {
- Ok: (httpResponse) => decodeUtf8(Uint8Array.from(httpResponse.body)),
- Err: (err) => ic.trap(err)
- });
-}
-
-$update;
-export async function ethGetBlockByNumber(number: nat32): Promise<JSON> {
- const httpResult = await managementCanister
- .http_request({
- url: match(stableStorage.get('ethereumUrl'), {
- Some: (url) => url,
- None: () => ''
- }),
- max_response_bytes: Opt.Some(2_000n),
- method: {
- post: null
- },
- headers: [],
- body: Opt.Some(
- new Uint8Array(
- encodeUtf8(
- JSON.stringify({
- jsonrpc: '2.0',
- method: 'eth_getBlockByNumber',
- params: [`0x${number.toString(16)}`, false],
- id: 1
- })
- )
- )
- ),
- transform: Opt.Some({
- function: [ic.id(), 'ethTransform'],
- context: Uint8Array.from([])
- })
- })
- .cycles(50_000_000n)
- .call();
-
- return match(httpResult, {
- Ok: (httpResponse) => decodeUtf8(Uint8Array.from(httpResponse.body)),
- Err: (err) => ic.trap(err)
- });
-}
-
-$query;
-export function ethTransform(args: HttpTransformArgs): HttpResponse {
- return {
- ...args.response,
- headers: []
- };
-}
+
+let stableStorage = StableBTreeMap(text, text, 0);
+
+export default Canister({
+ init: init([text], (ethereumUrl) => {
+ stableStorage.insert('ethereumUrl', ethereumUrl);
+ }),
+ ethGetBalance: update([text], text, async (ethereumAddress) => {
+ const urlOpt = stableStorage.get('ethereumUrl');
+
+ if ('None' in urlOpt) {
+ throw new Error('ethereumUrl is not defined');
+ }
+
+ const url = urlOpt.Some;
+
+ const httpResponse = await ic.call(managementCanister.http_request, {
+ args: [
+ {
+ url,
+ max_response_bytes: Some(2_000n),
+ method: {
+ post: null
+ },
+ headers: [],
+ body: Some(
+ Buffer.from(
+ JSON.stringify({
+ jsonrpc: '2.0',
+ method: 'eth_getBalance',
+ params: [ethereumAddress, 'earliest'],
+ id: 1
+ }),
+ 'utf-8'
+ )
+ ),
+ transform: Some({
+ function: [ic.id(), 'ethTransform'] as [
+ Principal,
+ string
+ ],
+ context: Uint8Array.from([])
+ })
+ }
+ ],
+ cycles: 50_000_000n
+ });
+
+ return Buffer.from(httpResponse.body.buffer).toString('utf-8');
+ }),
+ ethGetBlockByNumber: update([nat32], text, async (number) => {
+ const urlOpt = stableStorage.get('ethereumUrl');
+
+ if ('None' in urlOpt) {
+ throw new Error('ethereumUrl is not defined');
+ }
+
+ const url = urlOpt.Some;
+
+ const httpResponse = await ic.call(managementCanister.http_request, {
+ args: [
+ {
+ url,
+ max_response_bytes: Some(2_000n),
+ method: {
+ post: null
+ },
+ headers: [],
+ body: Some(
+ Buffer.from(
+ JSON.stringify({
+ jsonrpc: '2.0',
+ method: 'eth_getBlockByNumber',
+ params: [`0x${number.toString(16)}`, false],
+ id: 1
+ }),
+ 'utf-8'
+ )
+ ),
+ transform: Some({
+ function: [ic.id(), 'ethTransform'] as [
+ Principal,
+ string
+ ],
+ context: Uint8Array.from([])
+ })
+ }
+ ],
+ cycles: 50_000_000n
+ });
+
+ return Buffer.from(httpResponse.body.buffer).toString('utf-8');
+ }),
+ ethTransform: query([HttpTransformArgs], HttpResponse, (args) => {
+ return {
+ ...args.response,
+ headers: []
+ };
+ })
+});
diff --git a/the_azle_book/book/index.html b/the_azle_book/book/index.html
index 3df4ff18a9..843dae0009 100644
--- a/the_azle_book/book/index.html
+++ b/the_azle_book/book/index.html
@@ -83,7 +83,7 @@
diff --git a/the_azle_book/book/installation.html b/the_azle_book/book/installation.html
index 7adec042f9..d20c17bc0b 100644
--- a/the_azle_book/book/installation.html
+++ b/the_azle_book/book/installation.html
@@ -83,7 +83,7 @@
@@ -148,8 +148,9 @@ InstallationFollow the instructions exactly as stated below to avoid issues.
You should be using a *nix environment (Linux, Mac OS, WSL if using Windows) with bash and have the following installed on your system:
-- Node.js 18
-- dfx 0.14.2
+- Node.js 18
+- dfx 0.15.0
+- Build dependencies
Node.js
We highly recommend using nvm to install Node.js (and npm, which is included with Node.js). Run the following commands to install Node.js and npm with nvm:
@@ -158,19 +159,31 @@ Node.js
Now restart your terminal and run the following command:
nvm install 18
-dfx
-Run the following command to install dfx 0.14.2:
-DFX_VERSION=0.14.2 sh -ci "$(curl -fsSL https://sdk.dfinity.org/install.sh)"
+dfx 0.15.0
+Run the following command to install dfx 0.15.0:
+DFX_VERSION=0.15.0 sh -ci "$(curl -fsSL https://sdk.dfinity.org/install.sh)"
If after trying to run dfx
commands you encounter an error such as dfx: command not found
, you might need to add $HOME/bin
to your path. Here's an example of doing this in your .bashrc
:
echo 'export PATH="$PATH:$HOME/bin"' >> "$HOME/.bashrc"
+
+Build dependencies
+You may or may not need these dependencies based on your OS. If you run into errors while deploying, try the following:
+Ubuntu/WSL
+sudo apt install clang
+sudo apt install build-essential
+sudo apt install libssl-dev
+sudo apt install pkg-config
+
+Mac
+# Install the Xcode Command Line Tools
+xcode-select --install
token_canister
:import { ic, match, nat64, Principal, StableBTreeMap, $update } from 'azle';
+A cross-canister call begins with a definition of the canister to be called.
+Imagine a simple canister called token_canister
:
+import {
+ Canister,
+ ic,
+ nat64,
+ Opt,
+ Principal,
+ StableBTreeMap,
+ update
+} from 'azle';
-let accounts = new StableBTreeMap<Principal, nat64>(0, 38, 15);
+let accounts = StableBTreeMap(Principal, nat64, 0);
-$update;
-export function transfer(to: Principal, amount: nat64): nat64 {
- const from = ic.caller();
+export default Canister({
+ transfer: update([Principal, nat64], nat64, (to, amount) => {
+ const from = ic.caller();
- const fromBalance = match(accounts.get(from), {
- Some: (some) => some,
- None: () => 0n
- });
- const toBalance = match(accounts.get(to), {
- Some: (some) => some,
- None: () => 0n
- });
+ const fromBalance = getBalance(accounts.get(from));
+ const toBalance = getBalance(accounts.get(to));
- accounts.insert(from, fromBalance - amount);
- accounts.insert(to, toBalance + amount);
+ accounts.insert(from, fromBalance - amount);
+ accounts.insert(to, toBalance + amount);
- return amount;
-}
-
-Here's how you would create its service definition:
-import { CallResult, Principal, nat64, Service, serviceUpdate } from 'azle';
+ return amount;
+ })
+});
-class TokenCanister extends Service {
- @serviceUpdate
- transfer: (to: Principal, amount: nat64) => CallResult<nat64>;
+function getBalance(accountOpt: Opt<nat64>): nat64 {
+ if ('None' in accountOpt) {
+ return 0n;
+ } else {
+ return accountOpt.Some;
+ }
}
-Once you have a service definition you can instantiate it with the canister's Principal
and then invoke its methods.
-Here's how to instantiate TokenCanister
:
-const tokenCanister = new TokenCanister(
+Now that you have the canister definition, you can import and instantiate it in another canister:
+import { Canister, ic, nat64, Principal, update } from 'azle';
+import TokenCanister from './token_canister';
+
+const tokenCanister = TokenCanister(
Principal.fromText('r7inp-6aaaa-aaaaa-aaabq-cai')
);
+
+export default Canister({
+ payout: update([Principal, nat64], nat64, async (to, amount) => {
+ return await ic.call(tokenCanister.transfer, {
+ args: [to, amount]
+ });
+ })
+});
-And here's a more complete example of a canister called payout_canister
that performs a cross-canister call to token_canister
:
-import {
- CallResult,
- nat64,
- Principal,
- Result,
- Service,
- serviceUpdate,
- $update
-} from 'azle';
+If you don't have the actual definition of the token canister with the canister method implementations, you can always create your own canister definition without method implementations:
+import { Canister, ic, nat64, Principal, update } from 'azle';
-class TokenCanister extends Service {
- @serviceUpdate
- transfer: (to: Principal, amount: nat64) => CallResult<nat64>;
-}
+const TokenCanister = Canister({
+ transfer: update([Principal, nat64], nat64)
+});
-const tokenCanister = new TokenCanister(
+const tokenCanister = TokenCanister(
Principal.fromText('r7inp-6aaaa-aaaaa-aaabq-cai')
);
-$update;
-export async function payout(
- to: Principal,
- amount: nat64
-): Promise<Result<nat64, string>> {
- return await tokenCanister.transfer(to, amount).call();
-}
+export default Canister({
+ payout: update([Principal, nat64], nat64, async (to, amount) => {
+ return await ic.call(tokenCanister.transfer, {
+ args: [to, amount]
+ });
+ })
+});
-Notice that the tokenCanister.transfer
method, because it is a cross-canister method, returns a CallResult
. All cross-canister calls return CallResult
, which has an Ok
or Err
property depending on if the cross-canister call was successful or not.
-The IC guarantees that cross-canister calls will return. This means that, generally speaking, you will always receive a CallResult
. Azle does not throw on cross-canister calls. Wrapping your cross-canister call in a try...catch
most likely won't do anything useful.
-Let's add to our example code and explore adding some practical result-based error-handling to stop people from stealing tokens.
+The IC guarantees that cross-canister calls will return. This means that, generally speaking, you will always receive a response from ic.call
. If there are errors during the call, ic.call
will throw. Wrapping your cross-canister call in a try...catch
allows you to handle these errors.
+Let's add to our example code and explore adding some practical error-handling to stop people from stealing tokens.
token_canister
:
import {
+ Canister,
ic,
- match,
nat64,
+ Opt,
Principal,
- Result,
StableBTreeMap,
- $update,
- Variant
+ update
} from 'azle';
-let accounts = new StableBTreeMap<Principal, nat64>(0, 38, 15);
-
-$update;
-export function transfer(
- to: Principal,
- amount: nat64
-): Variant<
- Result<
- nat64,
- Variant<{
- InsufficientBalance: nat64;
- }>
- >
-> {
- const from = ic.caller();
-
- const fromBalance = match(accounts.get(from), {
- Some: (some) => some,
- None: () => 0n
- });
-
- if (fromBalance < amount) {
- return {
- Err: {
- InsufficientBalance: fromBalance
- }
- };
- }
+let accounts = StableBTreeMap(Principal, nat64, 0);
+
+export default Canister({
+ transfer: update([Principal, nat64], nat64, (to, amount) => {
+ const from = ic.caller();
+
+ const fromBalance = getBalance(accounts.get(from));
- const toBalance = match(accounts.get(to), {
- Some: (some) => some,
- None: () => 0n
- });
+ if (amount > fromBalance) {
+ throw new Error(`${from} has an insufficient balance`);
+ }
- accounts.insert(from, fromBalance - amount);
- accounts.insert(to, toBalance + amount);
+ const toBalance = getBalance(accounts.get(to));
- return {
- Ok: amount
- };
+ accounts.insert(from, fromBalance - amount);
+ accounts.insert(to, toBalance + amount);
+
+ return amount;
+ })
+});
+
+function getBalance(accountOpt: Opt<nat64>): nat64 {
+ if ('None' in accountOpt) {
+ return 0n;
+ } else {
+ return accountOpt.Some;
+ }
}
payout_canister
:
-import {
- CallResult,
- match,
- nat64,
- Principal,
- Result,
- Service,
- serviceUpdate,
- $update,
- Variant
-} from 'azle';
-
-class TokenCanister extends Service {
- @serviceUpdate
- transfer: (
- to: Principal,
- amount: nat64
- ) => CallResult<
- Result<
- nat64,
- Variant<{
- InsufficientBalance: nat64;
- }>
- >
- >;
-}
+import { Canister, ic, nat64, Principal, update } from 'azle';
+import TokenCanister from './index';
-const tokenCanister = new TokenCanister(
- Principal.fromText('r7inp-6aaaa-aaaaa-aaabq-cai')
+const tokenCanister = TokenCanister(
+ Principal.fromText('bkyz2-fmaaa-aaaaa-qaaaq-cai')
);
-$update;
-export async function payout(
- to: Principal,
- amount: nat64
-): Promise<Result<nat64, string>> {
- const callResult = await tokenCanister.transfer(to, amount).call();
-
- return match(callResult, {
- Ok: (transferResult) =>
- match(transferResult, {
- Ok: (ok) => ({ Ok: ok }),
- Err: (err) => ({ Err: JSON.stringify(err) })
- }),
- Err: (err) => ({ Err: err })
- });
-}
+export default Canister({
+ payout: update([Principal, nat64], nat64, async (to, amount) => {
+ try {
+ return await ic.call(tokenCanister.transfer, {
+ args: [to, amount]
+ });
+ } catch (error) {
+ console.log(error);
+ }
+
+ return 0n;
+ })
+});
-Azle provides a match
function that will help you handle variant branches. This provides some benefits over using in
, such as if ('Err' in result)
or if ('Ok' in result)
. There are other ways to check for the Ok
or Err
properties as well, feel free to experiment with the way that you prefer. They all have trade-offs.
+Throwing will allow you to express error conditions and halt execution, but you may find embracing the Result
variant as a better solution for error handling because of its composability and predictability.
So far we have only shown a cross-canister call from an update method. Update methods can call other update methods or query methods (but not composite query methods as discussed below). If an update method calls a query method, that query method will be called in replicated mode. Replicated mode engages the consensus process, but for queries the state will still be discarded.
Cross-canister calls can also be initiated from query methods. These are known as composite queries, and in Azle they are simply async
query methods. Composite queries can call other composite query methods and regular query methods. Composite queries cannot call update methods.
Here's an example of a composite query method:
-import {
- CallResult,
- Principal,
- $query,
- Result,
- Service,
- serviceQuery
-} from 'azle';
+import { bool, Canister, ic, Principal, query } from 'azle';
-class SomeCanister extends Service {
- @serviceQuery
- queryForBoolean: () => CallResult<boolean>;
-}
+const SomeCanister = Canister({
+ queryForBoolean: query([], bool)
+});
-const someCanister = new SomeCanister(
+const someCanister = SomeCanister(
Principal.fromText('ryjl3-tyaaa-aaaaa-aaaba-cai')
);
-$query;
-export async function querySomeCanister(): Promise<Result<boolean, string>> {
- return await someCanister.queryForBoolean().call();
-}
+export default Canister({
+ querySomeCanister: query([], bool, async () => {
+ return await ic.call(someCanister.queryForBoolean);
+ })
+});
-You can expect cross-canister calls within the same subnet to take up to a few seconds to complete, and cross-canister calls across subnets take about double that time.
+You can expect cross-canister calls within the same subnet to take up to a few seconds to complete, and cross-canister calls across subnets take about double that time. Composite queries should be much faster, similar to query calls in latency.
If you don't need to wait for your cross-canister call to return, you can use notify
:
-import {
- CallResult,
- Principal,
- RejectionCode,
- Result,
- Service,
- serviceUpdate,
- $update
-} from 'azle';
+import { Canister, ic, Principal, update, Void } from 'azle';
-class SomeCanister extends Service {
- @serviceUpdate
- receiveNotification: () => CallResult<void>;
-}
+const SomeCanister = Canister({
+ receiveNotification: update([], Void)
+});
-const someCanister = new SomeCanister(
+const someCanister = SomeCanister(
Principal.fromText('ryjl3-tyaaa-aaaaa-aaaba-cai')
);
-$update;
-export function sendNotification(): Result<null, RejectionCode> {
- return someCanister.receiveNotification().notify();
-}
+export default Canister({
+ sendNotification: update([], Void, () => {
+ return ic.notify(someCanister.receiveNotification);
+ })
+});
-If you need to send cycles with your cross-canister call, you can call cycles
before calling call
or notify
:
-import {
- CallResult,
- Principal,
- RejectionCode,
- Result,
- Service,
- serviceUpdate,
- $update
-} from 'azle';
+If you need to send cycles with your cross-canister call, you can add cycles
to the config
object of ic.notify
:
+import { Canister, ic, Principal, update, Void } from 'azle';
-class SomeCanister extends Service {
- @serviceUpdate
- receiveNotification: () => CallResult<void>;
-}
+const SomeCanister = Canister({
+ receiveNotification: update([], Void)
+});
-const someCanister = new SomeCanister(
+const someCanister = SomeCanister(
Principal.fromText('ryjl3-tyaaa-aaaaa-aaaba-cai')
);
-$update;
-export function sendNotification(): Result<null, RejectionCode> {
- return someCanister.receiveNotification().cycles(1_000_000n).notify();
-}
+export default Canister({
+ sendNotification: update([], Void, () => {
+ return ic.notify(someCanister.receiveNotification, {
+ cycles: 1_000_000n
+ });
+ })
+});
diff --git a/the_azle_book/book/cycles.html b/the_azle_book/book/cycles.html
index 30109de2ee..f865726fab 100644
--- a/the_azle_book/book/cycles.html
+++ b/the_azle_book/book/cycles.html
@@ -83,7 +83,7 @@
diff --git a/the_azle_book/book/deployment.html b/the_azle_book/book/deployment.html
index 4eaf38ce80..31d7ae5042 100644
--- a/the_azle_book/book/deployment.html
+++ b/the_azle_book/book/deployment.html
@@ -83,7 +83,7 @@
@@ -236,6 +236,13 @@ Dep
To deploy an individual canister:
dfx deploy --network ic canister_name
+Common deployment issues
+If you run into an error during deployment, try the following:
+
+- Add the
--verbose
flag to the build
command in your dfx.json
file like so: "build": "npx azle build hello_world --verbose
+- Ensure that you have followed the instructions correctly in the installation chapter, especially noting the build dependencies
+- Reach out on the Discord channel
+
diff --git a/the_azle_book/book/examples.html b/the_azle_book/book/examples.html
index 99d4e25f8c..b058129049 100644
--- a/the_azle_book/book/examples.html
+++ b/the_azle_book/book/examples.html
@@ -83,7 +83,7 @@
@@ -174,7 +174,7 @@ Examples
-
+
@@ -188,7 +188,7 @@ Examples
import {
blob,
+ bool,
+ Canister,
Func,
nat16,
+ None,
Opt,
- $query,
- Query,
+ query,
Record,
+ text,
Tuple,
Variant,
Vec
} from 'azle';
-type HttpRequest = Record<{
- method: string;
- url: string;
- headers: Vec<Header>;
- body: blob;
-}>;
-
-type HttpResponse = Record<{
- status_code: nat16;
- headers: Vec<Header>;
- body: blob;
- streaming_strategy: Opt<StreamingStrategy>;
- upgrade: Opt<boolean>;
-}>;
-
-type Header = Tuple<[string, string]>;
-
-type StreamingStrategy = Variant<{
- Callback: CallbackStrategy;
-}>;
-
-type CallbackStrategy = Record<{
- callback: Callback;
- token: Token;
-}>;
-
-type Callback = Func<Query<(t: Token) => StreamingCallbackHttpResponse>>;
-
-type StreamingCallbackHttpResponse = Record<{
- body: blob;
- token: Opt<Token>;
-}>;
-
-type Token = Record<{
- arbitrary_data: string;
-}>;
-
-$query;
-export function http_request(req: HttpRequest): HttpResponse {
- return {
- status_code: 200,
- headers: [],
- body: Uint8Array.from([]),
- streaming_strategy: Opt.None,
- upgrade: Opt.Some(false)
- };
-}
+const Token = Record({
+ // add whatever fields you'd like
+ arbitrary_data: text
+});
+
+const StreamingCallbackHttpResponse = Record({
+ body: blob,
+ token: Opt(Token)
+});
+
+export const Callback = Func([text], StreamingCallbackHttpResponse, 'query');
+
+const CallbackStrategy = Record({
+ callback: Callback,
+ token: Token
+});
+
+const StreamingStrategy = Variant({
+ Callback: CallbackStrategy
+});
+
+type HeaderField = [text, text];
+const HeaderField = Tuple(text, text);
+
+const HttpResponse = Record({
+ status_code: nat16,
+ headers: Vec(HeaderField),
+ body: blob,
+ streaming_strategy: Opt(StreamingStrategy),
+ upgrade: Opt(bool)
+});
+
+const HttpRequest = Record({
+ method: text,
+ url: text,
+ headers: Vec(HeaderField),
+ body: blob,
+ certificate_version: Opt(nat16)
+});
+
+export default Canister({
+ http_request: query([HttpRequest], HttpResponse, (req) => {
+ return {
+ status_code: 200,
+ headers: [],
+ body: Buffer.from('hello'),
+ streaming_strategy: None,
+ upgrade: None
+ };
+ })
+});
Outgoing HTTP requests
Examples:
@@ -219,118 +226,122 @@outgoing_http_requests
import {
- Alias,
+ Canister,
ic,
- $init,
- match,
+ init,
nat32,
- $query,
+ Principal,
+ query,
+ Some,
StableBTreeMap,
- $update,
- Opt
+ text,
+ update
} from 'azle';
import {
HttpResponse,
HttpTransformArgs,
managementCanister
} from 'azle/canisters/management';
-import decodeUtf8 from 'decode-utf8';
-import encodeUtf8 from 'encode-utf8';
-
-type JSON = Alias<string>;
-
-let stableStorage = new StableBTreeMap<string, string>(0, 25, 1_000);
-
-$init;
-export function init(ethereumUrl: string): void {
- stableStorage.insert('ethereumUrl', ethereumUrl);
-}
-
-$update;
-export async function ethGetBalance(ethereumAddress: string): Promise<JSON> {
- const httpResult = await managementCanister
- .http_request({
- url: match(stableStorage.get('ethereumUrl'), {
- Some: (url) => url,
- None: () => ''
- }),
- max_response_bytes: Opt.Some(2_000n),
- method: {
- post: null
- },
- headers: [],
- body: Opt.Some(
- new Uint8Array(
- encodeUtf8(
- JSON.stringify({
- jsonrpc: '2.0',
- method: 'eth_getBalance',
- params: [ethereumAddress, 'earliest'],
- id: 1
- })
- )
- )
- ),
- transform: Opt.Some({
- function: [ic.id(), 'ethTransform'],
- context: Uint8Array.from([])
- })
- })
- .cycles(50_000_000n)
- .call();
-
- return match(httpResult, {
- Ok: (httpResponse) => decodeUtf8(Uint8Array.from(httpResponse.body)),
- Err: (err) => ic.trap(err)
- });
-}
-
-$update;
-export async function ethGetBlockByNumber(number: nat32): Promise<JSON> {
- const httpResult = await managementCanister
- .http_request({
- url: match(stableStorage.get('ethereumUrl'), {
- Some: (url) => url,
- None: () => ''
- }),
- max_response_bytes: Opt.Some(2_000n),
- method: {
- post: null
- },
- headers: [],
- body: Opt.Some(
- new Uint8Array(
- encodeUtf8(
- JSON.stringify({
- jsonrpc: '2.0',
- method: 'eth_getBlockByNumber',
- params: [`0x${number.toString(16)}`, false],
- id: 1
- })
- )
- )
- ),
- transform: Opt.Some({
- function: [ic.id(), 'ethTransform'],
- context: Uint8Array.from([])
- })
- })
- .cycles(50_000_000n)
- .call();
-
- return match(httpResult, {
- Ok: (httpResponse) => decodeUtf8(Uint8Array.from(httpResponse.body)),
- Err: (err) => ic.trap(err)
- });
-}
-
-$query;
-export function ethTransform(args: HttpTransformArgs): HttpResponse {
- return {
- ...args.response,
- headers: []
- };
-}
+
+let stableStorage = StableBTreeMap(text, text, 0);
+
+export default Canister({
+ init: init([text], (ethereumUrl) => {
+ stableStorage.insert('ethereumUrl', ethereumUrl);
+ }),
+ ethGetBalance: update([text], text, async (ethereumAddress) => {
+ const urlOpt = stableStorage.get('ethereumUrl');
+
+ if ('None' in urlOpt) {
+ throw new Error('ethereumUrl is not defined');
+ }
+
+ const url = urlOpt.Some;
+
+ const httpResponse = await ic.call(managementCanister.http_request, {
+ args: [
+ {
+ url,
+ max_response_bytes: Some(2_000n),
+ method: {
+ post: null
+ },
+ headers: [],
+ body: Some(
+ Buffer.from(
+ JSON.stringify({
+ jsonrpc: '2.0',
+ method: 'eth_getBalance',
+ params: [ethereumAddress, 'earliest'],
+ id: 1
+ }),
+ 'utf-8'
+ )
+ ),
+ transform: Some({
+ function: [ic.id(), 'ethTransform'] as [
+ Principal,
+ string
+ ],
+ context: Uint8Array.from([])
+ })
+ }
+ ],
+ cycles: 50_000_000n
+ });
+
+ return Buffer.from(httpResponse.body.buffer).toString('utf-8');
+ }),
+ ethGetBlockByNumber: update([nat32], text, async (number) => {
+ const urlOpt = stableStorage.get('ethereumUrl');
+
+ if ('None' in urlOpt) {
+ throw new Error('ethereumUrl is not defined');
+ }
+
+ const url = urlOpt.Some;
+
+ const httpResponse = await ic.call(managementCanister.http_request, {
+ args: [
+ {
+ url,
+ max_response_bytes: Some(2_000n),
+ method: {
+ post: null
+ },
+ headers: [],
+ body: Some(
+ Buffer.from(
+ JSON.stringify({
+ jsonrpc: '2.0',
+ method: 'eth_getBlockByNumber',
+ params: [`0x${number.toString(16)}`, false],
+ id: 1
+ }),
+ 'utf-8'
+ )
+ ),
+ transform: Some({
+ function: [ic.id(), 'ethTransform'] as [
+ Principal,
+ string
+ ],
+ context: Uint8Array.from([])
+ })
+ }
+ ],
+ cycles: 50_000_000n
+ });
+
+ return Buffer.from(httpResponse.body.buffer).toString('utf-8');
+ }),
+ ethTransform: query([HttpTransformArgs], HttpResponse, (args) => {
+ return {
+ ...args.response,
+ headers: []
+ };
+ })
+});
diff --git a/the_azle_book/book/index.html b/the_azle_book/book/index.html
index 3df4ff18a9..843dae0009 100644
--- a/the_azle_book/book/index.html
+++ b/the_azle_book/book/index.html
@@ -83,7 +83,7 @@
diff --git a/the_azle_book/book/installation.html b/the_azle_book/book/installation.html
index 7adec042f9..d20c17bc0b 100644
--- a/the_azle_book/book/installation.html
+++ b/the_azle_book/book/installation.html
@@ -83,7 +83,7 @@
@@ -148,8 +148,9 @@ InstallationFollow the instructions exactly as stated below to avoid issues.
import {
- Alias,
+ Canister,
ic,
- $init,
- match,
+ init,
nat32,
- $query,
+ Principal,
+ query,
+ Some,
StableBTreeMap,
- $update,
- Opt
+ text,
+ update
} from 'azle';
import {
HttpResponse,
HttpTransformArgs,
managementCanister
} from 'azle/canisters/management';
-import decodeUtf8 from 'decode-utf8';
-import encodeUtf8 from 'encode-utf8';
-
-type JSON = Alias<string>;
-
-let stableStorage = new StableBTreeMap<string, string>(0, 25, 1_000);
-
-$init;
-export function init(ethereumUrl: string): void {
- stableStorage.insert('ethereumUrl', ethereumUrl);
-}
-
-$update;
-export async function ethGetBalance(ethereumAddress: string): Promise<JSON> {
- const httpResult = await managementCanister
- .http_request({
- url: match(stableStorage.get('ethereumUrl'), {
- Some: (url) => url,
- None: () => ''
- }),
- max_response_bytes: Opt.Some(2_000n),
- method: {
- post: null
- },
- headers: [],
- body: Opt.Some(
- new Uint8Array(
- encodeUtf8(
- JSON.stringify({
- jsonrpc: '2.0',
- method: 'eth_getBalance',
- params: [ethereumAddress, 'earliest'],
- id: 1
- })
- )
- )
- ),
- transform: Opt.Some({
- function: [ic.id(), 'ethTransform'],
- context: Uint8Array.from([])
- })
- })
- .cycles(50_000_000n)
- .call();
-
- return match(httpResult, {
- Ok: (httpResponse) => decodeUtf8(Uint8Array.from(httpResponse.body)),
- Err: (err) => ic.trap(err)
- });
-}
-
-$update;
-export async function ethGetBlockByNumber(number: nat32): Promise<JSON> {
- const httpResult = await managementCanister
- .http_request({
- url: match(stableStorage.get('ethereumUrl'), {
- Some: (url) => url,
- None: () => ''
- }),
- max_response_bytes: Opt.Some(2_000n),
- method: {
- post: null
- },
- headers: [],
- body: Opt.Some(
- new Uint8Array(
- encodeUtf8(
- JSON.stringify({
- jsonrpc: '2.0',
- method: 'eth_getBlockByNumber',
- params: [`0x${number.toString(16)}`, false],
- id: 1
- })
- )
- )
- ),
- transform: Opt.Some({
- function: [ic.id(), 'ethTransform'],
- context: Uint8Array.from([])
- })
- })
- .cycles(50_000_000n)
- .call();
-
- return match(httpResult, {
- Ok: (httpResponse) => decodeUtf8(Uint8Array.from(httpResponse.body)),
- Err: (err) => ic.trap(err)
- });
-}
-
-$query;
-export function ethTransform(args: HttpTransformArgs): HttpResponse {
- return {
- ...args.response,
- headers: []
- };
-}
+
+let stableStorage = StableBTreeMap(text, text, 0);
+
+export default Canister({
+ init: init([text], (ethereumUrl) => {
+ stableStorage.insert('ethereumUrl', ethereumUrl);
+ }),
+ ethGetBalance: update([text], text, async (ethereumAddress) => {
+ const urlOpt = stableStorage.get('ethereumUrl');
+
+ if ('None' in urlOpt) {
+ throw new Error('ethereumUrl is not defined');
+ }
+
+ const url = urlOpt.Some;
+
+ const httpResponse = await ic.call(managementCanister.http_request, {
+ args: [
+ {
+ url,
+ max_response_bytes: Some(2_000n),
+ method: {
+ post: null
+ },
+ headers: [],
+ body: Some(
+ Buffer.from(
+ JSON.stringify({
+ jsonrpc: '2.0',
+ method: 'eth_getBalance',
+ params: [ethereumAddress, 'earliest'],
+ id: 1
+ }),
+ 'utf-8'
+ )
+ ),
+ transform: Some({
+ function: [ic.id(), 'ethTransform'] as [
+ Principal,
+ string
+ ],
+ context: Uint8Array.from([])
+ })
+ }
+ ],
+ cycles: 50_000_000n
+ });
+
+ return Buffer.from(httpResponse.body.buffer).toString('utf-8');
+ }),
+ ethGetBlockByNumber: update([nat32], text, async (number) => {
+ const urlOpt = stableStorage.get('ethereumUrl');
+
+ if ('None' in urlOpt) {
+ throw new Error('ethereumUrl is not defined');
+ }
+
+ const url = urlOpt.Some;
+
+ const httpResponse = await ic.call(managementCanister.http_request, {
+ args: [
+ {
+ url,
+ max_response_bytes: Some(2_000n),
+ method: {
+ post: null
+ },
+ headers: [],
+ body: Some(
+ Buffer.from(
+ JSON.stringify({
+ jsonrpc: '2.0',
+ method: 'eth_getBlockByNumber',
+ params: [`0x${number.toString(16)}`, false],
+ id: 1
+ }),
+ 'utf-8'
+ )
+ ),
+ transform: Some({
+ function: [ic.id(), 'ethTransform'] as [
+ Principal,
+ string
+ ],
+ context: Uint8Array.from([])
+ })
+ }
+ ],
+ cycles: 50_000_000n
+ });
+
+ return Buffer.from(httpResponse.body.buffer).toString('utf-8');
+ }),
+ ethTransform: query([HttpTransformArgs], HttpResponse, (args) => {
+ return {
+ ...args.response,
+ headers: []
+ };
+ })
+});
You should be using a *nix environment (Linux, Mac OS, WSL if using Windows) with bash and have the following installed on your system:
-
-
- Node.js 18 -
- dfx 0.14.2 +
- Node.js 18 +
- dfx 0.15.0 +
- Build dependencies
Node.js
We highly recommend using nvm to install Node.js (and npm, which is included with Node.js). Run the following commands to install Node.js and npm with nvm:
@@ -158,19 +159,31 @@Node.js
Now restart your terminal and run the following command:
nvm install 18
-dfx
-Run the following command to install dfx 0.14.2:
-DFX_VERSION=0.14.2 sh -ci "$(curl -fsSL https://sdk.dfinity.org/install.sh)"
+dfx 0.15.0
+Run the following command to install dfx 0.15.0:
+DFX_VERSION=0.15.0 sh -ci "$(curl -fsSL https://sdk.dfinity.org/install.sh)"
If after trying to run dfx
commands you encounter an error such as dfx: command not found
, you might need to add $HOME/bin
to your path. Here's an example of doing this in your .bashrc
:
echo 'export PATH="$PATH:$HOME/bin"' >> "$HOME/.bashrc"
+
+Build dependencies
+You may or may not need these dependencies based on your OS. If you run into errors while deploying, try the following:
+Ubuntu/WSL
+sudo apt install clang
+sudo apt install build-essential
+sudo apt install libssl-dev
+sudo apt install pkg-config
+
+Mac
+# Install the Xcode Command Line Tools
+xcode-select --install