@onekeyfe/hd-transport-web-device 1.0.34-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.
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ import WebUsbTransport from './webusb';
2
+ import ElectronBleTransport from './electron-ble-transport';
3
+
4
+ export { WebUsbTransport, ElectronBleTransport };
@@ -0,0 +1,30 @@
1
+ import { MESSAGE_TOP_CHAR, MESSAGE_HEADER_BYTE } from '@onekeyfe/hd-transport';
2
+
3
+ export const isHeaderChunk = (chunk: Buffer | Uint8Array): boolean => {
4
+ if (chunk.length < 9) return false;
5
+ const [MagicQuestionMark, sharp1, sharp2] = chunk;
6
+
7
+ if (
8
+ String.fromCharCode(MagicQuestionMark) === String.fromCharCode(MESSAGE_TOP_CHAR) &&
9
+ String.fromCharCode(sharp1) === String.fromCharCode(MESSAGE_HEADER_BYTE) &&
10
+ String.fromCharCode(sharp2) === String.fromCharCode(MESSAGE_HEADER_BYTE)
11
+ ) {
12
+ return true;
13
+ }
14
+
15
+ return false;
16
+ };
17
+
18
+ export const isOnekeyDevice = (name: string | null, id?: string): boolean => {
19
+ if (id?.startsWith?.('MI')) {
20
+ return true;
21
+ }
22
+
23
+ // 过滤 BixinKeyxxx 和 Kxxxx 和 Txxxx
24
+ // i 忽略大小写模式
25
+ const re = /(BixinKey\d{10})|(K\d{4})|(T\d{4})|(Touch\s\w{4})|(Pro\s\w{4})/i;
26
+ if (name && re.exec(name)) {
27
+ return true;
28
+ }
29
+ return false;
30
+ };
package/src/webusb.ts ADDED
@@ -0,0 +1,298 @@
1
+ /* eslint-disable no-undef */
2
+ import transport, { AcquireInput, LogBlockCommand } from '@onekeyfe/hd-transport';
3
+ import { ERRORS, HardwareErrorCode, ONEKEY_WEBUSB_FILTER, wait } from '@onekeyfe/hd-shared';
4
+ import ByteBuffer from 'bytebuffer';
5
+
6
+ const { parseConfigure, buildEncodeBuffers, decodeProtocol, receiveOne, check } = transport;
7
+
8
+ const CONFIGURATION_ID = 1;
9
+ const INTERFACE_ID = 0;
10
+ const ENDPOINT_ID = 1;
11
+ const PACKET_SIZE = 64;
12
+ const HEADER_LENGTH = 6;
13
+
14
+ /**
15
+ * Device information with path and WebUSB device instance
16
+ */
17
+ interface DeviceInfo {
18
+ path: string;
19
+ device: USBDevice;
20
+ }
21
+
22
+ export default class WebUsbTransport {
23
+ messages: ReturnType<typeof transport.parseConfigure> | undefined;
24
+
25
+ name = 'WebUsbTransport';
26
+
27
+ stopped = false;
28
+
29
+ configured = false;
30
+
31
+ Log?: any;
32
+
33
+ usb?: USB;
34
+
35
+ /**
36
+ * Cached list of connected devices
37
+ * This is essential for maintaining device references between operations
38
+ */
39
+ deviceList: Array<DeviceInfo> = [];
40
+
41
+ configurationId = CONFIGURATION_ID;
42
+
43
+ endpointId = ENDPOINT_ID;
44
+
45
+ interfaceId = INTERFACE_ID;
46
+
47
+ /**
48
+ * Initialize WebUSB transport
49
+ */
50
+ init(logger: any) {
51
+ this.Log = logger;
52
+
53
+ const { usb } = navigator;
54
+ if (!usb) {
55
+ throw ERRORS.TypedError(
56
+ HardwareErrorCode.RuntimeError,
57
+ 'WebUSB is not supported by current browsers'
58
+ );
59
+ }
60
+ this.usb = usb;
61
+ }
62
+
63
+ /**
64
+ * Configure transport protocol
65
+ */
66
+ configure(signedData: any) {
67
+ const messages = parseConfigure(signedData);
68
+ this.configured = true;
69
+ this.messages = messages;
70
+ }
71
+
72
+ /**
73
+ * Request user to select a device
74
+ * This method must be called in response to a user action
75
+ * to comply with WebUSB security requirements
76
+ */
77
+ async promptDeviceAccess() {
78
+ if (!this.usb) return null;
79
+ try {
80
+ const device = await this.usb.requestDevice({ filters: ONEKEY_WEBUSB_FILTER });
81
+ return device;
82
+ } catch (e) {
83
+ this.Log.debug('requestDevice error: ', e);
84
+ return null;
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Enumerate already connected devices
90
+ * This method only returns devices that are already authorized by the browser
91
+ * It does NOT prompt the user to select a device
92
+ */
93
+ async enumerate() {
94
+ await this.getConnectedDevices();
95
+ return this.deviceList;
96
+ }
97
+
98
+ /**
99
+ * Get list of connected devices
100
+ */
101
+ async getConnectedDevices() {
102
+ if (!this.usb) return [];
103
+
104
+ const devices = await this.usb.getDevices();
105
+ const onekeyDevices = devices.filter(dev => {
106
+ const isOneKey = ONEKEY_WEBUSB_FILTER.some(
107
+ desc => dev.vendorId === desc.vendorId && dev.productId === desc.productId
108
+ );
109
+ const hasSerialNumber = typeof dev.serialNumber === 'string' && dev.serialNumber.length > 0;
110
+ return isOneKey && hasSerialNumber;
111
+ });
112
+
113
+ this.deviceList = onekeyDevices.map(device => ({
114
+ path: device.serialNumber as string,
115
+ device,
116
+ }));
117
+
118
+ return this.deviceList;
119
+ }
120
+
121
+ /**
122
+ * Acquire device control
123
+ */
124
+ async acquire(input: AcquireInput) {
125
+ if (!input.path) return;
126
+ try {
127
+ await this.connect(input.path ?? '', true);
128
+ return await Promise.resolve(input.path);
129
+ } catch (e) {
130
+ this.Log.debug('acquire error: ', e);
131
+ throw e;
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Find device by path
137
+ */
138
+ async findDevice(path: string) {
139
+ // If device list is empty, refresh it first
140
+ if (this.deviceList.length === 0) {
141
+ await this.getConnectedDevices();
142
+ }
143
+
144
+ let device = this.deviceList.find(d => d.path === path);
145
+
146
+ // If device not found after first attempt, try refreshing the list once more
147
+ if (device == null) {
148
+ await this.getConnectedDevices();
149
+ device = this.deviceList.find(d => d.path === path);
150
+
151
+ if (device == null) {
152
+ throw new Error('Action was interrupted.');
153
+ }
154
+ }
155
+
156
+ return device.device;
157
+ }
158
+
159
+ /**
160
+ * Connect to device with retry mechanism
161
+ */
162
+ async connect(path: string, first: boolean) {
163
+ const maxRetries = 5;
164
+ for (let i = 0; i < maxRetries; i++) {
165
+ try {
166
+ return await this.connectToDevice(path, first);
167
+ } catch (e) {
168
+ if (i === maxRetries - 1) {
169
+ throw e;
170
+ }
171
+ await wait(i * 200);
172
+ }
173
+ }
174
+ }
175
+
176
+ /**
177
+ * Connect to specific device
178
+ */
179
+ async connectToDevice(path: string, first: boolean) {
180
+ const device: USBDevice = await this.findDevice(path);
181
+ await device.open();
182
+
183
+ if (first) {
184
+ await device.selectConfiguration(this.configurationId);
185
+ try {
186
+ await device.reset();
187
+ } catch (error) {
188
+ // Ignore reset errors
189
+ }
190
+ }
191
+
192
+ await device.claimInterface(this.interfaceId);
193
+ }
194
+
195
+ async post(session: string, name: string, data: Record<string, unknown>) {
196
+ await this.call(session, name, data);
197
+ }
198
+
199
+ /**
200
+ * Call device method
201
+ */
202
+ async call(path: string, name: string, data: Record<string, unknown>) {
203
+ if (this.messages == null) {
204
+ throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
205
+ }
206
+
207
+ const device = await this.findDevice(path);
208
+ if (!device) {
209
+ throw ERRORS.TypedError(HardwareErrorCode.DeviceNotFound);
210
+ }
211
+
212
+ const { messages } = this;
213
+ if (LogBlockCommand.has(name)) {
214
+ this.Log.debug('call-', ' name: ', name);
215
+ } else {
216
+ this.Log.debug('call-', ' name: ', name, ' data: ', data);
217
+ }
218
+ const encodeBuffers = buildEncodeBuffers(messages, name, data);
219
+
220
+ for (const buffer of encodeBuffers) {
221
+ const newArray: Uint8Array = new Uint8Array(PACKET_SIZE);
222
+ newArray[0] = 63;
223
+ newArray.set(new Uint8Array(buffer), 1);
224
+ // console.log('send packet: ', newArray);
225
+
226
+ if (!device.opened) {
227
+ await this.connect(path, false);
228
+ }
229
+ await device.transferOut(this.endpointId, newArray);
230
+ }
231
+
232
+ const resData = await this.receiveData(path);
233
+ if (typeof resData !== 'string') {
234
+ throw ERRORS.TypedError(HardwareErrorCode.NetworkError, 'Returning data is not string.');
235
+ }
236
+ const jsonData = receiveOne(messages, resData);
237
+ return check.call(jsonData);
238
+ }
239
+
240
+ /**
241
+ * Receive data from device
242
+ */
243
+ async receiveData(path: string) {
244
+ const device: USBDevice = await this.findDevice(path);
245
+ if (!device.opened) {
246
+ await this.connect(path, false);
247
+ }
248
+
249
+ const firstPacket = await device.transferIn(this.endpointId, PACKET_SIZE);
250
+ const firstData = firstPacket.data?.buffer.slice(1);
251
+ console.log('receive first packet: ', firstPacket);
252
+ const { length, typeId, restBuffer } = decodeProtocol.decodeChunked(firstData as ArrayBuffer);
253
+
254
+ console.log('chunk length: ', length);
255
+
256
+ // eslint-disable-next-line @typescript-eslint/restrict-plus-operands
257
+ const lengthWithHeader = Number(length + HEADER_LENGTH);
258
+ const decoded = new ByteBuffer(lengthWithHeader);
259
+ decoded.writeUint16(typeId);
260
+ decoded.writeUint32(length);
261
+ if (length) {
262
+ decoded.append(restBuffer);
263
+ }
264
+ console.log('first decoded: ', decoded);
265
+
266
+ while (decoded.offset < lengthWithHeader) {
267
+ const res = await device.transferIn(this.endpointId, PACKET_SIZE);
268
+
269
+ if (!res.data) {
270
+ throw new Error('no data');
271
+ }
272
+ if (res.data.byteLength === 0) {
273
+ // empty data
274
+ console.warn('empty data');
275
+ }
276
+ const buffer = res.data.buffer.slice(1);
277
+ if (lengthWithHeader - decoded.offset >= PACKET_SIZE) {
278
+ decoded.append(buffer as unknown as ArrayBuffer);
279
+ } else {
280
+ decoded.append(
281
+ buffer.slice(0, lengthWithHeader - decoded.offset) as unknown as ArrayBuffer
282
+ );
283
+ }
284
+ }
285
+ decoded.reset();
286
+ const result = decoded.toBuffer();
287
+ return Buffer.from(result as unknown as ArrayBuffer).toString('hex');
288
+ }
289
+
290
+ /**
291
+ * Release device
292
+ */
293
+ async release(path: string) {
294
+ const device: USBDevice = await this.findDevice(path);
295
+ await device.releaseInterface(this.interfaceId);
296
+ await device.close();
297
+ }
298
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,11 @@
1
+ {
2
+ "extends": "../../tsconfig.lib.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist",
5
+ "types": [
6
+ "w3c-web-usb",
7
+ "web-bluetooth"
8
+ ]
9
+ },
10
+ "include": ["./src"]
11
+ }