@onekeyfe/hd-core 1.2.8 → 1.2.10-alpha.1

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 (151) hide show
  1. package/README.md +27 -0
  2. package/__tests__/DeviceCommands.test.ts +66 -13
  3. package/__tests__/allNetworkGetAddress-privacy-networks.test.ts +78 -0
  4. package/__tests__/btc-derive-context-hash.test.ts +200 -0
  5. package/__tests__/core-dispose.test.ts +114 -1
  6. package/__tests__/core-error-output.test.ts +55 -3
  7. package/__tests__/device-connector-protocol.test.ts +5 -2
  8. package/__tests__/device-lifecycle-events.test.ts +191 -27
  9. package/__tests__/device-pool-state.test.ts +50 -0
  10. package/__tests__/firmware-update/firmware-artifact-source.test.ts +11 -0
  11. package/__tests__/logBlockEvent.test.ts +331 -9
  12. package/__tests__/logger.test.ts +18 -0
  13. package/__tests__/monero-get-address.test.ts +147 -0
  14. package/__tests__/monero-key-image-sync.test.ts +272 -0
  15. package/__tests__/monero-sign-transaction.test.ts +436 -0
  16. package/__tests__/protocol-v2-bootloader-mode.test.ts +43 -2
  17. package/__tests__/protocol-v2-resources.test.ts +14 -0
  18. package/__tests__/protocol-v2.test.ts +841 -46
  19. package/__tests__/protocolV2FileWrite.test.ts +454 -7
  20. package/__tests__/refresh-device-state.test.ts +19 -17
  21. package/__tests__/search-devices.test.ts +36 -0
  22. package/__tests__/sol-sign-offchain-message.test.ts +8 -0
  23. package/__tests__/usb-identity-probe.test.ts +358 -0
  24. package/__tests__/zcash-protobuf.test.ts +141 -0
  25. package/__tests__/zcash-sign-pczt.test.ts +446 -0
  26. package/dist/api/BaseMethod.d.ts.map +1 -1
  27. package/dist/api/FileWrite.d.ts.map +1 -1
  28. package/dist/api/FirmwareUpdateV4.d.ts +5 -0
  29. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  30. package/dist/api/GetDeviceState.d.ts.map +1 -1
  31. package/dist/api/SearchDevices.d.ts +21 -1
  32. package/dist/api/SearchDevices.d.ts.map +1 -1
  33. package/dist/api/allnetwork/AllNetworkGetAddressBase.d.ts.map +1 -1
  34. package/dist/api/btc/BTCDeriveContextHash.d.ts +16 -0
  35. package/dist/api/btc/BTCDeriveContextHash.d.ts.map +1 -0
  36. package/dist/api/firmware/FirmwareArtifactSource.d.ts.map +1 -1
  37. package/dist/api/helpers/pathUtils.d.ts +1 -0
  38. package/dist/api/helpers/pathUtils.d.ts.map +1 -1
  39. package/dist/api/helpers/protocolV2FileWrite.d.ts +5 -1
  40. package/dist/api/helpers/protocolV2FileWrite.d.ts.map +1 -1
  41. package/dist/api/index.d.ts +7 -0
  42. package/dist/api/index.d.ts.map +1 -1
  43. package/dist/api/monero/MoneroGetAddress.d.ts +11 -0
  44. package/dist/api/monero/MoneroGetAddress.d.ts.map +1 -0
  45. package/dist/api/monero/MoneroGetWatchKey.d.ts +11 -0
  46. package/dist/api/monero/MoneroGetWatchKey.d.ts.map +1 -0
  47. package/dist/api/monero/MoneroKeyImageSync.d.ts +20 -0
  48. package/dist/api/monero/MoneroKeyImageSync.d.ts.map +1 -0
  49. package/dist/api/monero/MoneroSignTransaction.d.ts +17 -0
  50. package/dist/api/monero/MoneroSignTransaction.d.ts.map +1 -0
  51. package/dist/api/monero/helpers.d.ts +5 -0
  52. package/dist/api/monero/helpers.d.ts.map +1 -0
  53. package/dist/api/protocol-v2/DeviceReboot.d.ts.map +1 -1
  54. package/dist/api/protocol-v2/DeviceUploadNft.d.ts.map +1 -1
  55. package/dist/api/protocol-v2/DeviceUploadWallpaper.d.ts.map +1 -1
  56. package/dist/api/zcash/ZcashGetAddress.d.ts +12 -0
  57. package/dist/api/zcash/ZcashGetAddress.d.ts.map +1 -0
  58. package/dist/api/zcash/ZcashSignPczt.d.ts +16 -0
  59. package/dist/api/zcash/ZcashSignPczt.d.ts.map +1 -0
  60. package/dist/core/index.d.ts +3 -2
  61. package/dist/core/index.d.ts.map +1 -1
  62. package/dist/device/Device.d.ts +3 -0
  63. package/dist/device/Device.d.ts.map +1 -1
  64. package/dist/device/DeviceCommands.d.ts +4 -4
  65. package/dist/device/DeviceCommands.d.ts.map +1 -1
  66. package/dist/device/DevicePool.d.ts.map +1 -1
  67. package/dist/events/logBlockEvent.d.ts +10 -0
  68. package/dist/events/logBlockEvent.d.ts.map +1 -1
  69. package/dist/events/ui-request.d.ts +2 -0
  70. package/dist/events/ui-request.d.ts.map +1 -1
  71. package/dist/index.d.ts +196 -7
  72. package/dist/index.d.ts.map +1 -1
  73. package/dist/index.js +4363 -2792
  74. package/dist/inject.d.ts.map +1 -1
  75. package/dist/protocols/protocol-v2/resources.d.ts +1 -0
  76. package/dist/protocols/protocol-v2/resources.d.ts.map +1 -1
  77. package/dist/types/api/allNetworkGetAddress.d.ts +5 -1
  78. package/dist/types/api/allNetworkGetAddress.d.ts.map +1 -1
  79. package/dist/types/api/btcDeriveContextHash.d.ts +10 -0
  80. package/dist/types/api/btcDeriveContextHash.d.ts.map +1 -0
  81. package/dist/types/api/export.d.ts +7 -0
  82. package/dist/types/api/export.d.ts.map +1 -1
  83. package/dist/types/api/index.d.ts +14 -0
  84. package/dist/types/api/index.d.ts.map +1 -1
  85. package/dist/types/api/moneroGetAddress.d.ts +15 -0
  86. package/dist/types/api/moneroGetAddress.d.ts.map +1 -0
  87. package/dist/types/api/moneroGetWatchKey.d.ts +15 -0
  88. package/dist/types/api/moneroGetWatchKey.d.ts.map +1 -0
  89. package/dist/types/api/moneroKeyImageSync.d.ts +24 -0
  90. package/dist/types/api/moneroKeyImageSync.d.ts.map +1 -0
  91. package/dist/types/api/moneroSignTransaction.d.ts +32 -0
  92. package/dist/types/api/moneroSignTransaction.d.ts.map +1 -0
  93. package/dist/types/api/zcashGetAddress.d.ts +22 -0
  94. package/dist/types/api/zcashGetAddress.d.ts.map +1 -0
  95. package/dist/types/api/zcashSignPczt.d.ts +9 -0
  96. package/dist/types/api/zcashSignPczt.d.ts.map +1 -0
  97. package/dist/types/device.d.ts +10 -0
  98. package/dist/types/device.d.ts.map +1 -1
  99. package/dist/types/params.d.ts +1 -0
  100. package/dist/types/params.d.ts.map +1 -1
  101. package/dist/utils/logger.d.ts.map +1 -1
  102. package/dist/utils/patch.d.ts +1 -1
  103. package/dist/utils/patch.d.ts.map +1 -1
  104. package/package.json +4 -4
  105. package/src/api/BaseMethod.ts +2 -0
  106. package/src/api/FileWrite.ts +7 -1
  107. package/src/api/FirmwareUpdateV4.ts +366 -99
  108. package/src/api/GetDeviceState.ts +2 -5
  109. package/src/api/SearchDevices.ts +172 -7
  110. package/src/api/allnetwork/AllNetworkGetAddressBase.ts +38 -1
  111. package/src/api/btc/BTCDeriveContextHash.ts +90 -0
  112. package/src/api/firmware/FirmwareArtifactSource.ts +12 -3
  113. package/src/api/helpers/pathUtils.ts +23 -0
  114. package/src/api/helpers/protocolV2FileWrite.ts +382 -111
  115. package/src/api/index.ts +8 -0
  116. package/src/api/monero/MoneroGetAddress.ts +71 -0
  117. package/src/api/monero/MoneroGetWatchKey.ts +69 -0
  118. package/src/api/monero/MoneroKeyImageSync.ts +164 -0
  119. package/src/api/monero/MoneroSignTransaction.ts +319 -0
  120. package/src/api/monero/helpers.ts +28 -0
  121. package/src/api/protocol-v2/DeviceReboot.ts +2 -0
  122. package/src/api/protocol-v2/DeviceUploadNft.ts +6 -1
  123. package/src/api/protocol-v2/DeviceUploadWallpaper.ts +15 -1
  124. package/src/api/zcash/ZcashGetAddress.ts +119 -0
  125. package/src/api/zcash/ZcashSignPczt.ts +152 -0
  126. package/src/core/index.ts +52 -24
  127. package/src/data/messages/messages-protocol-v2.json +138 -1
  128. package/src/data/messages/messages.json +51 -0
  129. package/src/device/Device.ts +32 -4
  130. package/src/device/DeviceCommands.ts +15 -1
  131. package/src/device/DeviceConnector.ts +4 -4
  132. package/src/device/DevicePool.ts +12 -2
  133. package/src/events/core.ts +2 -2
  134. package/src/events/logBlockEvent.ts +57 -3
  135. package/src/events/ui-request.ts +2 -0
  136. package/src/index.ts +1 -0
  137. package/src/inject.ts +15 -0
  138. package/src/protocols/protocol-v2/resources.ts +6 -1
  139. package/src/types/api/allNetworkGetAddress.ts +9 -1
  140. package/src/types/api/btcDeriveContextHash.ts +15 -0
  141. package/src/types/api/export.ts +16 -0
  142. package/src/types/api/index.ts +22 -0
  143. package/src/types/api/moneroGetAddress.ts +27 -0
  144. package/src/types/api/moneroGetWatchKey.ts +28 -0
  145. package/src/types/api/moneroKeyImageSync.ts +45 -0
  146. package/src/types/api/moneroSignTransaction.ts +54 -0
  147. package/src/types/api/zcashGetAddress.ts +35 -0
  148. package/src/types/api/zcashSignPczt.ts +18 -0
  149. package/src/types/device.ts +18 -0
  150. package/src/types/params.ts +1 -1
  151. package/src/utils/logger.ts +4 -0
