diff --git a/docs/architecture.md b/docs/architecture.md index a0f1544f0..a527ea96b 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -2,119 +2,231 @@ ## 📁 核心架构 -OneKey Hardware SDK 采用三层架构设计: +这个仓库目前同时维护两条硬件接入路径: -``` -应用层 (DApps) +```text +OneKey 原生设备 +应用层 + ↓ +@onekeyfe/hd-core + ↓ +@onekeyfe/hd-transport + ↓ +WebUSB / Node USB / HTTP / Lowlevel(BLE) + ↓ +OneKey 设备 + +第三方硬件(当前重点是 Ledger) +应用层 + ↓ +@onekeyfe/hwk-adapter-core ↓ -SDK接口层 (@onekeyfe/core) +@onekeyfe/hwk-ledger-adapter ↓ -传输抽象层 (@onekeyfe/hd-transport) +@onekeyfe/hwk-ledger-connector-webhid / @onekeyfe/hwk-ledger-connector-ble ↓ -平台适配层 (WebUSB/BLE/HTTP) +Ledger Device Management Kit / Transport Kit ↓ -硬件设备层 (OneKey设备) +Ledger 设备 ``` +前者是 OneKey 自有协议栈;后者是多厂商硬件适配层,重点解决连接、会话、UI 事件和链方法的一致接口问题。 + ## 🏗️ 核心包结构 -### API层 -- **`@onekeyfe/hd-core`** - 核心API和业务逻辑 -- **`@onekeyfe/hd-transport`** - 传输层抽象 +### OneKey 原生协议栈 + +- **`@onekeyfe/hd-core`** - 核心 API、`BaseMethod` 生命周期、设备状态管理 +- **`@onekeyfe/hd-transport`** - 统一消息分包、序列化、传输抽象 +- **`@onekeyfe/hd-transport-web-device`** - 浏览器 WebUSB 传输 +- **`@onekeyfe/hd-transport-usb`** - Node.js USB 传输(libusb) +- **`@onekeyfe/hd-transport-http`** - HTTP Bridge +- **`@onekeyfe/hd-transport-lowlevel`** - 低层插件模式传输 +- **`@onekeyfe/hd-transport-react-native`** - React Native BLE 传输实现 -### 传输层 -- **`@onekeyfe/hd-transport-webusb`** - WebUSB传输(浏览器) -- **`@onekeyfe/hd-transport-usb`** - Node.js USB传输(CLI/服务端,基于 libusb) -- **`@onekeyfe/hd-transport-http`** - HTTP Bridge传输 -- **`@onekeyfe/hd-transport-lowlevel`** - 低层传输(BLE 插件模式) +### 第三方硬件适配层 -### 平台SDK -- **`@onekeyfe/hd-web-sdk`** - Web平台SDK -- **`@onekeyfe/hd-ble-sdk`** - 移动端BLE SDK +- **`@onekeyfe/hwk-adapter-core`** - `IConnector`、`IHardwareWallet`、UI 事件和通用错误约定 +- **`@onekeyfe/hwk-ledger-adapter`** - Ledger 链方法、设备指纹校验、作业队列、自动装 App 工作流 +- **`@onekeyfe/hwk-ledger-connector-webhid`** - WebHID connector +- **`@onekeyfe/hwk-ledger-connector-ble`** - React Native BLE connector ### 示例应用 -- **`@onekeyfe/connect-examples`** - 集成示例 - - `expo-example` - Web集成示例 - - `expo-playground` - 开发测试平台 -## 🔄 API调用流程 +- **`@onekeyfe/connect-examples`** + - `expo-example` - 常规集成示例和参数面板 + - `expo-playground` - 开发 / 调试 playground + +## 🔄 API 调用流程 + +### OneKey 原生调用链 ```typescript -// 典型调用链 -HardwareSDK.btcGetAddress() +HardwareSDK.evmSignTransaction(connectId, deviceId, params) + ↓ +BaseMethod.init() ↓ -BaseMethod.run() +Core.callAPI() ↓ -Device.call() +Device.run() ↓ -Transport.send() +Device.initialize() // 满足条件时可被 preInitialize 命中并跳过 ↓ -硬件设备响应 +Transport.call() + ↓ +设备响应 ``` -## 🎯 设计原则 +### Ledger 调用链 + +```typescript +ledgerAdapter.evmSignTransaction(connectId, deviceId, params, commonParams) + ↓ +LedgerAdapter.callChain() + ↓ +LedgerAdapter.connectorCall() + ↓ +IConnector.call(sessionId, method, params) + ↓ +LedgerConnectorBase + ↓ +Ledger DMK / transport kit + ↓ +设备响应或 connector ui-event / ui-request +``` + +## 🔥 BLE 预热流程:`preInitialize()` / `usePreInitialize` + +`preInitialize()` 是一个 **BLE 签名前预热信号**,目标是把代价较高的 `Initialize` 提前做掉,而不是给后续请求强行关闭初始化。 + +### 行为约束 -### 分层解耦 -- 业务逻辑与传输协议分离 -- 核心API与平台实现解耦 -- 支持独立测试和开发 +| 条件 | 源码行为 | 影响 | +| --- | --- | --- | +| `HardwareSDK.preInitialize(connectId, params)` | 走 `PreInitialize` 方法,标记为 `isPreWarmSignal` | 进入专用的预热分支 | +| 同一个预热 key 并发触发 | Core 侧会合并 in-flight 调用 | 避免重复抢占 BLE 链路 | +| 60 秒内重复预热 | 命中 TTL,直接返回 `true` | 不会重复做 `Initialize` | +| 后续业务调用设置 `usePreInitialize: true` | 只有方法自身 `allowUsePreInitialize = true` 时才允许跳过初始化 | 不是所有方法都支持 | +| `passphraseState` 不匹配、`connectId` 缺失、设备还没有 `features`、TTL 过期 | 记为 `[PRE-INIT][MISS]`,仍然执行正常 `Initialize` | 失败闭合,不会错误复用旧状态 | +| Cardano 签名 | `cardanoSignTransaction` / `cardanoSignMessage` 显式 `allowUsePreInitialize = false` | 近期修复后不再复用预热跳过初始化 | -### 平台无关 -- 一套API支持Web/Mobile/Desktop -- 平台差异在适配层处理 -- 核心逻辑完全复用 +### `deriveCardano` 的边界 -### 协议可扩展 -- 支持多种传输协议 -- 向后兼容旧版本 -- 便于添加新协议 +`Initialize` 是否带上 `deriveCardano` 不是只看显式参数。当前会在以下情况自动打开: -## 🧩 关键设计模式 +- 方法名以 `cardano` 开头 +- 调用参数里显式设置了 `deriveCardano: true` +- `allNetworkGetAddress()` 的 bundle 中包含 `network: 'ada'` + +这意味着: + +- `deriveCardano` 决定初始化阶段是否派生 Cardano 相关状态 +- `usePreInitialize` 决定后续业务调用能否跳过这次初始化 +- 两者相关,但不是同一个开关 + +### 推荐调用方式 -### 模板方法 (BaseMethod) ```typescript -abstract class BaseMethod { - async execute(): Promise { - this.validateParams(); - await this.checkDevice(); - return await this.run(); - } - - abstract run(): Promise; -} +await HardwareSDK.preInitialize(connectId, { + passphraseState, +}); + +const result = await HardwareSDK.evmSignTransaction(connectId, deviceId, { + path: "m/44'/60'/0'/0/0", + transaction, + passphraseState, + usePreInitialize: true, +}); ``` -### 策略模式 (Transport) +### 常见误区 + +- `preInitialize()` 成功返回 `true`,**不代表** 下一次业务调用一定会跳过 `Initialize` +- `usePreInitialize` 只对显式 opt-in 的签名类方法生效;地址、公钥类方法不会因为预热而跳过初始化 +- `passphraseState` 发生变化时必须重新预热,否则会被安全地降级回正常初始化流程 + +## 🔌 Ledger 适配层:公共接口、工作流与坑位 + +### 1) 入口对象和 transport 绑定 + +`LedgerAdapter` 在构造时绑定一个 connector;运行中不会动态切换 connector。Web 场景通常用 WebHID,React Native 场景通常用 BLE: + ```typescript -// 根据环境选择传输方式 -switch(env) { - case 'webusb': return new WebUsbTransport(); // 浏览器 WebUSB - case 'node-usb': return new NodeUsbTransport(); // Node.js libusb (CLI) - case 'lowlevel': return new LowlevelTransport(); // BLE 插件模式 - case 'http': return new HttpTransport(); // HTTP Bridge -} +import { LedgerAdapter } from '@onekeyfe/hwk-ledger-adapter'; +import { createLedgerWebHidConnector } from '@onekeyfe/hwk-ledger-connector-webhid'; + +const hw = new LedgerAdapter(createLedgerWebHidConnector()); ``` -## 📦 依赖关系 +### 2) 最小接入流程 +```typescript +const devices = await hw.searchDevices({ resetSession: true }); +const connectId = devices[0]?.connectId; +if (!connectId) throw new Error('No Ledger found'); + +await hw.connectDevice(connectId); + +const response = await hw.allNetworkGetAddress(connectId, '', { + autoInstallApp: true, + bundle: [ + { + network: 'evm', + methodName: 'evmGetAddress', + path: "m/44'/60'/0'/0/0", + }, + { + network: 'doge', + methodName: 'btcGetPublicKey', + path: "m/44'/3'/0'", + }, + ], +}); ``` -应用层 -├── @onekeyfe/hd-web-sdk -├── @onekeyfe/hd-ble-sdk - │ - ├── @onekeyfe/hd-core ←── 核心层 - │ └── @onekeyfe/hd-transport - │ - └── 传输层实现 - ├── @onekeyfe/hd-transport-webusb (浏览器) - ├── @onekeyfe/hd-transport-usb (Node.js CLI) - ├── @onekeyfe/hd-transport-lowlevel (BLE 插件) - └── @onekeyfe/hd-transport-http (Bridge) -``` + +### 3) `allNetworkGetAddress()` 的真实语义 + +- 只接受以下方法名:`evmGetAddress`、`btcGetAddress`、`btcGetPublicKey`、`solGetAddress`、`tronGetAddress` +- 每个条目会按链维度缓存 `chainFingerprint` +- 调用方没有传 `deviceId` 时,adapter 会为该链补做一次指纹引导;后续同链条目复用 +- BTC fork 网络会在 adapter 内部补齐 Ledger 需要的 `coin` 参数,目前包括 `tbtc`、`bch`、`doge`、`ltc`、`neurai` +- 顶层失败是 fail-fast 的:`DeviceMismatch`、`UserAborted`、`UserRejected` 会直接终止整个 bundle + +如果你的上层已经拿到并验证过同链指纹,应优先把它作为公共方法里的 `deviceId` 参数传入,避免把“首次建立信任”和“批量取地址”混在同一次调用里。 + +### 4) `autoInstallApp` 默认关闭,而且只重试一次 + +`ICommonCallParams.autoInstallApp` 默认是 `false`。打开后,缺少目标 App 时的流程是: + +1. 触发 `UI_REQUEST.REQUEST_INSTALL_APP` +2. 用户确认后调用 `installApp` +3. 安装期间持续发出 `ui-event: app-install-progress` +4. 原始链方法只会重试一次 + +这样设计有两个边界: + +- **未开启 `autoInstallApp`**:保持原始 “App not installed” 失败 +- **安装后 App 仍然缺失**:adapter 会触发 loop guard,直接报错,不会二次弹窗进入死循环 + +### 5) `AppInstallProgress` 事件如何转发 + +底层 connector 发出的 payload 键是 `sessionId`;`LedgerAdapter` 对外会把它重写成 `connectId`,并按 `connectId + appName` 维度节流: + +- 进度差值小于 `5%` 的中间帧会被丢弃 +- `progress >= 1` 的最终帧总会保留 +- 如果找不到活跃 session 映射,事件会被直接丢弃,而不是把过期 session 暴露给上层 + +适合把它直接当作 UI 展示事件使用,不适合当作强一致的审计日志。 + +### 6) 运行时排障建议 + +- **WebHID 设备刷新后 connectId 变了**:先用 `searchDevices({ resetSession: true })` 清理旧 session,再重新选设备 +- **用户拒绝装 App 或设备上拒绝确认**:`allNetworkGetAddress()` 会终止整个 bundle,这是刻意的 fail-fast 行为 +- **需要中断当前请求**:调用 `cancel()` 只会中断当前作业队列;如果设备上还有待确认提示,仍然要由用户在设备侧拒绝或等待超时 ## 🔧 开发工具 -- **Lerna** - Monorepo管理 +- **Lerna** - Monorepo 管理 - **TypeScript** - 类型安全 - **Jest** - 单元测试 diff --git a/docs/transport.md b/docs/transport.md index 2b3ef0620..44bc5057f 100644 --- a/docs/transport.md +++ b/docs/transport.md @@ -155,6 +155,55 @@ export default class WebUsbTransport { } ``` +### React Native BLE Firmware / OTA Writes + +`packages/hd-transport-react-native` 对普通消息和大 payload 的写入路径做了区分。最近这部分的改动主要集中在 `FirmwareUpload` 的 BLE 背压恢复。 + +#### 哪些消息会走特殊写入路径 + +| 消息名 | 写入方式 | 目的 | +| --- | --- | --- | +| `EmmcFileWrite` | `transport.writeWithRetry()` | 沿用现有分块重试逻辑 | +| `FirmwareUpload` | `writeWithoutResponse()` + 节流 + 有条件重连 | 减少 OTA 期间的 BLE 队列拥塞 | +| 其他消息 | 普通 63-byte HID 分包写入 | 保持统一协议路径 | + +#### `FirmwareUpload` 的节流参数 + +| 参数 | iOS | Android | 说明 | +| --- | --- | --- | --- | +| `requestMTU` | 256 | 256 | 连接时的首选 MTU;若 MTU 变更失败会回退 | +| 写入聚合容量 | `IOS_PACKET_LENGTH` | `192` bytes | Android 固件上传会使用更大的 packet capacity | +| `FIRMWARE_UPLOAD_WRITE_BURST_SIZE` | 4 | 5 | 每写完一组 burst 主动暂停 | +| `FIRMWARE_UPLOAD_WRITE_PAUSE_MS` | 8ms | 10ms | burst 间隔,给 BLE 队列排空时间 | +| `FIRMWARE_UPLOAD_WRITE_FLUSH_DELAY_MS` | 24ms | 30ms | 本轮分块写完后的 flush 延迟 | +| `FIRMWARE_UPLOAD_WRITE_MAX_RETRIES` | 8 | 8 | 单个 chunk 的最大恢复次数 | + +#### 重试与重连策略 + +- 只有以下错误会进入恢复逻辑: + - `GATT_CONGESTED` / `status 143` + - `DeviceDisconnected` + - `CharacteristicNotFound` +- **拥塞类错误**:按指数退避等待,延迟从 `200ms` 开始,上限 `1200ms` +- **可重连错误**:固定等待 `2000ms`,然后重建 transport、重新发现 characteristic,并重新挂上 notify / disconnect 订阅 +- 超过 `8` 次恢复后,错误会直接向上抛出,不再静默重试 + +#### 为什么只对 `FirmwareUpload` 特判 + +`FirmwareUpload` 的流量特征和普通 APDU 完全不同: + +- chunk 连续、持续时间长 +- Android 上更容易触发 BLE 写队列背压 +- 断连后不能只依赖普通的 `writeWithoutResponse()` 失败重试,需要把 characteristic 和订阅一起恢复 + +因此这里没有把策略做成全局默认值,而是只挂在 `name === 'FirmwareUpload'` 的路径上。 + +#### 排障建议 + +- **上传中反复出现 `GATT_CONGESTED`**:先观察是否能在 8 次内恢复;这类错误按设计会自动退避,不需要在上层再包一层立即重试 +- **上传中途断连**:重点检查 reconnect 之后是否重新拿到了 write / notify characteristic,以及 notify 订阅有没有恢复 +- **Android 连接后立刻报 MTU 变更失败**:transport 会把 `connectOptions` 回退为空对象再重连;如果你在上层保存了 transport 状态,不要假设每次连接都成功协商到 MTU 256 + ## Session Management ### Session Lifecycle @@ -259,16 +308,10 @@ export function receiveOne(messages: Root, data: string) { ## Summary -OneKey传输层通过分层架构、协议设计和错误恢复机制,成功解决了跨平台硬件钱包通信的复杂性: +当前传输层文档重点覆盖了三类已在源码中稳定存在的约定: -**核心特性:** -- **协议设计**: 统一的消息格式和分包机制 -- **会话管理**: 安全隔离的设备会话和自动清理 -- **错误恢复**: 智能重试和指数退避机制 -- **安全保护**: 全面防护常见攻击向量 +- **统一协议格式**:`[##][Type][Length][Payload]` 与 63-byte HID 分包 +- **会话模型**:`enumerate → acquire → configure → call → release` +- **平台特化恢复逻辑**:例如 React Native BLE 在 `FirmwareUpload` 场景下的节流、退避和重连 -**性能指标:** -- **延迟**: 典型操作亚秒级响应 -- **可靠性**: 高成功率和自动错误恢复 -- **兼容性**: 支持所有OneKey设备型号和固件版本 -- **稳定性**: 成熟的协议栈和传输机制 +如果你在排查跨平台差异,建议优先对照具体 transport 实现,而不要假设所有消息路径都共享同一套写入和恢复策略。