EVM Signer
EVM 硬件签名全链路指南,基于 @onekeyfe/hd-core 的能力,覆盖地址确认、EIP‑1559 / Legacy 交易、personal_sign、EIP‑712 等场景。
目录
工作原理
EVM 签名通过 HardwareSDK 与 OneKey 硬件 App 通信,内部使用 APDU 完成交互,外部以独立用例暴露(地址、交易、消息、Typed Data)。接口以 Promise { success, payload } 返回结果,交互提示通过 UI_REQUEST 事件触发(PIN、密码短语、打开 App、确认地址/交易/消息/Typed Data)。
EVM 签名的基本链路:
- 应用端构造参数(路径、交易/消息数据、chainId 等)。
- SDK 建立会话并发送命令,设备进入审阅流程。
- 设备显示路径、地址、金额、Gas、ChainId、消息摘要或 Typed Data 域。
- 用户确认后返回签名
{ r, s, v }或十六进制签名。 - 应用端使用
ethers/viem进行序列化、验签并广播。
中文思路:把“能跑、看得懂、能验签”作为首要目标,任何字段都以链上广播与设备显示一致为准。
安装
本模块依赖
@onekeyfe/hd-core及连接层(如@onekeyfe/hd-common-connect-sdk),请先完成基础 SDK 安装。
npm install @onekeyfe/hd-core @onekeyfe/hd-common-connect-sdk初始化
import HardwareSDK from '@onekeyfe/hd-core';
// 或使用 connect-sdk 封装的 HardwareSDK
await HardwareSDK.init({ env: 'webusb', debug: false, fetchConfig: true });
const [{ connectId }] = await HardwareSDK.searchDevices();
const deviceId = (await HardwareSDK.getFeatures(connectId)).payload?.device_id;保持固件为最新版本,确保 WebUSB/BLE 连接稳定;建议在签名前先完成一次
evmGetAddress(showOnOneKey: true)以确认路径与账户。
适用场景
evmGetAddress / evmSignTransaction / evmSignMessage / evmSignTypedData 对应独立用例,返回 Promise { success, payload }(用户交互通过事件通知,见下文)。
场景 1:获取地址
获取指定路径的 EVM 地址,可选择在设备上核对。
const res = await HardwareSDK.evmGetAddress(connectId, deviceId, {
path: "m/44'/60'/0'/0/0",
showOnOneKey: true,
chainId: 1,
});
// res.payload.address, publicKey?, chainCode?参数
path(string | number[]): 必填,BIP44 路径,例如"44'/60'/0'/0/0"。showOnOneKey?(boolean): 可选,是否在设备上展示并让用户确认地址。chainId?(number): 可选,显示在设备上的链 ID,建议填真实网络。
返回
Promise<{ success, payload: { address, path, publicKey?, chainCode? } }>type GetAddressResult = {
address: `0x${string}`;
path: string;
publicKey?: string;
chainCode?: string;
};场景 2:签名交易
支持 EIP‑1559 (type: 2) 与 Legacy 交易,需传入序列化前的字段。除 chainId 外,数值字段必须为 0x 十六进制字符串(SDK 会去掉前导 0)。
const { success, payload } = await HardwareSDK.evmSignTransaction(
connectId,
deviceId,
{
path: "m/44'/60'/0'/0/0",
transaction: tx,
keepSession: true,
},
);
// payload: { v, r, s, authorizationSignatures? }参数
path(string | number[]): 必填,签名路径。transaction(object): 必填,EIP‑1559 或 Legacy 字段:- 1559:
tovaluedatanoncegasLimitmaxFeePerGasmaxPriorityFeePerGaschainIdtype: 2 - Legacy:
tovaluedatanoncegasLimitgasPricechainId
- 1559:
keepSession?(boolean): 可选,多次签名时复用会话。domain?(string): 可选,交易中的域名标签(如 ENS),用于设备显示。
返回
Promise<{ success, payload: { v, r, s } }>可配合 ethers/viem 序列化为 raw tx。
场景 3:签名消息(personal_sign)
const res = await HardwareSDK.evmSignMessage(connectId, deviceId, {
path: "m/44'/60'/0'/0/0",
messageHex,
chainId: 1,
});参数
path:必填,签名路径。messageHex:必填,原文转 hex(不含0x或含0x皆可)。chainId?:可选,设备显示的链 ID。
返回
签名十六进制或 { r, s, v },可用 ethers.verifyMessage 校验。
场景 4:签名 Typed Data(EIP-712)
const res = await HardwareSDK.evmSignTypedData(
connectId,
deviceId,
{
path: "m/44'/60'/0'/0/0",
data: typedData,
chainId: 1,
},
);参数
path:必填。data:必填,EIP‑712 JSON v4,包含domain/types/primaryType/message。chainId:必填。
TypedData 结构参考:
interface TypedData {
domain: {
name?: string;
version?: string;
chainId?: number;
verifyingContract?: string;
salt?: string;
};
types: Record<string, Array<{ name: string; type: string }>>;
primaryType: string;
message: Record<string, unknown>;
}返回
Promise<{ success, payload: { signature } }>可用 ethers.verifyTypedData 校验。
交互与状态
- 接口以 Promise 结束;用户提示通过
UI_REQUEST事件触发(解锁设备、打开 EVM App、确认地址/交易/消息/Typed Data/授权)。 - 同一设备请串行调用;多次签名可使用
keepSession减少 PIN/密码短语提示。 - 在 UI 中监听
UI_REQUEST并引导用户操作,允许手动取消/重试。
示例
示例:EIP-1559 交易签名
import HardwareSDK, { UI_REQUEST, UI_RESPONSE } from '@onekeyfe/hd-core';
import { serialize, TransactionTypes } from '@ethersproject/transactions';
const [{ connectId }] = await HardwareSDK.searchDevices();
const deviceId = (await HardwareSDK.getFeatures(connectId)).payload?.device_id;
HardwareSDK.on(UI_REQUEST.REQUEST_PIN, () => {
// 提示用户在设备输入 PIN,或在自定义 UI 收集后调用 uiResponse
});
const accountPath = "m/44'/60'/0'/0/0";
await HardwareSDK.evmGetAddress(connectId, deviceId, { path: accountPath, showOnOneKey: true });
const tx = {
to: '0xd0d6d6c5fe4a677d343cc433536bb717bae167dd',
value: '0x0',
data: '0x',
nonce: '0x0',
gasLimit: '0x5208',
maxFeePerGas: '0x3b9aca00',
maxPriorityFeePerGas: '0x59682f00',
chainId: 1,
};
const { success, payload: sig } = await HardwareSDK.evmSignTransaction(connectId, deviceId, {
path: accountPath,
transaction: tx,
keepSession: true,
});
const rawTx = serialize({ ...tx, type: TransactionTypes.eip1559 }, {
r: sig.r,
s: sig.s,
v: Number(sig.v),
});
// 通过 RPC 广播,例如 ethers.js provider.sendTransaction(rawTx)示例:Legacy 交易
const legacyTx = {
to: '0xRecipient',
value: '0x2386f26fc10000', // 0.01 ETH
data: '0x',
nonce: '0x1',
gasLimit: '0x5208',
gasPrice: '0x3b9aca00', // 1 gwei
chainId: 1,
};
const sig = await HardwareSDK.evmSignTransaction(connectId, deviceId, {
path: accountPath,
transaction: legacyTx,
});示例:personal_sign 消息签名
const message = 'Hello OneKey';
const messageHex = Buffer.from(message).toString('hex');
const res = await HardwareSDK.evmSignMessage(connectId, deviceId, {
path: accountPath,
messageHex,
chainId: 1,
});
// res.payload.signature -> 用 ethers.verifyMessage 校验示例:Typed Data(EIP-712)
const typedData = {
types: {
EIP712Domain: [
{ name: 'name', type: 'string' },
{ name: 'version', type: 'string' },
{ name: 'chainId', type: 'uint256' },
{ name: 'verifyingContract', type: 'address' },
],
Mail: [
{ name: 'from', type: 'Person' },
{ name: 'to', type: 'Person' },
{ name: 'contents', type: 'string' },
],
Person: [
{ name: 'name', type: 'string' },
{ name: 'wallet', type: 'address' },
],
},
primaryType: 'Mail',
domain: {
name: 'Ether Mail',
version: '1',
chainId: 1,
verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',
},
message: {
from: { name: 'Cow', wallet: '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826' },
to: { name: 'Bob', wallet: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB' },
contents: 'Hello, Bob!',
},
};
const res = await HardwareSDK.evmSignTypedData(connectId, deviceId, {
path: accountPath,
data: typedData,
chainId: 1,
});
// res.payload.signature -> 用 ethers.verifyTypedData(...) 恢复签名者验签与排障
- 验签:
- 交易:用
ethers/viem重新序列化,校验签名哈希与设备显示一致。 - personal_sign:
ethers.verifyMessage(message, sig.signature)。 - EIP‑712:
ethers.verifyTypedData(domain, types, message, sig.signature)。
- 交易:用
- 常见限制:超大
data或异常字段可能被拒;chainId必须与广播网络一致。 - 常见错误:
- 路径不一致:默认
m/44'/60'/0'/0/0,按账户/地址递增;务必与地址确认一致。 - Gas 字段缺失:1559 必填
maxFeePerGas + maxPriorityFeePerGas;Legacy 仅填gasPrice。 - 设备交互阻塞:串行调用同一设备;使用
keepSession复用会话,避免重复 PIN/密码短语弹窗。 - 用户拒签/超时:提供重试/取消,不要自动重播。
- 路径不一致:默认
相关链方法文档:evmSignTransaction · evmSignMessage · evmSignTypedData。