Candid in JavaScriptalpha
How Candid types map to JavaScript values when you call actors in moxzi-web, and how to call without an idlFactory.
moxzi-web encodes arguments and decodes results with @dfinity/candid, using the idlFactory you pass to install. Values look exactly as they do with @dfinity/agent, so code written for a deployed canister works unchanged.
Type mapping#
| Candid | JavaScript |
|---|---|
nat, int, nat64, int64 | BigInt, such as 30n |
nat8, nat16, nat32, int8 … int32, float64 | number |
text | string |
bool | boolean |
blob (vec nat8) | An array of numbers going in; convert results with Uint8Array.from(value) |
opt T | [] for none, [value] for some |
vec T | An array |
record { … } | An object |
variant { … } | An object with one key: { ok: 1n }, { none: null } |
principal | A Principal from @dfinity/principal |
A method with no results returns undefined, one result is returned as is, and several are returned as an array.
An example#
This page calls the shop actor from Calling actors:
import Array "mo:core/Array";
import Runtime "mo:core/Runtime";
actor {
type Item = {
name : Text;
price : Nat;
tags : [Text];
note : ?Text;
};
type AddResult = { #ok : Nat; #err : Text };
var items : [Item] = [];
public func add(item : Item) : async AddResult {
if (item.price == 0) return #err "price must be positive";
items := Array.concat(items, [item]);
#ok(items.size())
};
public query func find(name : Text) : async ?Item {
Array.find<Item>(items, func(i) { i.name == name })
};
public query func count() : async Nat {
items.size()
};
public func addThenFail(item : Item) : async () {
items := Array.concat(items, [item]);
Runtime.trap("something went wrong");
};
public shared ({ caller }) func whoami() : async Principal {
caller
};
public query func double(n : Nat) : async Nat {
n * 2
};
};
moxzi build shop.mo -o public/shop.wasm
didc bind -t js public/shop.did > shop.did.js
import * as glue from 'moxzi-web/runtime';
import { Moxzi, ActorError } from 'moxzi-web';
import { IDL } from '@dfinity/candid';
import { Principal } from '@dfinity/principal';
import { idlFactory } from './shop.did.js';
const moxzi = await Moxzi.start({ glue });
const shop = await moxzi.install({ name: 'shop', wasm: '/shop.wasm', idlFactory });
const lines = [];
const show = (label, value) =>
lines.push(`${label}: ${JSON.stringify(value, (_, v) => (typeof v === 'bigint' ? `${v}n` : v))}`);
// A record argument. `opt` fields are [] (none) or [value].
show('add', await shop.add({ name: 'lamp', price: 30n, tags: ['home'], note: [] }));
show('add', await shop.add({ name: 'free', price: 0n, tags: [], note: ['promo'] }));
// An `opt` result: [] when absent, [value] when present.
show('find lamp', await shop.find('lamp'));
show('find sofa', await shop.find('sofa'));
// A principal comes back as a Principal object.
const caller = await shop.whoami();
show('whoami', caller instanceof Principal ? caller.toText() : caller);
// A trap becomes an ActorError.
try {
await shop.addThenFail({ name: 'chair', price: 50n, tags: [], note: [] });
} catch (e) {
show('addThenFail', e instanceof ActorError ? e.message : String(e));
}
show('count', await shop.count());
// Without an idlFactory: encode the arguments and decode the reply yourself.
const raw = moxzi.actor('shop');
const reply = await raw.queryRaw('canister_query double', new Uint8Array(IDL.encode([IDL.Nat], [21n])));
show('raw double', IDL.decode([IDL.Nat], new Uint8Array(reply))[0]);
document.querySelector('#out').textContent = lines.join('\n');
The page shows:
add: {"ok":"1n"}
add: {"err":"price must be positive"}
find lamp: [{"name":"lamp","note":[],"tags":["home"],"price":"30n"}]
find sofa: []
whoami: "2vxsx-fae"
addThenFail: "IC0503: Error from Canister rvdsr-ksg3r-pbryu-oeqeq-cai: Canister called `ic0.trap` with message: 'something went wrong'."
count: "1n"
raw double: "42n"
Errors#
A call to a method that traps or rejects throws an ActorError. Its message has the same format as on the IC, and .method names the method.
Calling without an idlFactory#
Without an idlFactory, an actor object has only callRaw and queryRaw. Encode the arguments yourself and use the method's export name:
// Without an idlFactory: encode the arguments and decode the reply yourself.
const raw = moxzi.actor('shop');
const reply = await raw.queryRaw('canister_query double', new Uint8Array(IDL.encode([IDL.Nat], [21n])));
show('raw double', IDL.decode([IDL.Nat], new Uint8Array(reply))[0]);
This is the raw double line in the output above.
Leave out the argument bytes for a method with no arguments.
Without type information, record fields and variant tags decode as numbers (hashes of their names), because Candid sends hashes rather than names. Pass an idlFactory, or decode with the right IDL types, to get the names.