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

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,4 +1,5 @@
1
1
  export declare const PACKET_SIZE = 64;
2
2
  export declare const REPORT_ID = 63;
3
+ export declare const PAYLOAD_SIZE: number;
3
4
  export declare const HEADER_LENGTH = 6;
4
5
  //# 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,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,YAAY,QAAkB,CAAC;AAG5C,eAAO,MAAM,aAAa,IAAI,CAAC"}
package/dist/index.d.ts CHANGED
@@ -1,7 +1,27 @@
1
- import { LowlevelTransportSharedPlugin } from '@onekeyfe/hd-transport';
1
+ import * as transport from '@onekeyfe/hd-transport';
2
+ import transport__default, { OneKeyDeviceInfo, AcquireInput } from '@onekeyfe/hd-transport';
3
+ import EventEmitter from 'events';
2
4
 
3
5
  declare const PACKET_SIZE = 64;
4
6
 
5
- declare const UsbPlugin: LowlevelTransportSharedPlugin;
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
+ }
6
26
 
7
- export { PACKET_SIZE, UsbPlugin, UsbPlugin as default };
27
+ export { PACKET_SIZE, NodeUsbTransport as default };
@@ -1 +1 @@
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"}
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"}
package/dist/index.js CHANGED
@@ -63,19 +63,59 @@ 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;
66
67
  const HEADER_LENGTH = 6;
67
68
 
68
- const { decodeProtocol } = transport__default["default"];
69
+ const { parseConfigure, buildEncodeBuffers, decodeProtocol, receiveOne, check } = transport__default["default"];
69
70
  const INTERFACE_NUMBER = 0;
70
71
  const ENDPOINT_IN = 0x81;
71
72
  const ENDPOINT_OUT = 0x01;
72
73
  const TRANSFER_TIMEOUT_MS = 30000;
