Skip to content

Commit 560a50e

Browse files
committed
feat: add canister-calls skill — Candid discovery + consolidated canister workflows
Introduces a single skill for discovering and calling any IC canister via its Candid interface, plus curated reference files for well-known infrastructure canisters (ICRC ledgers, ckBTC minter, EVM RPC). The idea: Candid is to canisters what --help is to CLIs. Agents should be able to fetch any canister's interface and figure out how to call it. For well-known canisters where raw Candid isn't enough (multi-step workflows, fees, pitfalls), reference files provide curated knowledge. Structure: - SKILL.md: lean router with Candid discovery + registry table - references/ckbtc.md: BTC deposit/withdrawal workflows - references/icrc-ledger.md: ICRC-1/2 token transfers - references/evm-rpc.md: Ethereum/EVM JSON-RPC proxy This may supersede the standalone ckbtc, icrc-ledger, and evm-rpc skills.
1 parent fa635d3 commit 560a50e

File tree

5 files changed

+2346
-0
lines changed

5 files changed

+2346
-0
lines changed

evaluations/canister-calls.json

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
{
2+
"skill": "canister-calls",
3+
"description": "Evaluation cases for the canister-calls skill. Tests whether agents can discover canister interfaces via Candid, use curated workflows for well-known canisters, avoid common pitfalls (wrong IDs, missing fees, incomplete flows), and fall back to generic Candid discovery for unknown canisters.",
4+
5+
"output_evals": [
6+
{
7+
"name": "Discover unknown canister API",
8+
"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?",
9+
"expected_behaviors": [
10+
"Suggests fetching the Candid interface via icp canister metadata or equivalent",
11+
"Explains how to read the returned .did file (method names, types, query vs update)",
12+
"Does NOT hallucinate method names for this canister",
13+
"Mentions generating typed Rust bindings from the .did (ic-cdk-bindgen)"
14+
]
15+
},
16+
{
17+
"name": "ICRC-1 token transfer",
18+
"prompt": "I need my Motoko canister to send 1 ICP to another principal. Show me the code.",
19+
"expected_behaviors": [
20+
"Uses the ICP ledger canister ID ryjl3-tyaaa-aaaaa-aaaba-cai",
21+
"Uses icrc1_transfer (not the legacy transfer method)",
22+
"Fee is 10000 e8s (not 10000 ICP)",
23+
"Amount is in e8s (100_000_000 for 1 ICP)",
24+
"Account format is { owner: Principal; subaccount: ?Blob }, not AccountIdentifier",
25+
"Handles the TransferError variant (not just Ok)"
26+
]
27+
},
28+
{
29+
"name": "ckBTC deposit flow",
30+
"prompt": "I'm building a Rust canister that accepts BTC deposits from users. Walk me through the full flow.",
31+
"expected_behaviors": [
32+
"Uses the correct minter canister ID mqygn-kiaaa-aaaar-qaadq-cai (not the ledger ID)",
33+
"Shows the complete flow: get_btc_address -> user sends BTC -> update_balance",
34+
"Explicitly mentions that update_balance must be called (minter does not auto-detect deposits)",
35+
"Derives per-user subaccounts from the caller's principal (32 bytes, padded)",
36+
"Sets owner to the canister's own principal (not the user's principal)"
37+
]
38+
},
39+
{
40+
"name": "ckBTC withdrawal",
41+
"prompt": "My canister holds ckBTC for users in subaccounts. A user wants to withdraw 0.001 BTC to their Bitcoin address. How do I implement this in Motoko?",
42+
"expected_behaviors": [
43+
"Uses the two-step flow: icrc2_approve on ledger, then retrieve_btc_with_approval on minter",
44+
"Approve amount includes the fee (amount + 10 satoshis)",
45+
"Spender in the approve call is the minter canister",
46+
"Mentions the minimum withdrawal amount (50,000 satoshis)",
47+
"Handles error variants from both the approve and retrieve calls"
48+
]
49+
},
50+
{
51+
"name": "EVM RPC call with cycles",
52+
"prompt": "I want to read the ETH balance of a wallet address from my Motoko canister using the EVM RPC canister. Show me how.",
53+
"expected_behaviors": [
54+
"Uses the EVM RPC canister ID 7hfb6-caaaa-aaaar-qadga-cai",
55+
"Attaches cycles using 'await (with cycles = ...)' syntax (not Cycles.add)",
56+
"Handles both #Consistent and #Inconsistent result variants",
57+
"Uses #EthMainnet variant for Ethereum L1",
58+
"Does NOT forget the null config parameter"
59+
]
60+
},
61+
{
62+
"name": "Adversarial: wrong canister ID",
63+
"prompt": "I want to check a user's ckBTC balance. I'll call icrc1_balance_of on mqygn-kiaaa-aaaar-qaadq-cai, right?",
64+
"expected_behaviors": [
65+
"Corrects the canister ID — mqygn is the minter, not the ledger",
66+
"Provides the correct ledger canister ID: mxzaz-hqaaa-aaaar-qaada-cai",
67+
"Explains the difference between the minter and ledger canisters"
68+
]
69+
},
70+
{
71+
"name": "Adversarial: missing update_balance",
72+
"prompt": "I set up ckBTC deposits. I call get_btc_address, the user sends BTC, and then I show their ckBTC balance. But it always shows 0. What's wrong?",
73+
"expected_behaviors": [
74+
"Identifies the missing update_balance call as the root cause",
75+
"Explains that the minter does not auto-detect BTC deposits",
76+
"Shows how to call update_balance with the correct owner and subaccount"
77+
]
78+
},
79+
{
80+
"name": "ICRC-2 approve and transferFrom",
81+
"prompt": "I'm building a marketplace canister in Rust. When a buyer purchases an item, my canister needs to transfer ICP from the buyer to the seller. How do I do this without the buyer calling my canister with the tokens directly?",
82+
"expected_behaviors": [
83+
"Explains the ICRC-2 approve/transferFrom flow",
84+
"Buyer calls icrc2_approve on the ledger to authorize the marketplace canister",
85+
"Marketplace calls icrc2_transfer_from to move tokens from buyer to seller",
86+
"Uses correct ICP ledger canister ID",
87+
"Mentions that approve must happen before transferFrom",
88+
"Handles InsufficientAllowance error variant"
89+
]
90+
},
91+
{
92+
"name": "Call canister from frontend",
93+
"prompt": "I have a canister deployed on mainnet and I want to call its methods from my TypeScript frontend. How do I generate the bindings and set up the actor?",
94+
"expected_behaviors": [
95+
"Recommends @icp-sdk/bindgen for generating TypeScript bindings",
96+
"Mentions @icp-sdk/core for the runtime actor",
97+
"Does NOT suggest dfx generate"
98+
]
99+
}
100+
],
101+
102+
"trigger_evals": {
103+
"description": "Queries to test whether the skill activates correctly. 'should_trigger' queries should cause the skill to load; 'should_not_trigger' queries should NOT activate this skill.",
104+
"should_trigger": [
105+
"How do I call a canister I found on the dashboard?",
106+
"Send ICP tokens from my canister to another principal",
107+
"I want to accept BTC deposits in my dapp using ckBTC",
108+
"Read an ERC-20 balance from my IC canister",
109+
"What's the Candid interface of this canister?",
110+
"How do I do icrc2_approve and transferFrom?",
111+
"My ckBTC balance shows 0 after sending BTC to the deposit address",
112+
"Call the EVM RPC canister from Motoko",
113+
"I need to interact with a canister but I don't know its API",
114+
"Transfer ckETH from my canister",
115+
"How do I generate TypeScript bindings for a canister?",
116+
"Withdraw ckBTC back to a Bitcoin address"
117+
],
118+
"should_not_trigger": [
119+
"Make an HTTP request to an external API from my canister",
120+
"How do I deploy my canister to mainnet?",
121+
"Add access control to my canister methods",
122+
"How does stable memory work for canister upgrades?",
123+
"Set up Internet Identity login for my frontend",
124+
"How do I handle inter-canister call failures safely?",
125+
"Configure my icp.yaml for a Rust canister",
126+
"What's the best way to store large files on IC?",
127+
"How do I set up a custom domain for my frontend?",
128+
"Monitor my canister's cycle balance"
129+
]
130+
}
131+
}

