moxzi
Docs / Browser / Candid encoding

Candid encodingalpha

How do I encode arguments and decode replies correctly?

moxzi-web speaks raw Candid bytes, because that is what the Internet Computer speaks: a call carries an export name (canister_update greet) and a byte string. The constraint that follows: the wire format carries field hashes, not field names, so a decode without a type recovers numbers where your record had labels.

The typed path#

Pass the idlFactory that dfx generate wrote and the library builds one JS method per service method. It reads func.argTypes / func.retTypes from the factory, encodes with @dfinity/candid's IDL.encode, and decodes the reply with IDL.decode:

import { idlFactory } from './declarations/greeter/greeter.did.js';

const greeter = await moxzi.install({ name: 'greeter', wasm: './greet.wasm', idlFactory });
await greeter.greet('world');     // "Hello, world!"
await greeter.visitors();         // 2n

@dfinity/candid is used unmodified, so the argument bytes are the bytes an agent would send.

CandidJavaScript
nat, int, nat64, int64, …BigInt (2n) — as agent-js users expect
textstring
blob / vec nat8number[] going in, Uint8Array-convertible coming back (Uint8Array.from(v))
opt T[] or [value]
variant { a; b }{ a: null }
principalPrincipal from @dfinity/principal

The return convention is agent-js's own, applied by the library: no return values give undefined, one comes back bare, several come back as a tuple array. Code written against a deployed canister does not have to tell the difference.

Query/update is decided from the factory's annotations, not by guessing from the method name: composite_query and query route through deliverQuery, everything else through deliver.

Blob arguments are ordinary arrays of bytes. From the MoxDB demo, handing a whole wasm module to an actor that will upgrade its rows with it:

const wasm = new Uint8Array(await (await fetch('./moxdb-rowv2.wasm')).arrayBuffer());
log(await db.alterTable([...wasm]));

and the reverse, unpacking a vec nat8 reply — from the knights demo's outcall handling:

const body = new TextDecoder().decode(Uint8Array.from(r.body));

The raw path#

Without an idlFactory an actor still exists; it has callRaw and queryRaw, which take and return bytes. You supply the method's full export name and encode the arguments yourself:

import { IDL } from '@dfinity/candid';

const a = moxzi.actor('greeter');                       // no factory: raw only
const reply = await a.callRaw('canister_update greet',
  new Uint8Array(IDL.encode([IDL.Text], ['world'])));
const [text] = IDL.decode([IDL.Text], new Uint8Array(reply));

A call with no arguments is the six-byte empty message DIDL\0\0, which is what callRaw and queryRaw send when you omit argBytes.

Hand-declared types work the same way, which is how the demos drive the compiler canister itself — its diagnostics are a real record type, declared once and reused:

const Pos    = IDL.Record({ column: IDL.Int, file: IDL.Text, line: IDL.Int });
const Region = IDL.Record({ left: Pos, right: Pos });
const StepResult = IDL.Variant({
  more: IDL.Null, needClass: IDL.Text,
  done: IDL.Vec(IDL.Nat8), err: IDL.Vec(Diag),
});

const [v] = IDL.decode([StepResult], forgeCompile('step', IDL.encode([IDL.Nat], [job])));
if ('done' in v) { const unlinked = Uint8Array.from(v.done); }

Field names are not on the wire#

A Candid record encodes each field as the 32-bit hash of its label:

h = 0
for each byte b of the UTF-8 label:  h = (h * 223 + b) mod 2^32

So a decoder that has no type can tell you the shape of a reply and the value of every field, but the best it can do for a label is print the hash. The devtools panel is exactly this situation — it inspects actors whose bytes never crossed into the page — and its generic decoder falls back to #1224700491-style keys when it has no name for an id.

Its recovery is worth copying if you need untyped decoding: fetch the interface, hash every identifier that appears in it, and look the hashes up.

function candidHash(name) {
  let h = 0n;
  for (const c of new TextEncoder().encode(name)) h = (h * 223n + BigInt(c)) % 4294967296n;
  return h;
}

const iface = await moxzi.candidOf('moxdb');          // the module's candid:service text
const names = new Map();
for (const m of (iface || '').matchAll(/[A-Za-z_][A-Za-z0-9_]*/g)) {
  names.set(candidHash(m[0]).toString(), m[0]);
}

candidOf reads the candid:service metadata of the module the runtime is executing, so it works for actors spawned by actors and tracks upgrades. It returns null when the module carries no such section — which is why an untyped decode has to survive missing names rather than assume them.

Two consequences worth stating plainly:

If you…Then…
Decode with the method's real typesYou get field names, because you supplied them
Decode with a type whose labels differThe hashes differ, so the fields you asked for are not the fields that arrived
Decode with no type at allYou get #<hash> keys; recover names from candidOf, or accept them

Principals#

idFromName(name) gives the ten-byte principal the library derives for a named actor. To show or send it, convert with the real library:

import { Principal } from '@dfinity/principal';
Principal.fromUint8Array(idFromName('greeter')).toText();

Things you cannot do#

Next#

On this pageThe typed pathThe raw pathField names are not on the wirePrincipalsThings you cannot doNext