73
- let activeDevice = null;
74
- const openDevices = new Map();
75
- function getDeviceId(dev) {
74
+ const PACKET_IO_MAX_RETRIES = 3;
75
+ const PACKET_IO_RETRY_DELAY = 300;
76
+ function getBusId(dev) {
76
77
  return `usb:${dev.busNumber}:${dev.deviceAddress}`;
77
78
  }
78
- function transferIn(ep, length) {
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) {
79
119
  return new Promise((resolve, reject) => {
80
120
  ep.transfer(length, (err, data) => {
81
121
  if (err)
@@ -86,7 +126,7 @@ function transferIn(ep, length) {
86
126
  });
87
127
  });
88
128
  }
89
- function transferOut(ep, data) {
129
+ function transferOutOnce(ep, data) {
90
130
  return new Promise((resolve, reject) => {
91
131
  ep.transfer(data, (err) => {
92
132
  if (err)
@@ -95,9 +135,43 @@ function transferOut(ep, data) {
95
135
  });
96
136
  });
97
137
  }
98
- function readPacket(dev) {
138
+ function wait(ms) {
139
+ return new Promise(resolve => {
140
+ setTimeout(resolve, ms);
141
+ });
142
+ }
143
+ function transferIn(ep, length) {
99
144
  return __awaiter(this, void 0, void 0, function* () {
100
- return transferIn(dev.epIn, PACKET_SIZE);
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;
101
175
  });
102
176
  }
103
177
  function skipReportByte(packet) {
@@ -109,12 +183,24 @@ function skipReportByte(packet) {
109
183
  function toArrayBuffer(buf) {
110
184
  return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
111
185
  }
112
- const UsbPlugin = {
113
- version: '1.0.0',
114
- init() {
115
- return __awaiter(this, void 0, void 0, function* () {
116
- });
117
- },
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
+ }
118
204
  enumerate() {
119
205
  return __awaiter(this, void 0, void 0, function* () {
120
206
  const allDevices = usb__namespace.getDeviceList();
@@ -122,24 +208,115 @@ const UsbPlugin = {
122
208
  const { idVendor, idProduct } = d.deviceDescriptor;
123
209
  return hdShared.ONEKEY_WEBUSB_FILTER.some(f => idVendor === f.vendorId && idProduct === f.productId);
124
210
  });
125
- return onekeyDevices.map(d => ({
126
- id: getDeviceId(d),
127
- name: 'OneKey',
128
- commType: 'usb',
129
- }));
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;
130
227
  });
131
- },
132
- connect(uuid) {
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) {
133
247
  return __awaiter(this, void 0, void 0, function* () {
134
- const existing = openDevices.get(uuid);
135
- if (existing) {
136
- activeDevice = existing;
248
+ const openDev = this.openDevices.get(path);
249
+ if (!openDev)
137
250
  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);
138
289
  }
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;
139
316
  const allDevices = usb__namespace.getDeviceList();
140
- const dev = allDevices.find(d => getDeviceId(d) === uuid);
317
+ const dev = allDevices.find(d => getBusId(d) === busId);
141
318
  if (!dev) {
142
- throw new Error(`USB device not found: ${uuid}`);
319
+ throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceNotFound, `USB device not found: ${path}`);
143
320
  }
144
321
  dev.open();
145
322
  dev.timeout = TRANSFER_TIMEOUT_MS;
@@ -150,7 +327,7 @@ const UsbPlugin = {
150
327
  iface.detachKernelDriver();
151
328
  }
152
329
  }
153
- catch (_a) {
330
+ catch (_b) {
154
331
  }
155
332
  }
156
333
  iface.claim();
@@ -158,60 +335,16 @@ const UsbPlugin = {
158
335
  const epOut = iface.endpoints.find((e) => e.direction === 'out' && e.address === ENDPOINT_OUT);
159
336
  if (!epIn || !epOut) {
160
337
  dev.close();
161
- throw new Error('USB endpoints not found (expected IN 0x81, OUT 0x01)');
338
+ throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceNotFound, 'USB endpoints not found (expected IN 0x81, OUT 0x01)');
162
339
  }
163
340
  epIn.timeout = TRANSFER_TIMEOUT_MS;
164
341
  epOut.timeout = TRANSFER_TIMEOUT_MS;
165
- const openDev = { device: dev, iface, epIn, epOut };
166
- openDevices.set(uuid, openDev);
167
- activeDevice = openDev;
168
- });
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);
342
+ this.openDevices.set(path, { device: dev, iface, epIn, epOut });
206
343
  });
207
- },
208
- receive() {
344
+ }
345
+ receiveData(dev) {
209
346
  return __awaiter(this, void 0, void 0, function* () {
210
- if (!activeDevice) {
211
- throw new Error('No active device for receive');
212
- }
213
- const dev = activeDevice;
214
- const firstPacket = yield readPacket(dev);
347
+ const firstPacket = yield transferIn(dev.epIn, PACKET_SIZE);
215
348
  const firstData = skipReportByte(firstPacket);
216
349
  const { length, typeId, restBuffer } = decodeProtocol.decodeChunked(toArrayBuffer(firstData));
217
350
  const lengthWithHeader = Number(length) + HEADER_LENGTH;
@@ -222,10 +355,10 @@ const UsbPlugin = {
222
355
  decoded.append(restBuffer);
223
356
  }
224
357
  while (decoded.offset < lengthWithHeader) {
225
- const packet = yield readPacket(dev);
358
+ const packet = yield transferIn(dev.epIn, PACKET_SIZE);
226
359
  const pktData = skipReportByte(packet);
227
360
  const buf = toArrayBuffer(pktData);
228
- if (lengthWithHeader - decoded.offset >= PACKET_SIZE) {
361
+ if (lengthWithHeader - decoded.offset >= PAYLOAD_SIZE) {
229
362
  decoded.append(buf);
230
363
  }
231
364
  else {
@@ -236,9 +369,8 @@ const UsbPlugin = {
236
369
  const result = decoded.toBuffer();
237
370
  return Buffer.from(result).toString('hex');
238
371
  });
239
- },
240
- };
372
+ }
373
+ }
241
374
 
242
375
  exports.PACKET_SIZE = PACKET_SIZE;
243
- exports.UsbPlugin = UsbPlugin;
244
- exports["default"] = UsbPlugin;
376
+ exports["default"] = NodeUsbTransport;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/hd-transport-usb",
3
- "version": "1.1.25-alpha.1",
3
+ "version": "1.1.26-alpha.0",
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.25-alpha.1",
24
- "@onekeyfe/hd-transport": "1.1.25-alpha.1",
23
+ "@onekeyfe/hd-shared": "1.1.26-alpha.0",
24
+ "@onekeyfe/hd-transport": "1.1.26-alpha.0",
25
25
  "bytebuffer": "^5.0.1",
26
26
  "usb": "^2.14.0"
27
27
  },
