moxzi
Docs / Server / Talking to the Internet Computer from an actor

Talking to the Internet Computer from an actor

An actor hosted by moxzid (or a moxzi game, or a browser tab) can be a client of the Internet Computer: mo:ic-agent speaks the IC's HTTP interface — signed envelopes, request ids, read_state certificates — from Motoko, over the HTTPS outcalls the host already provides. It is the same thing agent-js or ic-agent (Rust) do, in the language the actor is written in.

actor "aaaaa-aa" and actor(p) still mean "an actor in this runtime". The agent is how an off-chain actor reaches a canister that lives on the IC.

A query#

import Agent "mo:ic-agent";

persistent actor {
  transient let agent = Agent.Agent(Agent.defaults("https://icp-api.io", #anonymous));
  let ledger = Principal.fromText("ryjl3-tyaaa-aaaaa-aaaba-cai");

  public func ledgerName() : async Text {
    switch (await* agent.queryCall(ledger, "icrc1_name", to_candid())) {
      case (#ok reply) { let ?name : ?Text = from_candid(reply) else return "?"; name };
      case (#err e) debug_show e;
    }
  };
};

mops add ic-agent; the transport is http_request on the management canister, so moxzid must not be started with --no-outcalls, and a game declares capabilities: [http].

An update, without blocking#

An update call is two operations. submit sends the envelope and returns the request id at once; poll is one read_state round trip:

let #ok rid = await* agent.submit(canister, "inc", to_candid()) else return;
// … on a later message / tick:
switch (await* agent.poll(canister, rid)) {
  case (#ok(#replied bytes)) { … };
  case (#ok(#pending _)) { /* ask again later */ };
  case (#ok(#rejected r)) { /* r.code : #canisterReject | #sysTransient | … */ };
  case (#ok(#unknown)) { /* not seen yet, or expired */ };
  case _ {};
};

call(canister, method, arg, maxPolls) composes the two for code that can afford to wait — it polls up to maxPolls times and hands back #err(#pending rid) if the call is still in flight, so the caller can keep polling with the id.

Typed façades#

moxzi agent-bindings ledger.did --name Ledger -o Ledger.mo writes a Motoko module with every type of the interface and a Facade class over an agent:

import Ledger "Ledger";
transient let ledger = Ledger.connectText(agent, "ryjl3-tyaaa-aaaaa-aaaba-cai");
public func balance(owner : Principal) : async Nat {
  switch (await* ledger.icrc1_balance_of({ owner; subaccount = null })) { case (#ok n) n; case (#err _) 0 }
};

A façade is a proxy object, not an actor reference — the type says so. Query methods go out as queries; update methods as calls, and also as m_submit / m_poll for code that keeps ticking. Labels Motoko cannot write degrade to Any with a NOTE in the module header.

Identities#

#anonymousthe anonymous principal 2vxsx-fae
Identity.ed25519(seed)a 32-byte seed; signs the request bytes directly
Identity.secp256k1(secret)a 32-byte secret; ECDSA over sha256, r ‖ s; builds the generator table once per identity — keep it
#signer { publicKey; sign }any DER public key with a host-supplied signing function (a key moxzid holds, a hardware key)
Identity.delegated(rootKey, chain, session)an Internet Identity delegation chain: the sender principal is the root key's, the session key signs

The principal is the self-authenticating one the IC derives (sha224(DER) ‖ 0x02); a canister's caller sees exactly it.

Everything that comes back is verified#

With Config.verify = #required (the default) the agent trusts nothing a node sends until it is checked against the root key (Agent.MAINNET_ROOT_KEY, or the one status() reports for a local replica):

#skip turns verification off and prints a loud line for every response it skips.

StepWasm instructionsHow often
One certificate (one BLS signature)1.44 billionper certificate; two under a delegation
A delegated subnet's node keys2.96 billiononce per subnet per 5 minutes
One query response's node signature155 millionper query

The agent speaks the current endpoints (v3 query, v3 read_state, v4 synchronous call) and falls back once, visibly (apiVersions()), on a gateway that lacks them. Calls to the management canister are routed to the canister their Candid argument names.

Proof#

scripts/ic_agent_gate.sh runs four layers:

  1. Known-answer tests, built by both compilers with the same result.
  2. The interface spec against mainnet (test/SpecVectors.mo, 121 checks): real mainnet query responses and certificates, root and delegated subnets, a signed rejection, every forgery and staleness case, and 64 OpenSSL Ed25519 keys and signatures.
  3. Live against PocketIC, with an NNS plus application-subnet topology so answers come under a real delegation. It covers verified queries and rejections, calls, polling, canister information, management routing, canister signatures and certified data, with agent-js as an oracle.
  4. Live against mainnet: verified queries and certified reads on the root subnet and on a delegated subnet.

The demo: a high-score table on mainnet#

demos/highscore/ is the whole shape in three files: HighScore.mo (the canister), gen/HighScore.mo (its façade, generated by moxzi agent-bindings) and Board.mo (the game side, a moxzid actor that posts scores and reads the board). Run scripts/highscore_demo_gate.sh to see it against a PocketIC live gateway; deploy the table with scripts/deploy_highscore_mainnet.sh and the same gate reads it from mainnet.

On this pageA queryAn update, without blockingTyped façadesIdentitiesEverything that comes back is verifiedProofThe demo: a high-score table on mainnet