@onekeyfe/hd-core 1.2.8-alpha.2 → 1.2.8-alpha.21

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 (56) hide show
  1. package/README.md +27 -0
  2. package/__tests__/btc-derive-context-hash.test.ts +200 -0
  3. package/__tests__/core-dispose.test.ts +114 -1
  4. package/__tests__/core-error-output.test.ts +21 -0
  5. package/__tests__/device-connector-protocol.test.ts +5 -2
  6. package/__tests__/device-lifecycle-events.test.ts +73 -10
  7. package/__tests__/device-pool-state.test.ts +50 -0
  8. package/__tests__/protocol-v2-bootloader-mode.test.ts +12 -0
  9. package/__tests__/usb-identity-probe.test.ts +293 -0
  10. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  11. package/dist/api/SearchDevices.d.ts +21 -1
  12. package/dist/api/SearchDevices.d.ts.map +1 -1
  13. package/dist/api/btc/BTCDeriveContextHash.d.ts +16 -0
  14. package/dist/api/btc/BTCDeriveContextHash.d.ts.map +1 -0
  15. package/dist/api/index.d.ts +1 -0
  16. package/dist/api/index.d.ts.map +1 -1
  17. package/dist/api/protocol-v2/DeviceReboot.d.ts.map +1 -1
  18. package/dist/core/index.d.ts +3 -1
  19. package/dist/core/index.d.ts.map +1 -1
  20. package/dist/device/Device.d.ts +2 -0
  21. package/dist/device/Device.d.ts.map +1 -1
  22. package/dist/device/DeviceCommands.d.ts +4 -4
  23. package/dist/device/DevicePool.d.ts.map +1 -1
  24. package/dist/events/logBlockEvent.d.ts.map +1 -1
  25. package/dist/index.d.ts +47 -6
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +2593 -2305
  28. package/dist/inject.d.ts.map +1 -1
  29. package/dist/types/api/btcDeriveContextHash.d.ts +10 -0
  30. package/dist/types/api/btcDeriveContextHash.d.ts.map +1 -0
  31. package/dist/types/api/export.d.ts +1 -0
  32. package/dist/types/api/export.d.ts.map +1 -1
  33. package/dist/types/api/index.d.ts +2 -0
  34. package/dist/types/api/index.d.ts.map +1 -1
  35. package/dist/types/device.d.ts +8 -0
  36. package/dist/types/device.d.ts.map +1 -1
  37. package/dist/utils/patch.d.ts +1 -1
  38. package/dist/utils/patch.d.ts.map +1 -1
  39. package/package.json +4 -4
  40. package/src/api/FirmwareUpdateV4.ts +4 -7
  41. package/src/api/SearchDevices.ts +163 -7
  42. package/src/api/btc/BTCDeriveContextHash.ts +90 -0
  43. package/src/api/index.ts +1 -0
  44. package/src/api/protocol-v2/DeviceReboot.ts +2 -0
  45. package/src/core/index.ts +46 -15
  46. package/src/data/messages/messages.json +51 -0
  47. package/src/device/Device.ts +10 -0
  48. package/src/device/DeviceConnector.ts +4 -4
  49. package/src/device/DevicePool.ts +12 -2
  50. package/src/events/logBlockEvent.ts +1 -0
  51. package/src/index.ts +1 -0
  52. package/src/inject.ts +2 -0
  53. package/src/types/api/btcDeriveContextHash.ts +15 -0
  54. package/src/types/api/export.ts +1 -0
  55. package/src/types/api/index.ts +2 -0
  56. package/src/types/device.ts +10 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/hd-core",
3
- "version": "1.2.8-alpha.2",
3
+ "version": "1.2.8-alpha.21",
4
4
  "description": "Core processes and APIs for communicating with OneKey hardware devices.",
5
5
  "author": "OneKey",
6
6
  "homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme",
@@ -25,8 +25,8 @@
25
25
  "url": "https://github.com/OneKeyHQ/hardware-js-sdk/issues"
