@onekeyfe/hd-transport-usb 1.1.26-alpha.0 → 1.1.26-alpha.101

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,4 @@
1
1
  export declare const PACKET_SIZE = 64;
2
2
  export declare const REPORT_ID = 63;
3
- export declare const PAYLOAD_SIZE: number;
4
3
  export declare const HEADER_LENGTH = 6;
5
4
  //# sourceMappingURL=constants.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,WAAW,KAAK,CAAC;AAG9B,eAAO,MAAM,SAAS,KAAO,CAAC;AAG9B,eAAO,MAAM,YAAY,QAAkB,CAAC;AAG5C,eAAO,MAAM,aAAa,IAAI,CAAC"}
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,WAAW,KAAK,CAAC;AAG9B,eAAO,MAAM,SAAS,KAAO,CAAC;AAG9B,eAAO,MAAM,aAAa,IAAI,CAAC"}
package/dist/index.d.ts CHANGED
@@ -1,27 +1,7 @@
1
- import * as transport from '@onekeyfe/hd-transport';
2
- import transport__default, { OneKeyDeviceInfo, AcquireInput } from '@onekeyfe/hd-transport';
3
- import EventEmitter from 'events';
1
+ import { LowlevelTransportSharedPlugin } from '@onekeyfe/hd-transport';
4
2
 
5
3
  declare const PACKET_SIZE = 64;
6
4
 
7
- declare class NodeUsbTransport {
8
- messages: ReturnType<typeof transport__default.parseConfigure> | undefined;
9
- name: string;
10
- configured: boolean;
11
- Log?: any;
12
- emitter?: EventEmitter;
13
- private serialToBusId;
14
- private openDevices;
15
- init(logger: any, emitter?: EventEmitter): void;
16
- configure(signedData: any): void;
17
- listen(): void;
18
- enumerate(): Promise<OneKeyDeviceInfo[]>;
19
- acquire(input: AcquireInput): Promise<string>;
20
- release(path: string, _onclose?: boolean): Promise<void>;
21
- call(path: string, name: string, data: Record<string, unknown>): Promise<transport.MessageFromOneKey>;
22
- cancel(): void;
23
- private openDevice;
24
- private receiveData;
25
- }
5
+ declare const UsbPlugin: LowlevelTransportSharedPlugin;
26
6
 