28
- "gitHead": "38f1168a599c32919939102d6d40e6be7dd06cf7"
28
+ "gitHead": "8d3072a406b0672c9d20e2f58e25cef94a9759fb"
29
29
  }
package/src/constants.ts CHANGED
@@ -4,5 +4,8 @@ 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
+
7
10
  /** Protocol header length: typeId (2 bytes) + length (4 bytes) */
8
11
  export const HEADER_LENGTH = 6;
package/src/index.ts CHANGED
@@ -1,13 +1,14 @@
1
1
  import ByteBuffer from 'bytebuffer';
2
2
  import * as usb from 'usb';
3
- import transport from '@onekeyfe/hd-transport';
4
- import { ONEKEY_WEBUSB_FILTER } from '@onekeyfe/hd-shared';
3
+ import transport, { LogBlockCommand } from '@onekeyfe/hd-transport';
4
+ import { ERRORS, HardwareErrorCode, ONEKEY_WEBUSB_FILTER } from '@onekeyfe/hd-shared';
5
5
 
6
- import { HEADER_LENGTH, PACKET_SIZE, REPORT_ID } from './constants';
6
+ import { HEADER_LENGTH, PACKET_SIZE, PAYLOAD_SIZE, REPORT_ID } from './constants';
7
7
 
8
- import type { LowLevelDevice, LowlevelTransportSharedPlugin } from '@onekeyfe/hd-transport';
8
+ import type EventEmitter from 'events';
9
+ import type { AcquireInput, OneKeyDeviceInfo } from '@onekeyfe/hd-transport';
9
10
 
10
- const { decodeProtocol } = transport;
11
+ const { parseConfigure, buildEncodeBuffers, decodeProtocol, receiveOne, check } = transport;
11
12
 
12
13
  /** USB interface number for vendor-specific communication */
13
14
  const INTERFACE_NUMBER = 0;
@@ -18,6 +19,10 @@ const ENDPOINT_OUT = 0x01;
18
19
  /** Transfer timeout in milliseconds */
19
20
  const TRANSFER_TIMEOUT_MS = 30000;
20
21
 
22
+ /** Packet I/O retry configuration (matches WebUsbTransport) */
23
+ const PACKET_IO_MAX_RETRIES = 3;
24
+ const PACKET_IO_RETRY_DELAY = 300;
25
+
21
26
  /**
22
27
  * Opened device state — holds the USB device, claimed interface, and endpoints.
23
28
  */
@@ -29,26 +34,62 @@ interface OpenDevice {
29
34
  }
30
35
 
31
36
  /**
32
- * The currently active device used by receive() since
33
- * LowlevelTransportSharedPlugin.receive() takes no uuid parameter.
37
+ * Fallback identifier using bus topology (unstable across re-plugs).
34
38
  */
35
- let activeDevice: OpenDevice | null = null;
36
-
37
- /** Map of uuid → open device state */
38
- const openDevices = new Map<string, OpenDevice>();
39
+ function getBusId(dev: usb.Device): string {
40
+ return `usb:${dev.busNumber}:${dev.deviceAddress}`;
41
+ }
39
42
 
40
43
  /**
41
- * Build a unique identifier for a USB device.
42
- * Uses bus number + device address which is stable within a session.
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.
43
47
  */
44
- function getDeviceId(dev: usb.Device): string {
45
- return `usb:${dev.busNumber}:${dev.deviceAddress}`;
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
+ });
46
87
  }
47
88
 
48
89
  /**
49
- * Promisified USB IN transfer.
90
+ * Promisified USB IN transfer (single attempt).
50
91
  */