26
26
  },
27
27
  "dependencies": {
28
- "@onekeyfe/hd-shared": "1.2.8-alpha.2",
29
- "@onekeyfe/hd-transport": "1.2.8-alpha.2",
28
+ "@onekeyfe/hd-shared": "1.2.8-alpha.21",
29
+ "@onekeyfe/hd-transport": "1.2.8-alpha.21",
30
30
  "axios": "1.15.2",
31
31
  "bignumber.js": "^9.0.2",
32
32
  "buffer": "^6.0.3",
@@ -46,5 +46,5 @@
46
46
  "@types/w3c-web-usb": "^1.0.10",
47
47
  "@types/web-bluetooth": "^0.0.21"
48
48
  },
49
- "gitHead": "74ac64464df3806ef44ad54d313378754c3eaed2"
49
+ "gitHead": "acc31b02db0a507a597775fc88306c9104c1b5d1"
50
50
  }
@@ -352,13 +352,10 @@ const PROTOCOL_V2_TARGET_STATUS_BY_DECODED_NAME = new Map<string, number>([
352
352
  ['FW_MGMT_UPDATER_TASK_STATUS_FAILED_ENTRY_OUT_OF_BOUNDS', 10],
353
353
  ]);
354
354
 
355
- const isProtocolV2ReconnectProbeError = (error: unknown) => {
356
- const message = getProtocolV2UnknownErrorText(error).toLowerCase();
357
- return (
358
- (message.includes('device protocol mismatch') && message.includes('expected v2')) ||
359
- message.includes('did not respond to expected protocol')
360
- );
361
- };
355
+ const isProtocolV2ReconnectProbeError = (error: unknown) =>
356
+ getProtocolV2ErrorCode(error) === HardwareErrorCode.TransportProtocolProbeFailed &&
357
+ (error as { params?: { expectedProtocol?: unknown } } | undefined)?.params?.expectedProtocol ===
358
+ 'V2';
362
359
 
