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:
[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#
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:
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:
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
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:
// 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 result | Meaning |
|---|---|
#replied bytes | Finished. bytes is the Candid-encoded reply |
#pending | Still running. Poll again later |
#rejected r | The canister or the system rejected the call |
#done, #unknown | The IC no longer has the result, or never received the request |
Identities#
The agent signs requests as the identity in its configuration:
| Identity | Created with |
|---|---|
| Anonymous | #anonymous |
| Ed25519 key | Identity.ed25519(seed), from a 32-byte seed |
| secp256k1 key | Identity.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 delegation | Identity.delegated(rootKey, chain, session) |
// 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:
- Certificates (for update calls,
polland state reads): the BLS signature, any subnet delegation, and that the certificate is less than five minutes old. - Query replies: each node's signature, checked against node keys from a verified subnet certificate.
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#
| Limitation | What to do |
|---|---|
| Replies larger than 2 MB aren't supported | Page through large results |
| Verifying a certificate takes noticeable time in a browser tab | Cache results instead of re-reading them |