Skip to content
Merged
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
261 changes: 248 additions & 13 deletions docs/guides/defi/wallet-integration.md
Original file line number Diff line number Diff line change
@@ -1,22 +1,257 @@
---
title: "Wallet Integration"
description: "Connect ICRC signer-standard wallets like OISY, Plug, and NFID"
description: "Integrate ICRC signer-standard wallets with your dapp using explicit per-action user approval."
sidebar:
order: 4
---

TODO: Write content for this page.
Wallet integration on the Internet Computer uses a popup-based signer model where every meaningful action requires explicit user approval. The dapp opens a wallet popup, requests permission, and the wallet shows a human-readable consent message before executing each canister call.

<!-- Content Brief -->
Integrate ICRC signer-standard wallets with your dapp. Cover the ICRC-21/25/27/29/49 standards, the popup wallet model, consent messages for user approval, and the OISY signer implementation. Explain the difference between II (authentication) and wallet signers (transaction approval).
This guide covers integration using `@icp-sdk/signer`, the signer library in the ICP JavaScript SDK.

<!-- Source Material -->
- Portal: building-apps/authentication/integrate-misc-wallets.mdx
- icskills: wallet-integration
- icp-cli: guides/managing-identities.md
- Examples: hosting/oisy-signer-demo
## Authentication vs. wallet signing

<!-- Cross-Links -->
- guides/authentication/internet-identity -- primary authentication
- reference/token-standards -- ICRC standards
- guides/defi/token-ledgers -- wallet-initiated token transfers
Internet Identity and wallet signers serve different purposes:

| | Internet Identity | Wallet signer |
|---|---|---|
| **Purpose** | Authenticate a user (prove identity) | Approve and sign canister calls |
| **User sees** | Login prompt once | Consent message per action |
| **After approval** | Session delegation (sign-once, act-many) | Single call executed |
| **Use when** | Read data, frequent writes, session-based UX | Token transfers, approvals, high-value one-off actions |

Use Internet Identity for login. Use a wallet signer when your dapp needs users to explicitly approve individual transactions — token transfers, NFT operations, or any action where a per-operation confirmation dialog is appropriate.

## ICRC signer standards

The signer model is defined by a set of ICRC standards:

| Standard | What it covers |
|---|---|
| ICRC-21 | Canister call consent messages — human-readable summaries |
| ICRC-25 | Signer interaction standard — permission lifecycle |
| ICRC-27 | Accounts — requesting the user's principal |
| ICRC-29 | Window PostMessage transport — popup communication |
| ICRC-49 | Call canister — routing calls through the signer |

