Skip to content
Merged
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
66 changes: 66 additions & 0 deletions evaluations/wallet-integration.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
{
"skill": "wallet-integration",
"description": "Evaluation cases for the wallet-integration skill. Tests whether agents produce correct ICRC signer protocol code, avoid top-level await, and use the right wallet classes.",

"output_evals": [
{
"name": "No top-level await in wallet code",
"prompt": "Show me just the JavaScript code to connect an ICRC wallet and make a single token transfer. I'm using Vite with default settings. Keep it minimal — no signer-side code, no deploy steps.",
"expected_behaviors": [
"All await calls are inside async functions — no bare top-level await at module scope",
"Uses IcrcWallet or IcpWallet connect pattern",
"Shows wallet.transfer or wallet.icrc1Transfer inside an async function",
"Does NOT recommend changing build.target to 'esnext' or 'es2022' in Vite config"
]
},
{
"name": "IcpWallet vs IcrcWallet selection",
"prompt": "I want to send ICP tokens from my frontend using a wallet. Which class should I use? Just the class name, import path, and a one-line explanation of when to use each.",
"expected_behaviors": [
"Recommends IcpWallet for ICP ledger operations",
"Explains that IcpWallet does not require ledgerCanisterId (defaults to ICP ledger)",
"Explains that IcrcWallet is for any ICRC ledger and requires ledgerCanisterId",
"Shows the correct import from 'oisy-wallet-signer'"
]
},
{
"name": "Error handling pattern",
"prompt": "How do I handle errors when the user rejects a wallet transaction? Just show the try/catch pattern with the relevant error types.",
"expected_behaviors": [
"Shows try/catch around wallet operations",
"Mentions RelyingPartyResponseError with error codes (3000, 3001, 4000)",
"Mentions RelyingPartyDisconnectedError for popup closure"
]
},
{
"name": "Signer implementation",
"prompt": "Show me the minimal code to initialize a Signer and register all four required prompts (permissions, accounts, consent message, call canister). Just the signer-side setup, no dApp/relying-party code.",
"expected_behaviors": [
"Uses Signer.init() with owner identity and host",
"Shows signer.register() for each prompt type",
"Registers ICRC25_REQUEST_PERMISSIONS and ICRC27_ACCOUNTS prompts",
"Registers ICRC21_CALL_CONSENT_MESSAGE and ICRC49_CALL_CANISTER prompts"
]
}
],

"trigger_evals": {
"description": "Queries to test whether the skill activates correctly.",
"should_trigger": [
"Connect a wallet to my ICP dapp",
"How do I implement ICRC wallet signing?",
"I need to integrate Oisy wallet into my frontend",
"How does the ICRC signer protocol work?",
"Add wallet connect to my dapp",
"Implement the relying party side of wallet integration"
],
"should_not_trigger": [
"Add Internet Identity login to my app",
"How do I deploy my canister?",
"Set up stable memory in Rust",
"How do I make inter-canister calls?",
"Create an ICRC-1 token ledger",
"How do I use passkeys for authentication?"
]
}
}
171 changes: 97 additions & 74 deletions skills/wallet-integration/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,9 @@ This skill covers integration using `@dfinity/oisy-wallet-signer`. Other integra

## Prerequisites

- `@dfinity/oisy-wallet-signer` installed
- Peer dependencies installed: `@dfinity/utils`, `@dfinity/zod-schemas`, `@icp-sdk/canisters`, `@icp-sdk/core`, `zod`
- `@dfinity/oisy-wallet-signer` (>= 4.1.0)
- Peer dependencies: `@dfinity/utils` (>= 4.2.0), `@dfinity/zod-schemas` (>= 3.2.0), `@icp-sdk/canisters` (>= 3.5.0), `@icp-sdk/core` (>= 5.0.0), `zod`
- A non-anonymous identity on the signer side (e.g. `Ed25519KeyIdentity`)
- For local development: a running local network (`icp network start -d`)

```bash
npm i @dfinity/oisy-wallet-signer @dfinity/utils @dfinity/zod-schemas @icp-sdk/canisters @icp-sdk/core zod
Expand Down Expand Up @@ -157,95 +156,110 @@ import {IcrcWallet} from '@dfinity/oisy-wallet-signer/icrc-wallet';

#### Connect, Permissions, Accounts

All wallet operations are async. Wrap them in functions — do not use top-level `await`, which fails with Vite's default `es2020` build target.

```typescript
const wallet = await IcrcWallet.connect({
url: 'https://your-wallet.example.com/sign', // URL of the wallet implementing the signer
host: 'https://icp-api.io',
windowOptions: {width: 576, height: 625, position: 'center'},
connectionOptions: {timeoutInMilliseconds: 120_000},
onDisconnect: () => {
/* wallet popup closed */
}
});
// Wrapping in an async function avoids top-level await, which requires
// build.target >= es2022. This works with any bundler target.
async function connectWallet() {
const wallet = await IcrcWallet.connect({
url: 'https://your-wallet.example.com/sign', // URL of the wallet implementing the signer
host: 'https://icp-api.io',
windowOptions: {width: 576, height: 625, position: 'center'},
connectionOptions: {timeoutInMilliseconds: 120_000},
onDisconnect: () => {
/* wallet popup closed */
}
});

const {allPermissionsGranted} = await wallet.requestPermissionsNotGranted();
const {allPermissionsGranted} = await wallet.requestPermissionsNotGranted();

const accounts = await wallet.accounts();
const {owner} = accounts[0];
const accounts = await wallet.accounts();
const {owner} = accounts[0];
return {wallet, owner};
}
```

#### IcpWallet — ICP Transfers and Approvals

Uses `{owner, request}` — no `ledgerCanisterId` needed.

```typescript
const wallet = await IcpWallet.connect({url: 'https://your-wallet.example.com/sign'});
const accounts = await wallet.accounts();
const {owner} = accounts[0];

