wallet-integration

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

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)
在Internet Computer上集成钱包需遵循ICRC签名器标准——这是一种基于弹窗的模型,每个操作都需要用户通过JSON-RPC 2.0 over
window.postMessage
进行明确批准。
本文档介绍基于
@dfinity/oisy-wallet-signer
的集成方案。其他集成路径(如IdentityKit、signer-js)不在本文讨论范围内。
签名器模型 = 逐操作明确批准
connect()
仅用于建立通信通道,无其他作用。
本方案不是:
  • 会话系统
  • 委托身份(不支持ICRC-34)
  • 后台执行器
已实现的ICRC标准:
  • ICRC-21 — 容器调用同意消息
  • ICRC-25 — 签名器交互标准(权限)
  • ICRC-27 — 账户
  • ICRC-29 — Window PostMessage传输
  • ICRC-49 — 调用容器
未实现的标准:
  • ICRC-46 — 基于会话的委托(不支持;如需会话功能,请使用支持委托的模型)

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
  • 清晰、明确的高价值操作:代币转账(ICP / ICRC-1 / ICRC-2)、NFT铸造/申领、单次审批
  • 充值/存款流程:“充值”、“存入协议”
  • 任何需要逐操作确认对话框的场景

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
    installed
  • Peer dependencies installed:
    @dfinity/utils
    ,
    @dfinity/zod-schemas
    ,
    @icp-sdk/canisters
    ,
    @icp-sdk/core
    ,
    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
  • 已安装
    @dfinity/oisy-wallet-signer
  • 已安装 peer 依赖:
    @dfinity/utils
    @dfinity/zod-schemas
    @icp-sdk/canisters
    @icp-sdk/core
    zod
  • 签名器侧具备非匿名身份(如
    Ed25519KeyIdentity
  • 本地开发环境:运行中的本地网络(
    icp network start -d
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
text
1. dApp: IcrcWallet.connect({url})              → 打开弹窗,轮询icrc29_status
2. dApp: wallet.requestPermissionsNotGranted()   → 必要时提示用户
3. dApp: wallet.accounts()                       → 签名器提示用户,返回账户信息
4. dApp: wallet.transfer({...})                  → 签名器获取ICRC-21同意消息
                                                    → 签名器向用户展示同意提示
                                                    → 签名器执行容器调用
                                                    → 返回区块索引
5. dApp: wallet.disconnect()                     → 关闭弹窗,清理资源

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.
  1. 从错误的入口点导入类
    Signer
    RelyingParty
    IcpWallet
    IcrcWallet
    从主入口点导出。需从其专用子路径导入,否则会得到
    undefined
    typescript
    // 错误写法 — 会失败
    import {Signer} from '@dfinity/oisy-wallet-signer';
    
    // 正确写法
    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. 使用
    IcrcWallet
    时未传入
    ledgerCanisterId
    。与
    IcpWallet
    (默认使用ICP账本
    ryjl3-tyaaa-aaaaa-aaaba-cai
    )不同,
    IcrcWallet.transfer()
    .approve()
    .transferFrom()
    必须传入
    ledgerCanisterId
    。省略该参数会导致运行时错误。
  3. 忘记在签名器侧注册提示处理程序。如果请求到达时没有为其注册提示处理程序,签名器会返回错误501(
    PERMISSIONS_PROMPT_NOT_REGISTERED
    )。在签名器处理任何依赖方请求之前,需注册所有四个提示(
    ICRC25_REQUEST_PERMISSIONS
    ICRC27_ACCOUNTS
    ICRC21_CALL_CONSENT_MESSAGE
    ICRC49_CALL_CANISTER
    )。
  4. 向签名器发送并发请求。签名器一次只能处理一个请求。如果在一个请求处理过程中发送第二个请求,会返回错误503(
    BUSY
    )。请序列化调用——等待每个响应返回后再发送下一个请求。只读方法(
    icrc29_status
    icrc25_supported_standards
    )除外。
  5. 认为
    connect()
    等同于已认证会话
    connect()
    仅打开一个
    postMessage
    通道,用户并未预先授权任何操作。权限默认为
    ask_on_use
    ——签名器会在首次使用每个方法时提示用户。可在连接后调用
    requestPermissionsNotGranted()
    ,一次性请求所有权限,而非逐方法提示。
  6. 未处理同意消息状态机
    ICRC21_CALL_CONSENT_MESSAGE
    提示会多次触发,状态包括:
    loading
    result
    |
    error
    。如果仅处理
    result
    状态,UI在加载和错误状态下会崩溃。请始终根据
    payload.status
    进行分支处理。
  7. sender
    owner
    不匹配
    。签名器会验证每个
    icrc49_call_canister
    请求中的
    sender
    是否与签名器的
    owner
    身份匹配。不匹配会返回错误502(
    SENDER_NOT_ALLOWED
    )。请始终使用
    accounts()
    返回的
    owner
  8. 未调用
    disconnect()
    。在清理时必须同时调用
    Signer.disconnect()
    wallet.disconnect()
    。忘记调用会导致事件监听器泄漏,并使弹窗窗口保持打开状态。
  9. 忽略权限过期。权限默认有效期为7天。过期后,权限会自动恢复为
    ask_on_use
    。请勿在客户端缓存权限状态超过一个会话周期。
  10. 连接后自动触发签名。切勿在
    connect()
    后立即触发容器调用,应让用户主动发起操作。签名器专为用户驱动的明确操作设计。

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';
typescript
// 常量、错误和类型 — 从主入口点导入
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';

// 类 — 从专用子路径导入
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)

dApp侧(依赖方)

Choosing the Right Class

选择合适的类

ClassUse 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()
类名适用场景
IcpWallet
ICP账本操作 —
ledgerCanisterId
可选(默认使用ICP账本)
IcrcWallet
任何ICRC账本 —
ledgerCanisterId
必填
RelyingParty
通过受保护的
call()
进行底层自定义容器调用

Connect, Permissions, Accounts

连接、权限与账户

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 */
  }
});

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