363
360
  const isProtocolV2FirmwareStatusEndpointUnavailable = (error: unknown) => {
364
361
  const message = getProtocolV2UnknownErrorText(error).toLowerCase();
@@ -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,149 @@ 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
+ };
91
+ };
92
+
93
+ export type UsbIdentityProbeTransport = Pick<
94
+ Transport,
95
+ 'release' | 'call' | 'getProtocolType' | 'configureProtocolV2'
96
+ > & {
97
+ init: (logger: object) => unknown;
98
+ enumerate: () => Promise<UsbIdentityProbeDescriptor[]>;
99
+ acquire: (input: AcquireInput) => Promise<string | undefined>;
100
+ configure: (signedData: string) => void | Promise<void>;
101
+ };
102
+
103
+ // Identity discovery must not publish device events or log raw Features/session data.
104
+ const identityProbeLogger = {
105
+ debug: () => undefined,
106
+ log: () => undefined,
107
+ info: () => undefined,
108
+ warn: () => undefined,
109
+ error: () => undefined,
110
+ };
111
+
112
+ /** Runs on a separate transport; never initializes or switches the current Core. */
113
+ export const probeUsbDeviceIdentitiesWithTransport = async (
114
+ transport: UsbIdentityProbeTransport,
115
+ commType: UsbDeviceIdentity['commType'],
116
+ getOwnedIdentity?: (descriptor: UsbIdentityProbeDescriptor) => UsbDeviceIdentity | undefined
117
+ ): Promise<UsbDeviceIdentity[]> => {
118
+ if (commType !== 'webusb') return [];
119
+ await transport.init(identityProbeLogger);
120
+ const defaultMessages = JSON.stringify(DataManager.getProtobufMessages());
121
+ await transport.configure(defaultMessages);
122
+ await transport.configureProtocolV2?.(
123
+ JSON.stringify(DataManager.getProtobufMessages('v2Schema'))
124
+ );
125
+ const descriptors = await transport.enumerate();
126
+ const identities: UsbDeviceIdentity[] = [];
127
+ const readIdentity = async (
128
+ descriptor: UsbIdentityProbeDescriptor
129
+ ): Promise<UsbDeviceIdentity | undefined> => {
130
+ const { path } = descriptor;
131
+ if (!path || descriptor.commType !== commType) return undefined;
132
+ if (descriptors.filter(candidate => candidate.path === path).length !== 1) return undefined;
133
+ const owner = getOwnedIdentity?.(descriptor);
134
+ if (owner) {
135
+ return owner;
136
+ }
137
+ // Do not borrow a browser handle or replace a Bridge session owned by another call.
138
+ if (descriptor.session || (getUsbHandle(descriptor) as USBDevice | undefined)?.opened)
139
+ return undefined;
140
+
141
+ let session: string | undefined;
142
+ let identified: UsbDeviceIdentity | undefined;
143
+ try {
144
+ await transport.configure(defaultMessages);
145
+ session = await transport.acquire({ path, previous: null, forceProtocolDetection: true });
146
+ if (!session) return undefined;
147
+ const protocol = transport.getProtocolType(path);
148
+ if (protocol === 'V1') {
149
+ const response = await transport.call(session, 'GetFeatures', {}, { timeoutMs: 3000 });
150
+ if (response.type !== 'Features') return undefined;
151
+ let features = buildProtocolV1FeaturesPayload(response.message as PROTO.Features);
152
+ if (!features.serialNo) {
153
+ const { messages, protocolV1MessageSchema } = getSupportProtocolV1MessageSchema(features);
154
+ // Legacy V1 has no OnekeyGetFeatures; keep the identity confirmed by GetFeatures.
155
+ if (protocolV1MessageSchema === 'v1CurrentSchema') {
156
+ if (messages) await transport.configure(JSON.stringify(messages));
157
+ const vendor = await transport.call(
158
+ session,
159
+ 'OnekeyGetFeatures',
160
+ {},
161
+ { timeoutMs: 3000 }
162
+ );
163
+ if (vendor.type === 'OnekeyFeatures') {
164
+ features = buildProtocolV1FeaturesPayload({
165
+ ...response.message,
166
+ ...vendor.message,
167
+ } as PROTO.Features);
168
+ }
169
+ }
170
+ }
171
+ identified = {
172
+ path,
173
+ serialNo: features.serialNo || null,
174
+ deviceId: features.deviceId || null,
175
+ deviceType: features.deviceType,
176
+ connectProtocol: 'V1',
177
+ commType,
178
+ };
179
+ } else if (protocol === 'V2') {
180
+ const response = await transport.call(
181
+ session,
182
+ 'DeviceInfoGet',
183
+ PROTOCOL_V2_FEATURES_DEVICE_INFO_REQUEST,
184
+ { timeoutMs: 3000 }
185
+ );
186
+ if (response.type !== 'DeviceInfo') return undefined;
187
+ const { identity } = mapProtocolV2DeviceInfoToState(
188
+ response.message as ProtocolV2DeviceInfo
189
+ );
190
+ identified = {
191
+ path,
192
+ serialNo: identity?.serialNo || null,
193
+ deviceId: identity?.deviceId || null,
194
+ deviceType: identity?.deviceType ?? EDeviceType.Unknown,
195
+ connectProtocol: 'V2',
196
+ commType,
197
+ };
198
+ }
199
+ } catch {
200
+ // Discovery is best effort; a busy/disconnected device cannot confirm identity.
201
+ } finally {
202
+ if (session) {
203
+ try {
204
+ await transport.release(session, false);
205
+ } catch {
206
+ identified = undefined;
207
+ }
208
+ }
209
+ }
210
+ return identified;
211
+ };
212
+ for (const descriptor of descriptors) {
213
+ const identity = await readIdentity(descriptor);
214
+ if (identity) identities.push(identity);
215
+ }
216
+ return identities;
217
+ };
218
+
63
219
  // A USB path is a routing key (the USB serial, a synthesized usb-vid-pid-name, or the