await wallet.icrc1Transfer({
owner,
request: {to: {owner: recipientPrincipal, subaccount: []}, amount: 100_000_000n}
});

await wallet.icrc2Approve({
owner,
request: {spender: {owner: spenderPrincipal, subaccount: []}, amount: 500_000_000n}
});
async function icpWalletTransfers() {
const wallet = await IcpWallet.connect({url: 'https://your-wallet.example.com/sign'});
const accounts = await wallet.accounts();
const {owner} = accounts[0];

await wallet.icrc1Transfer({
owner,
request: {to: {owner: recipientPrincipal, subaccount: []}, amount: 100_000_000n}
});

await wallet.icrc2Approve({
owner,
request: {spender: {owner: spenderPrincipal, subaccount: []}, amount: 500_000_000n}
});
}
```

#### IcrcWallet — Any ICRC Ledger

Uses `{owner, ledgerCanisterId, params}` — `ledgerCanisterId` is **required**.

```typescript
const wallet = await IcrcWallet.connect({url: 'https://your-wallet.example.com/sign'});
const accounts = await wallet.accounts();
const {owner} = accounts[0];

await wallet.transfer({
owner,
ledgerCanisterId: 'mxzaz-hqaaa-aaaar-qaada-cai',
params: {to: {owner: recipientPrincipal, subaccount: []}, amount: 1_000_000n}
});

await wallet.approve({
owner,
ledgerCanisterId: 'mxzaz-hqaaa-aaaar-qaada-cai',
params: {spender: {owner: spenderPrincipal, subaccount: []}, amount: 5_000_000n}
});

await wallet.transferFrom({
owner,
ledgerCanisterId: 'mxzaz-hqaaa-aaaar-qaada-cai',
params: {from: {owner: fromPrincipal, subaccount: []}, to: {owner: toPrincipal, subaccount: []}, amount: 1_000_000n}
});
async function icrcWalletTransfers() {
const wallet = await IcrcWallet.connect({url: 'https://your-wallet.example.com/sign'});
const accounts = await wallet.accounts();
const {owner} = accounts[0];

await wallet.transfer({
owner,
ledgerCanisterId: 'mxzaz-hqaaa-aaaar-qaada-cai',
params: {to: {owner: recipientPrincipal, subaccount: []}, amount: 1_000_000n}
});

await wallet.approve({
owner,
ledgerCanisterId: 'mxzaz-hqaaa-aaaar-qaada-cai',
params: {spender: {owner: spenderPrincipal, subaccount: []}, amount: 5_000_000n}
});

await wallet.transferFrom({
owner,
ledgerCanisterId: 'mxzaz-hqaaa-aaaar-qaada-cai',
params: {from: {owner: fromPrincipal, subaccount: []}, to: {owner: toPrincipal, subaccount: []}, amount: 1_000_000n}
});
}
```

#### Query Methods and Disconnect

```typescript
const standards = await wallet.supportedStandards();
const currentPermissions = await wallet.permissions();
async function queryAndDisconnect(wallet: IcrcWallet) {
const standards = await wallet.supportedStandards();
const currentPermissions = await wallet.permissions();

await wallet.disconnect();
await wallet.disconnect();
}
```

#### Error Handling (dApp Side)

```typescript
try {
await wallet.transfer({...});
} catch (err) {
if (err instanceof RelyingPartyResponseError) {
switch (err.code) {
case 3000: /* PERMISSION_NOT_GRANTED */ break;
case 3001: /* ACTION_ABORTED — user rejected */ break;
case 4000: /* NETWORK_ERROR */ break;
async function safeTransfer(wallet: IcrcWallet) {
try {
await wallet.transfer({...});
} catch (err) {
if (err instanceof RelyingPartyResponseError) {
switch (err.code) {
case 3000: /* PERMISSION_NOT_GRANTED */ break;
case 3001: /* ACTION_ABORTED — user rejected */ break;
case 4000: /* NETWORK_ERROR */ break;
}
}
if (err instanceof RelyingPartyDisconnectedError) {
/* popup closed unexpectedly */
}
}
if (err instanceof RelyingPartyDisconnectedError) {
/* popup closed unexpectedly */
}
}
```
Expand Down Expand Up @@ -361,10 +375,13 @@ icp network start -d

```typescript
// dApp side — point to your local wallet's /sign route
const wallet = await IcrcWallet.connect({
url: 'http://localhost:5174/sign',
host: 'http://localhost:8000'
});
async function connectLocalWallet() {
const wallet = await IcrcWallet.connect({
url: 'http://localhost:5174/sign',
host: 'http://localhost:8000'
});
return wallet;
}

// Wallet/signer side — same local network host
const signer = Signer.init({
Expand All @@ -391,20 +408,26 @@ npm run dev:wallet # starts the pseudo wallet on port 5174
Then connect from your dApp:

```typescript
const wallet = await IcpWallet.connect({
url: 'http://localhost:5174/sign',
host: 'http://localhost:8000' // match your local network port
});
async function connectPseudoWallet() {
const wallet = await IcpWallet.connect({
url: 'http://localhost:5174/sign',
host: 'http://localhost:8000' // match your local network port
});
return wallet;
}
```

### Mainnet

On mainnet, point to the wallet's production signer URL and omit `host` (defaults to `https://icp-api.io`):

```typescript
const wallet = await IcpWallet.connect({
url: 'https://your-wallet.example.com/sign'
});
async function connectMainnetWallet() {
const wallet = await IcpWallet.connect({
url: 'https://your-wallet.example.com/sign'
});
return wallet;
}
```

## Expected Behavior
Expand Down
Loading