const accounts = await wallet.accounts();
const {owner} = accounts[0];
typescript
const wallet = await IcrcWallet.connect({
  url: 'https://your-wallet.example.com/sign', // 实现签名器的钱包URL
  host: 'https://icp-api.io',
  windowOptions: {width: 576, height: 625, position: 'center'},
  connectionOptions: {timeoutInMilliseconds: 120_000},
  onDisconnect: () => {
    /* 钱包弹窗已关闭 */
  }
});

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

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

IcpWallet — ICP Transfers and Approvals

IcpWallet — ICP转账与授权

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}
});
使用
{owner, request}
— 无需
ledgerCanisterId
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}
});

IcrcWallet — Any ICRC Ledger

IcrcWallet — 任意ICRC账本

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}
});
使用
{owner, ledgerCanisterId, params}
ledgerCanisterId
必填
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}
});

Query Methods and Disconnect

查询方法与断开连接

typescript
const standards = await wallet.supportedStandards();
const currentPermissions = await wallet.permissions();

await wallet.disconnect();
typescript
const standards = await wallet.supportedStandards();
const currentPermissions = await wallet.permissions();

await wallet.disconnect();

Error Handling (dApp Side)

错误处理(dApp侧)

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;
    }
  }
  if (err instanceof RelyingPartyDisconnectedError) {
    /* popup closed unexpectedly */
  }
}
typescript
try {
  await wallet.transfer({...});
} catch (err) {
  if (err instanceof RelyingPartyResponseError) {
    switch (err.code) {
      case 3000: /* PERMISSION_NOT_GRANTED */ break;
      case 3001: /* ACTION_ABORTED — 用户拒绝 */ break;
      case 4000: /* NETWORK_ERROR */ break;
    }
  }
  if (err instanceof RelyingPartyDisconnectedError) {
    /* 弹窗意外关闭 */
  }
}

Wallet Side (Signer)

钱包侧(签名器)

Initialise and Register All Prompts

初始化并注册所有提示处理程序

typescript
const signer = Signer.init({
  owner: identity,
  host: 'https://icp-api.io',
  sessionOptions: {
    sessionPermissionExpirationInMilliseconds: 7 * 24 * 60 * 60 * 1000
  }
});

signer.register({
  method: ICRC25_REQUEST_PERMISSIONS,
  prompt: ({requestedScopes, confirm, origin}: PermissionsPromptPayload) => {
    confirm(
      requestedScopes.map(({scope}) => ({
        scope,
        state: userApproved ? ICRC25_PERMISSION_GRANTED : ICRC25_PERMISSION_DENIED
      }))
    );
  }
});

signer.register({
  method: ICRC27_ACCOUNTS,
  prompt: ({approve, reject, origin}: AccountsPromptPayload) => {
    approve([{owner: identity.getPrincipal().toText()}]);
  }
});

signer.register({
  method: ICRC21_CALL_CONSENT_MESSAGE,
  prompt: (payload: ConsentMessagePromptPayload) => {
    if (payload.status === 'loading') {
      // show spinner
    } else if (payload.status === 'result') {
      // payload.consentInfo: { Ok: ... } (from canister) or { Warn: ... } (signer-generated fallback)
      // show consent UI, then: payload.approve() or payload.reject()
    } else if (payload.status === 'error') {
      // show error, optionally payload.details
    }
  }
});

