diff --git a/evaluations/canister-calls.json b/evaluations/canister-calls.json new file mode 100644 index 0000000..2c62f48 --- /dev/null +++ b/evaluations/canister-calls.json @@ -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 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 (\"\") : 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?" + ] + } +} diff --git a/skills/canister-calls/SKILL.md b/skills/canister-calls/SKILL.md new file mode 100644 index 0000000..92a0493 --- /dev/null +++ b/skills/canister-calls/SKILL.md @@ -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 candid:service -e ic > mycanister.did + +# Fetch from local replica +icp canister metadata 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` | — | +| `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 `.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 '()' -e ic + +# Query call (faster) +icp canister call '()' --query -e ic +```