Claude
Skills
Sign in
Back

wallet-integration

Included with Lifetime
$97 forever

Integrate wallets with IC dApps using ICRC signer standards (ICRC-21/25/27/29/49). Covers the popup-based signer model, consent messages, permission lifecycle, and transaction approval flows. Implementation uses @dfinity/oisy-wallet-signer. Do NOT use for Internet Identity login, delegation-based auth (ICRC-34/46), or threshold signing (chain-key). Use when the developer mentions wallet integration, OISY, oisy-wallet-signer, wallet signer, relying party, consent messages, wallet popup, or transaction approval.

Web3

What this skill does


# Wallet Integration

## What This Is

Wallet integration on the Internet Computer uses the ICRC signer standards — a popup-based model where every action requires explicit user approval via JSON-RPC 2.0 over `window.postMessage`.

This skill covers integration using `@dfinity/oisy-wallet-signer`. Other integration paths (IdentityKit, signer-js) exist but are not covered here.

**The signer model = explicit per-action approval.** `connect()` establishes a channel. Nothing more.

**It is not:**

- A session system
- A delegated identity (no ICRC-34)
- A background executor

**ICRC standards implemented:**

- ICRC-21 — Canister call consent messages
- ICRC-25 — Signer interaction standard (permissions)
- ICRC-27 — Accounts
- ICRC-29 — Window PostMessage transport
- ICRC-49 — Call canister

**Not implemented:**

- ICRC-46 — Session-based delegation (not supported; use a delegation-capable model if you need sessions)

## When to Use

- Clear, intentional, high-value actions: token transfers (ICP / ICRC-1 / ICRC-2), NFT mint/claim, single approvals
- Funding / deposit flows: "Top up", "Deposit into protocol"
- Any action where a confirmation dialogue per operation feels natural

## When NOT to Use

- **Delegation or sessions**: sign once / act many times, background execution, autonomous behaviour
- **High-frequency interactions**: games, social actions, rapid write operations
- **Invisible writes**: autosave, cron jobs, auto-compounding

> **Decision test:** If your app still feels good when every meaningful update shows a confirmation dialogue, this library is appropriate. If not, use a delegation-capable model instead.

## Prerequisites

- `@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`)

```bash
npm i @dfinity/oisy-wallet-signer @dfinity/utils @dfinity/zod-schemas @icp-sdk/canisters @icp-sdk/core zod
```

## How It Works

### End-to-End Lifecycle

```text
1. dApp: IcrcWallet.connect({url})              → opens popup, polls icrc29_status
2. dApp: wallet.requestPermissionsNotGranted()   → prompts user if needed
3. dApp: wallet.accounts()                       → signer prompts, returns accounts
4. dApp: wallet.transfer({...})                  → signer fetches ICRC-21 consent message
                                                    → signer prompts user with consent
                                                    → signer executes canister call
                                                    → returns block index
5. dApp: wallet.disconnect()                     → closes popup, cleans up
```

## Pitfalls

1. **Importing classes from the wrong entry point.** `Signer`, `RelyingParty`, `IcpWallet`, and `IcrcWallet` are **not** exported from the main entry point. Import them from their dedicated subpaths or you get `undefined`.

   ```typescript
   // WRONG — will fail
   import {Signer} from '@dfinity/oisy-wallet-signer';

   // CORRECT
   import {Signer} from '@dfinity/oisy-wallet-signer/signer';
   import {IcpWallet} from '@dfinity/oisy-wallet-signer/icp-wallet';
   import {IcrcWallet} from '@dfinity/oisy-wallet-signer/icrc-wallet';
   ```

2. **Using `IcrcWallet` without `ledgerCanisterId`.** Unlike `IcpWallet` (which defaults to the ICP ledger `ryjl3-tyaaa-aaaaa-aaaba-cai`), `IcrcWallet.transfer()`, `.approve()`, and `.transferFrom()` all **require** `ledgerCanisterId`. Omitting it causes a runtime error.