signer.register({
  method: ICRC49_CALL_CANISTER,
  prompt: (payload: CallCanisterPromptPayload) => {
    if (payload.status === 'executing') {
      /* show progress */
    } else if (payload.status === 'result') {
      /* call succeeded */
    } else if (payload.status === 'error') {
      /* call failed */
    }
  }
});
typescript
const signer = Signer.init({
  owner: identity,
  host: 'https://icp-api.io',
  sessionOptions: {
    sessionPermissionExpirationInMilliseconds: 7 * 24 * 60 * 60 * 1000
  }
});

signer.register({
  method: ICRC25_REQUEST_PERMISSIONS,
  prompt: ({requestedScopes, confirm, origin}: PermissionsPromptPayload) => {
    confirm(
      requestedScopes.map(({scope}) => ({
        scope,
        state: userApproved ? ICRC25_PERMISSION_GRANTED : ICRC25_PERMISSION_DENIED
      }))
    );
  }
});

signer.register({
  method: ICRC27_ACCOUNTS,
  prompt: ({approve, reject, origin}: AccountsPromptPayload) => {
    approve([{owner: identity.getPrincipal().toText()}]);
  }
});

signer.register({
  method: ICRC21_CALL_CONSENT_MESSAGE,
  prompt: (payload: ConsentMessagePromptPayload) => {
    if (payload.status === 'loading') {
      // 显示加载状态
    } else if (payload.status === 'result') {
      // payload.consentInfo: { Ok: ... }(来自容器,已验证)或 { Warn: ... }(签名器生成的回退信息)
      // 显示同意UI,然后调用 payload.approve() 或 payload.reject()
    } else if (payload.status === 'error') {
      // 显示错误信息,可选择展示 payload.details
    }
  }
});

signer.register({
  method: ICRC49_CALL_CANISTER,
  prompt: (payload: CallCanisterPromptPayload) => {
    if (payload.status === 'executing') {
      /* 显示进度 */
    } else if (payload.status === 'result') {
      /* 调用成功 */
    } else if (payload.status === 'error') {
      /* 调用失败 */
    }
  }
});

Consent Message:
Ok
vs
Warn

同意消息:
Ok
vs
Warn

  • { Ok: consentInfo }
    — canister implements ICRC-21; message is canister-verified
  • { Warn: { consentInfo, canisterId, method, arg } }
    — signer generated a fallback (for
    icrc1_transfer
    ,
    icrc2_approve
    ,
    icrc2_transfer_from
    )
Always distinguish these in the UI — warn the user when the message is signer-generated.
  • { Ok: consentInfo }
    — 容器已实现ICRC-21;消息已通过容器验证
  • { Warn: { consentInfo, canisterId, method, arg } }
    — 签名器生成的回退信息(适用于
    icrc1_transfer
    icrc2_approve
    icrc2_transfer_from
请在UI中明确区分这两种状态——当消息由签名器生成时,需向用户发出警告。

Disconnect

断开连接

typescript
signer.disconnect();
typescript
signer.disconnect();

Error Code Reference

错误码参考

CodeNameMeaning
500
ORIGIN_ERROR
Origin mismatch
501
PERMISSIONS_PROMPT_NOT_REGISTERED
Missing prompt handler
502
SENDER_NOT_ALLOWED
sender
owner
503
BUSY
Concurrent request rejected
504
NOT_INITIALIZED
Owner identity not set
1000
GENERIC_ERROR
Catch-all
2000
REQUEST_NOT_SUPPORTED
Method not supported
3000
PERMISSION_NOT_GRANTED
Permission denied
3001
ACTION_ABORTED
User cancelled
4000
NETWORK_ERROR
IC call failure
错误码名称含义
500
ORIGIN_ERROR
来源不匹配
501
PERMISSIONS_PROMPT_NOT_REGISTERED
缺少提示处理程序
502
SENDER_NOT_ALLOWED
sender
owner
503
BUSY
并发请求被拒绝
504
NOT_INITIALIZED
未设置所有者身份
1000
GENERIC_ERROR
通用错误
2000
REQUEST_NOT_SUPPORTED
方法不支持
3000
PERMISSION_NOT_GRANTED
权限被拒绝
3001
ACTION_ABORTED
用户已取消
4000
NETWORK_ERROR
IC调用失败

Permission States

权限状态

StateConstantBehavior
Granted
ICRC25_PERMISSION_GRANTED
Proceeds without prompting
Denied
ICRC25_PERMISSION_DENIED
Rejected immediately (error 3000)
Ask on use
ICRC25_PERMISSION_ASK_ON_USE
Prompts user on access (default)
Permissions stored in
localStorage
as
oisy_signer_{origin}_{owner}
with timestamps. Default validity: 7 days.
状态常量行为
已授权
ICRC25_PERMISSION_GRANTED
无需提示直接执行
已拒绝
ICRC25_PERMISSION_DENIED
立即拒绝(错误码3000)
使用时询问
ICRC25_PERMISSION_ASK_ON_USE
首次访问时提示用户(默认)
权限以
oisy_signer_{origin}_{owner}
为键存储在
localStorage
中,并包含时间戳。默认有效期:7天。

Deploy & Test

部署与测试

Local Development — Your Own Signer

本地开发 — 自定义签名器

If you are building both the dApp and the wallet/signer, start a local network and pass
host
to both sides:
bash
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'
});

