@onekeyfe/hd-core 1.2.8-alpha.10 → 1.2.8-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.
Files changed (69) hide show
  1. package/__tests__/DeviceCommands.test.ts +66 -13
  2. package/__tests__/core-dispose.test.ts +114 -1
  3. package/__tests__/core-error-output.test.ts +55 -3
  4. package/__tests__/device-connector-protocol.test.ts +5 -2
  5. package/__tests__/device-lifecycle-events.test.ts +44 -20
  6. package/__tests__/logBlockEvent.test.ts +43 -0
  7. package/__tests__/protocol-v2-bootloader-mode.test.ts +43 -2
  8. package/__tests__/protocol-v2.test.ts +0 -47
  9. package/__tests__/refresh-device-state.test.ts +19 -17
  10. package/__tests__/usb-identity-probe.test.ts +358 -0
  11. package/__tests__/zcash-protobuf.test.ts +141 -0
  12. package/__tests__/zcash-sign-pczt.test.ts +387 -0
  13. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  14. package/dist/api/GetDeviceState.d.ts.map +1 -1
  15. package/dist/api/SearchDevices.d.ts +21 -1
  16. package/dist/api/SearchDevices.d.ts.map +1 -1
  17. package/dist/api/index.d.ts +2 -0
  18. package/dist/api/index.d.ts.map +1 -1
  19. package/dist/api/protocol-v2/DeviceReboot.d.ts.map +1 -1
  20. package/dist/api/zcash/ZcashGetAddress.d.ts +12 -0
  21. package/dist/api/zcash/ZcashGetAddress.d.ts.map +1 -0
  22. package/dist/api/zcash/ZcashSignPczt.d.ts +16 -0
  23. package/dist/api/zcash/ZcashSignPczt.d.ts.map +1 -0
  24. package/dist/core/index.d.ts +3 -2
  25. package/dist/core/index.d.ts.map +1 -1
  26. package/dist/device/DeviceCommands.d.ts +4 -4
  27. package/dist/device/DeviceCommands.d.ts.map +1 -1
  28. package/dist/events/logBlockEvent.d.ts.map +1 -1
  29. package/dist/index.d.ts +71 -6
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +3369 -2887
  32. package/dist/inject.d.ts.map +1 -1
  33. package/dist/types/api/export.d.ts +2 -0
  34. package/dist/types/api/export.d.ts.map +1 -1
  35. package/dist/types/api/index.d.ts +4 -0
  36. package/dist/types/api/index.d.ts.map +1 -1
  37. package/dist/types/api/zcashGetAddress.d.ts +22 -0
  38. package/dist/types/api/zcashGetAddress.d.ts.map +1 -0
  39. package/dist/types/api/zcashSignPczt.d.ts +9 -0
  40. package/dist/types/api/zcashSignPczt.d.ts.map +1 -0
  41. package/dist/types/device.d.ts +9 -0
  42. package/dist/types/device.d.ts.map +1 -1
  43. package/dist/types/params.d.ts +1 -0
  44. package/dist/types/params.d.ts.map +1 -1
  45. package/dist/utils/patch.d.ts +1 -1
  46. package/dist/utils/patch.d.ts.map +1 -1
  47. package/package.json +4 -4
  48. package/src/api/FirmwareUpdateV4.ts +12 -32
  49. package/src/api/GetDeviceState.ts +2 -5
  50. package/src/api/SearchDevices.ts +172 -7
  51. package/src/api/index.ts +2 -0
  52. package/src/api/protocol-v2/DeviceReboot.ts +2 -0
  53. package/src/api/zcash/ZcashGetAddress.ts +121 -0
  54. package/src/api/zcash/ZcashSignPczt.ts +155 -0
  55. package/src/core/index.ts +47 -20
  56. package/src/data/messages/messages-protocol-v2.json +138 -1
  57. package/src/device/Device.ts +1 -1
  58. package/src/device/DeviceCommands.ts +15 -1
  59. package/src/device/DeviceConnector.ts +4 -4
  60. package/src/events/core.ts +2 -2
  61. package/src/events/logBlockEvent.ts +4 -2
  62. package/src/index.ts +1 -0
  63. package/src/inject.ts +4 -0
  64. package/src/types/api/export.ts +2 -0
  65. package/src/types/api/index.ts +8 -0
  66. package/src/types/api/zcashGetAddress.ts +36 -0
  67. package/src/types/api/zcashSignPczt.ts +18 -0
  68. package/src/types/device.ts +12 -0
  69. package/src/types/params.ts +1 -1
@@ -5,10 +5,20 @@ import TransportManager from '../data-manager/TransportManager';
5
5
  import { DataManager } from '../data-manager';
6
6
  import { LoggerNames, getDeviceTypeByBleName, getLogger } from '../utils';
7
7
  import { DevicePool } from '../device/DevicePool';
