--- name: moxzi-ic-agent description: "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 `, 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)." license: BUSL-1.1 compatibility: "ic-agent >= 0.2.0; moxzi >= 0.1.0-alpha.7" metadata: title: mo:ic-agent — IC client in Motoko category: moxzi --- # Calling the IC from a Motoko actor 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"` (the `Agent` module) and `"mo:ic-agent/Identity"`. - Keep `Config.verify = #required` (the default). Every certificate (BLS, scoped delegation, `/time` within 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`); set `Config.rootKey` explicitly only for a local replica or PocketIC. - Keep an `Identity` value 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 `submit` then `poll` on later turns; `call` waits for the result. **NEVER:** - Use `#skip` in production. It prints a loud `Debug.print` for every response it did not verify; there is no silent mode, by design. - Trust the root key from the `status` endpoint on mainnet (the spec says so too). - Expect a browser transport yet: the default `Transport` is 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 and `from_candid`. ## Minimal query ```motoko 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 ```motoko 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 ```sh moxzi agent-bindings ledger.did --name Ledger -o Ledger.mo ``` ```motoko 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`.