3. **Forgetting to register prompts on the signer side.** The signer returns error 501 (`PERMISSIONS_PROMPT_NOT_REGISTERED`) if a request arrives and no prompt handler is registered for it. Register all four prompts (`ICRC25_REQUEST_PERMISSIONS`, `ICRC27_ACCOUNTS`, `ICRC21_CALL_CONSENT_MESSAGE`, `ICRC49_CALL_CANISTER`) before the signer can handle any relying party traffic.

4. **Sending concurrent requests to the signer.** The signer processes one request at a time. A second request while one is in-flight returns error 503 (`BUSY`). Serialize your calls — wait for each response before sending the next. Read-only methods (`icrc29_status`, `icrc25_supported_standards`) are exempt.

5. **Assuming `connect()` = authenticated session.** `connect()` only opens a `postMessage` channel. The user has not pre-authorized anything. Permissions default to `ask_on_use` — the signer will prompt the user on first use of each method. Call `requestPermissionsNotGranted()` after connecting to request all permissions upfront in a single prompt instead of per-method prompts.

6. **Not handling the consent message state machine.** The `ICRC21_CALL_CONSENT_MESSAGE` prompt fires multiple times with different statuses: `loading` → `result` | `error`. If you only handle `result`, the UI breaks on loading and error states. Always branch on `payload.status`.

7. **`sender` not matching `owner`.** The signer validates that `sender` in every `icrc49_call_canister` request matches the signer's `owner` identity. A mismatch returns error 502 (`SENDER_NOT_ALLOWED`). Always use the `owner` from `accounts()`.

8. **Not calling `disconnect()`.** Both `Signer.disconnect()` and `wallet.disconnect()` must be called on clean-up. Forgetting this leaks event listeners and leaves popup windows open.

9. **Ignoring permission expiration.** Permissions default to a 7-day validity period. After expiry, they silently revert to `ask_on_use`. Don't cache permission state client-side beyond a session.

10. **Auto-triggering signing on connect.** Never fire a canister call immediately after `connect()`. Let the user initiate the action. The signer is designed for intentional, user-driven operations.

## Implementation

### Import Map

```typescript
// Constants, errors, and types — from main entry point
import {
  ICRC25_REQUEST_PERMISSIONS,
  ICRC25_PERMISSION_GRANTED,
  ICRC25_PERMISSION_DENIED,
  ICRC25_PERMISSION_ASK_ON_USE,
  ICRC27_ACCOUNTS,
  ICRC21_CALL_CONSENT_MESSAGE,
  ICRC49_CALL_CANISTER,
  DEFAULT_SIGNER_WINDOW_CENTER,
  DEFAULT_SIGNER_WINDOW_TOP_RIGHT,
  RelyingPartyResponseError,
  RelyingPartyDisconnectedError
} from '@dfinity/oisy-wallet-signer';

import type {
  PermissionsPromptPayload,
  AccountsPromptPayload,
  ConsentMessagePromptPayload,
  CallCanisterPromptPayload,
  IcrcAccounts,
  SignerOptions,
  RelyingPartyOptions
} from '@dfinity/oisy-wallet-signer';

// Classes — from dedicated subpaths
import {Signer} from '@dfinity/oisy-wallet-signer/signer';
import {RelyingParty} from '@dfinity/oisy-wallet-signer/relying-party';
import {IcpWallet} from '@dfinity/oisy-wallet-signer/icp-wallet';
import {IcrcWallet} from '@dfinity/oisy-wallet-signer/icrc-wallet';
```

### dApp Side (Relying Party)

#### Choosing the Right Class

| Class          | Use for                                                                      |
| -------------- | ---------------------------------------------------------------------------- |
| `IcpWallet`    | ICP ledger operations — `ledgerCanisterId` optional (defaults to ICP ledger) |
| `IcrcWallet`   | Any ICRC ledger — `ledgerCanisterId` **required**                            |
| `RelyingParty` | Low-level custom canister calls via protected `call()`                       |

#### 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
// 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},
    onD
Files: 1
Size: 16.8 KB
Complexity: 29/100
Category: Web3

Related in Web3