8
+ import { buildProtocolV1FeaturesPayload } from '../deviceProfile/buildDeviceFeatures';
9
+ import { mapProtocolV2DeviceInfoToState } from '../device/DeviceStateMapper';
10
+ import { getSupportProtocolV1MessageSchema } from '../utils/deviceFeaturesUtils';
11
+ import { PROTOCOL_V2_FEATURES_DEVICE_INFO_REQUEST } from '../protocols/protocol-v2/features';
8
12
 
9
- import type { SearchDevice } from '../types/device';
13
+ import type { SearchDevice, UsbDeviceIdentity } from '../types/device';
14
+ import type { PROTO } from '../constants';
10
15
  import type DeviceConnector from '../device/DeviceConnector';
11
- import type { OneKeyDeviceInfo as DeviceDescriptor } from '@onekeyfe/hd-transport';
16
+ import type {
17
+ AcquireInput,
18
+ OneKeyDeviceInfo as DeviceDescriptor,
19
+ ProtocolV2DeviceInfo,
20
+ Transport,
21
+ } from '@onekeyfe/hd-transport';
12
22
  import type { Device } from '../device/Device';
13
23
 
14
24
  const Log = getLogger(LoggerNames.DevicePool);
@@ -18,7 +28,10 @@ type RequestQueueLookup = {
18
28
  getTask: (requestId: number) => { method: { device?: Device } } | undefined;
19
29
  };
20
30
 
21
- const getDescriptorKeys = (descriptor: DeviceDescriptor) =>
31
+ export type UsbIdentityProbeDescriptor = Pick<DeviceDescriptor, 'path' | 'commType'> &
32
+ Partial<Pick<DeviceDescriptor, 'id' | 'session'>>;
33
+
34
+ const getDescriptorKeys = (descriptor: UsbIdentityProbeDescriptor) =>
22
35
  [descriptor.path, descriptor.id].filter(
23
36
  (key): key is string => typeof key === 'string' && key.length > 0
24
37
  );
@@ -33,11 +46,11 @@ const isOwnedByActiveWebUsbRequest = (
33
46
  );
34
47
  };
35
48
 
36
- const getUsbHandle = (descriptor?: DeviceDescriptor) =>
49
+ const getUsbHandle = (descriptor?: UsbIdentityProbeDescriptor) =>
37
50
  (descriptor as { device?: unknown } | undefined)?.device;
38
51
 
