moxzi
Docs / Server / Calling the Internet Computer

Calling the Internet Computeralpha

Use mo:ic-agent to query and call canisters on the Internet Computer from an actor running in moxzid, a browser or a game.

An actor running outside the Internet Computer can call canisters on it with the ic-agent package. It does what @dfinity/agent does in JavaScript: it signs requests, sends them to an IC gateway over HTTPS, and verifies the certificates and signatures in the replies. It works in moxzid, in moxzi-web worker mode, and in games that declare the http capability.

actor "…" references still refer to actors in the same runtime. Use the agent to reach a canister on the IC.

Set up#

Add the package:

mops.toml
[dependencies]
core = "2.5.0"
ic-agent = "0.2.0"

The agent uses HTTPS outcalls, which moxzid refuses unless you grant them. Start it with --outcall-public-host icp-api.io to allow the one host the agent talks to:

moxzid -m actors.json --outcall-public-host icp-api.io

A query#

Main.mo
transient let ledgerId = Principal.fromText("ryjl3-tyaaa-aaaaa-aaaba-cai");

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

queryCall takes the canister id, the method name and the Candid-encoded argument, and returns the Candid-encoded reply. The agent itself is created once:

Main.mo
transient let agent = Agent.Agent(Agent.defaults("https://icp-api.io", #anonymous));

A typed client#

moxzi agent-bindings generates a Motoko module from a canister's Candid interface, with one method per canister method:

ledger.did
type Account = record { owner : principal; subaccount : opt blob };
service : {
  icrc1_name : () -> (text) query;
  icrc1_symbol : () -> (text) query;
  icrc1_decimals : () -> (nat8) query;
  icrc1_balance_of : (Account) -> (nat) query;
}
moxzi agent-bindings ledger.did --name Ledger -o Ledger.mo
agent-bindings: ledger.did -> Ledger.mo
Main.mo
transient let ledger = Ledger.connect(agent, ledgerId);

public func symbol() : async Text {
  switch (await* ledger.icrc1_symbol()) {
    case (#ok symbol) symbol;
    case (#err e) debug_show e;
  }
};

public func decimals() : async Nat8 {
  switch (await* ledger.icrc1_decimals()) {
    case (#ok n) n;
    case (#err _) 0;
  }
};

Each method returns #ok with the decoded result, or #err with what went wrong. Query methods are sent as queries and update methods as calls. Update methods also have m_submit and m_poll variants (see below).

Build and call the actor:

moxzi build Main.mo --stack-budget 20000
curl -s -w '\n' -d '[]' http://127.0.0.1:7000/call-json/client/name
curl -s -w '\n' -d '[]' http://127.0.0.1:7000/call-json/client/symbol
curl -s -w '\n' -d '[]' http://127.0.0.1:7000/call-json/client/decimals
["Internet Computer"]
["ICP"]
[8]

The generated object isn't an actor reference: you call its methods with await*, not await.

Update calls#

An update call on the IC takes a few seconds. agent.call(canister, method, arg, maxPolls) sends it and waits for the result. To avoid holding a message open that long, send the call with submit and check on it later with poll:

Main.mo
// Send an update call and return at once. Poll for the result on later messages.
var pending : ?(Principal, Agent.RequestId) = null;

public func startIncrement(counter : Principal) : async Text {
  switch (await* agent.submit(counter, "inc", to_candid ())) {
    case (#ok rid) { pending := ?(counter, rid); "submitted" };
    case (#err e) debug_show e;
  }
};

public func checkIncrement() : async Text {
  let ?(counter, rid) = pending else return "nothing pending";
  switch (await* agent.poll(counter, rid)) {
    case (#ok(#replied bytes)) {
      pending := null;
      let ?n : ?Nat = from_candid (bytes) else return "unexpected reply";
      "count is " # debug_show n
    };
    case (#ok(#pending _)) "still running";
    case (#ok(#rejected r)) { pending := null; "rejected: " # r.message };
    case (#ok(#done or #unknown)) { pending := null; "result no longer available" };
    case (#err e) debug_show e;
  }
};
poll resultMeaning
#replied bytesFinished. bytes is the Candid-encoded reply
#pendingStill running. Poll again later
#rejected rThe canister or the system rejected the call
#done, #unknownThe IC no longer has the result, or never received the request

Identities#

The agent signs requests as the identity in its configuration:

IdentityCreated with
Anonymous#anonymous
Ed25519 keyIdentity.ed25519(seed), from a 32-byte seed
secp256k1 keyIdentity.secp256k1(secret), from a 32-byte secret key
Any key with an external signer#signer { publicKey; sign }, with a DER public key and a signing function
Internet Identity delegationIdentity.delegated(rootKey, chain, session)
Main.mo
// An agent that signs as a fixed Ed25519 key. Keep the seed secret.
public func signingAgentPrincipal(seed : Blob) : async Principal {
  let id = Identity.ed25519(seed);
  let _signed = Agent.Agent(Agent.defaults("https://icp-api.io", id));
  Identity.principal(id)
};
curl -s -w '\n' http://127.0.0.1:7000/call-json/client/signingAgentPrincipal \
  -d '["AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="]'
["535yc-uxytb-gfk7h-tny7p-vjkoe-i4krp-3qmcl-uqfgr-cpgej-yqtjq-rqe"]

The principal is the one the IC derives from the public key, so a canister sees the same msg.caller it would see from dfx or @dfinity/agent with the same key.

Import the identity helpers with import Identity "mo:ic-agent/Identity".

Verification#

By default the agent verifies everything before returning it:

A failed check is returned as #err(#certificate …) or #err(#querySignature …). Verification costs compute: about 2 billion instructions to check a mainnet certificate (3.8 billion with a subnet delegation), and about 155 million per query reply. Node keys are cached for five minutes per subnet.

ic-agent has a native kernel for the BLS signature check. With it, a certificate check costs about 0.37 billion instructions (0.61 billion with a delegation). To opt in, run moxzi kernel add ic-agent in your application's directory and build with moxzi. Under moc or without the opt-in, the Motoko code runs and gives the same results.

To talk to a local replica, set rootKey in the configuration to the replica's key. Setting verify = #skip turns verification off and prints a warning for every reply.

Limitations#

LimitationWhat to do
Replies larger than 2 MB aren't supportedPage through large results
Verifying a certificate takes noticeable time in a browser tabCache results instead of re-reading them
On this pageSet upA queryA typed clientUpdate callsIdentitiesVerificationLimitations