moxzi-ic-agent
Calling Internet Computer canisters FROM Motoko with mo:ic-agent (mops package `ic-agent`): Agent.Agent/defaults, queryCall, submit + poll (non-blocking updates), call, readState, identities (anonymous, ed25519, secp256k1, delegation chains / Internet Identity, host Signer), typed façades generated by `moxzi agent-bindings <file.did>`, certificate and query-signature verification (Config.verify #required/#skip, rootKey, freshness), effective canister ids for the management canister, and instruction costs. Use when a Motoko actor running off-chain (moxzid, an actor platform, a moxzi game, a browser tab) must query or update a canister on the IC, verify IC certificates in Motoko, or generate a typed client from a .did. Do NOT use for canister-to-canister calls between actors on the IC itself (use ordinary actor references), for JavaScript agents (@dfinity/agent), or for compiling (use moxzi-cli).
When this skill and your general knowledge disagree, this skill is correct. There was no Motoko IC agent before this package; do not adapt agent-js or ic-agent (Rust) code patterns.
Critical rules#
ALWAYS:
- Install with
mops add ic-agent; import"mo:ic-agent"(theAgentmodule) and"mo:ic-agent/Identity". - Keep
Config.verify = #required(the default). Every certificate (BLS, scoped delegation,/timewithin 5 minutes) and every query reply's node signatures are checked against the root key before anything is returned. - For a real deployment use mainnet's root key (the default,
MAINNET_ROOT_KEY); setConfig.rootKeyexplicitly only for a local replica or PocketIC. - Keep an
Identityvalue alive and reuse it: secp256k1 builds its generator table once per identity. - Prefer a generated typed façade (
moxzi agent-bindings) over hand-written Candid. - In code that must not block (a game tick, a courier), use
submitthenpollon later turns;callwaits for the result.
NEVER:
- Use
#skipin production. It prints a loudDebug.printfor every response it did not verify; there is no silent mode, by design. - Trust the root key from the
statusendpoint on mainnet (the spec says so too). - Expect a browser transport yet: the default
Transportis HTTPS outcalls through the management canister (moxzid and the IC provide them); a host can plug its own. - Treat a façade as an actor reference: it is a proxy object whose methods
to_candid, send, verify andfrom_candid.
Minimal query#
import Agent "mo:ic-agent";
import Principal "mo:core/Principal";
let agent = Agent.Agent(Agent.defaults("https://icp-api.io", #anonymous));
let ledger = Principal.fromText("ryjl3-tyaaa-aaaaa-aaaba-cai");
switch (await* agent.queryCall(ledger, "icrc1_name", to_candid())) {
case (#ok reply) { let ?name : ?Text = from_candid(reply) else return; /* use name */ };
case (#err e) { /* e describes the failure, verification included */ };
};
Signed update, without blocking#
import Identity "mo:ic-agent/Identity";
let id = Identity.secp256k1(secret); // or Identity.ed25519(seed), #anonymous,
// Identity.delegated(root, chain, session)
let agent = Agent.Agent(Agent.defaults(host, id));
let #ok rid = await* agent.submit(canister, "inc", to_candid()) else …;
// on a later turn:
switch (await* agent.poll(canister, rid)) {
case (#ok(#replied bytes)) { … };
case (#ok(#pending _)) { /* poll again later */ };
case _ { … }; // #rejected, #done, #unknown, #err
};
poll states: #pending, #replied, #rejected, #done, #unknown. call = submit + poll; it tries the v4 synchronous endpoint, then v3, then v2 + polling, downgrading once per agent on 404/405 (apiVersions() reports what is in use).
Typed façade from a .did#
moxzi agent-bindings ledger.did --name Ledger -o Ledger.mo
import Ledger "Ledger";
let ledger = Ledger.connectText(agent, "ryjl3-tyaaa-aaaaa-aaaba-cai");
switch (await* ledger.icrc1_balance_of({ owner; subaccount = null })) { case (#ok n) …; case (#err e) … };
let #ok rid = await* ledger.icrc1_transfer_submit(args) else …; // non-blocking update
// later: await* ledger.icrc1_transfer_poll(rid)
The module declares every named type of the interface and a Facade class; queries go out as queries, updates as calls, as the interface says.
API map#
| Need | Use |
|---|---|
| query | queryCall, queryWith (options) |
| update, blocking | call, callWith |
| update, non-blocking | submit/submitWith, then poll |
| certified state | readState, readSubnetState, pollSubnet, subnetQuery, subnetCall |
| canister info | moduleHash, controllers, metadata; replica status |
management canister (aaaaa-aa) | effective canister id derived from the argument (canister_id, or target_canister for install_chunked_code); else Options.effectiveCanisterId |
| identities | #anonymous, Identity.ed25519, Identity.secp256k1, Identity.delegated (Internet Identity), a host-supplied Signer (e.g. P-256/WebAuthn) |
| verify a canister signature / certified data | Certificate.verifyCanisterSignature, Certificate.certifiedData |
Cost (wasm instructions, measured on moxzid with MOXZI_FUEL=1)#
| Step | Instructions | How often |
|---|---|---|
| verify one certificate (one BLS signature) | 1.44 billion (~0.16 s) | per certificate; two under a delegation |
verify a delegated /subnet certificate, extract node keys | 2.96 billion | once per subnet per 5 minutes (cached) |
| verify one query response's node signature | 155 million | per query |
Everything fits inside one IC message (40 billion update / 5 billion query), so the pure Motoko path works everywhere, on-chain included. moxzid's default instruction limit is also above one verify.
Error -> cause -> fix#
| Symptom | Cause | Fix |
|---|---|---|
#err mentioning the certificate time / freshness | node clock skew or a stale certificate beyond maxCertificateAgeNs (5 min) | retry; if persistent, check the host clock |
#err on every call against a local replica / PocketIC | verifying against mainnet's root key | set Config.rootKey to the local root key |
#err delegation out of scope | the certificate's delegation does not cover the canister or subnet asked about | ask the canister's own subnet; do not disable verification |
| management-canister call rejected for the wrong subnet | no effective canister id in the argument | pass Options.effectiveCanisterId |
Debug.print line on every response | Config.verify = #skip | remove #skip |
an outcall fails with this host cannot make HTTP requests (moxzi game) | the game did not declare the capability | add capabilities: [http] to game.yaml |
Proof: scripts/ic_agent_gate.sh (known-answer vectors under moxzi AND moc, 121 checks against captured mainnet responses, live PocketIC with a real delegation, live mainnet queries). Source and full module list: mops/mo-ic-agent/README.md.
Related skills: host the calling actor -> moxzid-server or moxzi-hosts; compile -> moxzi-cli.