39
52
  const getOwningRequestDevice = (
40
- descriptor: DeviceDescriptor,
53
+ descriptor: UsbIdentityProbeDescriptor,
41
54
  requestQueue?: RequestQueueLookup
42
55
  ) => {
43
56
  if (!requestQueue) return undefined;
@@ -60,6 +73,158 @@ const getOwningRequestDevice = (
60
73
  return undefined;
61
74
  };
62
75
 
76
+ export const getOwnedUsbDeviceIdentity = (
77
+ descriptor: UsbIdentityProbeDescriptor,
78
+ requestQueue: RequestQueueLookup
79
+ ): UsbDeviceIdentity | undefined => {
80
+ const owner = getOwningRequestDevice(descriptor, requestQueue);
81
+ if (!owner || descriptor.commType !== 'webusb' || owner.originalDescriptor?.commType !== 'webusb')
82
+ return undefined;
83
+ return {
84
+ path: descriptor.path,
85
+ serialNo: owner.getCurrentSerialNo() || null,
86
+ deviceId: owner.getCurrentDeviceId() || null,
87
+ deviceType: owner.getCurrentDeviceType(),
88
+ connectProtocol: owner.getProtocol(),
89
+ commType: 'webusb',
90
+ mode: owner.features?.mode ?? undefined,
91
+ };
92
+ };
93
+
94
+ export type UsbIdentityProbeTransport = Pick<
95
+ Transport,
96
+ 'release' | 'call' | 'getProtocolType' | 'configureProtocolV2'
97
+ > & {
98
+ init: (logger: object) => unknown;
99
+ enumerate: () => Promise<UsbIdentityProbeDescriptor[]>;
100
+ acquire: (input: AcquireInput) => Promise<string | undefined>;
101
+ configure: (signedData: string) => void | Promise<void>;
102
+ };
103
+
104
+ // Identity discovery must not publish device events or log raw Features/session data.
105
+ const identityProbeLogger = {
106
+ debug: () => undefined,
107
+ log: () => undefined,
108
+ info: () => undefined,
109
+ warn: () => undefined,
110
+ error: () => undefined,
111
+ };
112
+
113
+ /** Runs on a separate transport; never initializes or switches the current Core. */
114
+ export const probeUsbDeviceIdentitiesWithTransport = async (
115
+ transport: UsbIdentityProbeTransport,
116
+ commType: UsbDeviceIdentity['commType'],
117
+ getOwnedIdentity?: (descriptor: UsbIdentityProbeDescriptor) => UsbDeviceIdentity | undefined
118
+ ): Promise<UsbDeviceIdentity[]> => {
119
+ if (commType !== 'webusb') return [];
120
+ await transport.init(identityProbeLogger);
121
+ const defaultMessages = JSON.stringify(DataManager.getProtobufMessages());
122
+ await transport.configure(defaultMessages);
123
+ await transport.configureProtocolV2?.(
124
+ JSON.stringify(DataManager.getProtobufMessages('v2Schema'))
125
+ );
126
+ const descriptors = await transport.enumerate();
127
+ const identities: UsbDeviceIdentity[] = [];
128
+ const readIdentity = async (
129
+ descriptor: UsbIdentityProbeDescriptor
130
+ ): Promise<UsbDeviceIdentity | undefined> => {
131
+ const { path } = descriptor;
132
+ if (!path || descriptor.commType !== commType) return undefined;
133
+ if (descriptors.filter(candidate => candidate.path === path).length !== 1) return undefined;
134
+ const owner = getOwnedIdentity?.(descriptor);
135
+ if (owner) {
136
+ return owner;
137
+ }
138
+ // Do not borrow a browser handle or replace a Bridge session owned by another call.
139
+ if (descriptor.session || (getUsbHandle(descriptor) as USBDevice | undefined)?.opened)
140
+ return undefined;
141
+
142
+ let session: string | undefined;
143
+ let identified: UsbDeviceIdentity | undefined;
144
+ try {
145
+ await transport.configure(defaultMessages);
146
+ session = await transport.acquire({ path, previous: null, forceProtocolDetection: true });
147
+ if (!session) return undefined;
148
+ const protocol = transport.getProtocolType(path);
149
+ if (protocol === 'V1') {
150
+ const response = await transport.call(session, 'GetFeatures', {}, { timeoutMs: 3000 });
151
+ if (response.type !== 'Features') return undefined;
152
+ let features = buildProtocolV1FeaturesPayload(response.message as PROTO.Features);
153
+ // Classic-family loaders expose their supported information in Features.
154
+ // Touch/Pro retain their existing vendor-feature query.
155
+ if (
156
+ !features.serialNo &&
157
+ (features.mode !== 'bootloader' ||
158
+ features.deviceType === EDeviceType.Touch ||
159
+ features.deviceType === EDeviceType.Pro)
160
+ ) {
161
+ const { messages, protocolV1MessageSchema } = getSupportProtocolV1MessageSchema(features);
162
+ // Legacy V1 has no OnekeyGetFeatures; keep the identity confirmed by GetFeatures.
163
+ if (protocolV1MessageSchema === 'v1CurrentSchema') {
164
+ if (messages) await transport.configure(JSON.stringify(messages));
165
+ const vendor = await transport.call(
166
+ session,
167
+ 'OnekeyGetFeatures',
168
+ {},
169
+ { timeoutMs: 3000 }
170
+ );
171
+ if (vendor.type === 'OnekeyFeatures') {
172
+ features = buildProtocolV1FeaturesPayload({
173
+ ...response.message,
174
+ ...vendor.message,
175
+ } as PROTO.Features);
176
+ }
177
+ }
178
+ }
179
+ identified = {
180
+ path,
181
+ serialNo: features.serialNo || null,
182
+ deviceId: features.deviceId || null,
183
+ deviceType: features.deviceType,
184
+ connectProtocol: 'V1',
185
+ commType,
186
+ mode: features.mode ?? undefined,
187
+ };
188
+ } else if (protocol === 'V2') {
189
+ const response = await transport.call(
190
+ session,
191
+ 'DeviceInfoGet',
192
+ PROTOCOL_V2_FEATURES_DEVICE_INFO_REQUEST,
193
+ { timeoutMs: 3000 }
194
+ );
195
+ if (response.type !== 'DeviceInfo') return undefined;
196
+ const { identity } = mapProtocolV2DeviceInfoToState(
197
+ response.message as ProtocolV2DeviceInfo
198
+ );
199
+ identified = {
200
+ path,
201
+ serialNo: identity?.serialNo || null,
202
+ deviceId: identity?.deviceId || null,
203
+ deviceType: identity?.deviceType ?? EDeviceType.Unknown,
204
+ connectProtocol: 'V2',
205
+ commType,
206
+ };
207
+ }
208
+ } catch {
209
+ // Discovery is best effort; a busy/disconnected device cannot confirm identity.
210
+ } finally {
211
+ if (session) {
212
+ try {
213
+ await transport.release(session, false);
214
+ } catch {
215
+ identified = undefined;
216
+ }
217
+ }
218
+ }
219
+ return identified;
220
+ };
221
+ for (const descriptor of descriptors) {
222
+ const identity = await readIdentity(descriptor);
223
+ if (identity) identities.push(identity);
224
+ }
225
+ return identities;
226
+ };
227
+
63
228
  // A USB path is a routing key (the USB serial, a synthesized usb-vid-pid-name, or the
64
229
  // bootloader placeholder), not a hardware identity, so it never fills uuid/serialNo.
65
230
  const toSearchDeviceFromDescriptor = (descriptor: DeviceDescriptor): SearchDevice => {
@@ -81,7 +246,7 @@ const toSearchDevice = (device: Device): SearchDevice | null => {
81
246
  };
82
247
 
83
248
  export default class SearchDevices extends BaseMethod {
84
- connector?: DeviceConnector;
249
+ connector?: DeviceConnector = undefined;
85
250
 
86
251
  init() {
87
252
  this.useDevice = false;
@@ -130,7 +295,7 @@ export default class SearchDevices extends BaseMethod {
130
295
  // BLE discovery cannot provide a physical-device identity before initialization.
131
296
  uuid: '',
132
297
  deviceId: null,
133
- name: bleName || device.name,
298
+ name: bleName || device.name || '',
134
299
  deviceType: getDeviceTypeByBleName(bleName),
135
300
  });
136
301
  }
package/src/api/index.ts CHANGED
@@ -175,3 +175,5 @@ export { default as benfenSignTransaction } from './benfen/BenfenSignTransaction
175
175
 
176
176
  export { default as neoGetAddress } from './neo/NeoGetAddress';
177
177
  export { default as neoSignTransaction } from './neo/NeoSignTransaction';
178
+ export { default as zcashGetAddress } from './zcash/ZcashGetAddress';
179
+ export { default as zcashSignPczt } from './zcash/ZcashSignPczt';
@@ -1,5 +1,6 @@
1
1
  import { DeviceSessionPinType } from '@onekeyfe/hd-transport';
2
2
 
3
+ import { UI_REQUEST } from '../../constants/ui-request';
3
4
  import { BaseMethod } from '../BaseMethod';
4
5
  import { normalizeRebootType } from './helpers';
5
6
 
@@ -11,6 +12,7 @@ export default class DeviceReboot extends BaseMethod<DeviceRebootParams> {
11
12
  }
12
13
 
13
14
  init() {
15
+ this.allowDeviceMode.push(UI_REQUEST.BOOTLOADER);
14
16
  this.skipForceUpdateCheck = true;
15
17
  this.useDevicePassphraseState = false;
16
18
  this.unlockPolicy = 'unlock-before-run';
@@ -0,0 +1,121 @@
1
+ import { ZcashAddressScope, ZcashAddressType } from '@onekeyfe/hd-transport';
2
+
3
+ import { BaseMethod } from '../BaseMethod';
4
+ import { serializedPath, toHardened, validatePath } from '../helpers/pathUtils';
5
+ import { invalidParameter, validateParams, validateResult } from '../helpers/paramsValidator';
6
+ import { UI_REQUEST } from '../../constants/ui-request';
7
+
8
+ import type { ZcashGetAddress as HardwareZcashGetAddress } from '@onekeyfe/hd-transport';
9
+ import type { ZcashGetAddressParams, ZcashAddress } from '../../types/api/zcashGetAddress';
10
+ import type { DeviceFirmwareRange } from '../../types';
11
+
12
+ export default class ZcashGetAddress extends BaseMethod<HardwareZcashGetAddress[]> {
13
+ hasBundle = false;
14
+
15
+ getSupportedProtocols() {
16
+ return ['V2'] as const;
17
+ }
18
+
19
+ init() {
20
+ this.checkDeviceId = true;
21
+ this.allowDeviceMode = [...this.allowDeviceMode, UI_REQUEST.NOT_INITIALIZE];
22
+ this.strictCheckDeviceSupport = true;
23
+
24
+ this.hasBundle = !!this.payload?.bundle;
25
+ const payload = this.hasBundle ? this.payload : { bundle: [this.payload] };
26
+
27
+ validateParams(payload, [{ name: 'bundle', type: 'array' }]);
28
+
29
+ this.params = [];
30
+ payload.bundle.forEach((batch: ZcashGetAddressParams) => {
31
+ // ZIP-32 account path: m/32'/133'/account'
32
+ const addressN = validatePath(batch.path, 3);
33
+ if (
34
+ addressN.length !== 3 ||
35
+ addressN[0] !== toHardened(32) ||
36
+ addressN[1] !== toHardened(133) ||
37
+ addressN[2] < toHardened(0) ||
38
+ addressN[2] > 0xffffffff
39
+ ) {
40
+ throw invalidParameter("Zcash requires a ZIP-32 account path m/32'/133'/account'");
41
+ }
42
+ const addressType = batch.addressType ?? ZcashAddressType.UNIFIED_ORCHARD_P2PKH;
43
+ const scope = batch.scope ?? ZcashAddressScope.EXTERNAL;
44
+ const diversifierIndex = batch.diversifierIndex ?? 0;
45
+ if (
46
+ ![
47
+ ZcashAddressType.UNIFIED_ORCHARD_P2PKH,
48
+ ZcashAddressType.TRANSPARENT_P2PKH,
49
+ ZcashAddressType.UNIFIED_ORCHARD,
50
+ ].includes(addressType) ||
51
+ ![ZcashAddressScope.EXTERNAL, ZcashAddressScope.INTERNAL].includes(scope)
52
+ ) {
53
+ throw invalidParameter('Invalid Zcash address type or scope');
54
+ }
55
+ const maxIndex = addressType === ZcashAddressType.UNIFIED_ORCHARD ? 0xffffffff : 0x7fffffff;
56
+ if (
57
+ !Number.isSafeInteger(diversifierIndex) ||
58
+ diversifierIndex < 0 ||
59
+ diversifierIndex > maxIndex
60
+ ) {
61
+ throw invalidParameter('Invalid Zcash diversifier index');
62
+ }
63
+
64
+ validateParams(batch, [
65
+ { name: 'path', required: true },
66
+ { name: 'showOnOneKey', type: 'boolean' },
67
+ { name: 'includeUfvk', type: 'boolean' },
68
+ { name: 'includeSeedFingerprint', type: 'boolean' },
69
+ ]);
70
+
71
+ this.params.push({
72
+ address_n: addressN,
73
+ address_type: addressType,
74
+ scope,
75
+ diversifier_index: diversifierIndex,
76
+ show_display: batch.showOnOneKey ?? true,
77
+ include_ufvk: batch.includeUfvk ?? false,
78
+ include_seed_fingerprint: batch.includeSeedFingerprint ?? false,
79
+ });
80
+ });
81
+ }
82
+
83
+ getVersionRange(): DeviceFirmwareRange {
84
+ return {
85
+ model_pro2: {
86
+ min: '0.0.0',
87
+ },
88
+ };
89
+ }
90
+
91
+ async run() {
92
+ const responses: ZcashAddress[] = [];
93
+
94
+ for (let i = 0; i < this.params.length; i++) {
95
+ const param = this.params[i];
96
+
97
+ const res = await this.device.commands.typedCall('ZcashGetAddress', 'ZcashAddress', {
98
+ ...param,
99
+ });
100
+
101
+ const path = serializedPath(param.address_n);
102
+ responses.push({
103
+ path,
104
+ address: res.message.address,
105
+ ufvk: res.message.ufvk,
106
+ seedFingerprint: res.message.seed_fingerprint,
107
+ });
108
+
109
+ this.postPreviousAddressMessage({
110
+ address: res.message.address,
111
+ path,
112
+ });
113
+ }
114
+
115
+ validateResult(responses, ['address'], {
116
+ expectedLength: this.params.length,
117
+ });
118
+
119
+ return Promise.resolve(this.hasBundle ? responses : responses[0]);
120
+ }
121
+ }
@@ -0,0 +1,155 @@
1
+ import { bytesToHex } from '@noble/hashes/utils';
2
+ import { ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared';
3
+
4
+ import { BaseMethod } from '../BaseMethod';
5
+ import { invalidParameter, validateParams } from '../helpers/paramsValidator';
6
+ import { stripHexPrefix } from '../helpers/hexUtils';
7
+ import { UI_REQUEST } from '../../constants/ui-request';
8
+
9
+ import type { ZcashSignPczt as HardwareZcashSignPczt } from '@onekeyfe/hd-transport';
10
+ import type { TypedCall, TypedResponseMessage } from '../../device/DeviceCommands';
11
+ import type { ZcashSignedPczt } from '../../types/api/zcashSignPczt';
12
+ import type { DeviceFirmwareRange } from '../../types';
13
+
14
+ const CHUNK_BYTE_SIZE = 1024;
15
+ // Host memory sanity bound on the device-announced length, not a device limit.
16
+ const SIGNED_PCZT_SANITY_MAX_BYTES = 16 * 1024 * 1024;
17
+
18
+ export default class ZcashSignPczt extends BaseMethod<HardwareZcashSignPczt> {
19
+ pczt: Buffer = Buffer.alloc(0);
20
+
21
+ getSupportedProtocols() {
22
+ return ['V2'] as const;
23
+ }
24
+
25
+ init() {
26
+ this.checkDeviceId = true;
27
+ this.allowDeviceMode = [...this.allowDeviceMode, UI_REQUEST.NOT_INITIALIZE];
28
+ this.strictCheckDeviceSupport = true;
29
+
30
+ validateParams(this.payload, [{ name: 'pczt', type: 'hexString', required: true }]);
31
+
32
+ // Reject odd-length hex instead of left-padding it, which would shift every nibble.
33
+ const pcztHex = stripHexPrefix(this.payload.pczt);
34
+ if (pcztHex.length % 2 !== 0) {
35
+ throw invalidParameter('pczt hex must have an even length');
36
+ }
37
+ this.pczt = Buffer.from(pcztHex, 'hex');
38
+ if (this.pczt.length === 0) {
39
+ throw invalidParameter('pczt is empty');
40
+ }
41
+
42
+ this.params = {
43
+ pczt_length: this.pczt.length,
44
+ pczt_initial_chunk: bytesToHex(this.pczt.subarray(0, CHUNK_BYTE_SIZE)),
45
+ };
46
+ }
47
+
48
+ getVersionRange(): DeviceFirmwareRange {
49
+ return {
50
+ model_pro2: {
51
+ min: '0.0.0',
52
+ },
53
+ };
54
+ }
55
+
56
+ // Upload: the device decides every chunk length via ZcashPcztChunkRequest and
57
+ // the host must answer with exactly that many bytes.
58
+ uploadPczt = async (
59
+ typedCall: TypedCall,
60
+ res: TypedResponseMessage<'ZcashSignedPczt'> | TypedResponseMessage<'ZcashPcztChunkRequest'>,
61
+ offset: number
62
+ ): Promise<TypedResponseMessage<'ZcashSignedPczt'>> => {
63
+ if (res.type === 'ZcashSignedPczt') {
64
+ if (offset !== this.pczt.length) {
65
+ throw ERRORS.TypedError(
66
+ HardwareErrorCode.ResponseUnexpectTypeError,
67
+ 'device completed signing before receiving the whole pczt'
68
+ );
69
+ }
70
+ return res;
71
+ }
72
+
73
+ const { chunk_length: chunkLength } = res.message;
74
+ if (
75
+ !Number.isSafeInteger(chunkLength) ||
76
+ chunkLength <= 0 ||
77
+ offset + chunkLength > this.pczt.length
78
+ ) {
79
+ throw ERRORS.TypedError(
80
+ HardwareErrorCode.ResponseUnexpectTypeError,
81
+ `device requested ${chunkLength} bytes at offset ${offset} of ${this.pczt.length}`
82
+ );
83
+ }
84
+
85
+ const chunk = this.pczt.subarray(offset, offset + chunkLength);
86
+ const next = await typedCall(
87
+ 'ZcashPcztChunkAck',
88
+ ['ZcashSignedPczt', 'ZcashPcztChunkRequest'],
89
+ {
90
+ data_chunk: bytesToHex(chunk),
91
+ }
92
+ );
93
+
94
+ return this.uploadPczt(typedCall, next, offset + chunkLength);
95
+ };
96
+
97
+ // Download: the host decides chunk lengths via ZcashSignedPcztChunkRequest
98
+ // until the announced pczt_length has been received.
99
+ downloadSignedPczt = async (
100
+ typedCall: TypedCall,
101
+ res: TypedResponseMessage<'ZcashSignedPczt'>
102
+ ): Promise<Buffer> => {
103
+ const { pczt_length: total, pczt_initial_chunk: initial } = res.message;
104
+ if (!Number.isSafeInteger(total) || total <= 0 || total > SIGNED_PCZT_SANITY_MAX_BYTES) {
105
+ throw ERRORS.TypedError(
106
+ HardwareErrorCode.ResponseUnexpectTypeError,
107
+ `device announced invalid signed pczt length ${total}`
108
+ );
109
+ }
110
+
111
+ const parts: Buffer[] = [Buffer.from(initial ?? '', 'hex')];
112
+ let received = parts[0].length;
113
+ if (received > total) {
114
+ throw ERRORS.TypedError(
115
+ HardwareErrorCode.ResponseUnexpectTypeError,
116
+ 'device returned more initial pczt bytes than announced'
117
+ );
118
+ }
119
+
120
+ while (received < total) {
121
+ const chunkLength = Math.min(CHUNK_BYTE_SIZE, total - received);
122
+ const ack = await typedCall('ZcashSignedPcztChunkRequest', 'ZcashPcztChunkAck', {
123
+ chunk_length: chunkLength,
124
+ });
125
+ const chunk = Buffer.from(ack.message.data_chunk ?? '', 'hex');
126
+ if (chunk.length !== chunkLength) {
127
+ throw ERRORS.TypedError(
128
+ HardwareErrorCode.ResponseUnexpectTypeError,
129
+ `device returned ${chunk.length} of ${chunkLength} requested bytes at offset ${received} of ${total}`
130
+ );
131
+ }
132
+ parts.push(chunk);
133
+ received += chunk.length;
134
+ }
135
+
136
+ return Buffer.concat(parts);
137
+ };
138
+
139
+ async run(): Promise<ZcashSignedPczt> {
140
+ const typedCall = this.device.getCommands().typedCall.bind(this.device.getCommands());
141
+
142
+ const first = await typedCall('ZcashSignPczt', ['ZcashSignedPczt', 'ZcashPcztChunkRequest'], {
143
+ ...this.params,
144
+ });
145
+
146
+ const signed = await this.uploadPczt(
147
+ typedCall,
148
+ first,
149
+ Math.min(CHUNK_BYTE_SIZE, this.pczt.length)
150
+ );
151
+ const pczt = await this.downloadSignedPczt(typedCall, signed);
152
+
153
+ return { pczt: bytesToHex(pczt) };
154
+ }
155
+ }
package/src/core/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import semver from 'semver';
2
+ import { getOwnedUsbDeviceIdentity } from '../api/SearchDevices';
2
3
  import EventEmitter from 'events';
3
4
  import {
4
5
  DeviceSessionPinType,
@@ -23,7 +24,6 @@ import {
23
24
  createNeedUpgradeFirmwareHardwareError,
24
25
  createNewFirmwareForceUpdateHardwareError,
25
26
  createNewFirmwareUnReleaseHardwareError,
26
- isBleStaleBondHardwareError,
27
27
  } from '@onekeyfe/hd-shared';
28
28
 
29
29
  import { LoggerNames, enableLog, getLogger, setLoggerPostMessage, wait } from '../utils';
@@ -126,6 +126,8 @@ const parseInitOptions = (method?: BaseMethod): InitOptions => ({
126
126
  });
127
127
 
128
128
  let _core: Core | undefined;
129
+ let transportSwitchGeneration = 0;
130
+ let pendingTransportSwitch: Promise<void> = Promise.resolve();
129
131
  let _deviceList: DeviceList | undefined;
130
132
  let _connector: DeviceConnector | undefined;
131
133
  let _uiPromises: UiPromise<UiPromiseResponse['type']>[] = []; // Waiting for ui response
@@ -991,15 +993,11 @@ function canSkipInitialize(method: BaseMethod, device: Device): boolean {
991
993
  }
992
994
 
993
995
  export function isRetryableBleProtocolV2ProbeError(method: BaseMethod, error: unknown) {
994
- const typedError = error as { errorCode?: unknown; message?: unknown };
995
- const message =
996
- typeof typedError?.message === 'string' ? typedError.message : String(error ?? '');
996
+ const typedError = error as { errorCode?: unknown; params?: { expectedProtocol?: unknown } };
997
997
  return (
998
998
  method.payload.connectProtocol === 'V2' &&
999
- typedError?.errorCode === HardwareErrorCode.RuntimeError &&
1000
- message.includes('Device protocol mismatch') &&
1001
- message.includes('expected V2') &&
1002
- message.includes('did not respond to expected protocol')
999
+ typedError?.errorCode === HardwareErrorCode.TransportProtocolProbeFailed &&
1000
+ typedError.params?.expectedProtocol === 'V2'
1003
1001
  );
1004
1002
  }
1005
1003
 
@@ -1009,7 +1007,7 @@ export function isRetryableBleConnectionError(method: BaseMethod, error: unknown
1009
1007
  }
1010
1008
  const typedError = error as {
1011
1009
  errorCode?: unknown;
1012
- params?: { acquireDeadlineExceeded?: unknown };
1010
+ params?: { acquireDeadlineExceeded?: unknown; expectedProtocol?: unknown };
1013
1011
  };
1014
1012
  if (
1015
1013
  typedError?.errorCode === HardwareErrorCode.BleTimeoutError &&
@@ -1020,6 +1018,8 @@ export function isRetryableBleConnectionError(method: BaseMethod, error: unknown
1020
1018
  return (
1021
1019
  typedError?.errorCode === HardwareErrorCode.BleTimeoutError ||
1022
1020
  typedError?.errorCode === HardwareErrorCode.BleConnectedError ||
1021
+ (typedError?.errorCode === HardwareErrorCode.TransportProtocolProbeFailed &&
1022
+ !typedError.params?.expectedProtocol) ||
1023
1023
  isRetryableBleProtocolV2ProbeError(method, error) ||
1024
1024
  isMissingDetectedProtocolV2Error(method, error)
1025
1025
  );
@@ -1035,10 +1035,6 @@ export function isMissingDetectedProtocolV2Error(method: BaseMethod, error: unkn
1035
1035
  );
1036
1036
  }
1037
1037
 
1038
- export function isTerminalBleStaleBondError(error: unknown) {
1039
- return isBleStaleBondHardwareError(error);
1040
- }
1041
-
1042
1038
  export function isDeviceIdentityMismatchError(error: unknown) {
1043
1039
  return (
1044
1040
  (error as { errorCode?: unknown })?.errorCode === HardwareErrorCode.DeviceCheckDeviceIdError
@@ -1277,6 +1273,9 @@ const ensureConnected = async (
1277
1273
  HardwareErrorCode.BridgeNotInstalled,
1278
1274
  HardwareErrorCode.BridgeTimeoutError,
1279
1275
  HardwareErrorCode.BridgeNeedsPermission,
1276
+ HardwareErrorCode.WebUsbDeviceAccessError,
1277
+ HardwareErrorCode.TransportProtocolProbeFailed,
1278
+ HardwareErrorCode.TransportProtocolMismatch,
1280
1279
  ].includes(error.errorCode)
1281
1280
  ) {
1282
1281
  _deviceList = undefined;
@@ -1363,6 +1362,9 @@ const ensureConnected = async (
1363
1362
  HardwareErrorCode.DeviceDetectInBootloaderMode,
1364
1363
  HardwareErrorCode.BleCharacteristicNotifyChangeFailure,
1365
1364
  HardwareErrorCode.BridgeNeedsPermission,
1365
+ HardwareErrorCode.WebUsbDeviceAccessError,
1366
+ HardwareErrorCode.TransportProtocolProbeFailed,
1367
+ HardwareErrorCode.TransportProtocolMismatch,
1366
1368
  HardwareErrorCode.DeviceInterruptedFromUser,
1367
1369
  HardwareErrorCode.CallQueueActionCancelled,
1368
1370
  ].includes(error.errorCode) ||
@@ -1370,7 +1372,6 @@ const ensureConnected = async (
1370
1372
  [HardwareErrorCode.BleDeviceDisconnected, HardwareErrorCode.PollingTimeout].includes(
1371
1373
  error.errorCode
1372
1374
  )) ||
1373
- isTerminalBleStaleBondError(error) ||
1374
1375
  isDeviceIdentityMismatchError(error)
1375
1376
  ) {
1376
1377
  reject(error);
@@ -1830,6 +1831,10 @@ export default class Core extends EventEmitter {
1830
1831
 
1831
1832
  private requestQueue = new RequestQueue();
1832
1833
 
1834
+ getOwnedUsbDeviceIdentity(descriptor: Parameters<typeof getOwnedUsbDeviceIdentity>[0]) {
1835
+ return getOwnedUsbDeviceIdentity(descriptor, this.requestQueue);
1836
+ }
1837
+
1833
1838
  private disposePromise?: Promise<void>;
1834
1839
 
1835
1840
  // background task
@@ -1963,6 +1968,8 @@ export default class Core extends EventEmitter {
1963
1968
 
1964
1969
  private async disposeResources(): Promise<void> {
1965
1970
  Log.debug(`[Core] Disposing SDK instance: ${this.sdkInstanceId}`);
1971
+ transportSwitchGeneration += 1;
1972
+ pendingTransportSwitch = Promise.resolve();
1966
1973
  pollingManager.stopAll();
1967
1974
  this.requestQueue.abortAllRequests();
1968
1975
  cleanup();
@@ -1997,6 +2004,8 @@ export default class Core extends EventEmitter {
1997
2004
  }
1998
2005
 
1999
2006
  export const initCore = () => {
2007
+ transportSwitchGeneration += 1;
2008
+ pendingTransportSwitch = Promise.resolve();
2000
2009
  const core = new Core();
2001
2010
  _core = core;
2002
2011
  return core;
@@ -2026,6 +2035,8 @@ export const init = async (
2026
2035
  plugin?: LowlevelTransportSharedPlugin
2027
2036
  ) => {
2028
2037
  try {
2038
+ transportSwitchGeneration += 1;
2039
+ pendingTransportSwitch = Promise.resolve();
2029
2040
  await DataManager.load(settings);
2030
2041
  initTransport(Transport, plugin);
2031
2042
  enableLog(DataManager.getSettings('debug'));
@@ -2051,10 +2062,26 @@ export const switchTransport = ({
2051
2062
  Transport: any;
2052
2063
  plugin?: LowlevelTransportSharedPlugin;
2053
2064
  }) => {
2054
- DataManager.updateEnv(env);
2055
- TransportManager.setTransport(Transport, plugin);
2056
- _deviceList = undefined;
2057
- DevicePool.resetState();
2058
- _connector = undefined;
2059
- initConnector();
2065
+ const generation = transportSwitchGeneration;
2066
+ // Preserve request order while native transport cleanup is pending.
2067
+ const switching = pendingTransportSwitch
2068
+ .catch(() => undefined)
2069
+ .then(async () => {
2070
+ if (generation !== transportSwitchGeneration) {
2071
+ throw ERRORS.TypedError(HardwareErrorCode.NotInitialized);
2072
+ }
2073
+ _connector?.stop();
2074
+ await TransportManager.getTransport()?.stop?.();
2075
+ if (generation !== transportSwitchGeneration) {
2076
+ throw ERRORS.TypedError(HardwareErrorCode.NotInitialized);
2077
+ }
2078
+ DataManager.updateEnv(env);
2079
+ TransportManager.setTransport(Transport, plugin);
2080
+ _deviceList = undefined;
2081
+ DevicePool.resetState();
2082
+ _connector = undefined;
2083
+ initConnector();
2084
+ });
2085
+ pendingTransportSwitch = switching;
2086
+ return switching;
2060
2087
  };