64
220
  // bootloader placeholder), not a hardware identity, so it never fills uuid/serialNo.
65
221
  const toSearchDeviceFromDescriptor = (descriptor: DeviceDescriptor): SearchDevice => {
@@ -81,7 +237,7 @@ const toSearchDevice = (device: Device): SearchDevice | null => {
81
237
  };
82
238
 
83
239
  export default class SearchDevices extends BaseMethod {
84
- connector?: DeviceConnector;
240
+ connector?: DeviceConnector = undefined;
85
241
 
86
242
  init() {
87
243
  this.useDevice = false;
@@ -130,7 +286,7 @@ export default class SearchDevices extends BaseMethod {
130
286
  // BLE discovery cannot provide a physical-device identity before initialization.
131
287
  uuid: '',
132
288
  deviceId: null,
133
- name: bleName || device.name,
289
+ name: bleName || device.name || '',
134
290
  deviceType: getDeviceTypeByBleName(bleName),
135
291
  });
136
292
  }
@@ -0,0 +1,90 @@
1
+ import { ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared';
2
+ import { Buffer } from 'buffer';
3
+
4
+ import { BaseMethod } from '../BaseMethod';
5
+ import { invalidParameter, validateParams } from '../helpers/paramsValidator';
6
+ import { getScriptType, validatePath } from '../helpers/pathUtils';
7
+
8
+ import type { BabylonDeriveContextHash, CanonicalBitcoinNetwork } from '@onekeyfe/hd-transport';
9
+ import type { BTCDeriveContextHashParams } from '../../types';
10
+
11
+ const NETWORKS: Record<BTCDeriveContextHashParams['network'], CanonicalBitcoinNetwork> = {
12
+ 'bitcoin-mainnet': 'BITCOIN_MAINNET',
13
+ 'bitcoin-testnet': 'BITCOIN_TESTNET',
14
+ 'bitcoin-signet': 'BITCOIN_SIGNET',
15
+ 'bitcoin-regtest': 'BITCOIN_REGTEST',
16
+ };
17
+
18
+ export default class BTCDeriveContextHash extends BaseMethod<BabylonDeriveContextHash> {
19
+ strictCheckDeviceSupport = true;
20
+
21
+ init() {
22
+ this.checkDeviceId = true;
23
+ this.allowUsePreInitialize = true;
24
+
25
+ validateParams(this.payload, [
26
+ { name: 'path', required: true },
27
+ { name: 'appName', type: 'string', required: true },
28
+ { name: 'context', type: 'string', required: true },
29
+ { name: 'network', type: 'string', required: true },
30
+ ]);
31
+
32
+ const { path, appName, context, network } = this.payload;
33
+ if (typeof appName !== 'string' || !/^[a-z0-9-]{1,64}$/.test(appName)) {
34
+ throw invalidParameter(
35
+ 'appName must contain 1..64 ASCII lowercase letters, digits or hyphens'
36
+ );
37
+ }
38
+ if (typeof context !== 'string' || !/^(?:[0-9a-f]{2}){1,1024}$/.test(context)) {
39
+ throw invalidParameter('context must be 1..1024 bytes of lowercase hex without a prefix');
40
+ }
41
+ if (typeof network !== 'string' || !Object.prototype.hasOwnProperty.call(NETWORKS, network)) {
42
+ throw invalidParameter('Unsupported Bitcoin network');
43
+ }
44
+
45
+ // Reject malformed or overflowing indices before validatePath can truncate them.
46
+ if (
47
+ (typeof path === 'string' &&
48
+ (!/^m(?:\/\d+'?)+$/i.test(path) ||
49
+ path
50
+ .split('/')
51
+ .slice(1)
52
+ .some(index => Number(index.replace(/'/g, '')) >= 0x80000000))) ||
53
+ (Array.isArray(path) &&
54
+ path.findIndex(index => !Number.isInteger(index) || index < 0 || index > 0xffffffff) !== -1)
55
+ ) {
56
+ throw invalidParameter('Not a valid BIP-32 path');
57
+ }
58
+ const addressN = validatePath(path, 3);
59
+
60
+ this.params = {
61
+ address_n: addressN,
62
+ script_type: getScriptType(addressN),
63
+ app_name: Buffer.from(appName, 'ascii').toString('hex'),
64
+ context,
65
+ network: NETWORKS[network as BTCDeriveContextHashParams['network']],
66
+ };
67
+ }
68
+
69
+ getVersionRange() {
70
+ return {
71
+ classic1s: { min: '3.21.0' },
72
+ classicpure: { min: '3.21.0' },
73
+ };
74
+ }
75
+
76
+ async run() {
77
+ const { message } = await this.device.commands.typedCall(
78
+ 'BabylonDeriveContextHash',
79
+ 'BabylonDerivedContextHash',
80
+ this.params
81
+ );
82
+ if (typeof message.secret !== 'string' || !/^[0-9a-f]{64}$/.test(message.secret)) {
83
+ throw ERRORS.TypedError(
84
+ HardwareErrorCode.CallMethodError,
85
+ 'Invalid Babylon context hash response'
86
+ );
87
+ }
88
+ return message;
89
+ }
90
+ }
package/src/api/index.ts CHANGED
@@ -60,6 +60,7 @@ export { default as allNetworkGetAddressByLoop } from './allnetwork/AllNetworkGe
60
60
 
61
61
  export { default as btcGetAddress } from './btc/BTCGetAddress';
62
62
  export { default as btcGetPublicKey } from './btc/BTCGetPublicKey';
63
+ export { default as btcDeriveContextHash } from './btc/BTCDeriveContextHash';
63
64
  export { default as btcSignMessage } from './btc/BTCSignMessage';
64
65
  export { default as btcSignPsbt } from './btc/BTCSignPsbt';
65
66
  export { default as btcSignTransaction } from './btc/BTCSignTransaction';
@@ -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';
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,
@@ -125,6 +126,8 @@ const parseInitOptions = (method?: BaseMethod): InitOptions => ({
125
126
  });
126
127
 
127
128
  let _core: Core | undefined;
129
+ let transportSwitchGeneration = 0;
130
+ let pendingTransportSwitch: Promise<void> = Promise.resolve();
128
131
  let _deviceList: DeviceList | undefined;
129
132
  let _connector: DeviceConnector | undefined;
130
133
  let _uiPromises: UiPromise<UiPromiseResponse['type']>[] = []; // Waiting for ui response
@@ -990,15 +993,11 @@ function canSkipInitialize(method: BaseMethod, device: Device): boolean {
990
993
  }
991
994
 
992
995
  export function isRetryableBleProtocolV2ProbeError(method: BaseMethod, error: unknown) {
993
- const typedError = error as { errorCode?: unknown; message?: unknown };
994
- const message =
995
- typeof typedError?.message === 'string' ? typedError.message : String(error ?? '');
996
+ const typedError = error as { errorCode?: unknown; params?: { expectedProtocol?: unknown } };
996
997
  return (
997
998
  method.payload.connectProtocol === 'V2' &&
998
- typedError?.errorCode === HardwareErrorCode.RuntimeError &&
999
- message.includes('Device protocol mismatch') &&
1000
- message.includes('expected V2') &&
1001
- message.includes('did not respond to expected protocol')
999
+ typedError?.errorCode === HardwareErrorCode.TransportProtocolProbeFailed &&
1000
+ typedError.params?.expectedProtocol === 'V2'
1002
1001
  );
1003
1002
  }
1004
1003
 
@@ -1008,7 +1007,7 @@ export function isRetryableBleConnectionError(method: BaseMethod, error: unknown
1008
1007
  }
1009
1008
  const typedError = error as {
1010
1009
  errorCode?: unknown;
1011
- params?: { acquireDeadlineExceeded?: unknown };
1010
+ params?: { acquireDeadlineExceeded?: unknown; expectedProtocol?: unknown };
1012
1011
  };
1013
1012
  if (
1014
1013
  typedError?.errorCode === HardwareErrorCode.BleTimeoutError &&
@@ -1019,6 +1018,8 @@ export function isRetryableBleConnectionError(method: BaseMethod, error: unknown
1019
1018
  return (
1020
1019
  typedError?.errorCode === HardwareErrorCode.BleTimeoutError ||
1021
1020
  typedError?.errorCode === HardwareErrorCode.BleConnectedError ||
1021
+ (typedError?.errorCode === HardwareErrorCode.TransportProtocolProbeFailed &&
1022
+ !typedError.params?.expectedProtocol) ||
1022
1023
  isRetryableBleProtocolV2ProbeError(method, error) ||
1023
1024
  isMissingDetectedProtocolV2Error(method, error)
1024
1025
  );
@@ -1273,6 +1274,8 @@ const ensureConnected = async (
1273
1274
  HardwareErrorCode.BridgeTimeoutError,
1274
1275
  HardwareErrorCode.BridgeNeedsPermission,
1275
1276
  HardwareErrorCode.WebUsbDeviceAccessError,
1277
+ HardwareErrorCode.TransportProtocolProbeFailed,
1278
+ HardwareErrorCode.TransportProtocolMismatch,
1276
1279
  ].includes(error.errorCode)
1277
1280
  ) {
1278
1281
  _deviceList = undefined;
@@ -1360,6 +1363,8 @@ const ensureConnected = async (
1360
1363
  HardwareErrorCode.BleCharacteristicNotifyChangeFailure,
1361
1364
  HardwareErrorCode.BridgeNeedsPermission,
1362
1365
  HardwareErrorCode.WebUsbDeviceAccessError,
1366
+ HardwareErrorCode.TransportProtocolProbeFailed,
1367
+ HardwareErrorCode.TransportProtocolMismatch,
1363
1368
  HardwareErrorCode.DeviceInterruptedFromUser,
1364
1369
  HardwareErrorCode.CallQueueActionCancelled,
1365
1370
  ].includes(error.errorCode) ||
@@ -1589,7 +1594,7 @@ const onDeviceConnectHandler = (device: Device) => {
1589
1594
 
1590
1595
  const onDeviceDisconnectHandler = (device: Device) => {
1591
1596
  device.clearPreInitialized();
1592
- const deviceObject = device.toMessageObject();
1597
+ const deviceObject = device.toDisconnectMessageObject();
1593
1598
  if (!deviceObject) return;
1594
1599
  postMessage(createDeviceMessage(DEVICE.DISCONNECT, { device: deviceObject }));
1595
1600
  };
@@ -1826,6 +1831,10 @@ export default class Core extends EventEmitter {
1826
1831
 
1827
1832
  private requestQueue = new RequestQueue();
1828
1833
 
1834
+ getOwnedUsbDeviceIdentity(descriptor: Parameters<typeof getOwnedUsbDeviceIdentity>[0]) {
1835
+ return getOwnedUsbDeviceIdentity(descriptor, this.requestQueue);
1836
+ }
1837
+
1829
1838
  private disposePromise?: Promise<void>;
1830
1839
 
1831
1840
  // background task
@@ -1959,6 +1968,8 @@ export default class Core extends EventEmitter {
1959
1968
 
1960
1969
  private async disposeResources(): Promise<void> {
1961
1970
  Log.debug(`[Core] Disposing SDK instance: ${this.sdkInstanceId}`);
1971
+ transportSwitchGeneration += 1;
1972
+ pendingTransportSwitch = Promise.resolve();
1962
1973
  pollingManager.stopAll();
1963
1974
  this.requestQueue.abortAllRequests();
1964
1975
  cleanup();
@@ -1993,6 +2004,8 @@ export default class Core extends EventEmitter {
1993
2004
  }
1994
2005
 
1995
2006
  export const initCore = () => {
2007
+ transportSwitchGeneration += 1;
2008
+ pendingTransportSwitch = Promise.resolve();
1996
2009
  const core = new Core();
1997
2010
  _core = core;
1998
2011
  return core;
@@ -2022,6 +2035,8 @@ export const init = async (
2022
2035
  plugin?: LowlevelTransportSharedPlugin
2023
2036
  ) => {
2024
2037
  try {
2038
+ transportSwitchGeneration += 1;
2039
+ pendingTransportSwitch = Promise.resolve();
2025
2040
  await DataManager.load(settings);
2026
2041
  initTransport(Transport, plugin);
2027
2042
  enableLog(DataManager.getSettings('debug'));
@@ -2047,10 +2062,26 @@ export const switchTransport = ({
2047
2062
  Transport: any;
2048
2063
  plugin?: LowlevelTransportSharedPlugin;
2049
2064
  }) => {
2050
- DataManager.updateEnv(env);
2051
- TransportManager.setTransport(Transport, plugin);
2052
- _deviceList = undefined;
2053
- DevicePool.resetState();
2054
- _connector = undefined;
2055
- 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;
2056
2087
  };
@@ -12485,6 +12485,8 @@
12485
12485
  "(has_bitcoin_only_values)": true
12486
12486
  },
12487
12487
  "values": {
12488
+ "MessageType_BabylonDeriveContextHash": 10054,
12489
+ "MessageType_BabylonDerivedContextHash": 10055,
12488
12490
  "MessageType_Initialize": 0,
12489
12491
  "MessageType_Ping": 1,
12490
12492
  "MessageType_Success": 2,
@@ -12949,6 +12951,55 @@
12949
12951
  "MessageType_UpgradeFileHeader": 10050
12950
12952
  }
12951
12953
  },
12954
+ "CanonicalBitcoinNetwork": {
12955
+ "values": {
12956
+ "BITCOIN_MAINNET": 0,
12957
+ "BITCOIN_TESTNET": 1,
12958
+ "BITCOIN_SIGNET": 2,
12959
+ "BITCOIN_REGTEST": 3
12960
+ }
12961
+ },
12962
+ "BabylonDeriveContextHash": {
12963
+ "fields": {
12964
+ "address_n": {
12965
+ "rule": "repeated",
12966
+ "type": "uint32",
12967
+ "id": 1,
12968
+ "options": {
12969
+ "packed": false
12970
+ }
12971
+ },
12972
+ "script_type": {
12973
+ "rule": "required",
12974
+ "type": "InputScriptType",
12975
+ "id": 2
12976
+ },
12977
+ "app_name": {
12978
+ "rule": "required",
12979
+ "type": "bytes",
12980
+ "id": 3
12981
+ },
12982
+ "context": {
12983
+ "rule": "required",
12984
+ "type": "bytes",
12985
+ "id": 4
12986
+ },
12987
+ "network": {
12988
+ "rule": "required",
12989
+ "type": "CanonicalBitcoinNetwork",
12990
+ "id": 5
12991
+ }
12992
+ }
12993
+ },
12994
+ "BabylonDerivedContextHash": {
12995
+ "fields": {
12996
+ "secret": {
12997
+ "rule": "required",
12998
+ "type": "bytes",
12999
+ "id": 1
13000
+ }
13001
+ }
13002
+ },
12952
13003
  "google": {
12953
13004
  "nested": {
12954
13005
  "protobuf": {
@@ -248,6 +248,9 @@ export class Device extends EventEmitter {
248
248
  /** Canonical device-state cache; legacy Features is a compatibility projection. */
249
249
  private stateStore = new DeviceStateStore();
250
250
 
251
+ /** Retain the old public identity only for disconnect notifications after wipe. */
252
+ private deviceBeforeWipe: DeviceTyped | null = null;
253
+
251
254
  /** Force the next initialization to reload DeviceInfo after reconnect or reboot. */
252
255
  private protocolV2StateNeedsReload = false;
253
256
 
@@ -351,6 +354,8 @@ export class Device extends EventEmitter {
351
354
  toMessageObject(): DeviceTyped | null {
352
355
  if (this.isUnacquired() || !this.features) return null;
353
356
 
357
+ this.deviceBeforeWipe = null;
358
+
354
359
  const env = DataManager.getSettings('env');
355
360
  const deviceType = this.getCurrentDeviceType();
356
361
 
@@ -395,6 +400,10 @@ export class Device extends EventEmitter {
395
400
  };
396
401
  }
397
402
 
403
+ toDisconnectMessageObject(): DeviceTyped | null {
404
+ return this.toMessageObject() ?? this.deviceBeforeWipe;
405
+ }
406
+
398
407
  /**
399
408
  * Device connect
400
409
  * @returns {Promise<boolean>}
@@ -1364,6 +1373,7 @@ export class Device extends EventEmitter {
1364
1373
  }
1365
1374
 
1366
1375
  invalidateAfterWipe() {
1376
+ this.deviceBeforeWipe = this.toMessageObject() ?? this.deviceBeforeWipe;
1367
1377
  const deviceId = this.getCurrentDeviceId();
1368
1378
  if (deviceId) {
1369
1379
  deviceWalletSessionStore.deleteDevice(deviceId);
@@ -133,10 +133,10 @@ export default class DeviceConnector {
133
133
  : undefined;
134
134
  const detectedProtocol = acquiredProtocol ?? transport.getProtocolType(path);
135
135
  if (detectedProtocol !== expectedProtocol) {
136
- throw ERRORS.TypedError(
137
- HardwareErrorCode.RuntimeError,
138
- `Device protocol mismatch: expected ${expectedProtocol}, detected ${detectedProtocol}`
139
- );
136
+ throw ERRORS.TypedError(HardwareErrorCode.TransportProtocolMismatch, undefined, {
137
+ expectedProtocol,
138
+ detectedProtocol,
139
+ });
140
140
  }
141
141
  }
142
142
  return res;
@@ -114,7 +114,11 @@ export class DevicePool extends EventEmitter {
114
114
  const exist = canPathIdentifyDevice(cachedPath)
115
115
  ? descriptorList.find(d => d.path === cachedPath)
116
116
  : undefined;
117
- if (exist && !initOptions?.forceProtocolDetection) {
117
+ if (
118
+ exist &&
119
+ !initOptions?.forceProtocolDetection &&
120
+ !(device.needReloadDevice && device.isUnacquired())
121
+ ) {
118
122
  // Log.debug('find existed Device: ', connectId);
119
123
  device.updateDescriptor(exist, true);
120
124
  await this._refreshRuntimeState(device, initOptions);
@@ -170,7 +174,13 @@ export class DevicePool extends EventEmitter {
170
174
  const forceProtocolDetection = initOptions?.forceProtocolDetection === true;
171
175
  const isNewDevice = !cachedDevice;
172
176
  const device = cachedDevice ?? Device.fromDescriptor(descriptor);
173
- if (isNewDevice || forceProtocolDetection) {
177
+ if (
178
+ isNewDevice ||
179
+ forceProtocolDetection ||
180
+ (device.needReloadDevice && device.isUnacquired())
181
+ ) {
182
+ // Wipe invalidates state while the same USB path can remain discoverable.
183
+ if (cachedDevice) device.updateDescriptor(descriptor, true);
174
184
  device.deviceConnector = this.connector;
175
185
  // Search/discovery acquires outside Device.run(). Start a new attempt so
176
186
  // a previous user cancel does not hide this device from later searches.
@@ -11,6 +11,7 @@ export const LogBlockEvent: Set<string> = new Set([
11
11
  // These resource/file APIs did not exist in 1.1.32. Skip the payload so
12
12
  // the log layer does not copy huge Base64 or binary data.
13
13
  const LogPayloadBlockMethod: Set<string> = new Set([
14
+ 'btcDeriveContextHash',
14
15
  'deviceUploadNft',
15
16
  'deviceUploadWallpaper',
16
17
  'uploadPortfolio',