Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 109 additions & 0 deletions evaluations/canister-calls.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
{
"skill": "canister-calls",
"description": "Evaluation cases for the canister-calls skill. Tests whether agents can discover canister interfaces via Candid, generate typed bindings, handle Candid ↔ JS/TS type mapping correctly (variant as object, opt as T | null, nat as BigInt), initialize clients with the correct agentOptions pattern, and use ic-cdk 0.19+ Call API for Rust inter-canister calls.",

"output_evals": [
{
"name": "Discover unknown canister API",
"prompt": "I found this canister on mainnet: rdmx6-jaaaa-aaaaa-aaadq-cai. I want to call it from my Rust canister but I have no idea what methods it exposes. How do I figure out its API?",
"expected_behaviors": [
"Suggests fetching the Candid interface via `icp canister metadata <ID> candid:service -e ic`",
"Explains how to read the returned .did file (method names, types, query vs update)",
"Does NOT hallucinate method names for this canister",
"Mentions generating typed Rust bindings from the .did (ic-cdk-bindgen or manual)"
]
},
{
"name": "Variant type handling in TypeScript",
"prompt": "My canister method returns `variant { Ok : nat64; Err : text }`. After calling it from TypeScript I check `if (result === 'Ok')` but it never matches. What's wrong?",
"expected_behaviors": [
"Explains that Candid variants are objects in JS/TS, not strings",
"Shows the correct check: `if ('Ok' in result)` or `'Ok' in result`",
"Shows how to access the value: `result.Ok` (not `result`)",
"Mentions that nat64 is BigInt in JS/TS"
]
},
{
"name": "opt T handling in TypeScript",
"prompt": "My canister returns `opt text` for a query method. I'm checking `if (result.length > 0)` to see if a value is present, but TypeScript is complaining. What's the right pattern?",
"expected_behaviors": [
"Explains that @icp-sdk/bindgen wraps opt T as T | null, not [] | [T]",
"Shows the correct check: `if (result !== null)`",
"Explains that [] | [T] is the raw Candid encoding used by @dfinity/agent"
]
},
{
"name": "Generate JS/TS bindings and initialize actor",
"prompt": "I have a .did file for my backend canister at `./backend/backend.did`. I'm using Vite. Show me how to generate TypeScript bindings and create an authenticated actor after Internet Identity login.",
"expected_behaviors": [
"Shows the Vite plugin approach: `icpBindgen({ didFile, outDir })` in vite.config.js",
"Uses `createActor(canisterId, { agentOptions })` — NOT `{ agent }`",
"Passes `identity` from `authClient.getIdentity()` inside `agentOptions`",
"Uses `rootKey: canisterEnv?.IC_ROOT_KEY` from `safeGetCanisterEnv()`"
]
},
{
"name": "Rust inter-canister call (ic-cdk >= 0.19)",
"prompt": "I want to call another canister's `greet(name: text) -> (text)` method from my Rust canister. Show me the code.",
"expected_behaviors": [
"Uses `Call::unbounded_wait` or `Call::bounded_wait` from `ic_cdk::call`",
"Uses `.with_arg()` and `.candid_tuple()` for encoding/decoding",
"Does NOT use `ic_cdk::call()` or `Call::new()` — these do not exist in ic-cdk >= 0.19"
]
},
{
"name": "Motoko dynamic actor reference",
"prompt": "I want to call a method on a canister from my Motoko canister. I have the canister ID and the method signature. Show me how.",
"expected_behaviors": [
"Uses `actor (\"<canister-id>\") : actor { ... }` syntax",
"Shows the method typed with `shared` or `shared query` keyword",
"Calls the method with `await`"
]
},
{
"name": "Adversarial: { agent } instead of { agentOptions }",
"prompt": "I generated TypeScript bindings with @icp-sdk/bindgen. I'm passing my authenticated HttpAgent like this: `createActor(canisterId, { agent: myAgent })`. The calls work but always come back as if I'm anonymous. Why?",
"expected_behaviors": [
"Identifies that `{ agent }` is the wrong parameter — @icp-sdk/bindgen uses `{ agentOptions }`",
"Explains that passing `{ agent }` silently falls back to anonymous identity",
"Shows the correct pattern: `createActor(canisterId, { agentOptions: { identity, host, rootKey } })`"
]
},
{
"name": "Adversarial: ic_cdk::call() removed",
"prompt": "I'm trying to call another canister from my Rust canister using `ic_cdk::call(canister_id, \"method\", (arg,)).await` but it doesn't compile. How do I fix this?",
"expected_behaviors": [
"Explains that `ic_cdk::call()` was removed in ic-cdk 0.19",
"Shows the replacement: `Call::unbounded_wait(canister_id, \"method\").with_arg(arg).await`",
"Imports `use ic_cdk::call::Call`"
]
}
],

"trigger_evals": {
"description": "Queries to test whether the skill activates correctly.",
"should_trigger": [
"I have a .did file, how do I generate TypeScript bindings?",
"How do I generate bindings for a canister and call it from JavaScript?",
"Candid opt type is showing as array in TypeScript",
"How do I handle a variant return type in TypeScript?",
"nat64 type from Candid in JavaScript",
"Generate TypeScript bindings for a canister on mainnet",
"How do I create an actor from generated bindings in JavaScript?",
"My TypeScript canister call always returns anonymous even though I'm logged in",
"How do I use @icp-sdk/bindgen to call a canister from my frontend?"
],
"should_not_trigger": [
"Send ICP tokens from my canister to another principal",
"I want to accept BTC deposits in my dapp using ckBTC",
"Read an ERC-20 balance from my IC canister",
"Make an HTTP request to an external API from my canister",
"How do I deploy my canister to mainnet?",
"Add access control to my canister methods",
"How does stable memory work for canister upgrades?",
"Set up Internet Identity login for my frontend",
"Configure my icp.yaml for a Rust canister",
"How do I handle inter-canister call failures?"
]
}
}
204 changes: 204 additions & 0 deletions skills/canister-calls/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
---
name: canister-calls
description: "Generate typed bindings from a Candid interface and call a canister from JavaScript/TypeScript, Rust, or Motoko. Covers Candid ↔ JS/TS type mapping (nat as BigInt, opt as T | null, variant as object not string), binding generation with @icp-sdk/bindgen or ic-cdk-bindgen, and actor/client initialization. Use when you have a canister ID or .did file and need to call it from frontend or canister code, or when handling Candid types in TypeScript. Do NOT use for token transfer workflows — use icrc-ledger instead. Do NOT use for ckBTC — use ckbtc instead. Do NOT use for EVM JSON-RPC — use evm-rpc instead. Do NOT use for inter-canister call patterns and error handling — use multi-canister instead."
license: Apache-2.0
compatibility: "icp-cli >= 0.2.2, Node.js >= 22"
metadata:
title: Canister Calls & Bindings
category: Integration
---

