特殊

Ledger 硬件钱包

使用 Ledger 硬件钱包进行安全签名

前提条件
  • 拥有 Ledger 硬件钱包(Ledger Nano X 或 Ledger Nano S)
  • 安装 Ledger Live
  • 通过 Ledger Live 安装 Æternity app (v0.4.4+)
  • 将 Ledger 连接到电脑,解锁,并打开 Æternity 应用
基本用法

首先选择传输实现方式。Ledger 可通过 USB 或蓝牙连接,需要安装对应的 NPM 包

步骤 1创建账户工厂
import { AccountLedgerFactory } from '@aeternity/aepp-sdk';
import TransportWebUSB from '@ledgerhq/hw-transport-webusb';

// 创建 USB 传输
const transport = await TransportWebUSB.create();

// 创建 Ledger 账户工厂
const accountFactory = new AccountLedgerFactory(transport);
步骤 2初始化账户
// 通过索引创建账户实例
const account = await accountFactory.initialize(0);

console.log(account.address); // 'ak_2dA...'

// 签名交易(在 Ledger 设备上确认)
const signedTx = await account.signTransaction('tx_...');
console.log(signedTx); // 'tx_...' (带签名)
安全特性:私钥在 Ledger 设备上派生,永远不会离开设备
账户验证(防止中间人攻击)

为防止 MITM 攻击,建议让用户确认应用显示的地址与 Ledger 设备屏幕上的地址一致:

// 触发验证流程,Ledger 会在屏幕上显示地址
const address = await accountFactory.getAddress(0, true);

// 用户在 Ledger 上确认后返回地址
console.log('已验证地址:', address);
账户持久化

保存账户的 indexaddress 属性,可在应用重启后恢复:

import { AccountLedger } from '@aeternity/aepp-sdk';

// 保存
const accountIndex = accountToPersist.index;
const accountAddress = accountToPersist.address;
localStorage.setItem('ledgerAccount', JSON.stringify({ index: accountIndex, address: accountAddress }));

// 恢复
const saved = JSON.parse(localStorage.getItem('ledgerAccount'));
const restoredAccount = new AccountLedger(transport, saved.index, saved.address);
账户发现

自动发现链上已使用过的账户(用于用户在新应用中恢复账户):

import { Node } from '@aeternity/aepp-sdk';

const node = new Node('https://testnet.aeternity.io');

// 发现所有链上活跃的账户
const accounts = await accountFactory.discover(node);

console.log('发现账户:');
accounts.forEach(acc => console.log(acc.address));
错误处理

用户在 Ledger 上拒绝操作时会抛出异常:

import { TransportStatusError } from '@ledgerhq/hw-transport';

try {
  const signedTx = await account.signTransaction(tx);
} catch (err) {
  if (err instanceof TransportStatusError) {
    if (err.statusCode === 0x6985) {
      console.log('用户在 Ledger 上拒绝了操作');
      // err.statusText === 'CONDITIONS_OF_USE_NOT_SATISFIED'
    }
  }
}