Ic-agent-mo - IC agent in Motoko

Introducing ic-agent — an Internet Computer agent written in Motoko

Hi everyone,

We just published ic-agent to mops: a client for the Internet Computer’s HTTP interface,
written in Motoko. It does what agent-js or the Rust ic-agent do: sign requests, submit
calls, poll for results and read state. It verifies everything it receives:
certificates and query responses alike.

The library was funded by ICDevs.org (a 501c3 non-profit). If you find it useful, please
consider chipping in at https://icdevs.org/donation.html.

mops add ic-agent

Repo: https://github.com/icdevsorg/ic-agent-mo (release v0.2.0)


Why a Motoko agent?

Inside a canister you never need one — you call another canister with await. But Motoko
increasingly runs outside canisters: we’ve been supporting moxzi, a
self-hosted Motoko compiler with off-chain runtimes — a server (moxzid), a browser runtime,
and a game player — that run the same compiled actors you’d deploy on the IC. Those actors
need to talk to real canisters: read a ledger, post a high score, call a DAO. That’s an
agent’s job, and until now it meant leaving Motoko.

ic-agent runs anywhere an actor can make HTTPS outcalls (through the management canister’s
http_request, or a transport the host plugs in).

What it does

import Agent "mo:ic-agent";
import Identity "mo:ic-agent/Identity";

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; /* "Internet Computer" */ };
  case (#err e) { /* typed error */ };
};
  • Calls: queryCall, call, readState, status — and call split into its two
    halves, submit + poll. submit returns the request id immediately and poll is one
    read_state round trip, so code that must not block (a game loop, a scheduler) submits on
    one tick and polls on later ones. call uses the synchronous v4 call endpoint and only
    falls back to polling when the replica answers 202.

  • Identities: anonymous, ed25519, secp256k1, any Signer the host supplies, and
    Internet Identity delegation chains. Principals are the standard self-authenticating
    ones.

  • Everything verified, by default. Certificates (from call, poll, readState and the
    canister-info reads) are checked for their BLS signature, a delegation scoped to the
    canister or subnet you asked about, a well-formed tree, and a fresh /time. Query
    replies and query rejections are checked against the node signatures they carry,
    using node keys from a verified /subnet certificate. #skip exists, and prints a loud
    warning on every response it skips.

  • The rest of the HTTPS interface: v3 query, v3 read_state and v4 synchronous call, with a
    visible fallback for older gateways; subnet-scoped read_state, query and call; management
    canister calls routed to the right effective canister id; certified module_hash,
    controllers and metadata; sender_info; and helpers to verify certified data and
    canister signatures, which is how Internet Identity signs.

  • Typed façades: with the moxzi CLI, moxzi agent-bindings ledger.did --name Ledger
    generates a Motoko module with every type of the interface and a proxy object whose methods
    encode arguments, go out as queries or calls as the .did says, and decode the reply — you
    never touch Candid:

    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) … };
    

Performance

Verification is the expensive part of an agent. ic-agent uses our
bls12-381 library for certificates and a small Ed25519 verifier
of its own (RFC 8032 signing and verification) for node signatures and ed25519 identities. Measured on moxzid:

Step Wasm instructions How often
One certificate (one BLS signature) 1.44 billion per certificate; two under a delegation
A delegated subnet’s node keys 2.96 billion once per subnet per 5 minutes
One query response’s node signature 155 million per query

The IC allows 40 billion instructions per update message and 5 billion per query, so every
step fits in a single message.

How it’s tested

  • Known-answer tests, built by both moc and moxzi with identical results: request ids
    from the interface spec and agent-js, a signed envelope byte for byte, ed25519 and
    secp256k1 keys, principals and signatures, hash trees, and BLS hash-to-curve vectors.
  • The spec against real mainnet data: captured query responses and certificates from the
    ICP ledger (root subnet), ckBTC (a delegated subnet) and an Internet Identity rejection.
    Their node signatures must verify. Tampered replies, wrong request ids, other subnets’
    keys, stale timestamps, out-of-scope delegations and a wrong root key must all fail. 64
    Ed25519 keys and signatures made by OpenSSL must be reproduced byte for byte, verify, and
    fail when altered.
  • Live against PocketIC, with an NNS subnet plus an application subnet so every answer
    comes under a real delegation. It covers verified queries and rejections, update calls,
    submit plus poll, canister info, management calls, a canister signature and certified data
    issued by a real canister. agent-js reads the same canister as an independent oracle.
  • Live against mainnet: verified queries and certified reads on the root subnet and on a
    delegated subnet.

Limits (0.2.0)

  • No update calls on mainnet in CI. Update calls are proven against PocketIC; the mainnet
    tests are read-only and anonymous.
  • No browser transport yet. Use HTTPS outcalls or a transport the host supplies.
  • No native P-256 or WebAuthn signing. A host can supply those as a Signer.
  • The subnet call endpoint is untested live. PocketIC’s gateway does not serve it, and it
    only creates canisters.
  • secp256k1 signing builds its generator table once per identity, so keep the identity around.

If you used 0.1.0

Upgrade. 0.1.0 trusted query responses without checking their node signatures, and its
ed25519 identities sent an invalid signature on about 1 request in 24. That second problem
came from the ed25519 0.1.0 mops package: shifting zero right returned 255, which breaks
signing whenever the nonce has a zero top byte. We sent the one-character fix upstream
(imsk17/ed25519#1). If you depend on that package
directly, your signatures are affected too.

0.2.0 has two small breaking changes: Config has two new fields, so build it with
{ Agent.defaults(host, identity) with … }, and poll takes the request’s effective canister
id, which is the same canister as before except for calls to aaaaa-aa.

Issues and PRs welcome. If you’re running Motoko off-chain and want your actors to talk to the
IC, we’d love to hear what you build.

1 Like