# Canister Calls & Bindings

## What This Is

Every canister on the Internet Computer exposes a Candid interface — a typed API description embedded in the WASM module. This skill covers how to fetch that interface, generate typed bindings for your language, and call the canister from JavaScript/TypeScript, Rust, or Motoko.

## Prerequisites

- **JavaScript/TypeScript**: `@icp-sdk/core` (>= 5.0.0), `@icp-sdk/bindgen` (>= 0.3.0)
- **Rust**: `ic-cdk` (>= 0.19), `candid` (>= 0.10), `ic-cdk-bindgen` (build-time, optional)
- **CLI**: `icp-cli` (>= 0.2.2)

## Fetching the Candid Interface

```bash
# Fetch from mainnet and save to a .did file
icp canister metadata <CANISTER_ID> candid:service -e ic > mycanister.did

# Fetch from local replica
icp canister metadata <CANISTER_ID> candid:service > mycanister.did
```

This returns the full Candid service definition: method names, argument and return types, and whether each method is `query` or `update`.

## Reading a Candid Interface

```candid
service : {
// update — goes through consensus, can mutate state (~2s)
submit : (OrderRequest) -> (variant { Ok : OrderId; Err : Text });

// query — fast read-only, does not go through consensus (~200ms)
get_order : (OrderId) -> (opt Order) query;

// vec = array, nat = BigInt in JS
list_orders : () -> (vec Order) query;
}
```

All referenced types (`OrderRequest`, `Order`, etc.) are defined earlier in the same `.did` file.

## Candid ↔ JavaScript/TypeScript Type Mapping

Agents with `@dfinity/agent` background frequently get these wrong:

| Candid type | JS/TS type (bindgen wrapper) | Common mistake |
|-------------|------------------------------|----------------|
| `nat`, `nat64` | `BigInt` | Using `number` — silent overflow for large values |
| `nat32`, `nat16`, `nat8` | `number` | Fine as `number` |
| `opt T` | `T \| null` | Using `[] \| [T]` — raw Candid encoding; bindgen wrapper converts to `T \| null` |
| `variant { Ok : T; Err : E }` | `{ Ok: T } \| { Err: E }` | Checking `result === 'Ok'` — variants are objects, not strings |
| `record { field : T }` | `{ field: T }` | — |
| `vec T` | `Array<T>` | — |
| `principal` | `Principal` | Passing as string — use `Principal.fromText()` |
| `blob` | `Uint8Array` | — |