51
- function transferIn(ep: usb.InEndpoint, length: number): Promise<Buffer> {
92
+ function transferInOnce(ep: usb.InEndpoint, length: number): Promise<Buffer> {
52
93
  return new Promise((resolve, reject) => {
53
94
  ep.transfer(length, (err: Error | undefined, data: Buffer | undefined) => {
54
95
  if (err) return reject(err);
@@ -59,9 +100,9 @@ function transferIn(ep: usb.InEndpoint, length: number): Promise<Buffer> {
59
100
  }
60
101
 
61
102
  /**
62
- * Promisified USB OUT transfer.
103
+ * Promisified USB OUT transfer (single attempt).
63
104
  */
64
- function transferOut(ep: usb.OutEndpoint, data: Buffer): Promise<void> {
105
+ function transferOutOnce(ep: usb.OutEndpoint, data: Buffer): Promise<void> {
65
106
  return new Promise((resolve, reject) => {
66
107
  ep.transfer(data, (err: Error | undefined) => {
67
108
  if (err) return reject(err);
@@ -70,16 +111,50 @@ function transferOut(ep: usb.OutEndpoint, data: Buffer): Promise<void> {
70
111
  });
71
112
  }
72
113
 
114
+ function wait(ms: number): Promise<void> {
115
+ return new Promise(resolve => {
116
+ setTimeout(resolve, ms);
117
+ });
118
+ }
119
+
73
120
  /**
74
- * Read a single 64-byte packet from the device via libusb bulk/interrupt transfer.
121
+ * USB IN transfer with retry.
75
122
  */
76
- async function readPacket(dev: OpenDevice): Promise<Buffer> {
77
- return transferIn(dev.epIn, PACKET_SIZE);
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;
78
154
  }
79
155
 
80
156
  /**
81
157
  * Skip the 0x3F protocol marker byte from a USB packet.
82
- * The device sends 0x3F as the first byte of every response packet.
83
158
  */
84
159
  function skipReportByte(packet: Buffer): Buffer {
85
160
  if (packet[0] === REPORT_ID) {
@@ -96,24 +171,56 @@ function toArrayBuffer(buf: Buffer): ArrayBuffer {
96
171
  }
97
172
 
98
173
  /**
99
- * Node.js USB plugin for LowlevelTransport.
174
+ * Node.js USB Transport complete transport implementation using libusb.
100
175
  *
101
- * Implements the 6-method LowlevelTransportSharedPlugin interface
102
- * using the `usb` (libusb) library for direct USB communication
103
- * with OneKey hardware wallets.
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.
104
179
  *
105
- * Uses libusb to access the vendor-specific interface (class 255)
106
- * which is NOT visible to the HID framework on macOS.
180
+ * Modeled after WebUsbTransport.
107
181
  */
108
- export const UsbPlugin: LowlevelTransportSharedPlugin = {
109
- version: '1.0.0',
182
+ export default class NodeUsbTransport {
183
+ messages: ReturnType<typeof transport.parseConfigure> | undefined;
110
184
 
111
- async init(): Promise<void> {
112
- // libusb requires no global initialization
113
- },
185
+ name = 'NodeUsbTransport';
114
186
 
115
- // eslint-disable-next-line @typescript-eslint/require-await
116
- async enumerate(): Promise<LowLevelDevice[]> {
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
+ }
213
+
214
+ listen() {
215
+ // empty — could add hotplug events via usb.on('attach'/'detach')
216
+ }
217
+
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[]> {
117
224
  const allDevices = usb.getDeviceList();
118
225
 
119
226
  const onekeyDevices = allDevices.filter(d => {
@@ -121,26 +228,132 @@ export const UsbPlugin: LowlevelTransportSharedPlugin = {
121
228
  return ONEKEY_WEBUSB_FILTER.some(f => idVendor === f.vendorId && idProduct === f.productId);
122
229
  });
123
230
 
124
- return onekeyDevices.map(d => ({
125
- id: getDeviceId(d),
126
- name: 'OneKey',
127
- commType: 'usb' as const,
128
- }));
129
- },
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
+ }
130
258
 
131
- // eslint-disable-next-line @typescript-eslint/require-await
132
- async connect(uuid: string): Promise<void> {
133
- const existing = openDevices.get(uuid);
134
- if (existing) {
135
- activeDevice = existing;
136
- return;
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
+ }
137
292
  }
293
+ this.openDevices.delete(path);
294
+ }
295
+
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
+ // 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;
138
350
 
139
- // Find the device by our uuid (bus:address)
351
+ // Resolve serial bus id, then find a fresh device object
352
+ const busId = this.serialToBusId.get(path) ?? path;
140
353
  const allDevices = usb.getDeviceList();
141
- const dev = allDevices.find(d => getDeviceId(d) === uuid);
354
+ const dev = allDevices.find(d => getBusId(d) === busId);
142
355
  if (!dev) {
143
- throw new Error(`USB device not found: ${uuid}`);
356
+ throw ERRORS.TypedError(HardwareErrorCode.DeviceNotFound, `USB device not found: ${path}`);
144
357
  }
145
358
 
146
359
  dev.open();
@@ -161,7 +374,6 @@ export const UsbPlugin: LowlevelTransportSharedPlugin = {
161
374
 
162
375
  iface.claim();
163
376
 
164
- // Find IN and OUT endpoints
165
377
  const epIn = iface.endpoints.find(
166
378
  (e): e is usb.InEndpoint => e.direction === 'in' && e.address === ENDPOINT_IN
167
379
  );
@@ -171,74 +383,31 @@ export const UsbPlugin: LowlevelTransportSharedPlugin = {
171
383
 
172
384
  if (!epIn || !epOut) {
173
385
  dev.close();
174
- throw new Error('USB endpoints not found (expected IN 0x81, OUT 0x01)');
386
+ throw ERRORS.TypedError(
387
+ HardwareErrorCode.DeviceNotFound,
388
+ 'USB endpoints not found (expected IN 0x81, OUT 0x01)'
389
+ );
175
390
  }
176
391
 
177
392
  epIn.timeout = TRANSFER_TIMEOUT_MS;
178
393
  epOut.timeout = TRANSFER_TIMEOUT_MS;
179
394
 
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');
221
-
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;
395
+ this.openDevices.set(path, { device: dev, iface, epIn, epOut });
396
+ }
232
397
 
233
- // Mirrors WebUsbTransport.receiveData() exactly:
234
- // 1. Read first 64-byte packet, skip byte[0] (0x3F marker)
235
- const firstPacket = await readPacket(dev);
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);
236
405
  const firstData = skipReportByte(firstPacket);
237
406
 
238
- // 2. Use SDK's decodeChunked to parse ## header → { typeId, length, restBuffer }
407
+ // Decode header: ## marker → { typeId, length, restBuffer }
239
408
  const { length, typeId, restBuffer } = decodeProtocol.decodeChunked(toArrayBuffer(firstData));
240
409
 
241
- // 3. Allocate result buffer: typeId(2) + length(4) + payload(length)
410
+ // Allocate result: typeId(2) + length(4) + payload(length)
242
411
  const lengthWithHeader = Number(length) + HEADER_LENGTH;
243
412
  const decoded = new ByteBuffer(lengthWithHeader);
244
413
  decoded.writeUint16(typeId);
@@ -247,24 +416,22 @@ export const UsbPlugin: LowlevelTransportSharedPlugin = {
247
416
  decoded.append(restBuffer);
248
417
  }
249
418
 
250
- // 4. Read subsequent packets until complete
419
+ // Read subsequent packets until complete
251
420
  while (decoded.offset < lengthWithHeader) {
252
- const packet = await readPacket(dev);
421
+ const packet = await transferIn(dev.epIn, PACKET_SIZE);
253
422
  const pktData = skipReportByte(packet);
254
423
  const buf = toArrayBuffer(pktData);
255
- if (lengthWithHeader - decoded.offset >= PACKET_SIZE) {
424
+ if (lengthWithHeader - decoded.offset >= PAYLOAD_SIZE) {
256
425
  decoded.append(buf);
257
426
  } else {
258
427
  decoded.append(buf.slice(0, lengthWithHeader - decoded.offset));
259
428
  }
260
429
  }
261
430
 
262
- // 5. Return as hex string
263
431
  decoded.reset();
264
432
  const result = decoded.toBuffer();
265
433
  return Buffer.from(result as unknown as ArrayBuffer).toString('hex');
266
- },
267
- };
434
+ }
435
+ }
268
436
 
269
- export default UsbPlugin;
270
437
  export { PACKET_SIZE } from './constants';