skills/canister-calls/SKILL.md

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
---
2+
name: canister-calls
3+
description: "Discover and call any Internet Computer canister. Covers retrieving Candid interfaces from deployed canisters (both off-chain and via inter-canister calls), reading type signatures, generating typed client bindings, and constructing calls using any IC agent library. Includes curated workflows for well-known infrastructure canisters (ICRC ledgers, ckBTC minter, EVM RPC). Use when making any canister call, exploring an unfamiliar canister's API, integrating with IC infrastructure canisters, working with token transfers, ckBTC deposits/withdrawals, or Ethereum/EVM calls from IC."
4+
license: Apache-2.0
5+
compatibility: "icp-cli >= 0.1.0"
6+
metadata:
7+
title: Canister Calls & Interface Discovery
8+
category: Integration
9+
---
10+
11+
# Canister Calls & Interface Discovery
12+
13+
## What This Is
14+
15+
Every canister on the Internet Computer exposes a Candid interface — a typed API description embedded in the WASM module. Candid is to canisters what `--help` is to CLI tools: the standard way to discover what a canister can do and how to call it. This skill teaches you how to retrieve, read, and use Candid interfaces to call any canister, plus curated workflows for well-known infrastructure canisters where raw Candid alone isn't enough.
16+
17+
## Prerequisites
18+
19+
- For Motoko: `mops` package manager, `core = "2.0.0"` in mops.toml
20+
- For Rust: `ic-cdk >= 0.19`, `candid >= 0.10`
21+
- For JavaScript/TypeScript: `@icp-sdk/core` (runtime), `@icp-sdk/bindgen` (codegen)
22+
- For Rust bindings: `ic-cdk-bindgen` (build-time Candid-to-Rust codegen)
23+
24+
## Discovering a Canister's Interface
25+
26+
### From Outside IC (Off-Chain)
27+
28+
Retrieve the Candid interface of any deployed canister:
29+
30+
```bash
31+
# Fetch the .did file from a deployed canister (local or mainnet)
32+
icp canister metadata <CANISTER_ID> candid:service -e ic
33+
34+
# Example: get the ICP ledger's interface
35+
icp canister metadata ryjl3-tyaaa-aaaaa-aaaba-cai candid:service -e ic
36+
```
37+
38+
This returns the full Candid service definition with all method signatures, types, and documentation comments (if the canister author included them).
39+
40+
### From Inside a Canister (Inter-Canister)
41+
42+
When your canister needs to call another canister dynamically, you can fetch its Candid interface at runtime using the management canister:
43+
44+
```bash
45+
# The management canister exposes canister metadata
46+
icp canister call aaaaa-aa canister_metadata '(record { canister_id = principal "<CANISTER_ID>"; path = "candid:service" })' -e ic
47+
```
48+
49+
### Reading Candid Interfaces
50+
51+
A Candid interface describes:
52+
- **Method names** and whether they are `query` (fast, read-only) or `update` (consensus-based, can mutate state)
53+
- **Argument types** and **return types** — fully typed, including records, variants, optionals, vectors
54+
- **Documentation comments** (if the canister author included them, prefixed with `///` in the .did file)
55+
56+
Example Candid snippet:
57+
```candid
58+
service : {
59+
icrc1_transfer : (TransferArg) -> (variant { Ok : nat; Err : TransferError });
60+
icrc1_balance_of : (Account) -> (nat) query;
61+
icrc1_fee : () -> (nat) query;
62+
}
63+
```
64+
65+
This tells you: `icrc1_transfer` is an update call taking `TransferArg` and returning a result variant. `icrc1_balance_of` is a query call. The types (`TransferArg`, `Account`, `TransferError`) are defined elsewhere in the same .did file.
66+
67+
### Generating Typed Client Bindings
68+
69+
Each language has a dedicated tool for generating typed bindings from .did files:
70+
71+
#### Rust
72+
73+
Use `ic-cdk-bindgen` to generate typed Rust bindings from .did files at build time. Add it to your `build-dependencies` in `Cargo.toml` and configure it in `build.rs`. See https://crates.io/crates/ic-cdk-bindgen for setup.
74+
75+
#### JavaScript / TypeScript
76+
77+
Use `@icp-sdk/bindgen` to generate typed JS/TS bindings from .did files:
78+
79+
```bash
80+
npx @icp-sdk/bindgen --canister <CANISTER_ID> -e ic
81+
```
82+
83+
See https://www.npmjs.com/package/@icp-sdk/bindgen for options.
84+
85+
### Calling Any Canister via CLI
86+
87+
Once you know the method signature from the Candid interface:
88+
89+
```bash
90+
# Call any method on any canister
91+
icp canister call <CANISTER_ID> <METHOD_NAME> '(<CANDID_ARGS>)' -e ic
92+
93+
# Query call (faster, read-only)
94+
icp canister call <CANISTER_ID> <METHOD_NAME> '(<CANDID_ARGS>)' --query -e ic
95+
```
96+
97+
### Calling Any Canister from Code
98+
99+
#### Motoko — Dynamic Actor Reference
100+
101+
```motoko
102+
// Reference a remote canister by principal with a typed interface
103+
transient let remote = actor ("aaaaa-bbbbb-ccccc-ddddd-cai") : actor {
104+
some_method : shared (Nat) -> async Text;
105+
some_query : shared query () -> async Nat;
106+
};
107+
108+
// Call it
109+
let result = await remote.some_method(42);
110+
```
111+
112+
#### Rust — Using ic-cdk Call API
113+
114+
```rust
115+
use ic_cdk::call::Call;
116+
use candid::Principal;
117+
118+
let canister_id = Principal::from_text("aaaaa-bbbbb-ccccc-ddddd-cai").unwrap();
119+
120+
// Unbounded wait (guaranteed response)
121+
let (result,): (String,) = Call::unbounded_wait(canister_id, "some_method")
122+
.with_arg(42u64)
123+
.await
124+
.expect("Call failed")
125+
.candid_tuple()
126+
.expect("Decode failed");
127+
```
128+
129+
## What Candid Doesn't Tell You
130+
131+
Candid gives you the shape of an API but not the workflow. For well-known infrastructure canisters, you need to know:
132+
- **Which canisters to call and in what order** (e.g., ckBTC deposit is a multi-step flow across minter + ledger)
133+
- **Cycle costs** (e.g., EVM RPC requires cycles attached to calls)
134+
- **Fee amounts and units** (e.g., ICP fee is 10,000 e8s, not 10,000 ICP)
135+
- **Pitfalls that cause silent failures** (e.g., forgetting `update_balance` after a BTC deposit)
136+
137+
The reference files below contain this curated knowledge for each well-known canister.
138+
139+
## Well-Known Canister Registry
140+
141+
| Canister | ID (Mainnet) | What It Does | Reference |
142+
|----------|-------------|-------------|-----------|
143+
| ICP Ledger | `ryjl3-tyaaa-aaaaa-aaaba-cai` | ICP token transfers, balances, ICRC-1/2 | `references/icrc-ledger.md` |
144+
| ckBTC Ledger | `mxzaz-hqaaa-aaaar-qaada-cai` | ckBTC token transfers | `references/icrc-ledger.md` |
145+
| ckBTC Minter | `mqygn-kiaaa-aaaar-qaadq-cai` | BTC deposit/withdrawal via ckBTC | `references/ckbtc.md` |
146+
| ckETH Ledger | `ss2fx-dyaaa-aaaar-qacoq-cai` | ckETH token transfers | `references/icrc-ledger.md` |
147+
| EVM RPC | `7hfb6-caaaa-aaaar-qadga-cai` | Ethereum/EVM JSON-RPC proxy | `references/evm-rpc.md` |
148+
149+
**For any canister not listed here**, use the Candid discovery flow above: fetch the .did, read the types, generate bindings, and call.
150+
151+
### When to Read a Reference File
152+
153+
- **Making token transfers (ICP, ckBTC, ckETH)** or working with **ICRC-1/ICRC-2 approve/transferFrom** -> Read `references/icrc-ledger.md`
154+
- **Integrating Bitcoin** (BTC deposits, ckBTC minting, BTC withdrawals) -> Read `references/ckbtc.md`
155+
- **Calling Ethereum/EVM chains** (ETH balances, ERC-20 reads, sending transactions) -> Read `references/evm-rpc.md`

0 commit comments

Comments
 (0)