@onekeyfe/hd-core 1.2.8-alpha.9 → 1.2.9

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 (154) hide show
  1. package/README.md +27 -0
  2. package/__tests__/AllNetworkGetAddressBase.tracing.test.ts +13 -4
  3. package/__tests__/DeviceCommands.test.ts +25 -0
  4. package/__tests__/allNetworkGetAddress-privacy-networks.test.ts +77 -0
  5. package/__tests__/allNetworkGetAddress-skippable.test.ts +49 -0
  6. package/__tests__/btc-derive-context-hash.test.ts +200 -0
  7. package/__tests__/core-dispose.test.ts +114 -1
  8. package/__tests__/core-error-output.test.ts +21 -0
  9. package/__tests__/device-connector-protocol.test.ts +5 -2
  10. package/__tests__/device-lifecycle-events.test.ts +127 -10
  11. package/__tests__/firmware-update/firmware-artifact-source.test.ts +11 -0
  12. package/__tests__/logBlockEvent.test.ts +331 -9
  13. package/__tests__/logger.test.ts +18 -0
  14. package/__tests__/monero-get-address.test.ts +147 -0
  15. package/__tests__/monero-key-image-sync.test.ts +272 -0
  16. package/__tests__/monero-sign-transaction.test.ts +436 -0
  17. package/__tests__/protocol-v2-key-export-confirmation.test.ts +75 -0
  18. package/__tests__/protocol-v2-resources.test.ts +14 -0
  19. package/__tests__/protocol-v2.test.ts +845 -3
  20. package/__tests__/protocolV2FileWrite.test.ts +454 -7
  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 +152 -0
  25. package/__tests__/zcash-sign-pczt.test.ts +525 -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/SearchDevices.d.ts +21 -1
  31. package/dist/api/SearchDevices.d.ts.map +1 -1
  32. package/dist/api/allnetwork/AllNetworkGetAddressBase.d.ts +3 -0
  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 +8 -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/DeviceUploadNft.d.ts.map +1 -1
  54. package/dist/api/protocol-v2/DeviceUploadWallpaper.d.ts.map +1 -1
  55. package/dist/api/zcash/ZcashGetAddress.d.ts +12 -0
  56. package/dist/api/zcash/ZcashGetAddress.d.ts.map +1 -0
  57. package/dist/api/zcash/ZcashGetFullViewingKey.d.ts +12 -0
  58. package/dist/api/zcash/ZcashGetFullViewingKey.d.ts.map +1 -0
  59. package/dist/api/zcash/ZcashSignPczt.d.ts +16 -0
  60. package/dist/api/zcash/ZcashSignPczt.d.ts.map +1 -0
  61. package/dist/api/zcash/helpers.d.ts +2 -0
  62. package/dist/api/zcash/helpers.d.ts.map +1 -0
  63. package/dist/core/index.d.ts +3 -1
  64. package/dist/core/index.d.ts.map +1 -1
  65. package/dist/device/Device.d.ts +1 -0
  66. package/dist/device/Device.d.ts.map +1 -1
  67. package/dist/device/DeviceCommands.d.ts +4 -4
  68. package/dist/device/DeviceCommands.d.ts.map +1 -1
  69. package/dist/events/logBlockEvent.d.ts +10 -0
  70. package/dist/events/logBlockEvent.d.ts.map +1 -1
  71. package/dist/events/ui-request.d.ts +2 -0
  72. package/dist/events/ui-request.d.ts.map +1 -1
  73. package/dist/index.d.ts +202 -7
  74. package/dist/index.d.ts.map +1 -1
  75. package/dist/index.js +5849 -4205
  76. package/dist/inject.d.ts.map +1 -1
  77. package/dist/protocols/protocol-v2/resources.d.ts +1 -0
  78. package/dist/protocols/protocol-v2/resources.d.ts.map +1 -1
  79. package/dist/protocols/protocol-v2/uiInteraction.d.ts +1 -0
  80. package/dist/protocols/protocol-v2/uiInteraction.d.ts.map +1 -1
  81. package/dist/types/api/allNetworkGetAddress.d.ts +4 -1
  82. package/dist/types/api/allNetworkGetAddress.d.ts.map +1 -1
  83. package/dist/types/api/btcDeriveContextHash.d.ts +10 -0
  84. package/dist/types/api/btcDeriveContextHash.d.ts.map +1 -0
  85. package/dist/types/api/export.d.ts +8 -0
  86. package/dist/types/api/export.d.ts.map +1 -1
  87. package/dist/types/api/index.d.ts +16 -0
  88. package/dist/types/api/index.d.ts.map +1 -1
  89. package/dist/types/api/moneroGetAddress.d.ts +15 -0
  90. package/dist/types/api/moneroGetAddress.d.ts.map +1 -0
  91. package/dist/types/api/moneroGetWatchKey.d.ts +15 -0
  92. package/dist/types/api/moneroGetWatchKey.d.ts.map +1 -0
  93. package/dist/types/api/moneroKeyImageSync.d.ts +24 -0
  94. package/dist/types/api/moneroKeyImageSync.d.ts.map +1 -0
  95. package/dist/types/api/moneroSignTransaction.d.ts +32 -0
  96. package/dist/types/api/moneroSignTransaction.d.ts.map +1 -0
  97. package/dist/types/api/zcashGetAddress.d.ts +20 -0
  98. package/dist/types/api/zcashGetAddress.d.ts.map +1 -0
  99. package/dist/types/api/zcashGetFullViewingKey.d.ts +13 -0
  100. package/dist/types/api/zcashGetFullViewingKey.d.ts.map +1 -0
  101. package/dist/types/api/zcashSignPczt.d.ts +9 -0
  102. package/dist/types/api/zcashSignPczt.d.ts.map +1 -0
  103. package/dist/types/device.d.ts +10 -0
  104. package/dist/types/device.d.ts.map +1 -1
  105. package/dist/utils/logger.d.ts.map +1 -1
  106. package/dist/utils/patch.d.ts +1 -1
  107. package/dist/utils/patch.d.ts.map +1 -1
  108. package/package.json +4 -4
  109. package/src/api/BaseMethod.ts +2 -0
  110. package/src/api/FileWrite.ts +7 -1
  111. package/src/api/FirmwareUpdateV4.ts +358 -74
  112. package/src/api/SearchDevices.ts +172 -7
  113. package/src/api/allnetwork/AllNetworkGetAddressBase.ts +55 -15
  114. package/src/api/btc/BTCDeriveContextHash.ts +90 -0
  115. package/src/api/firmware/FirmwareArtifactSource.ts +12 -3
  116. package/src/api/helpers/pathUtils.ts +23 -0
  117. package/src/api/helpers/protocolV2FileWrite.ts +382 -111
  118. package/src/api/index.ts +9 -0
  119. package/src/api/monero/MoneroGetAddress.ts +71 -0
  120. package/src/api/monero/MoneroGetWatchKey.ts +75 -0
  121. package/src/api/monero/MoneroKeyImageSync.ts +170 -0
  122. package/src/api/monero/MoneroSignTransaction.ts +319 -0
  123. package/src/api/monero/helpers.ts +28 -0
  124. package/src/api/protocol-v2/DeviceUploadNft.ts +6 -1
  125. package/src/api/protocol-v2/DeviceUploadWallpaper.ts +15 -1
  126. package/src/api/zcash/ZcashGetAddress.ts +108 -0
  127. package/src/api/zcash/ZcashGetFullViewingKey.ts +78 -0
  128. package/src/api/zcash/ZcashSignPczt.ts +152 -0
  129. package/src/api/zcash/helpers.ts +16 -0
  130. package/src/core/index.ts +49 -17
  131. package/src/data/messages/messages-protocol-v2.json +152 -0
  132. package/src/data/messages/messages.json +51 -0
  133. package/src/device/Device.ts +21 -3
  134. package/src/device/DeviceCommands.ts +17 -0
  135. package/src/device/DeviceConnector.ts +4 -4
  136. package/src/events/logBlockEvent.ts +58 -3
  137. package/src/events/ui-request.ts +2 -0
  138. package/src/index.ts +1 -0
  139. package/src/inject.ts +17 -0
  140. package/src/protocols/protocol-v2/resources.ts +6 -1
  141. package/src/protocols/protocol-v2/uiInteraction.ts +1 -1
  142. package/src/types/api/allNetworkGetAddress.ts +8 -1
  143. package/src/types/api/btcDeriveContextHash.ts +15 -0
  144. package/src/types/api/export.ts +17 -0
  145. package/src/types/api/index.ts +24 -0
  146. package/src/types/api/moneroGetAddress.ts +27 -0
  147. package/src/types/api/moneroGetWatchKey.ts +28 -0
  148. package/src/types/api/moneroKeyImageSync.ts +45 -0
  149. package/src/types/api/moneroSignTransaction.ts +54 -0
  150. package/src/types/api/zcashGetAddress.ts +32 -0
  151. package/src/types/api/zcashGetFullViewingKey.ts +24 -0
  152. package/src/types/api/zcashSignPczt.ts +18 -0
  153. package/src/types/device.ts +18 -0
  154. package/src/utils/logger.ts +4 -0