A compliant wallet (such as [OISY](https://oisy.com)) implements all five standards.

## How it works

The lifecycle of a wallet-initiated call:

1. Your dapp creates a `Signer` pointing to the wallet's signer URL
2. Call `getAccounts()` — the wallet popup opens and prompts the user to share their account
3. Construct a `SignerAgent` using the returned principal
4. Use the agent with any canister actor — the wallet intercepts every call, fetches an ICRC-21 consent message from the target canister, shows it to the user, and only executes if the user approves

The key insight: a `SignerAgent` is a drop-in replacement for `HttpAgent`. Code that creates actors with `HttpAgent` can switch to `SignerAgent` to add wallet approval to every call.

## Prerequisites

```bash
npm install @icp-sdk/signer @icp-sdk/core
```

To interact with token ledgers, also install:

```bash
npm install @icp-sdk/canisters
```

## Connect and request accounts

```javascript
import { Signer } from '@icp-sdk/signer';
import { PostMessageTransport } from '@icp-sdk/signer/web';

const signer = new Signer({
transport: new PostMessageTransport({ url: 'https://oisy.com/sign' }),
});

// Opens the wallet popup. User approves account sharing.
// Returns an array of { owner: Principal, subaccount?: Uint8Array }
const accounts = await signer.getAccounts();
const principal = accounts[0].owner;
```

`getAccounts()` triggers the wallet's `icrc27_accounts` flow. The popup opens, the user approves, and you receive their principal.

You can request permissions upfront before calling `getAccounts()` to batch all permission prompts into a single interaction:

```javascript
// Request all needed permissions at once (optional but recommended)
await signer.requestPermissions([{ method: 'icrc27_accounts' }]);

const accounts = await signer.getAccounts();
```

If you skip this step, the signer handles permissions per-method — the user sees a permissions prompt the first time each method is called.

## Create a SignerAgent

`SignerAgent` wraps a `Signer` and acts as a drop-in replacement for `HttpAgent`. Any canister actor built with it routes calls through the wallet for approval.

```javascript
import { SignerAgent } from '@icp-sdk/signer/agent';
import { HttpAgent } from '@icp-sdk/core/agent';

// Create a read-only agent for balance queries (no wallet needed)
const readAgent = await HttpAgent.create({ host: 'https://icp0.io' });

// Create a SignerAgent for wallet-approved calls
const signerAgent = await SignerAgent.create({
signer,
account: principal, // principal from getAccounts()
agent: readAgent, // optional: share the HttpAgent for root key fetch
});
```

## Query balances (no wallet needed)

Read operations don't require wallet approval. Use a plain `HttpAgent` for queries:

```javascript
import { IcrcLedgerCanister } from '@icp-sdk/canisters/ledger/icrc';
import { Principal } from '@icp-sdk/core/principal';

const ledger = IcrcLedgerCanister.create({
agent: readAgent,
canisterId: Principal.fromText('mxzaz-hqaaa-aaaar-qaada-cai'), // ckBTC ledger
});

const balance = await ledger.balance({ owner: principal });
```

Separate read and write agents: use `readAgent` for queries, `signerAgent` for transfers.

## Perform a token transfer

Using the signer agent with `IcrcLedgerCanister` routes the transfer through the wallet. The wallet fetches the ICRC-21 consent message and presents it to the user before the call executes.

```javascript
const signedLedger = IcrcLedgerCanister.create({
agent: signerAgent,
canisterId: Principal.fromText('mxzaz-hqaaa-aaaar-qaada-cai'),
});

// The wallet popup opens, shows a consent message, user approves
const blockIndex = await signedLedger.transfer({
to: { owner: recipientPrincipal, subaccount: [] },
amount: 1_000_000n, // in base units (e.g. 0.01 ckBTC = 1_000_000 e8s)
});
```

## Disconnect

Call `closeChannel()` on the signer when the user logs out or closes the session:

```javascript
await signer.closeChannel();
```

`closeChannel()` closes the open communication channel with the wallet popup.

## Session persistence

The signer session is tied to the browser tab. After a page reload, the user's principal is no longer available from the signer. To avoid opening the popup again immediately, store the principal in `sessionStorage` and restore it on mount — then re-establish the signer session lazily when the user initiates a transfer:

```javascript
import { Principal } from '@icp-sdk/core/principal';

const SESSION_KEY = 'wallet-principal';

// On connect: store principal
sessionStorage.setItem(SESSION_KEY, principal.toText());

// On mount: restore principal without opening popup
const stored = sessionStorage.getItem(SESSION_KEY);
if (stored) {
const restoredPrincipal = Principal.fromText(stored);
// Use restoredPrincipal for read-only queries
// Only call getAccounts() again when user initiates a write
}

// On disconnect: clear storage
sessionStorage.removeItem(SESSION_KEY);
```

## Error handling

```javascript
import { SignerError } from '@icp-sdk/signer';

try {
await signer.getAccounts();
} catch (err) {
if (err instanceof SignerError) {
switch (err.code) {
case 3001: // ACTION_ABORTED — user closed the popup or rejected the prompt
break;
case 3000: // PERMISSION_NOT_GRANTED — permission was denied
break;
default:
console.error('Signer error', err.code, err.message);
}
}
}
```

Common `err.code` values from the ICRC-25 standard:

| Code | Meaning |
|------|---------|
| `3000` | Permission not granted |
| `3001` | Action aborted — user closed the popup or rejected |
| `4000` | Network error — IC call failed |

## Local development

For local development against a running local network:

```javascript
const signer = new Signer({
transport: new PostMessageTransport({ url: 'http://localhost:5174/sign' }),
});

const readAgent = await HttpAgent.create({ host: 'http://localhost:8000' });
```

For a test signer target, you can use any ICRC-25-compliant wallet running locally that exposes a `/sign` endpoint — for example, a local instance of [OISY](https://github.com/dfinity/oisy-wallet) or a custom signer built with `@icp-sdk/signer`.

On mainnet, omit `host` from `HttpAgent.create()` — it defaults to `https://icp0.io`.

## Working example

The [oisy-signer-demo](https://github.com/dfinity/examples/tree/master/hosting/oisy-signer-demo) example shows a complete dapp that:

1. Connects to OISY and fetches the user's accounts
2. Queries ICRC-1 token balances using a read-only agent
3. Performs self-transfers using the signer agent

To run locally:

```bash
git clone https://github.com/dfinity/examples
cd examples/hosting/oisy-signer-demo
icp network start -d
npm install
icp deploy
```

## Ecosystem libraries

Two additional libraries are available for more advanced wallet integration scenarios:

- [`@dfinity/ledger-wallet-identity`](https://www.npmjs.com/package/@dfinity/ledger-wallet-identity) — hardware wallet identity support
- [`@dfinity/icrc21-agent`](https://www.npmjs.com/package/@dfinity/icrc21-agent) — standalone ICRC-21 consent message agent

Both libraries are expected to move to the `@icp-sdk` namespace on npm and will likely be covered in the wallet-integration skill going forward. They are not documented in detail here.

## Next steps

- [Internet Identity integration](../authentication/internet-identity.md) — add authentication alongside wallet signing
- [Token ledgers](token-ledgers.md) — work with ICRC-1 and ICRC-2 token standards
- [Token standards reference](../../reference/token-standards.md) — ICRC-1, ICRC-2, and related standards

<!-- Upstream: informed by dfinity/icskills — skills/wallet-integration/SKILL.md; dfinity/examples — hosting/oisy-signer-demo; dfinity/icp-js-sdk-docs — signer/latest.zip -->
Loading