27
- export { PACKET_SIZE, NodeUsbTransport as default };
7
+ export { PACKET_SIZE, UsbPlugin, UsbPlugin as default };
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA,OAAO,SAA8B,MAAM,wBAAwB,CAAC;AAKpE,OAAO,KAAK,YAAY,MAAM,QAAQ,CAAC;AACvC,OAAO,KAAK,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AA6K7E,MAAM,CAAC,OAAO,OAAO,gBAAgB;IACnC,QAAQ,EAAE,UAAU,CAAC,OAAO,SAAS,CAAC,cAAc,CAAC,GAAG,SAAS,CAAC;IAElE,IAAI,SAAsB;IAE1B,UAAU,UAAS;IAEnB,GAAG,CAAC,EAAE,GAAG,CAAC;IAEV,OAAO,CAAC,EAAE,YAAY,CAAC;IAGvB,OAAO,CAAC,aAAa,CAA6B;IAGlD,OAAO,CAAC,WAAW,CAAiC;IAMpD,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,OAAO,CAAC,EAAE,YAAY;IAKxC,SAAS,CAAC,UAAU,EAAE,GAAG;IAMzB,MAAM;IASA,SAAS,IAAI,OAAO,CAAC,gBAAgB,EAAE,CAAC;IA8BxC,OAAO,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC;IAkB7C,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IA6BxD,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAqCpE,MAAM;YAUQ,UAAU;YAuDV,WAAW;CAiC1B;AAED,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAkB,6BAA6B,EAAE,MAAM,wBAAwB,CAAC;AAoG5F,eAAO,MAAM,SAAS,EAAE,6BA+JvB,CAAC;AAEF,eAAe,SAAS,CAAC;AACzB,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC"}
package/dist/index.js CHANGED
@@ -63,59 +63,19 @@ typeof SuppressedError === "function" ? SuppressedError : function (error, suppr
63
63
 
64
64
  const PACKET_SIZE = 64;
65
65
  const REPORT_ID = 0x3f;
66
- const PAYLOAD_SIZE = PACKET_SIZE - 1;
67
66
  const HEADER_LENGTH = 6;
68
67
 
69
- const { parseConfigure, buildEncodeBuffers, decodeProtocol, receiveOne, check } = transport__default["default"];
68
+ const { decodeProtocol } = transport__default["default"];
70
69
  const INTERFACE_NUMBER = 0;
71
70
  const ENDPOINT_IN = 0x81;
72
71
  const ENDPOINT_OUT = 0x01;
73
72
  const TRANSFER_TIMEOUT_MS = 30000;
74
- const PACKET_IO_MAX_RETRIES = 3;
75
- const PACKET_IO_RETRY_DELAY = 300;
76
- function getBusId(dev) {
73
+ let activeDevice = null;
74
+ const openDevices = new Map();
75
+ function getDeviceId(dev) {
77
76
  return `usb:${dev.busNumber}:${dev.deviceAddress}`;
78
77
  }
79
- function readSerialNumber(dev, openDevices) {
80
- const { iSerialNumber } = dev.deviceDescriptor;
81
- if (!iSerialNumber)
82
- return Promise.resolve(getBusId(dev));
83
- const busId = getBusId(dev);
84
- if (openDevices) {
85
- for (const [serial, od] of openDevices) {
86
- if (od.device === dev || getBusId(od.device) === busId) {
87
- return Promise.resolve(serial);
88
- }
89
- }
90
- }
91
- return new Promise(resolve => {
92
- try {
93
- dev.open();
94
- try {
95
- dev.getStringDescriptor(iSerialNumber, (_err, data) => {
96
- try {
97
- dev.close();
98
- }
99
- catch (_a) {
100
- }
101
- resolve(data || busId);
102
- });
103
- }
104
- catch (_a) {
105
- try {
106
- dev.close();
107
- }
108
- catch (_b) {
109
- }
110
- resolve(busId);
111
- }
112
- }
113
- catch (_c) {
114
- resolve(busId);
115
- }
116
- });
117
- }
118
- function transferInOnce(ep, length) {
78
+ function transferIn(ep, length) {
119
79
  return new Promise((resolve, reject) => {
120
80
  ep.transfer(length, (err, data) => {
121
81
  if (err)
@@ -126,7 +86,7 @@ function transferInOnce(ep, length) {
126
86
  });
127
87
  });
128
88
  }
129
- function transferOutOnce(ep, data) {
89
+ function transferOut(ep, data) {
130
90
  return new Promise((resolve, reject) => {
131
91
  ep.transfer(data, (err) => {
132
92
  if (err)
@@ -135,43 +95,9 @@ function transferOutOnce(ep, data) {
135
95
  });
136
96
  });
137
97
  }
138
- function wait(ms) {
139
- return new Promise(resolve => {
140
- setTimeout(resolve, ms);
141
- });
142
- }
143
- function transferIn(ep, length) {
98
+ function readPacket(dev) {
144
99
  return __awaiter(this, void 0, void 0, function* () {
145
- let lastError;
146
- for (let attempt = 1; attempt <= PACKET_IO_MAX_RETRIES; attempt++) {
147
- try {
148
- return yield transferInOnce(ep, length);
149
- }
150
- catch (err) {
151
- lastError = err;
152
- if (attempt < PACKET_IO_MAX_RETRIES) {
153
- yield wait(attempt * PACKET_IO_RETRY_DELAY);
154
- }
155
- }
156
- }
157
- throw lastError;
158
- });
159
- }
160
- function transferOut(ep, data) {
161
- return __awaiter(this, void 0, void 0, function* () {
162
- let lastError;
163
- for (let attempt = 1; attempt <= PACKET_IO_MAX_RETRIES; attempt++) {
164
- try {
165
- return yield transferOutOnce(ep, data);
166
- }
167
- catch (err) {
168
- lastError = err;
169
- if (attempt < PACKET_IO_MAX_RETRIES) {
170
- yield wait(attempt * PACKET_IO_RETRY_DELAY);
171
- }
172
- }
173
- }
174
- throw lastError;
100
+ return transferIn(dev.epIn, PACKET_SIZE);
175
101
  });
176
102
  }
177
103
  function skipReportByte(packet) {
@@ -183,24 +109,12 @@ function skipReportByte(packet) {
183
109
  function toArrayBuffer(buf) {
184
110
  return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
185
111
  }
186
- class NodeUsbTransport {
187
- constructor() {
188
- this.name = 'NodeUsbTransport';
189
- this.configured = false;
190
- this.serialToBusId = new Map();
191
- this.openDevices = new Map();
192
- }
193
- init(logger, emitter) {
194
- this.Log = logger;
195
- this.emitter = emitter;
196
- }
197
- configure(signedData) {
198
- const messages = parseConfigure(signedData);
199
- this.configured = true;
200
- this.messages = messages;
201
- }
202
- listen() {
203
- }
112
+ const UsbPlugin = {
113
+ version: '1.0.0',
114
+ init() {
115
+ return __awaiter(this, void 0, void 0, function* () {
116
+ });
117
+ },
204
118
  enumerate() {
205
119
  return __awaiter(this, void 0, void 0, function* () {
206
120
  const allDevices = usb__namespace.getDeviceList();
@@ -208,115 +122,24 @@ class NodeUsbTransport {
208
122
  const { idVendor, idProduct } = d.deviceDescriptor;
209
123
  return hdShared.ONEKEY_WEBUSB_FILTER.some(f => idVendor === f.vendorId && idProduct === f.productId);
210
124
  });
211
- const newSerialToBusId = new Map();
212
- const results = [];
213
- for (const d of onekeyDevices) {
214
- const busId = getBusId(d);
215
- const serial = yield readSerialNumber(d, this.openDevices);
216
- newSerialToBusId.set(serial, busId);
217
- results.push({
218
- path: serial,
219
- id: serial,
220
- name: 'OneKey',
221
- commType: 'usb',
222
- debug: false,
223
- });
224
- }
225
- this.serialToBusId = newSerialToBusId;
226
- return results;
125
+ return onekeyDevices.map(d => ({
126
+ id: getDeviceId(d),
127
+ name: 'OneKey',
128
+ commType: 'usb',
129
+ }));
227
130
  });
228
- }
229
- acquire(input) {
230
- var _a, _b, _c;
231
- return __awaiter(this, void 0, void 0, function* () {
232
- const path = (_a = input.path) !== null && _a !== void 0 ? _a : '';
233
- if (!path) {
234
- throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceNotFound, 'No device path provided');
235
- }
236
- try {
237
- yield this.openDevice(path);
238
- return path;
239
- }
240
- catch (error) {
241
- (_b = this.Log) === null || _b === void 0 ? void 0 : _b.debug('NodeUsbTransport acquire error: ', error);
242
- throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceNotFound, (_c = error.message) !== null && _c !== void 0 ? _c : String(error));
243
- }
244
- });
245
- }
246
- release(path, _onclose) {
131
+ },
132
+ connect(uuid) {
247
133
  return __awaiter(this, void 0, void 0, function* () {
248
- const openDev = this.openDevices.get(path);
249
- if (!openDev)
134
+ const existing = openDevices.get(uuid);
135
+ if (existing) {
136
+ activeDevice = existing;
250
137
  return;
251
- try {
252
- yield new Promise(resolve => {
253
- openDev.iface.release(() => {
254
- try {
255
- openDev.device.close();
256
- }
257
- catch (_a) {
258
- }
259
- resolve();
260
- });
261
- });
262
- }
263
- catch (_a) {
264
- try {
265
- openDev.device.close();
266
- }
267
- catch (_b) {
268
- }
269
- }
270
- this.openDevices.delete(path);
271
- });
272
- }
273
- call(path, name, data) {
274
- var _a, _b;
275
- return __awaiter(this, void 0, void 0, function* () {
276
- if (!this.messages) {
277
- throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.TransportNotConfigured);
278
- }
279
- const openDev = this.openDevices.get(path);
280
- if (!openDev) {
281
- throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceNotFound, `Device not acquired: ${path}`);
282
- }
283
- const { messages } = this;
284
- if (transport.LogBlockCommand.has(name)) {
285
- (_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug('NodeUsbTransport call-', ' name: ', name);
286
- }
287
- else {
288
- (_b = this.Log) === null || _b === void 0 ? void 0 : _b.debug('NodeUsbTransport call-', ' name: ', name, ' data: ', data);
289
138
  }
290
- const encodeBuffers = buildEncodeBuffers(messages, name, data);
291
- for (const buffer of encodeBuffers) {
292
- const packet = new Uint8Array(PACKET_SIZE);
293
- packet[0] = REPORT_ID;
294
- packet.set(new Uint8Array(buffer), 1);
295
- yield transferOut(openDev.epOut, Buffer.from(packet));
296
- }
297
- const resData = yield this.receiveData(openDev);
298
- if (typeof resData !== 'string') {
299
- throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.NetworkError, 'Returning data is not string.');
300
- }
301
- const jsonData = receiveOne(messages, resData);
302
- return check.call(jsonData);
303
- });
304
- }
305
- cancel() {
306
- var _a;
307
- (_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug('NodeUsbTransport cancel');
308
- }
309
- openDevice(path) {
310
- var _a;
311
- return __awaiter(this, void 0, void 0, function* () {
312
- const existing = this.openDevices.get(path);
313
- if (existing)
314
- return;
315
- const busId = (_a = this.serialToBusId.get(path)) !== null && _a !== void 0 ? _a : path;
316
139
  const allDevices = usb__namespace.getDeviceList();
317
- const dev = allDevices.find(d => getBusId(d) === busId);
140
+ const dev = allDevices.find(d => getDeviceId(d) === uuid);
318
141
  if (!dev) {
319
- throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceNotFound, `USB device not found: ${path}`);
142
+ throw new Error(`USB device not found: ${uuid}`);
320
143
  }
321
144
  dev.open();
322
145
  dev.timeout = TRANSFER_TIMEOUT_MS;
@@ -327,7 +150,7 @@ class NodeUsbTransport {
327
150
  iface.detachKernelDriver();
328
151
  }
329
152
  }
330
- catch (_b) {
153
+ catch (_a) {
331
154
  }
332
155
  }
333
156
  iface.claim();
@@ -335,16 +158,60 @@ class NodeUsbTransport {
335
158
  const epOut = iface.endpoints.find((e) => e.direction === 'out' && e.address === ENDPOINT_OUT);
336
159
  if (!epIn || !epOut) {
337
160
  dev.close();
338
- throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceNotFound, 'USB endpoints not found (expected IN 0x81, OUT 0x01)');
161
+ throw new Error('USB endpoints not found (expected IN 0x81, OUT 0x01)');
339
162
  }
340
163
  epIn.timeout = TRANSFER_TIMEOUT_MS;
341
164
  epOut.timeout = TRANSFER_TIMEOUT_MS;
342
- this.openDevices.set(path, { device: dev, iface, epIn, epOut });
165
+ const openDev = { device: dev, iface, epIn, epOut };
166
+ openDevices.set(uuid, openDev);
167
+ activeDevice = openDev;
343
168
  });
344
- }
345
- receiveData(dev) {
169
+ },
170
+ disconnect(uuid) {
171
+ return __awaiter(this, void 0, void 0, function* () {
172
+ const openDev = openDevices.get(uuid);
173
+ if (openDev) {
174
+ try {
175
+ openDev.iface.release(() => {
176
+ try {
177
+ openDev.device.close();
178
+ }
179
+ catch (_a) {
180
+ }
181
+ });
182
+ }
183
+ catch (_a) {
184
+ try {
185
+ openDev.device.close();
186
+ }
187
+ catch (_b) {
188
+ }
189
+ }
190
+ openDevices.delete(uuid);
191
+ if (activeDevice === openDev) {
192
+ activeDevice = null;
193
+ }
194
+ }
195
+ });
196
+ },
197
+ send(uuid, data) {
198
+ return __awaiter(this, void 0, void 0, function* () {
199
+ const openDev = openDevices.get(uuid);
200
+ if (!openDev) {
201
+ throw new Error(`Device not connected: ${uuid}`);
202
+ }
203
+ activeDevice = openDev;
204
+ const dataBuffer = Buffer.from(data, 'hex');
205
+ yield transferOut(openDev.epOut, dataBuffer);
206
+ });
207
+ },
208
+ receive() {
346
209
  return __awaiter(this, void 0, void 0, function* () {
347
- const firstPacket = yield transferIn(dev.epIn, PACKET_SIZE);
210
+ if (!activeDevice) {
211
+ throw new Error('No active device for receive');
212
+ }
213
+ const dev = activeDevice;
214
+ const firstPacket = yield readPacket(dev);
348
215
  const firstData = skipReportByte(firstPacket);
349
216
  const { length, typeId, restBuffer } = decodeProtocol.decodeChunked(toArrayBuffer(firstData));
350
217
  const lengthWithHeader = Number(length) + HEADER_LENGTH;
@@ -355,10 +222,10 @@ class NodeUsbTransport {
355
222
  decoded.append(restBuffer);
356
223
  }
357
224
  while (decoded.offset < lengthWithHeader) {
358
- const packet = yield transferIn(dev.epIn, PACKET_SIZE);
225
+ const packet = yield readPacket(dev);
359
226
  const pktData = skipReportByte(packet);
360
227
  const buf = toArrayBuffer(pktData);
361
- if (lengthWithHeader - decoded.offset >= PAYLOAD_SIZE) {
228
+ if (lengthWithHeader - decoded.offset >= PACKET_SIZE) {
362
229
  decoded.append(buf);
363
230
  }
364
231
  else {
@@ -369,8 +236,9 @@ class NodeUsbTransport {
369
236
  const result = decoded.toBuffer();
370
237
  return Buffer.from(result).toString('hex');
371
238
  });
372
- }
373
- }
239
+ },
240
+ };
374
241
 
375
242
  exports.PACKET_SIZE = PACKET_SIZE;
376
- exports["default"] = NodeUsbTransport;
243
+ exports.UsbPlugin = UsbPlugin;
244
+ exports["default"] = UsbPlugin;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/hd-transport-usb",
3
- "version": "1.1.26-alpha.0",
3
+ "version": "1.1.26-alpha.101",
4
4
  "description": "OneKey hardware wallet direct USB transport plugin (libusb)",
5
5
  "homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme",
6
6
  "license": "MIT",
@@ -20,10 +20,10 @@
20
20
  "lint:fix": "eslint . --fix"
21
21
  },
22
22
  "dependencies": {
23
- "@onekeyfe/hd-shared": "1.1.26-alpha.0",
24
- "@onekeyfe/hd-transport": "1.1.26-alpha.0",
23
+ "@onekeyfe/hd-shared": "1.1.26-alpha.101",
24
+ "@onekeyfe/hd-transport": "1.1.26-alpha.101",
25
25
  "bytebuffer": "^5.0.1",
26
26
  "usb": "^2.14.0"
27
27
  },
28
- "gitHead": "8d3072a406b0672c9d20e2f58e25cef94a9759fb"
28
+ "gitHead": "4001d720918ca2ae8c0f55e1ca6309a6a3c8d413"
29
29
  }
package/src/constants.ts CHANGED
@@ -4,8 +4,5 @@ export const PACKET_SIZE = 64;
4
4
  /** Protocol marker byte (0x3F = '?') — first byte of every packet */
5
5
  export const REPORT_ID = 0x3f;
6
6
 
7
- /** Usable payload per packet after stripping the 0x3F report byte */
8
- export const PAYLOAD_SIZE = PACKET_SIZE - 1;
9
-
10
7
  /** Protocol header length: typeId (2 bytes) + length (4 bytes) */
11
8
  export const HEADER_LENGTH = 6;
package/src/index.ts CHANGED
@@ -1,14 +1,13 @@
1
1
  import ByteBuffer from 'bytebuffer';
2
2
  import * as usb from 'usb';
3
- import transport, { LogBlockCommand } from '@onekeyfe/hd-transport';
4
- import { ERRORS, HardwareErrorCode, ONEKEY_WEBUSB_FILTER } from '@onekeyfe/hd-shared';
3
+ import transport from '@onekeyfe/hd-transport';
4
+ import { ONEKEY_WEBUSB_FILTER } from '@onekeyfe/hd-shared';
5
5
 
6
- import { HEADER_LENGTH, PACKET_SIZE, PAYLOAD_SIZE, REPORT_ID } from './constants';
6
+ import { HEADER_LENGTH, PACKET_SIZE, REPORT_ID } from './constants';
7
7
 
8
- import type EventEmitter from 'events';
9
- import type { AcquireInput, OneKeyDeviceInfo } from '@onekeyfe/hd-transport';
8
+ import type { LowLevelDevice, LowlevelTransportSharedPlugin } from '@onekeyfe/hd-transport';
10
9
 
11
- const { parseConfigure, buildEncodeBuffers, decodeProtocol, receiveOne, check } = transport;
10
+ const { decodeProtocol } = transport;
12
11
 
13
12
  /** USB interface number for vendor-specific communication */
14
13
  const INTERFACE_NUMBER = 0;
@@ -19,10 +18,6 @@ const ENDPOINT_OUT = 0x01;
19
18
  /** Transfer timeout in milliseconds */
20
19
  const TRANSFER_TIMEOUT_MS = 30000;
21
20
 
22
- /** Packet I/O retry configuration (matches WebUsbTransport) */
23
- const PACKET_IO_MAX_RETRIES = 3;
24
- const PACKET_IO_RETRY_DELAY = 300;
25
-
26
21
  /**
27
22
  * Opened device state — holds the USB device, claimed interface, and endpoints.
28
23
  */
@@ -34,62 +29,26 @@ interface OpenDevice {
34
29
  }
35
30
 
36
31
  /**
37
- * Fallback identifier using bus topology (unstable across re-plugs).
32
+ * The currently active device used by receive() since
33
+ * LowlevelTransportSharedPlugin.receive() takes no uuid parameter.
38
34
  */
39
- function getBusId(dev: usb.Device): string {
40
- return `usb:${dev.busNumber}:${dev.deviceAddress}`;
41
- }
35
+ let activeDevice: OpenDevice | null = null;
36
+
37
+ /** Map of uuid → open device state */
38
+ const openDevices = new Map<string, OpenDevice>();
42
39
 
43
40
  /**
44
- * Read USB string descriptor serial number from a device.
45
- * Opens device briefly, reads serial, then closes.
46
- * Falls back to bus path if serial cannot be read.
41
+ * Build a unique identifier for a USB device.
42
+ * Uses bus number + device address which is stable within a session.
47
43
  */
48
- function readSerialNumber(dev: usb.Device, openDevices?: Map<string, OpenDevice>): Promise<string> {
49
- const { iSerialNumber } = dev.deviceDescriptor;
50
- if (!iSerialNumber) return Promise.resolve(getBusId(dev));
51
-
52
- // If the device is already open (acquired), read serial without open/close
53
- const busId = getBusId(dev);
54
- if (openDevices) {
55
- for (const [serial, od] of openDevices) {
56
- if (od.device === dev || getBusId(od.device) === busId) {
57
- return Promise.resolve(serial);
58
- }
59
- }
60
- }
61
-
62
- return new Promise(resolve => {
63
- try {
64
- dev.open();
65
- try {
66
- dev.getStringDescriptor(iSerialNumber, (_err: Error | undefined, data?: string) => {
67
- try {
68
- dev.close();
69
- } catch {
70
- /* ignore */
71
- }
72
- resolve(data || busId);
73
- });
74
- } catch {
75
- try {
76
- dev.close();
77
- } catch {
78
- /* ignore */
79
- }
80
- resolve(busId);
81
- }
82
- } catch {
83
- // dev.open() failed (e.g. LIBUSB_ERROR_BUSY if already open elsewhere)
84
- resolve(busId);
85
- }
86
- });
44
+ function getDeviceId(dev: usb.Device): string {
45
+ return `usb:${dev.busNumber}:${dev.deviceAddress}`;
87
46
  }
88
47
 
89
48
  /**
90
- * Promisified USB IN transfer (single attempt).
49
+ * Promisified USB IN transfer.
91
50
  */
92
- function transferInOnce(ep: usb.InEndpoint, length: number): Promise<Buffer> {
51
+ function transferIn(ep: usb.InEndpoint, length: number): Promise<Buffer> {
93
52
  return new Promise((resolve, reject) => {
94
53
  ep.transfer(length, (err: Error | undefined, data: Buffer | undefined) => {
95
54
  if (err) return reject(err);
@@ -100,9 +59,9 @@ function transferInOnce(ep: usb.InEndpoint, length: number): Promise<Buffer> {
100
59
  }
101
60
 
102
61
  /**
103
- * Promisified USB OUT transfer (single attempt).
62
+ * Promisified USB OUT transfer.
104
63
  */
105
- function transferOutOnce(ep: usb.OutEndpoint, data: Buffer): Promise<void> {
64
+ function transferOut(ep: usb.OutEndpoint, data: Buffer): Promise<void> {
106
65
  return new Promise((resolve, reject) => {
107
66
  ep.transfer(data, (err: Error | undefined) => {
108
67
  if (err) return reject(err);
@@ -111,50 +70,16 @@ function transferOutOnce(ep: usb.OutEndpoint, data: Buffer): Promise<void> {
111
70
  });
112
71
  }
113
72
 
114
- function wait(ms: number): Promise<void> {
115
- return new Promise(resolve => {
116
- setTimeout(resolve, ms);
117
- });
118
- }
119
-
120
73
  /**
121
- * USB IN transfer with retry.
74
+ * Read a single 64-byte packet from the device via libusb bulk/interrupt transfer.
122
75
  */
123
- async function transferIn(ep: usb.InEndpoint, length: number): Promise<Buffer> {
124
- let lastError: unknown;
125
- for (let attempt = 1; attempt <= PACKET_IO_MAX_RETRIES; attempt++) {
126
- try {
127
- return await transferInOnce(ep, length);
128
- } catch (err) {
129
- lastError = err;
130
- if (attempt < PACKET_IO_MAX_RETRIES) {
131
- await wait(attempt * PACKET_IO_RETRY_DELAY);
132
- }
133
- }
134
- }
135
- throw lastError;
136
- }
137
-
138
- /**
139
- * USB OUT transfer with retry.
140
- */
141
- async function transferOut(ep: usb.OutEndpoint, data: Buffer): Promise<void> {
142
- let lastError: unknown;
143
- for (let attempt = 1; attempt <= PACKET_IO_MAX_RETRIES; attempt++) {
144
- try {
145
- return await transferOutOnce(ep, data);
146
- } catch (err) {
147
- lastError = err;
148
- if (attempt < PACKET_IO_MAX_RETRIES) {
149
- await wait(attempt * PACKET_IO_RETRY_DELAY);
150
- }
151
- }
152
- }
153
- throw lastError;
76
+ async function readPacket(dev: OpenDevice): Promise<Buffer> {
77
+ return transferIn(dev.epIn, PACKET_SIZE);
154
78
  }
155
79
 
156
80
  /**
157
81
  * Skip the 0x3F protocol marker byte from a USB packet.
82
+ * The device sends 0x3F as the first byte of every response packet.
158
83
  */
159
84
  function skipReportByte(packet: Buffer): Buffer {
160
85
  if (packet[0] === REPORT_ID) {
@@ -171,56 +96,24 @@ function toArrayBuffer(buf: Buffer): ArrayBuffer {
171
96
  }
172
97
 
173
98
  /**
174
- * Node.js USB Transport complete transport implementation using libusb.
99
+ * Node.js USB plugin for LowlevelTransport.
175
100
  *
176
- * Unlike the old UsbPlugin (which was a LowlevelTransportSharedPlugin piped
177
- * through LowlevelTransport), this class is a standalone transport that handles
178
- * both protocol encoding/decoding and USB I/O directly.
101
+ * Implements the 6-method LowlevelTransportSharedPlugin interface
102
+ * using the `usb` (libusb) library for direct USB communication
103
+ * with OneKey hardware wallets.
179
104
  *
180
- * Modeled after WebUsbTransport.
105
+ * Uses libusb to access the vendor-specific interface (class 255)
106
+ * which is NOT visible to the HID framework on macOS.
181
107
  */
182
- export default class NodeUsbTransport {
183
- messages: ReturnType<typeof transport.parseConfigure> | undefined;
184
-
185
- name = 'NodeUsbTransport';
186
-
187
- configured = false;
188
-
189
- Log?: any;
190
-
191
- emitter?: EventEmitter;
192
-
193
- /** serial → bus id, built during enumerate */
194
- private serialToBusId = new Map<string, string>();
195
-
196
- /** path → opened device state */
197
- private openDevices = new Map<string, OpenDevice>();
198
-
199
- /**
200
- * Initialize transport.
201
- * Signature matches the Transport.init interface (logger, emitter).
202
- */
203
- init(logger: any, emitter?: EventEmitter) {
204
- this.Log = logger;
205
- this.emitter = emitter;
206
- }
207
-
208
- configure(signedData: any) {
209
- const messages = parseConfigure(signedData);
210
- this.configured = true;
211
- this.messages = messages;
212
- }
108
+ export const UsbPlugin: LowlevelTransportSharedPlugin = {
109
+ version: '1.0.0',
213
110
 
214
- listen() {
215
- // empty could add hotplug events via usb.on('attach'/'detach')
216
- }
111
+ async init(): Promise<void> {
112
+ // libusb requires no global initialization
113
+ },
217
114
 
218
- /**
219
- * Enumerate connected OneKey USB devices.
220
- * Opens each device briefly to read its serial number (used as `path`),
221
- * then closes it. acquire() re-opens from a fresh getDeviceList().
222
- */
223
- async enumerate(): Promise<OneKeyDeviceInfo[]> {
115
+ // eslint-disable-next-line @typescript-eslint/require-await
116
+ async enumerate(): Promise<LowLevelDevice[]> {
224
117
  const allDevices = usb.getDeviceList();
225
118
 
226
119
  const onekeyDevices = allDevices.filter(d => {
@@ -228,132 +121,26 @@ export default class NodeUsbTransport {
228
121
  return ONEKEY_WEBUSB_FILTER.some(f => idVendor === f.vendorId && idProduct === f.productId);
229
122
  });
230
123
 
231
- const newSerialToBusId = new Map<string, string>();
232
- const results: OneKeyDeviceInfo[] = [];
233
- for (const d of onekeyDevices) {
234
- const busId = getBusId(d);
235
- const serial = await readSerialNumber(d, this.openDevices);
236
- newSerialToBusId.set(serial, busId);
237
- results.push({
238
- path: serial,
239
- id: serial,
240
- name: 'OneKey',
241
- commType: 'usb',
242
- debug: false,
243
- });
244
- }
245
- // Atomic swap — concurrent acquire() always sees a complete map
246
- this.serialToBusId = newSerialToBusId;
247
- return results;
248
- }
249
-
250
- /**
251
- * Acquire device — open USB device, claim interface, return path (string).
252
- */
253
- async acquire(input: AcquireInput): Promise<string> {
254
- const path = input.path ?? '';
255
- if (!path) {
256
- throw ERRORS.TypedError(HardwareErrorCode.DeviceNotFound, 'No device path provided');
257
- }
258
-
259
- try {
260
- await this.openDevice(path);
261
- return path;
262
- } catch (error: any) {
263
- this.Log?.debug('NodeUsbTransport acquire error: ', error);
264
- throw ERRORS.TypedError(HardwareErrorCode.DeviceNotFound, error.message ?? String(error));
265
- }
266
- }
267
-
268
- /**
269
- * Release device — release interface and close.
270
- */
271
- async release(path: string, _onclose?: boolean): Promise<void> {
272
- const openDev = this.openDevices.get(path);
273
- if (!openDev) return;
274
-
275
- try {
276
- await new Promise<void>(resolve => {
277
- openDev.iface.release(() => {
278
- try {
279
- openDev.device.close();
280
- } catch {
281
- /* ignore */
282
- }
283
- resolve();
284
- });
285
- });
286
- } catch {
287
- try {
288
- openDev.device.close();
289
- } catch {
290
- /* ignore */
291
- }
292
- }
293
- this.openDevices.delete(path);
294
- }
124
+ return onekeyDevices.map(d => ({
125
+ id: getDeviceId(d),
126
+ name: 'OneKey',
127
+ commType: 'usb' as const,
128
+ }));
129
+ },
295
130
 
296
- /**
297
- * Call device method — encode protobuf, send packets, receive response.
298
- * This is the core method that replaces LowlevelTransport's call + UsbPlugin's send/receive.
299
- */
300
- async call(path: string, name: string, data: Record<string, unknown>) {
301
- if (!this.messages) {
302
- throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
303
- }
304
-
305
- const openDev = this.openDevices.get(path);
306
- if (!openDev) {
307
- throw ERRORS.TypedError(HardwareErrorCode.DeviceNotFound, `Device not acquired: ${path}`);
308
- }
309
-
310
- const { messages } = this;
311
- if (LogBlockCommand.has(name)) {
312
- this.Log?.debug('NodeUsbTransport call-', ' name: ', name);
313
- } else {
314
- this.Log?.debug('NodeUsbTransport call-', ' name: ', name, ' data: ', data);
315
- }
316
-
317
- // Encode protobuf message into 63-byte chunks (same as WebUsbTransport)
318
- const encodeBuffers = buildEncodeBuffers(messages, name, data);
319
-
320
- // Send each chunk with 0x3F report ID prefix
321
- for (const buffer of encodeBuffers) {
322
- const packet = new Uint8Array(PACKET_SIZE);
323
- packet[0] = REPORT_ID;
324
- packet.set(new Uint8Array(buffer), 1);
325
- await transferOut(openDev.epOut, Buffer.from(packet));
326
- }
327
-
328
- // Receive response
329
- const resData = await this.receiveData(openDev);
330
- if (typeof resData !== 'string') {
331
- throw ERRORS.TypedError(HardwareErrorCode.NetworkError, 'Returning data is not string.');
332
- }
333
- const jsonData = receiveOne(messages, resData);
334
- return check.call(jsonData);
335
- }
336
-
337
- cancel() {
338
- this.Log?.debug('NodeUsbTransport cancel');
339
- }
340
-
341
- // --- Private helpers ---
342
-
343
- /**
344
- * Open a USB device by path (serial number), claim interface, cache endpoints.
345
- */
346
131
  // eslint-disable-next-line @typescript-eslint/require-await
347
- private async openDevice(path: string): Promise<void> {
348
- const existing = this.openDevices.get(path);
349
- if (existing) return;
132
+ async connect(uuid: string): Promise<void> {
133
+ const existing = openDevices.get(uuid);
134
+ if (existing) {
135
+ activeDevice = existing;
136
+ return;
137
+ }
350
138
 
351
- // Resolve serial bus id, then find a fresh device object
352
- const busId = this.serialToBusId.get(path) ?? path;
139
+ // Find the device by our uuid (bus:address)
353
140
  const allDevices = usb.getDeviceList();
354
- const dev = allDevices.find(d => getBusId(d) === busId);
141
+ const dev = allDevices.find(d => getDeviceId(d) === uuid);
355
142
  if (!dev) {
356
- throw ERRORS.TypedError(HardwareErrorCode.DeviceNotFound, `USB device not found: ${path}`);
143
+ throw new Error(`USB device not found: ${uuid}`);
357
144
  }
358
145
 
359
146
  dev.open();
@@ -374,6 +161,7 @@ export default class NodeUsbTransport {
374
161
 
375
162
  iface.claim();
376
163
 
164
+ // Find IN and OUT endpoints
377
165
  const epIn = iface.endpoints.find(
378
166
  (e): e is usb.InEndpoint => e.direction === 'in' && e.address === ENDPOINT_IN
379
167
  );
@@ -383,31 +171,74 @@ export default class NodeUsbTransport {
383
171
 
384
172
  if (!epIn || !epOut) {
385
173
  dev.close();
386
- throw ERRORS.TypedError(
387
- HardwareErrorCode.DeviceNotFound,
388
- 'USB endpoints not found (expected IN 0x81, OUT 0x01)'
389
- );
174
+ throw new Error('USB endpoints not found (expected IN 0x81, OUT 0x01)');
390
175
  }
391
176
 
392
177
  epIn.timeout = TRANSFER_TIMEOUT_MS;
393
178
  epOut.timeout = TRANSFER_TIMEOUT_MS;
394
179
 
395
- this.openDevices.set(path, { device: dev, iface, epIn, epOut });
396
- }
180
+ const openDev: OpenDevice = { device: dev, iface, epIn, epOut };
181
+ openDevices.set(uuid, openDev);
182
+ activeDevice = openDev;
183
+ },
184
+
185
+ // eslint-disable-next-line @typescript-eslint/require-await
186
+ async disconnect(uuid: string): Promise<void> {
187
+ const openDev = openDevices.get(uuid);
188
+ if (openDev) {
189
+ try {
190
+ openDev.iface.release(() => {
191
+ try {
192
+ openDev.device.close();
193
+ } catch {
194
+ /* ignore */
195
+ }
196
+ });
197
+ } catch {
198
+ try {
199
+ openDev.device.close();
200
+ } catch {
201
+ /* ignore */
202
+ }
203
+ }
204
+ openDevices.delete(uuid);
205
+ if (activeDevice === openDev) {
206
+ activeDevice = null;
207
+ }
208
+ }
209
+ },
210
+
211
+ async send(uuid: string, data: string): Promise<void> {
212
+ const openDev = openDevices.get(uuid);
213
+ if (!openDev) {
214
+ throw new Error(`Device not connected: ${uuid}`);
215
+ }
216
+ activeDevice = openDev;
217
+
218
+ // data is a hex string of a 64-byte packet (0x3F + 63 bytes payload),
219
+ // already framed by LowlevelTransport's buildBuffers().
220
+ const dataBuffer = Buffer.from(data, 'hex');
397
221
 
398
- /**
399
- * Receive a complete protobuf response from the device.
400
- * Reads 64-byte packets, strips 0x3F marker, reassembles into hex string.
401
- */
402
- private async receiveData(dev: OpenDevice): Promise<string> {
403
- // Read first packet, skip report byte
404
- const firstPacket = await transferIn(dev.epIn, PACKET_SIZE);
222
+ // libusb transfers the raw packet directly — no Report ID prepend needed
223
+ // (Report ID is a HID concept; libusb operates at USB level)
224
+ await transferOut(openDev.epOut, dataBuffer);
225
+ },
226
+
227
+ async receive(): Promise<string> {
228
+ if (!activeDevice) {
229
+ throw new Error('No active device for receive');
230
+ }
231
+ const dev = activeDevice;
232
+
233
+ // Mirrors WebUsbTransport.receiveData() exactly:
234
+ // 1. Read first 64-byte packet, skip byte[0] (0x3F marker)
235
+ const firstPacket = await readPacket(dev);
405
236
  const firstData = skipReportByte(firstPacket);
406
237
 
407
- // Decode header: ## marker → { typeId, length, restBuffer }
238
+ // 2. Use SDK's decodeChunked to parse ## header → { typeId, length, restBuffer }
408
239
  const { length, typeId, restBuffer } = decodeProtocol.decodeChunked(toArrayBuffer(firstData));
409
240
 
410
- // Allocate result: typeId(2) + length(4) + payload(length)
241
+ // 3. Allocate result buffer: typeId(2) + length(4) + payload(length)
411
242
  const lengthWithHeader = Number(length) + HEADER_LENGTH;
412
243
  const decoded = new ByteBuffer(lengthWithHeader);
413
244
  decoded.writeUint16(typeId);
@@ -416,22 +247,24 @@ export default class NodeUsbTransport {
416
247
  decoded.append(restBuffer);
417
248
  }
418
249
 
419
- // Read subsequent packets until complete
250
+ // 4. Read subsequent packets until complete
420
251
  while (decoded.offset < lengthWithHeader) {
421
- const packet = await transferIn(dev.epIn, PACKET_SIZE);
252
+ const packet = await readPacket(dev);
422
253
  const pktData = skipReportByte(packet);
423
254
  const buf = toArrayBuffer(pktData);
424
- if (lengthWithHeader - decoded.offset >= PAYLOAD_SIZE) {
255
+ if (lengthWithHeader - decoded.offset >= PACKET_SIZE) {
425
256
  decoded.append(buf);
426
257
  } else {
427
258
  decoded.append(buf.slice(0, lengthWithHeader - decoded.offset));
428
259
  }
429
260
  }
430
261
 
262
+ // 5. Return as hex string
431
263
  decoded.reset();
432
264
  const result = decoded.toBuffer();
433
265
  return Buffer.from(result as unknown as ArrayBuffer).toString('hex');
434
- }
435
- }
266
+ },
267
+ };
436
268
 
269
+ export default UsbPlugin;
437
270
  export { PACKET_SIZE } from './constants';