### Variant handling

```typescript
const result = await actor.submit(request);

// Wrong — variants are objects, not strings
if (result === "Ok") { ... }

// Correct — check for the key
if ("Ok" in result) {
console.log("Order ID:", result.Ok);
} else {
console.error("Error:", result.Err);
}
```

### opt T handling

```typescript
const order = await actor.get_order(orderId);

// Wrong — raw Candid array style (@dfinity/agent legacy)
if (order.length > 0) { const o = order[0]; }

// Correct — bindgen wrapper converts opt T to T | null
if (order !== null) {
console.log(order.status);
}
```

## Generating Typed Bindings

### JavaScript / TypeScript — Vite plugin (recommended)

```js
// vite.config.js
import { icpBindgen } from "@icp-sdk/bindgen/plugins/vite";

export default defineConfig({
plugins: [
icpBindgen({
didFile: "../backend/backend.did",
outDir: "./src/bindings",
}),
],
});
```

Each `icpBindgen()` generates a `<name>.ts` file in `outDir` with a `createActor` function. The `.did` file must be committed to the repo — see the **icp-cli** skill for how to configure `candid:` in the recipe so the `.did` is generated at build time.

### JavaScript / TypeScript — CLI (non-Vite)

```bash
npx @icp-sdk/bindgen --did ./mycanister.did --out ./src/bindings
```

### Rust

Use `ic-cdk-bindgen` in `build.rs` to generate Rust types from `.did` files at build time. See https://crates.io/crates/ic-cdk-bindgen for setup.

## Initializing a Client

### JavaScript / TypeScript

```typescript
import { createActor } from "./bindings/backend";
import { safeGetCanisterEnv } from "@icp-sdk/core/agent/canister-env";

const canisterEnv = safeGetCanisterEnv();

// Unauthenticated actor
const actor = createActor(canisterId, {
agentOptions: {
host: window.location.origin,
rootKey: canisterEnv?.IC_ROOT_KEY,
},
});

// Authenticated actor (after Internet Identity login)
const authedActor = createActor(canisterId, {
agentOptions: {
identity, // from authClient.getIdentity()
host: window.location.origin,
rootKey: canisterEnv?.IC_ROOT_KEY,
},
});
```

**Pitfall:** passing `{ agent }` instead of `{ agentOptions }`. The `createActor` generated by `@icp-sdk/bindgen` takes `{ agentOptions }` and creates the agent internally. Passing `{ agent }` silently falls back to an anonymous identity — calls return empty data or access denied with no error.

### Rust — Inter-Canister Call (ic-cdk >= 0.19)

```rust
use ic_cdk::call::Call;
use candid::Principal;

let canister_id = Principal::from_text("aaaaa-bbbbb-ccccc-ddddd-cai").unwrap();

// unbounded_wait: no timeout, always gets a response or rejection
let (result,): (String,) = Call::unbounded_wait(canister_id, "get_greeting")
.with_arg("world")
.await
.expect("call failed")
.candid_tuple()
.expect("decode failed");

// bounded_wait: completes when the called canister responds or times out
let (result,): (String,) = Call::bounded_wait(canister_id, "get_greeting")
.with_arg("world")
.await
.expect("call failed or timed out")
.candid_tuple()
.expect("decode failed");
```

**Pitfall:** `ic_cdk::call()` and `Call::new()` do not exist in ic-cdk >= 0.19. Use `Call::unbounded_wait` or `Call::bounded_wait`.

### Motoko — Dynamic Actor Reference

```motoko
// Type the remote interface inline — no .did file needed at compile time
transient let remote = actor ("aaaaa-bbbbb-ccccc-ddddd-cai") : actor {
get_greeting : shared query (Text) -> async Text;
submit : shared (OrderRequest) -> async { #Ok : OrderId; #Err : Text };
};

let greeting = await remote.get_greeting("world");
```

## Calling via CLI

```bash
# Update call
icp canister call <CANISTER_ID> <METHOD> '(<CANDID_ARGS>)' -e ic

# Query call (faster)
icp canister call <CANISTER_ID> <METHOD> '(<CANDID_ARGS>)' --query -e ic
```
Loading