// Wallet/signer side — same local network host
const signer = Signer.init({
  owner: identity,
  host: 'http://localhost:8000'
});
如果同时开发dApp和钱包/签名器,请启动本地网络,并在两侧传入
host
参数:
bash
icp network start -d
typescript
// dApp侧 — 指向本地钱包的/sign路由
const wallet = await IcrcWallet.connect({
  url: 'http://localhost:5174/sign',
  host: 'http://localhost:8000'
});

// 钱包/签名器侧 — 使用相同的本地网络host
const signer = Signer.init({
  owner: identity,
  host: 'http://localhost:8000'
});

Local Development — Using the Pseudo Wallet Signer

本地开发 — 使用伪钱包签名器

If you are building a dApp (relying party) and need a signer to test against locally, the library provides a pseudo wallet signer in its demo:
bash
git clone https://github.com/dfinity/oisy-wallet-signer
cd oisy-wallet-signer
npm ci

cd demo
npm ci
npm run sync:all
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
});
如果仅开发dApp(依赖方),需要一个签名器进行本地测试,本库在demo中提供了伪钱包签名器:
bash
git clone https://github.com/dfinity/oisy-wallet-signer
cd oisy-wallet-signer
npm ci

cd demo
npm ci
npm run sync:all
npm run dev:wallet    # 在端口5174启动伪钱包
然后从dApp连接:
typescript
const wallet = await IcpWallet.connect({
  url: 'http://localhost:5174/sign',
  host: 'http://localhost:8000' // 与本地网络端口保持一致
});

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'
});
在主网环境中,指向钱包的生产签名器URL,并省略
host
参数(默认值为
https://icp-api.io
):
typescript
const wallet = await IcpWallet.connect({
  url: 'https://your-wallet.example.com/sign'
});

Expected Behavior

预期行为

Connection

连接

  • connect()
    resolves with a wallet instance; throws
    RelyingPartyDisconnectedError
    on timeout
  • wallet.supportedStandards()
    returns an array containing at least ICRC-21, ICRC-25, ICRC-27, ICRC-29, ICRC-49
  • connect()
    会返回钱包实例;超时会抛出
    RelyingPartyDisconnectedError
  • wallet.supportedStandards()
    返回的数组至少包含ICRC-21、ICRC-25、ICRC-27、ICRC-29、ICRC-49

Permissions

权限

  • requestPermissionsNotGranted()
    triggers the signer's permissions prompt
  • After approval,
    wallet.permissions()
    returns scopes with state
    granted
  • A second call returns
    {allPermissionsGranted: true}
    without prompting again
  • requestPermissionsNotGranted()
    会触发签名器的权限提示
  • 授权后,
    wallet.permissions()
    返回的权限范围状态为
    granted
  • 再次调用会返回
    {allPermissionsGranted: true}
    ,无需再次提示

Accounts

账户

  • wallet.accounts()
    returns at least one
    {owner: string}
    (principal as text)
  • The returned
    owner
    matches the signer's identity principal
  • wallet.accounts()
    至少返回一个
    {owner: string}
    (主体的文本形式)
  • 返回的
    owner
    与签名器的身份主体一致

Transfers and Approvals

转账与授权

  • icrc1Transfer()
    /
    transfer()
    ,
    icrc2Approve()
    /
    approve()
    , and
    transferFrom()
    all resolve with a
    bigint
    block index
  • Each triggers the consent message prompt on the signer before execution
  • icrc1Transfer()
    /
    transfer()
    icrc2Approve()
    /
    approve()
    以及
    transferFrom()
    都会返回
    bigint
    类型的区块索引
  • 每个操作在执行前都会触发签名器的同意消息提示