package/README.md CHANGED
@@ -29,4 +29,31 @@ import Core from '@onekeyfe/hd-core';
29
29
  Documentation is available [Hardware SDK Getting Started](https://developer.onekey.so/en/hardware-sdk/getting-started)
30
30
 
31
31
  ## Examples
32
+
33
+ ### Babylon context hash
34
+
35
+ `btcDeriveContextHash(connectId, deviceId, params)` requires Classic 1s firmware
36
+ 3.21.0 or later and Protocol V1. It uses the existing wallet session and device
37
+ confirmation flow. Other device families and Protocol V2 are not supported.
38
+
39
+ ```typescript
40
+ const result = await HardwareSDK.btcDeriveContextHash(connectId, deviceId, {
41
+ path: "m/84'/0'/0'/0/0", // The currently selected Bitcoin key, not the IKM path.
42
+ appName: 'babylon-btc-vault',
43
+ context: 'deadbeef',
44
+ network: 'bitcoin-mainnet',
45
+ });
46
+ // result.payload.secret is a 32-byte value encoded as 64 lowercase hex characters.
47
+ ```
48
+
49
+ `appName` must contain 1–64 ASCII lowercase letters, digits or hyphens. `context`
50
+ must be lowercase hex without `0x`, containing 1–1024 decoded bytes. `network` is
51
+ required: `bitcoin-mainnet`, `bitcoin-testnet` (testnet3/testnet4), `bitcoin-signet`,
52
+ or `bitcoin-regtest`. The SDK infers the Bitcoin input script type from `path` and
53
+ encodes `appName` as ASCII bytes; the device performs the derivation internally.
54
+ Device rejection and unsupported firmware errors propagate through the normal
55
+ SDK response. Treat the returned secret as sensitive and do not log or persist it.
56
+
57
+ See the [Babylon specification](https://github.com/babylonlabs-io/babylon-toolkit/blob/main/docs/specs/derive-context-hash.md).
58
+
32
59
  // TODO: add example url
@@ -130,19 +130,6 @@ describe('DeviceCommands failure mapping', () => {
130
130
  );
131
131
  });
132
132
 
133
- it('preserves the Protocol V2 peer-removed pairing error code', async () => {
134
- const commands = createCommands();
135
- const transportError = Object.assign(new Error('Peer removed pairing information'), {
136
- errorCode: HardwareErrorCode.BlePeerRemovedPairingInformation,
137
- });
138
- commands.mainId = 'main-id';
139
- commands.transport = {
140
- call: jest.fn().mockRejectedValue(transportError),
141
- } as any;
142
-
143
- await expect(commands._commonCall('DeviceInfoGet', {})).rejects.toBe(transportError);
144
- });
145
-
146
133
  it('logs canonical DeviceStatus response fields without exposing the device ID', async () => {
147
134
  const commands = createCommands();
148
135
  const log = getLogger(LoggerNames.DeviceCommands);
@@ -414,6 +401,27 @@ describe('DeviceCommands failure mapping', () => {
414
401
  ).rejects.toMatchObject({ errorCode: HardwareErrorCode.DeviceBusy });
415
402
  });
416
403
 
404
+ it('maps commands rejected during a Protocol V2 firmware update to DeviceBusy', async () => {
405
+ const commands = createCommands();
406
+ const message = 'Requested command not allowed while a firmware update is in progress';
407
+
408
+ await expect(
409
+ commands._filterCommonTypes(
410
+ {
411
+ type: 'Failure',
412
+ message: { code: 'Failure_ProcessError', message },
413
+ } as any,
414
+ 'ProtocolInfoRequest'
415
+ )
416
+ ).rejects.toMatchObject({
417
+ errorCode: HardwareErrorCode.DeviceBusy,
418
+ params: {
419
+ failureCode: 'Failure_ProcessError',
420
+ firmwareMessage: message,
421
+ },
422
+ });
423
+ });
424
+
417
425
  it('maps the current AskPin passphrase-disabled response without relying on a subcode', async () => {
418
426
  const commands = createCommands();
419
427
 
@@ -551,6 +559,51 @@ describe('DeviceCommands failure mapping', () => {
551
559
  }
552
560
  );
553
561
 
562
+ it('maps the Protocol V2 seed-session busy response to DeviceBusy', async () => {
563
+ const commands = createCommands();
564
+
565
+ await expect(
566
+ commands._filterCommonTypes(
567
+ {
568
+ type: 'Failure',
569
+ message: {
570
+ code: 'Failure_ProcessError',
571
+ subcode: DeviceSessionErrorCode.DeviceSessionError_UserCancelled,
572
+ message: 'PIN cancelled',
573
+ },
574
+ } as any,
575
+ 'SignTx'
576
+ )
577
+ ).rejects.toMatchObject({
578
+ errorCode: HardwareErrorCode.DeviceBusy,
579
+ params: {
580
+ failureCode: 'Failure_ProcessError',
581
+ subcode: DeviceSessionErrorCode.DeviceSessionError_UserCancelled,
582
+ firmwareMessage: 'PIN cancelled',
583
+ },
584
+ });
585
+ });
586
+
587
+ it('keeps the same Protocol V2 response as ActionCancelled for DeviceSession calls', async () => {
588
+ const commands = createCommands();
589
+
590
+ await expect(
591
+ commands._filterCommonTypes(
592
+ {
593
+ type: 'Failure',
594
+ message: {
595
+ code: 'Failure_ProcessError',
596
+ subcode: DeviceSessionErrorCode.DeviceSessionError_UserCancelled,
597
+ message: 'PIN cancelled',
598
+ },
599
+ } as any,
600
+ 'DeviceSessionAskPin'
601
+ )
602
+ ).rejects.toMatchObject({
603
+ errorCode: HardwareErrorCode.ActionCancelled,
604
+ });
605
+ });
606
+
554
607
  it('does not use the legacy cancellation-message fallback for Protocol V1', async () => {
555
608
  const commands = createCommands();
556
609
  (commands.device.isProtocolV2 as jest.Mock).mockReturnValue(false);
@@ -0,0 +1,78 @@
1
+ import AllNetworkGetAddress from '../src/api/allnetwork/AllNetworkGetAddress';
2
+
3
+ import type { AllNetworkAddressParams } from '../src/types/api/allNetworkGetAddress';
4
+
5
+ jest.mock('../src/data/config', () => ({
6
+ getSDKVersion: jest.fn(() => '1.0.0'),
7
+ DEFAULT_DOMAIN: 'https://jssdk.onekey.so/1.0.0/',
8
+ }));
9
+
10
+ // Breaks the api index import cycle, as the all-network tests do.
11
+ jest.mock('../src/api/utils', () => ({
12
+ findMethod: jest.fn(),
13
+ }));
14
+
15
+ function mapItem(payload: AllNetworkAddressParams) {
16
+ const method = new AllNetworkGetAddress({
17
+ id: 1,
18
+ payload: { method: 'allNetworkGetAddress', bundle: [payload] },
19
+ });
20
+ return method.generateMethodName({ network: payload.network, payload, originalIndex: 0 });
21
+ }
22
+
23
+ describe('allNetworkGetAddress privacy networks', () => {
24
+ test.each([
25
+ [undefined, 0],
26
+ ['mainnet', 0],
27
+ ['testnet', 1],
28
+ ['stagenet', 2],
29
+ ])('asks moneroGetAddress on the %s network type', (chainName, networkType) => {
30
+ const payload = {
31
+ network: 'xmr' as const,
32
+ path: "m/44'/128'/0'",
33
+ chainName,
34
+ showOnOneKey: false,
35
+ };
36
+
37
+ expect(mapItem(payload)).toEqual(
38
+ expect.objectContaining({
39
+ methodName: 'moneroGetAddress',
40
+ params: {
41
+ path: "m/44'/128'/0'",
42
+ networkType,
43
+ showOnOneKey: false,
44
+ originPayload: payload,
45
+ },
46
+ })
47
+ );
48
+ });
49
+
50
+ test.each(['fakechain', 'constructor'])(
51
+ 'rejects the unknown Monero chainName %s instead of using mainnet',
52
+ chainName => {
53
+ expect(() =>
54
+ mapItem({ network: 'xmr', path: "m/44'/128'/0'", chainName, showOnOneKey: false })
55
+ ).toThrow(`Unsupported Monero chainName: ${chainName}`);
56
+ }
57
+ );
58
+
59
+ test('asks zcashGetAddress for the Orchard address with its viewing key', () => {
60
+ const payload = { network: 'zcash' as const, path: "m/32'/133'/0'", showOnOneKey: false };
61
+
62
+ expect(mapItem(payload)).toEqual(
63
+ expect.objectContaining({
64
+ methodName: 'zcashGetAddress',
65
+ params: {
66
+ path: "m/32'/133'/0'",
67
+ showOnOneKey: false,
68
+ addressType: 2,
69
+ scope: 0,
70
+ diversifierIndex: 0,
71
+ includeUfvk: true,
72
+ includeSeedFingerprint: true,
73
+ originPayload: payload,
74
+ },
75
+ })
76
+ );
77
+ });
78
+ });
@@ -0,0 +1,200 @@
1
+ import { EDeviceType, EFirmwareType, HardwareErrorCode } from '@onekeyfe/hd-shared';
2
+
3
+ import BTCDeriveContextHash from '../src/api/btc/BTCDeriveContextHash';
4
+ import { findMethod } from '../src/api/utils';
5
+ import { getLogBlockLabel, getSafeLogPayload } from '../src/events/logBlockEvent';
6
+ import { createCoreApi } from '../src/inject';
7
+ import { Device } from '../src/device/Device';
8
+
9
+ import type { DeviceFirmwareRange } from '../src/types';
10
+
11
+ jest.mock('../src/data/config', () => ({
12
+ getSDKVersion: jest.fn(() => '1.0.0'),
13
+ DEFAULT_DOMAIN: 'https://jssdk.onekey.so/1.0.0/',
14
+ }));
15
+
16
+ const createMethod = (overrides: Record<string, unknown> = {}) =>
17
+ new BTCDeriveContextHash({
18
+ id: 1,
19
+ payload: {
20
+ method: 'btcDeriveContextHash',
21
+ path: "m/84'/0'/0'/0/0",
22
+ appName: 'test-app',
23
+ context: 'deadbeef',
24
+ network: 'bitcoin-mainnet',
25
+ ...overrides,
26
+ },
27
+ });
28
+
29
+ describe('btcDeriveContextHash', () => {
30
+ test('forwards the public API call through the existing dispatcher', async () => {
31
+ const call = jest
32
+ .fn()
33
+ .mockResolvedValue({ success: true, payload: { secret: '00'.repeat(32) } });
34
+ const api = createCoreApi(call);
35
+ const params = {
36
+ path: "m/84'/0'/0'/0/0",
37
+ appName: 'test-app',
38
+ context: 'deadbeef',
39
+ network: 'bitcoin-mainnet' as const,
40
+ };
41
+ await api.btcDeriveContextHash('connect-id', 'device-id', params);
42
+ expect(call).toHaveBeenCalledWith({
43
+ ...params,
44
+ connectId: 'connect-id',
45
+ deviceId: 'device-id',
46
+ method: 'btcDeriveContextHash',
47
+ });
48
+ });
49
+
50
+ test.each([
51
+ ['bitcoin-mainnet', 'BITCOIN_MAINNET'],
52
+ ['bitcoin-testnet', 'BITCOIN_TESTNET'],
53
+ ['bitcoin-signet', 'BITCOIN_SIGNET'],
54
+ ['bitcoin-regtest', 'BITCOIN_REGTEST'],
55
+ ])('maps %s and returns the device result', async (network, wireNetwork) => {
56
+ const method = createMethod({ network });
57
+ method.init();
58
+ const response = { secret: '00'.repeat(32) };
59
+ const typedCall = jest.fn().mockResolvedValue({ message: response });
60
+ method.device = { commands: { typedCall } } as unknown as Device;
61
+
62
+ await expect(method.run()).resolves.toBe(response);
63
+ expect(typedCall).toHaveBeenCalledWith(
64
+ 'BabylonDeriveContextHash',
65
+ 'BabylonDerivedContextHash',
66
+ {
67
+ address_n: [2147483732, 2147483648, 2147483648, 0, 0],
68
+ script_type: 'SPENDWITNESS',
69
+ app_name: '746573742d617070',
70
+ context: 'deadbeef',
71
+ network: wireNetwork,
72
+ }
73
+ );
74
+ });
75
+
76
+ test.each([
77
+ ["m/44'/0'/0'/0/0", 'SPENDADDRESS'],
78
+ ["m/49'/0'/0'/0/0", 'SPENDP2SHWITNESS'],
79
+ [[2147483734, 2147483648, 2147483648, 0, 0], 'SPENDTAPROOT'],
80
+ ])('uses the selected path script type for %s', (path, scriptType) => {
81
+ const method = createMethod({ path });
82
+ method.init();
83
+ expect(method.params.script_type).toBe(scriptType);
84
+ });
85
+
86
+ test('accepts the maximum app name and context sizes', () => {
87
+ const method = createMethod({ appName: 'a'.repeat(64), context: 'ab'.repeat(1024) });
88
+ expect(() => method.init()).not.toThrow();
89
+ expect(method.params.context).toHaveLength(2048);
90
+ });
91
+
92
+ test.each([
93
+ { appName: '' },
94
+ { appName: 'a'.repeat(65) },
95
+ { appName: 'Test-App' },
96
+ { appName: 'test_app' },
97
+ { appName: '你好' },
98
+ { appName: null },
99
+ { context: '' },
100
+ { context: 'abc' },
101
+ { context: 'DEADBEEF' },
102
+ { context: '0xdeadbeef' },
103
+ { context: 'gg' },
104
+ { context: 'ab'.repeat(1025) },
105
+ { context: null },
106
+ { network: '' },
107
+ { network: 'litecoin' },
108
+ { network: 'toString' },
109
+ { network: 1 },
110
+ { path: 'm' },
111
+ { path: "m/84x'/0'/0'/0/0" },
112
+ { path: "m/2147483648'/0'/0'/0/0" },
113
+ { path: [2147483732, 2147483648, 2147483648, 0, 0.5] },
114
+ { path: [2147483732, 2147483648, 2147483648, 0, 4294967296] },
115
+ ])('rejects invalid inputs before a device call: %j', overrides => {
116
+ expect(() => createMethod(overrides).init()).toThrow(
117
+ expect.objectContaining({ errorCode: HardwareErrorCode.CallMethodInvalidParameter })
118
+ );
119
+ });
120
+
121
+ test('rejects sparse numeric paths before a device call', () => {
122
+ const sparsePath = [2147483732, 2147483648, 2147483648, 0, 5];
123
+ delete sparsePath[3];
124
+
125
+ [Array<number>(3), sparsePath].forEach(path => {
126
+ expect(() => createMethod({ path }).init()).toThrow(
127
+ expect.objectContaining({ errorCode: HardwareErrorCode.CallMethodInvalidParameter })
128
+ );
129
+ });
130
+ });
131
+
132
+ test('registers the public method and restricts it to supported firmware and protocol', () => {
133
+ const method = findMethod({ id: 1, payload: { ...createMethod().payload } });
134
+ method.init();
135
+ expect(method).toBeInstanceOf(BTCDeriveContextHash);
136
+ expect(method.strictCheckDeviceSupport).toBe(true);
137
+ expect(() => method.assertProtocolSupported('V2', EFirmwareType.Universal)).toThrow(
138
+ expect.objectContaining({ errorCode: HardwareErrorCode.DeviceNotSupportMethod })
139
+ );
140
+ });
141
+
142
+ test.each([EDeviceType.Classic1s, EDeviceType.ClassicPure])(
143
+ 'resolves the firmware requirement for %s through the device support check',
144
+ deviceType => {
145
+ const method = createMethod();
146
+ const device = Object.create(Device.prototype) as Device;
147
+ device.getCurrentDeviceType = jest.fn(() => deviceType);
148
+ const range: DeviceFirmwareRange = method.getVersionRange();
149
+
150
+ expect(device.getCurrentMethodVersionRange(type => range[type])).toEqual({ min: '3.21.0' });
151
+ expect(() => method.assertProtocolSupported('V1', EFirmwareType.Universal)).not.toThrow();
152
+ }
153
+ );
154
+
155
+ test.each([EDeviceType.Classic, EDeviceType.Mini, EDeviceType.Touch, EDeviceType.Pro])(
156
+ 'keeps %s outside the supported firmware range',
157
+ deviceType => {
158
+ const method = createMethod();
159
+ const device = Object.create(Device.prototype) as Device;
160
+ device.getCurrentDeviceType = jest.fn(() => deviceType);
161
+ const range: DeviceFirmwareRange = method.getVersionRange();
162
+
163
+ expect(device.getCurrentMethodVersionRange(type => range[type])).toBeUndefined();
164
+ }
165
+ );
166
+
167
+ test('propagates device rejection without returning a secret', async () => {
168
+ const method = createMethod();
169
+ method.init();
170
+ const error = new Error('User rejected');
171
+ method.device = {
172
+ commands: { typedCall: jest.fn().mockRejectedValue(error) },
173
+ } as unknown as Device;
174
+ await expect(method.run()).rejects.toBe(error);
175
+ });
176
+
177
+ test.each([undefined, '00', '00'.repeat(33)])(
178
+ 'rejects malformed device responses',
179
+ async secret => {
180
+ const method = createMethod();
181
+ method.init();
182
+ method.device = {
183
+ commands: { typedCall: jest.fn().mockResolvedValue({ message: { secret } }) },
184
+ } as unknown as Device;
185
+ await expect(method.run()).rejects.toMatchObject({
186
+ errorCode: HardwareErrorCode.CallMethodError,
187
+ });
188
+ }
189
+ );
190
+
191
+ test('blocks API request and response payloads in logs', () => {
192
+ const request = { payload: { method: 'btcDeriveContextHash' } };
193
+ const label = getLogBlockLabel(request);
194
+ expect(label).toBe('btcDeriveContextHash');
195
+ expect(getSafeLogPayload({ payload: { secret: '00'.repeat(32) } }, label)).toEqual({
196
+ method: 'btcDeriveContextHash',
197
+ payload: '[REDACTED]',
198
+ });
199
+ });
200
+ });
@@ -1,4 +1,7 @@
1
- import Core, { initConnector } from '../src/core';
1
+ import { HardwareErrorCode, createDeferred } from '@onekeyfe/hd-shared';
2
+
3
+ import Core, { init, initConnector, initCore, switchTransport } from '../src/core';
4
+ import DataManager from '../src/data-manager/DataManager';
2
5
  import TransportManager from '../src/data-manager/TransportManager';
3
6
  import { DevicePool } from '../src/device/DevicePool';
4
7
 
@@ -31,4 +34,114 @@ describe('Core.dispose', () => {
31
34
  expect(transportStop).toHaveBeenCalledTimes(1);
32
35
  expect(disposeDevicePool).toHaveBeenCalledTimes(1);
33
36
  });
37
+
38
+ test('disposal rejects a queued switch without recreating a transport or connector', async () => {
39
+ const previous = TransportManager.transport;
40
+ const { settings } = DataManager;
41
+ const active = { stop: jest.fn().mockResolvedValue(undefined) };
42
+ const replacement = jest.fn(() => ({ stop: jest.fn().mockResolvedValue(undefined) }));
43
+ TransportManager.transport = active as never;
44
+ const core = initCore();
45
+ initConnector();
46
+ const switching = switchTransport({ env: 'emulator', Transport: replacement });
47
+ const rejected = expect(switching).rejects.toMatchObject({
48
+ errorCode: HardwareErrorCode.NotInitialized,
49
+ });
50
+ try {
51
+ await core.dispose();
52
+ await rejected;
53
+ expect(replacement).not.toHaveBeenCalled();
54
+ expect(TransportManager.transport).toBe(active);
55
+ expect(DataManager.settings).toBe(settings);
56
+ expect(DevicePool.emitter.eventNames()).toHaveLength(0);
57
+ } finally {
58
+ await Promise.allSettled([switching]);
59
+ await core.dispose();
60
+ TransportManager.transport = previous;
61
+ DataManager.settings = settings;
62
+ }
63
+ });
64
+
65
+ test.each(['dispose/reinit', 'init', 'initCore'] as const)(
66
+ '%s invalidates running and queued switches without blocking a new lifecycle',
67
+ async lifecycle => {
68
+ const previous = TransportManager.transport;
69
+ const { settings } = DataManager;
70
+ const entered = createDeferred<void>();
71
+ const closing = createDeferred<void>();
72
+ const loading = createDeferred<void>();
73
+ const active = {
74
+ stop: jest.fn(() => {
75
+ entered.resolve();
76
+ return closing.promise;
77
+ }),
78
+ };
79
+ const intermediate = jest.fn(() => ({ stop: jest.fn().mockResolvedValue(undefined) }));
80
+ const stale = jest.fn(() => ({ stop: jest.fn().mockResolvedValue(undefined) }));
81
+ const fresh = { stop: jest.fn().mockResolvedValue(undefined) };
82
+ const current = { stop: jest.fn().mockResolvedValue(undefined) };
83
+ const Fresh = jest.fn(() => fresh);
84
+ jest.spyOn(DataManager, 'load').mockImplementation(async next => {
85
+ await loading.promise;
86
+ DataManager.settings = next;
87
+ });
88
+ TransportManager.transport = active as never;
89
+ let core = initCore();
90
+ initConnector();
91
+ const first = switchTransport({ env: 'emulator', Transport: intermediate });
92
+ await entered.promise;
93
+ const second = switchTransport({ env: 'webusb', Transport: stale });
94
+ const rejected = Promise.all([
95
+ expect(first).rejects.toMatchObject({ errorCode: HardwareErrorCode.NotInitialized }),
96
+ expect(second).rejects.toMatchObject({ errorCode: HardwareErrorCode.NotInitialized }),
97
+ ]);
98
+ rejected.catch(() => undefined);
99
+ let initializing: ReturnType<typeof init> | undefined;
100
+ try {
101
+ if (lifecycle === 'dispose/reinit') {
102
+ const disposing = core.dispose();
103
+ closing.resolve();
104
+ await disposing;
105
+ }
106
+ if (lifecycle === 'initCore') {
107
+ TransportManager.transport = fresh as never;
108
+ DataManager.settings = { ...settings, env: 'desktop-web-ble' };
109
+ core = initCore();
110
+ initConnector();
111
+ } else {
112
+ initializing = init({ ...settings, env: 'desktop-web-ble' }, Fresh);
113
+ if (lifecycle === 'init') {
114
+ closing.resolve();
115
+ await rejected;
116
+ }
117
+ loading.resolve();
118
+ const initialized = await initializing;
119
+ if (!initialized) throw new Error('Core initialization failed');
120
+ core = initialized;
121
+ }
122
+ await switchTransport({
123
+ env: 'emulator',
124
+ Transport: function Current() {
125
+ return current;
126
+ },
127
+ });
128
+ closing.resolve();
129
+ await rejected;
130
+ expect(intermediate).not.toHaveBeenCalled();
131
+ expect(stale).not.toHaveBeenCalled();
132
+ expect(TransportManager.transport).toBe(current);
133
+ expect(DataManager.getSettings('env')).toBe('emulator');
134
+ expect(fresh.stop).toHaveBeenCalledTimes(1);
135
+ expect(current.stop).not.toHaveBeenCalled();
136
+ } finally {
137
+ closing.resolve();
138
+ loading.resolve();
139
+ await initializing;
140
+ await Promise.allSettled([first, second, rejected]);
141
+ await core.dispose();
142
+ TransportManager.transport = previous;
143
+ DataManager.settings = settings;
144
+ }
145
+ }
146
+ );
34
147
  });
@@ -3,7 +3,7 @@ import { ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared';
3
3
  import { initConnector, initCore } from '../src/core';
4
4
  import { DataManager } from '../src/data-manager';
5
5
  import TransportManager from '../src/data-manager/TransportManager';
6
- import { IFRAME } from '../src/events';
6
+ import { IFRAME, createErrorMessage } from '../src/events';
7
7
  import SearchDevices from '../src/api/SearchDevices';
8
8
  import { DeviceList } from '../src/device/DeviceList';
9
9
  import { Device } from '../src/device/Device';
@@ -28,6 +28,40 @@ describe('Core 错误输出边界', () => {
28
28
  jest.restoreAllMocks();
29
29
  });
30
30
 
31
+ test('entry-point errors preserve USB recovery context across JSON serialization', () => {
32
+ const params = {
33
+ operation: 'open',
34
+ nativeErrorName: 'SecurityError',
35
+ nativeErrorMessage: 'Access denied',
36
+ };
37
+ const error = ERRORS.TypedError(
38
+ HardwareErrorCode.BridgeNeedsPermission,
39
+ 'Access denied',
40
+ params
41
+ );
42
+ expect(JSON.parse(JSON.stringify(createErrorMessage(error)))).toEqual({
43
+ success: false,
44
+ payload: { code: HardwareErrorCode.BridgeNeedsPermission, error: 'Access denied', params },
45
+ });
46
+ });
47
+
48
+ test.each([
49
+ [HardwareErrorCode.TransportProtocolProbeFailed, { expectedProtocol: 'V1' }],
50
+ [
51
+ HardwareErrorCode.TransportProtocolMismatch,
52
+ { expectedProtocol: 'V2', detectedProtocol: 'V1' },
53
+ ],
54
+ ] as const)(
55
+ 'preserves protocol error %s and structured context across the public boundary',
56
+ (code, params) => {
57
+ const error = ERRORS.TypedError(code, undefined, params);
58
+ expect(JSON.parse(JSON.stringify(createErrorMessage(error)))).toEqual({
59
+ success: false,
60
+ payload: { code, error: error.message, params },
61
+ });
62
+ }
63
+ );
64
+
31
65
  test.each([
32
66
  ['webusb', false],
33
67
  ['desktop-webusb', false],
@@ -108,14 +142,32 @@ describe('Core 错误输出边界', () => {
108
142
  HardwareErrorCode.BleDeviceBondedCanceled,
109
143
  HardwareErrorCode.BleDeviceDisconnected,
110
144
  HardwareErrorCode.PollingTimeout,
145
+ HardwareErrorCode.BlePoweredOff,
146
+ HardwareErrorCode.BleUnsupported,
147
+ HardwareErrorCode.BridgeNeedsPermission,
148
+ HardwareErrorCode.WebUsbDeviceAccessError,
149
+ HardwareErrorCode.TransportProtocolProbeFailed,
150
+ HardwareErrorCode.TransportProtocolMismatch,
111
151
  ])(
112
- 'preserves terminal BLE error %s in the public response without polling again',
152
+ 'preserves terminal transport error %s in the public response without polling again',
113
153
  async errorCode => {
114
- jest.spyOn(DataManager, 'getSettings').mockReturnValue('react-native' as never);
154
+ jest
155
+ .spyOn(DataManager, 'getSettings')
156
+ .mockReturnValue(
157
+ [
158
+ HardwareErrorCode.BridgeNeedsPermission,
159
+ HardwareErrorCode.WebUsbDeviceAccessError,
160
+ HardwareErrorCode.TransportProtocolProbeFailed,
161
+ HardwareErrorCode.TransportProtocolMismatch,
162
+ ].includes(errorCode)
163
+ ? ('desktop-webusb' as never)
164
+ : ('react-native' as never)
165
+ );
115
166
  const error = ERRORS.TypedError(errorCode);
116
167
  const acquire = jest.fn().mockRejectedValue(error);
117
168
  jest.spyOn(TransportManager, 'getTransport').mockReturnValue({
118
169
  acquire,
170
+ enumerate: jest.fn().mockResolvedValue([{ path: 'ble-pairing-test' }]),
119
171
  release: jest.fn().mockResolvedValue(true),
120
172
  stop: jest.fn().mockResolvedValue(undefined),
121
173
  } as never);
@@ -75,8 +75,11 @@ describe('DeviceConnector protocol validation', () => {
75
75
  });
76
76
  const connector = new DeviceConnector();
77
77
 
78
- await expect(connector.acquire('classic-id', undefined, undefined, 'V2')).rejects.toThrow(
79
- 'Device protocol mismatch: expected V2, detected V1'
78
+ await expect(connector.acquire('classic-id', undefined, undefined, 'V2')).rejects.toMatchObject(
79
+ {
80
+ errorCode: HardwareErrorCode.TransportProtocolMismatch,
81
+ params: { expectedProtocol: 'V2', detectedProtocol: 'V1' },
82
+ }
80
83
  );
81
84
  });
82
85