@@ -80,6 +80,102 @@ describe('public device lifecycle events', () => {
80
80
  jest.restoreAllMocks();
81
81
  });
82
82
 
83
+ test.each([
84
+ ['react-native', 'V2', EDeviceType.Pro2, true],
85
+ ['react-native', 'V2', EDeviceType.Neo, true],
86
+ ['desktop-web-ble', 'V2', EDeviceType.Pro2, true],
87
+ ['desktop-web-ble', 'V2', EDeviceType.Neo, true],
88
+ ['react-native', 'V1', EDeviceType.Pro, false],
89
+ ['desktop-web-ble', 'V1', EDeviceType.Touch, false],
90
+ ['react-native', 'V1', EDeviceType.Pro2, false],
91
+ ['react-native', 'V2', EDeviceType.Unknown, false],
92
+ ['desktop-webusb', 'V2', EDeviceType.Pro2, false],
93
+ ['lowlevel', 'V2', EDeviceType.Neo, false],
94
+ ] as const)(
95
+ 'releases the task and keeps only the intended BLE links: %s / %s / %s',
96
+ async (env, protocol, deviceType, keepConnection) => {
97
+ jest.spyOn(DataManager, 'getSettings').mockReturnValue(env as never);
98
+ const device = createInitializedDevice(protocol);
99
+ device.features = { ...device.features, deviceType } as never;
100
+ device.originalDescriptor.session = device.mainId;
101
+ (device as unknown as { deviceAcquired: boolean }).deviceAcquired = true;
102
+ const release = jest.fn().mockResolvedValue(true);
103
+ const disconnect = jest.fn().mockResolvedValue(undefined);
104
+ device.deviceConnector = { release, disconnect } as never;
105
+ const commands = {
106
+ disposed: false,
107
+ dispose: jest.fn(() => {
108
+ commands.disposed = true;
109
+ return Promise.resolve();
110
+ }),
111
+ cancel: jest.fn().mockResolvedValue(undefined),
112
+ };
113
+ device.commands = commands as never;
114
+
115
+ await device.release();
116
+
117
+ expect(release).toHaveBeenCalledWith(device.mainId, false, keepConnection);
118
+ expect(device.hasDeviceAcquire()).toBe(false);
119
+ expect(commands.disposed).toBe(true);
120
+ expect(device.keepSession).toBe(false);
121
+
122
+ await device.interruptionFromUser();
123
+ expect(disconnect).toHaveBeenCalledTimes(keepConnection ? 0 : 1);
124
+ }
125
+ );
126
+
127
+ test('still disconnects when setup is cancelled after a kept-alive release', async () => {
128
+ jest.spyOn(DataManager, 'getSettings').mockReturnValue('react-native' as never);
129
+ const device = createInitializedDevice('V2');
130
+ const pendingAcquire = createDeferred<{ uuid: string; protocolType: 'V2' }>();
131
+ const release = jest.fn().mockResolvedValue(true);
132
+ const disconnect = jest.fn().mockResolvedValue(undefined);
133
+ device.deviceConnector = {
134
+ release,
135
+ disconnect,
136
+ acquire: jest.fn(() => pendingAcquire.promise),
137
+ } as never;
138
+ device.commands = {
139
+ disposed: true,
140
+ dispose: jest.fn().mockResolvedValue(undefined),
141
+ cancel: jest.fn().mockResolvedValue(undefined),
142
+ } as never;
143
+ await device.release();
144
+ device.beginConnectionAttempt();
145
+ const acquiring = device.acquire('V2');
146
+
147
+ await device.interruptionFromUser();
148
+ expect(disconnect).toHaveBeenCalledWith(device.mainId);
149
+ pendingAcquire.resolve({ uuid: 'late-kept-alive-session', protocolType: 'V2' });
150
+ await expect(acquiring).rejects.toMatchObject({
151
+ errorCode: HardwareErrorCode.DeviceInterruptedFromUser,
152
+ });
153
+ expect(disconnect).toHaveBeenLastCalledWith('late-kept-alive-session');
154
+ });
155
+
156
+ test.each(['release failure', 'disconnect event'])(
157
+ 'does not treat %s as a kept-alive connection',
158
+ async reason => {
159
+ jest.spyOn(DataManager, 'getSettings').mockReturnValue('react-native' as never);
160
+ const device = createInitializedDevice('V2');
161
+ const release = jest.fn().mockResolvedValue(true);
162
+ if (reason === 'release failure') release.mockRejectedValueOnce(new Error('release failed'));
163
+ const disconnect = jest.fn().mockResolvedValue(undefined);
164
+ device.deviceConnector = { release, disconnect } as never;
165
+ device.commands = {
166
+ disposed: true,
167
+ dispose: jest.fn().mockResolvedValue(undefined),
168
+ cancel: jest.fn().mockResolvedValue(undefined),
169
+ } as never;
170
+ await device.release();
171
+ if (reason === 'disconnect event') device.markTransportDisconnected();
172
+
173
+ await device.interruptionFromUser();
174
+
175
+ expect(disconnect).toHaveBeenCalledWith(device.mainId);
176
+ }
177
+ );
178
+
83
179
  test.each(['V1', 'V2'] as const)(
84
180
  'preserves the %s acknowledged result and waits for release before the next acquire',
85
181
  async protocol => {
@@ -363,11 +459,11 @@ describe('public device lifecycle events', () => {
363
459
 
364
460
  test('clears the cleanup barrier when its deadline expires', async () => {
365
461
  const realSetTimeout = setTimeout;
366
- jest
367
- .spyOn(global, 'setTimeout')
368
- .mockImplementation((callback, delay, ...args) =>
369
- realSetTimeout(callback, delay === 5_000 ? 0 : delay, ...args)
370
- );
462
+ const cleanupDeadline = createDeferred<() => void>();
463
+ jest.spyOn(global, 'setTimeout').mockImplementation((callback, delay, ...args) => {
464
+ if (delay === 5_000) cleanupDeadline.resolve(() => callback(...args));
465
+ return realSetTimeout(callback, delay, ...args);
466
+ });
371
467
  {
372
468
  jest.spyOn(DataManager, 'getSettings').mockReturnValue('react-native' as never);
373
469
  const acquire = jest.spyOn(Device.prototype, 'acquire').mockResolvedValue(undefined);
@@ -381,13 +477,15 @@ describe('public device lifecycle events', () => {
381
477
  type: IFRAME.CALL,
382
478
  payload: { method: 'getDeviceState', connectId: 'draining-device', connectProtocol: 'V2' },
383
479
  } as CoreMessage);
384
- setImmediate(() => cancel(context, 'draining-device'));
480
+ const expireCleanupDeadline = await cleanupDeadline.promise;
481
+ cancel(context, 'draining-device');
482
+ expireCleanupDeadline();
385
483
  await expect(result).resolves.toBeDefined();
386
- gate.resolve();
387
484
  await new Promise(resolve => {
388
485
  setImmediate(resolve);
389
486
  });
390
487
  expect(context.getPrePendingCallPromise('draining-device')).toBeUndefined();
488
+ gate.resolve();
391
489
  }
392
490
  });
393
491
 
@@ -608,7 +706,9 @@ describe('public device lifecycle events', () => {
608
706
  );
609
707
 
610
708
  test.each([
611
- [HardwareErrorCode.RuntimeError, true],
709
+ [HardwareErrorCode.TransportProtocolProbeFailed, true],
710
+ [HardwareErrorCode.TransportProtocolMismatch, false],
711
+ [HardwareErrorCode.RuntimeError, false],
612
712
  [HardwareErrorCode.BleDeviceBondError, false],
613
713
  ] as const)(
614
714
  'retries a Protocol V2 probe mismatch with error code %s: %s',
@@ -616,8 +716,8 @@ describe('public device lifecycle events', () => {
616
716
  const method = { payload: { connectProtocol: 'V2' } } as never;
617
717
  const error = {
618
718
  errorCode,
619
- message:
620
- 'Device protocol mismatch: expected V2, but device did not respond to expected protocol',
719
+ message: 'diagnostic wording is not used for classification',
720
+ params: { expectedProtocol: 'V2' },
621
721
  };
622
722
 
623
723
  expect(isRetryableBleProtocolV2ProbeError(method, error)).toBe(expected);
@@ -627,6 +727,8 @@ describe('public device lifecycle events', () => {
627
727
  test.each([
628
728
  [HardwareErrorCode.BleConnectedError, true],
629
729
  [HardwareErrorCode.BleTimeoutError, true],
730
+ [HardwareErrorCode.TransportProtocolProbeFailed, true],
731
+ [HardwareErrorCode.TransportProtocolMismatch, false],
630
732
  [HardwareErrorCode.PollingTimeout, false],
631
733
  [HardwareErrorCode.BleDeviceBondError, false],
632
734
  ] as const)('retries a BLE connection error with error code %s: %s', (errorCode, expected) => {
@@ -639,6 +741,21 @@ describe('public device lifecycle events', () => {
639
741
  expect(isRetryableBleConnectionError(method, error)).toBe(expected);
640
742
  });
641
743
 
744
+ test('does not retry a failed strict V1 probe as a V2 probe or after user cancellation', () => {
745
+ const error = ERRORS.TypedError(HardwareErrorCode.TransportProtocolProbeFailed, undefined, {
746
+ expectedProtocol: 'V1',
747
+ });
748
+ const method = { payload: { connectProtocol: 'V2' } } as never;
749
+ expect(isRetryableBleProtocolV2ProbeError(method, error)).toBe(false);
750
+ expect(isRetryableBleConnectionError(method, error)).toBe(false);
751
+ expect(
752
+ isRetryableBleConnectionError(
753
+ { payload: {}, device: { wasInterruptedByUser: () => true } } as never,
754
+ ERRORS.TypedError(HardwareErrorCode.TransportProtocolProbeFailed)
755
+ )
756
+ ).toBe(false);
757
+ });
758
+
642
759
  test('does not retry the desktop acquire deadline fallback', () => {
643
760
  const method = { payload: { connectProtocol: 'V2' } } as never;
644
761
  const error = {
@@ -31,6 +31,10 @@ const createReader = (bytes = artifactBytes) => {
31
31
  };
32
32
 
33
33
  describe('FirmwareArtifactSource', () => {
34
+ afterEach(() => {
35
+ jest.restoreAllMocks();
36
+ });
37
+
34
38
  it('materializes verified bytes before exposing the source', async () => {
35
39
  const backingBytes = Uint8Array.from(artifactBytes);
36
40
  const { reader, read, close } = createReader(backingBytes);
@@ -86,6 +90,10 @@ describe('FirmwareArtifactSource', () => {
86
90
  sha256: bytesToHex(sha256(bytes)),
87
91
  };
88
92
  const { reader, read } = createReader(bytes);
93
+ const hasher = sha256.create();
94
+ const update = jest.spyOn(hasher, 'update');
95
+ const digest = jest.spyOn(hasher, 'digest');
96
+ jest.spyOn(sha256, 'create').mockReturnValue(hasher);
89
97
 
90
98
  const source = await openFirmwareByteSource({ artifact: largeArtifact, reader });
91
99
  if (!source) {
@@ -103,6 +111,9 @@ describe('FirmwareArtifactSource', () => {
103
111
  length: 3,
104
112
  },
105
113
  ]);
114
+ expect(update).toHaveBeenCalledTimes(2);
115
+ expect(update.mock.invocationCallOrder[0]).toBeLessThan(read.mock.invocationCallOrder[1]);
116
+ expect(digest.mock.invocationCallOrder[0]).toBeGreaterThan(update.mock.invocationCallOrder[1]);
106
117
  await source.close();
107
118
  });
108
119
 
@@ -4,7 +4,18 @@ import {
4
4
  formatLogMethodLabel,
5
5
  getLogBlockLabel,
6
6
  getSafeLogPayload,
7
+ getSafeLogTask,
7
8
  } from '../src/events';
9
+ import { getLog, initLog, setLoggerPostMessage } from '../src/utils/logger';
10
+ import MoneroGetAddress from '../src/api/monero/MoneroGetAddress';
11
+ import ZcashGetAddress from '../src/api/zcash/ZcashGetAddress';
12
+
13
+ import type { Device } from '../src/device/Device';
14
+
15
+ jest.mock('../src/data/config', () => ({
16
+ getSDKVersion: jest.fn(() => '1.0.0'),
17
+ DEFAULT_DOMAIN: 'https://jssdk.onekey.so/1.0.0/',
18
+ }));
8
19
 
9
20
  describe('getLogBlockLabel', () => {
10
21
  it('keeps existing sensitive UI response blocking', () => {
@@ -52,11 +63,17 @@ describe('getLogBlockLabel', () => {
52
63
  }
53
64
  );
54
65
 
55
- it('logs signing requests and responses as-is', () => {
66
+ it.each([false, true])('redacts btcSignMessage logs with debug=%s', enabled => {
67
+ const method = 'btcSignMessage';
68
+ const prefix = `sensitive-btc-sign-message-${enabled}`;
69
+ const logger = initLog(prefix, enabled);
70
+ const postMessage = jest.fn();
71
+ setLoggerPostMessage(postMessage);
72
+ const consoleLog = jest.spyOn(console, 'log').mockImplementation(() => undefined);
56
73
  const request = {
57
- method: 'btcSignMessage',
74
+ method,
58
75
  path: "m/44'/0'/0'/0/0",
59
- messageHex: '68656c6c6f',
76
+ messageHex: Buffer.from('synthetic-message-marker').toString('hex'),
60
77
  coin: 'Bitcoin',
61
78
  };
62
79
  const iframeRequest = {
@@ -66,15 +83,39 @@ describe('getLogBlockLabel', () => {
66
83
  const response = {
67
84
  success: true,
68
85
  payload: {
69
- signature: 'signature-bytes',
70
- address: 'bc1qexample',
86
+ signature: 'synthetic-signature-marker',
87
+ address: 'synthetic-address-marker',
71
88
  },
72
89
  };
90
+ const task = { id: 7, method: { name: method, payload: request } };
91
+ const originals = JSON.stringify([request, iframeRequest, response, task]);
92
+ const blockLabel = getLogBlockLabel(iframeRequest);
73
93
 
74
- expect(getLogBlockLabel(request)).toBeUndefined();
75
- expect(getLogBlockLabel(iframeRequest)).toBeUndefined();
76
- expect(getSafeLogPayload(request)).toBe(request);
77
- expect(getSafeLogPayload(response)).toBe(response);
94
+ try {
95
+ expect(getLogBlockLabel(request)).toBe(method);
96
+ expect(blockLabel).toBe(method);
97
+ logger.debug('request:', getSafeLogPayload(iframeRequest, blockLabel));
98
+ logger.debug('response:', getSafeLogPayload(response, blockLabel));
99
+ logger.debug('Cancel Api connect task: ', getSafeLogTask(task));
100
+
101
+ const cached = getLog().filter(entry => entry.prefix === prefix);
102
+ expect(cached.map(entry => entry.message[1])).toEqual([
103
+ { method, payload: '[REDACTED]' },
104
+ { method, payload: '[REDACTED]' },
105
+ { id: task.id, method, payload: '[REDACTED]' },
106
+ ]);
107
+ expect(postMessage).toHaveBeenCalledTimes(3);
108
+ expect(consoleLog).toHaveBeenCalledTimes(enabled ? 3 : 0);
109
+ for (const output of [cached, postMessage.mock.calls, consoleLog.mock.calls]) {
110
+ const serialized = JSON.stringify(output);
111
+ expect(serialized).not.toContain('synthetic-');
112
+ expect(serialized).not.toContain(request.messageHex);
113
+ }
114
+ expect(JSON.stringify([request, iframeRequest, response, task])).toBe(originals);
115
+ } finally {
116
+ consoleLog.mockRestore();
117
+ setLoggerPostMessage(jest.fn());
118
+ }
78
119
  });
79
120
 
80
121
  it('keeps ordinary API requests and responses visible', () => {
@@ -96,4 +137,285 @@ describe('getLogBlockLabel', () => {
96
137
  it('does not classify events without an API method as method calls', () => {
97
138
  expect(getLogBlockLabel({ event: 'DEVICE_EVENT', type: 'device-connect' })).toBeUndefined();
98
139
  });
140
+
141
+ it.each(['zcashGetAddress', 'zcashGetFullViewingKey', 'zcashSignPczt'])(
142
+ 'keeps %s wallet data out of logger caches and events with debug disabled',
143
+ method => {
144
+ const postMessage = jest.fn();
145
+ const prefix = `sensitive-zcash-${method}`;
146
+ const logger = initLog(prefix, false);
147
+ setLoggerPostMessage(postMessage);
148
+ const request = {
149
+ event: 'iframe-call',
150
+ payload: { method, pczt: 'synthetic-pczt-marker', path: 'synthetic-path-marker' },
151
+ };
152
+ const response = {
153
+ success: true,
154
+ payload: [
155
+ {
156
+ ufvk: 'synthetic-viewing-key-marker',
157
+ seedFingerprint: 'synthetic-fingerprint-marker',
158
+ pczt: 'synthetic-signed-pczt-marker',
159
+ },
160
+ ],
161
+ };
162
+ const blockLabel = getLogBlockLabel(request);
163
+
164
+ expect(blockLabel).toBe(method);
165
+ expect(getLogBlockLabel(request.payload)).toBe(method);
166
+ logger.debug('request:', getSafeLogPayload(request, blockLabel));
167
+ logger.debug('response:', getSafeLogPayload(response, blockLabel));
168
+
169
+ const cached = getLog().filter(entry => entry.prefix === prefix);
170
+ expect(cached.map(entry => entry.message[1])).toEqual([
171
+ { method, payload: '[REDACTED]' },
172
+ { method, payload: '[REDACTED]' },
173
+ ]);
174
+ expect(postMessage).toHaveBeenCalledTimes(2);
175
+ for (const output of [JSON.stringify(cached), JSON.stringify(postMessage.mock.calls)]) {
176
+ expect(output).not.toContain('synthetic-');
177
+ expect(output).toContain('[REDACTED]');
178
+ }
179
+ expect(response.payload[0].ufvk).toBe('synthetic-viewing-key-marker');
180
+ }
181
+ );
182
+
183
+ it.each(['moneroGetAddress', 'moneroGetWatchKey'])(
184
+ 'keeps %s addresses and view keys out of logger caches',
185
+ method => {
186
+ const prefix = `sensitive-monero-${method}`;
187
+ const logger = initLog(prefix, false);
188
+ setLoggerPostMessage(jest.fn());
189
+ const request = { event: 'iframe-call', payload: { method, path: "m/44'/128'/0'" } };
190
+ const response = {
191
+ success: true,
192
+ payload: { address: 'synthetic-address-marker', watchKey: 'synthetic-view-key-marker' },
193
+ };
194
+ const blockLabel = getLogBlockLabel(request);
195
+
196
+ expect(blockLabel).toBe(method);
197
+ logger.debug('response:', getSafeLogPayload(response, blockLabel));
198
+
199
+ const cached = getLog().filter(entry => entry.prefix === prefix);
200
+ expect(cached.map(entry => entry.message[1])).toEqual([{ method, payload: '[REDACTED]' }]);
201
+ expect(JSON.stringify(cached)).not.toContain('synthetic-');
202
+ }
203
+ );
204
+
205
+ it('keeps moneroKeyImageSync transfers and key images out of logger caches', () => {
206
+ const method = 'moneroKeyImageSync';
207
+ const prefix = 'sensitive-monero-key-image-sync';
208
+ const logger = initLog(prefix, false);
209
+ setLoggerPostMessage(jest.fn());
210
+ const request = {
211
+ event: 'iframe-call',
212
+ payload: {
213
+ method,
214
+ path: "m/44'/128'/0'",
215
+ transfers: [{ outKey: 'synthetic-out-key-marker', txPubKey: 'synthetic-tx-key-marker' }],
216
+ },
217
+ };
218
+ const response = {
219
+ success: true,
220
+ payload: {
221
+ encKey: 'synthetic-enc-key-marker',
222
+ keyImages: [{ iv: 'synthetic-iv-marker', blob: 'synthetic-key-image-marker' }],
223
+ },
224
+ };
225
+ const blockLabel = getLogBlockLabel(request);
226
+
227
+ expect(blockLabel).toBe(method);
228
+ logger.debug('request:', getSafeLogPayload(request, blockLabel));
229
+ logger.debug('response:', getSafeLogPayload(response, blockLabel));
230
+
231
+ const cached = getLog().filter(entry => entry.prefix === prefix);
232
+ expect(cached.map(entry => entry.message[1])).toEqual([
233
+ { method, payload: '[REDACTED]' },
234
+ { method, payload: '[REDACTED]' },
235
+ ]);
236
+ expect(JSON.stringify(cached)).not.toContain('synthetic-');
237
+ });
238
+
239
+ // The request reveals the real ring member, masks and amounts; the result
240
+ // carries the key that opens the signatures.
241
+ it('keeps moneroSignTransaction inputs and the opening key out of logger caches', () => {
242
+ const method = 'moneroSignTransaction';
243
+ const prefix = 'sensitive-monero-sign-transaction';
244
+ const logger = initLog(prefix, false);
245
+ setLoggerPostMessage(jest.fn());
246
+ const payload = {
247
+ method,
248
+ path: "m/44'/128'/0'",
249
+ tsxData: { fee: 'synthetic-fee-marker', outputs: [{ amount: 'synthetic-amount-marker' }] },
250
+ inputs: [{ real_output: 'synthetic-real-index-marker', mask: 'synthetic-mask-marker' }],
251
+ };
252
+ const response = {
253
+ success: true,
254
+ payload: {
255
+ signatures: ['synthetic-sealed-signature-marker'],
256
+ openingKey: 'synthetic-opening-key-marker',
257
+ },
258
+ };
259
+ const blockLabel = getLogBlockLabel({ event: 'iframe-call', payload });
260
+
261
+ expect(blockLabel).toBe(method);
262
+ logger.debug('request:', getSafeLogPayload(payload, blockLabel));
263
+ logger.debug('response:', getSafeLogPayload(response, blockLabel));
264
+ logger.debug('Cancel Api connect task: ', getSafeLogTask({ id: 9, method: { payload } }));
265
+
266
+ const cached = getLog().filter(entry => entry.prefix === prefix);
267
+ expect(cached.map(entry => entry.message[1])).toEqual([
268
+ { method, payload: '[REDACTED]' },
269
+ { method, payload: '[REDACTED]' },
270
+ { id: 9, method, payload: '[REDACTED]' },
271
+ ]);
272
+ expect(JSON.stringify(cached)).not.toContain('synthetic-');
273
+ });
274
+
275
+ // Cancel logs the queued task, which holds the method's raw payload.
276
+ it('redacts a queued Zcash task the same way, keeping its id', () => {
277
+ const postMessage = jest.fn();
278
+ const prefix = 'sensitive-zcash-cancel';
279
+ const logger = initLog(prefix, false);
280
+ setLoggerPostMessage(postMessage);
281
+ const task = {
282
+ id: 7,
283
+ method: {
284
+ name: 'zcashSignPczt',
285
+ payload: { method: 'zcashSignPczt', pczt: 'synthetic-pczt-marker' },
286
+ pczt: 'synthetic-pczt-marker',
287
+ },
288
+ };
289
+
290
+ logger.debug('Cancel Api connect task: ', getSafeLogTask(task));
291
+
292
+ const cached = getLog().filter(entry => entry.prefix === prefix);
293
+ expect(cached.map(entry => entry.message[1])).toEqual([
294
+ { id: 7, method: 'zcashSignPczt', payload: '[REDACTED]' },
295
+ ]);
296
+ expect(JSON.stringify([cached, postMessage.mock.calls])).not.toContain('synthetic-');
297
+ });
298
+
299
+ it('keeps Zcash and Monero results of an all-network call out of logs, others intact', () => {
300
+ const response = {
301
+ success: true,
302
+ payload: [
303
+ {
304
+ network: 'evm',
305
+ path: "m/44'/60'/0'/0/0",
306
+ success: true,
307
+ payload: { address: '0xabc', rootFingerprint: 1 },
308
+ },
309
+ {
310
+ network: 'zcash',
311
+ path: "m/32'/133'/0'",
312
+ success: true,
313
+ payload: {
314
+ address: 'u1-marker',
315
+ ufvk: 'synthetic-viewing-key-marker',
316
+ rootFingerprint: 1,
317
+ },
318
+ },
319
+ {
320
+ network: 'xmr',
321
+ path: "m/44'/128'/0'",
322
+ success: true,
323
+ payload: { address: 'synthetic-xmr-address-marker', rootFingerprint: 1 },
324
+ },
325
+ {
326
+ network: 'zcash',
327
+ path: "m/32'/133'/1'",
328
+ success: false,
329
+ payload: { error: 'Device not support', code: 416 },
330
+ },
331
+ ],
332
+ };
333
+
334
+ const safe = getSafeLogPayload(response) as typeof response;
335
+
336
+ expect(safe.payload[0]).toBe(response.payload[0]);
337
+ expect(safe.payload[1]).toEqual({ ...response.payload[1], payload: '[REDACTED]' });
338
+ expect(safe.payload[2]).toEqual({ ...response.payload[2], payload: '[REDACTED]' });
339
+ expect(safe.payload[3]).toBe(response.payload[3]);
340
+ expect(JSON.stringify(safe)).not.toContain('marker');
341
+ expect(response.payload[1].payload).toHaveProperty('ufvk');
342
+ });
343
+
344
+ it('redacts a privacy network result delivered one by one as a callback', () => {
345
+ const message = {
346
+ event: 'IFRAME_CALLBACK',
347
+ payload: {
348
+ callbackId: 'item',
349
+ data: {
350
+ network: 'zcash',
351
+ success: true,
352
+ payload: { ufvk: 'synthetic-viewing-key-marker' },
353
+ index: 0,
354
+ },
355
+ },
356
+ };
357
+
358
+ expect(JSON.stringify(getSafeLogPayload(message))).not.toContain('marker');
359
+ });
360
+
361
+ it.each([
362
+ {
363
+ method: 'moneroGetAddress',
364
+ create: () =>
365
+ new MoneroGetAddress({
366
+ id: 1,
367
+ payload: { method: 'moneroGetAddress', path: "m/44'/128'/0'" },
368
+ }),
369
+ reply: { address: Buffer.from('synthetic-xmr-address-marker').toString('hex') },
370
+ },
371
+ {
372
+ method: 'zcashGetAddress',
373
+ create: () =>
374
+ new ZcashGetAddress({
375
+ id: 1,
376
+ payload: { method: 'zcashGetAddress', path: "m/32'/133'/0'" },
377
+ }),
378
+ reply: { address: 'synthetic-zcash-address-marker' },
379
+ },
380
+ ])('keeps the $method previous-address event out of logs', async ({ method, create, reply }) => {
381
+ const instance = create();
382
+ instance.init();
383
+ const commands = { typedCall: jest.fn().mockResolvedValue({ message: reply }) };
384
+ instance.device = {
385
+ commands,
386
+ getCommands: () => commands,
387
+ toMessageObject: () => ({}),
388
+ } as unknown as Device;
389
+ const postMessage = jest.fn();
390
+ instance.postMessage = postMessage;
391
+ await instance.run();
392
+
393
+ const [[event]] = postMessage.mock.calls;
394
+ expect(event).toMatchObject({
395
+ type: UI_REQUEST.PREVIOUS_ADDRESS_RESULT,
396
+ payload: { method, data: { address: expect.stringContaining('synthetic-') } },
397
+ });
398
+ const blockLabel = getLogBlockLabel(event);
399
+ expect(blockLabel).toBe(method);
400
+ expect(JSON.stringify(getSafeLogPayload(event, blockLabel))).not.toContain('synthetic-');
401
+ });
402
+
403
+ it('keeps previous-address events of other methods visible', () => {
404
+ const event = {
405
+ event: 'UI_EVENT',
406
+ type: UI_REQUEST.PREVIOUS_ADDRESS_RESULT,
407
+ payload: { device: {}, method: 'btcGetAddress', data: { address: 'bc1qexample' } },
408
+ };
409
+ expect(getLogBlockLabel(event)).toBeUndefined();
410
+ expect(getSafeLogPayload(event)).toBe(event);
411
+ });
412
+
413
+ it('logs other queued tasks as before', () => {
414
+ const task = {
415
+ id: 8,
416
+ method: { name: 'getDeviceState', payload: { method: 'getDeviceState' } },
417
+ };
418
+ expect(getSafeLogTask(task)).toBe(task);
419
+ expect(getSafeLogTask(undefined)).toBeUndefined();
420
+ });
99
421
  });
@@ -0,0 +1,18 @@
1
+ import { LoggerNames, enableLog, getLogger } from '../src/utils/logger';
2
+
3
+ describe('logger', () => {
4
+ afterEach(() => {
5
+ enableLog(false);
6
+ jest.restoreAllMocks();
7
+ });
8
+
9
+ test('enables method logs that share a prefix with common-connect-sdk', () => {
10
+ const consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => undefined);
11
+ enableLog(true);
12
+
13
+ expect(getLogger(LoggerNames.Method)).toBe(getLogger(LoggerNames.HdCommonConnectSdk));
14
+ getLogger(LoggerNames.Method).debug('shared prefix message');
15
+
16
+ expect(consoleSpy).toHaveBeenCalledWith(LoggerNames.Method, 'shared prefix message');
17
+ });
18
+ });