@onekeyfe/hd-core 1.2.8-alpha.10 → 1.2.8-alpha.101
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/__tests__/DeviceCommands.test.ts +66 -13
- package/__tests__/core-dispose.test.ts +114 -1
- package/__tests__/core-error-output.test.ts +55 -3
- package/__tests__/device-connector-protocol.test.ts +5 -2
- package/__tests__/device-lifecycle-events.test.ts +44 -20
- package/__tests__/logBlockEvent.test.ts +43 -0
- package/__tests__/protocol-v2-bootloader-mode.test.ts +43 -2
- package/__tests__/protocol-v2.test.ts +0 -47
- package/__tests__/refresh-device-state.test.ts +19 -17
- package/__tests__/usb-identity-probe.test.ts +358 -0
- package/__tests__/zcash-protobuf.test.ts +141 -0
- package/__tests__/zcash-sign-pczt.test.ts +387 -0
- package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
- package/dist/api/GetDeviceState.d.ts.map +1 -1
- package/dist/api/SearchDevices.d.ts +21 -1
- package/dist/api/SearchDevices.d.ts.map +1 -1
- package/dist/api/index.d.ts +2 -0
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/protocol-v2/DeviceReboot.d.ts.map +1 -1
- package/dist/api/zcash/ZcashGetAddress.d.ts +12 -0
- package/dist/api/zcash/ZcashGetAddress.d.ts.map +1 -0
- package/dist/api/zcash/ZcashSignPczt.d.ts +16 -0
- package/dist/api/zcash/ZcashSignPczt.d.ts.map +1 -0
- package/dist/core/index.d.ts +3 -2
- package/dist/core/index.d.ts.map +1 -1
- package/dist/device/DeviceCommands.d.ts +4 -4
- package/dist/device/DeviceCommands.d.ts.map +1 -1
- package/dist/events/logBlockEvent.d.ts.map +1 -1
- package/dist/index.d.ts +71 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3369 -2887
- package/dist/inject.d.ts.map +1 -1
- package/dist/types/api/export.d.ts +2 -0
- package/dist/types/api/export.d.ts.map +1 -1
- package/dist/types/api/index.d.ts +4 -0
- package/dist/types/api/index.d.ts.map +1 -1
- package/dist/types/api/zcashGetAddress.d.ts +22 -0
- package/dist/types/api/zcashGetAddress.d.ts.map +1 -0
- package/dist/types/api/zcashSignPczt.d.ts +9 -0
- package/dist/types/api/zcashSignPczt.d.ts.map +1 -0
- package/dist/types/device.d.ts +9 -0
- package/dist/types/device.d.ts.map +1 -1
- package/dist/types/params.d.ts +1 -0
- package/dist/types/params.d.ts.map +1 -1
- package/dist/utils/patch.d.ts +1 -1
- package/dist/utils/patch.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/api/FirmwareUpdateV4.ts +12 -32
- package/src/api/GetDeviceState.ts +2 -5
- package/src/api/SearchDevices.ts +172 -7
- package/src/api/index.ts +2 -0
- package/src/api/protocol-v2/DeviceReboot.ts +2 -0
- package/src/api/zcash/ZcashGetAddress.ts +121 -0
- package/src/api/zcash/ZcashSignPczt.ts +155 -0
- package/src/core/index.ts +47 -20
- package/src/data/messages/messages-protocol-v2.json +138 -1
- package/src/device/Device.ts +1 -1
- package/src/device/DeviceCommands.ts +15 -1
- package/src/device/DeviceConnector.ts +4 -4
- package/src/events/core.ts +2 -2
- package/src/events/logBlockEvent.ts +4 -2
- package/src/index.ts +1 -0
- package/src/inject.ts +4 -0
- package/src/types/api/export.ts +2 -0
- package/src/types/api/index.ts +8 -0
- package/src/types/api/zcashGetAddress.ts +36 -0
- package/src/types/api/zcashSignPczt.ts +18 -0
- package/src/types/device.ts +12 -0
- package/src/types/params.ts +1 -1
|
@@ -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);
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import
|
|
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
|
|
152
|
+
'preserves terminal transport error %s in the public response without polling again',
|
|
113
153
|
async errorCode => {
|
|
114
|
-
jest
|
|
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.
|
|
79
|
-
|
|
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
|
|
|
@@ -10,7 +10,6 @@ import {
|
|
|
10
10
|
isMissingDetectedProtocolV2Error,
|
|
11
11
|
isRetryableBleConnectionError,
|
|
12
12
|
isRetryableBleProtocolV2ProbeError,
|
|
13
|
-
isTerminalBleStaleBondError,
|
|
14
13
|
resolveBleConnectProtocol,
|
|
15
14
|
} from '../src/core';
|
|
16
15
|
import { DataManager } from '../src/data-manager';
|
|
@@ -611,17 +610,18 @@ describe('public device lifecycle events', () => {
|
|
|
611
610
|
);
|
|
612
611
|
|
|
613
612
|
test.each([
|
|
614
|
-
[HardwareErrorCode.
|
|
613
|
+
[HardwareErrorCode.TransportProtocolProbeFailed, true],
|
|
614
|
+
[HardwareErrorCode.TransportProtocolMismatch, false],
|
|
615
|
+
[HardwareErrorCode.RuntimeError, false],
|
|
615
616
|
[HardwareErrorCode.BleDeviceBondError, false],
|
|
616
|
-
[HardwareErrorCode.BlePeerRemovedPairingInformation, false],
|
|
617
617
|
] as const)(
|
|
618
618
|
'retries a Protocol V2 probe mismatch with error code %s: %s',
|
|
619
619
|
(errorCode, expected) => {
|
|
620
620
|
const method = { payload: { connectProtocol: 'V2' } } as never;
|
|
621
621
|
const error = {
|
|
622
622
|
errorCode,
|
|
623
|
-
message:
|
|
624
|
-
|
|
623
|
+
message: 'diagnostic wording is not used for classification',
|
|
624
|
+
params: { expectedProtocol: 'V2' },
|
|
625
625
|
};
|
|
626
626
|
|
|
627
627
|
expect(isRetryableBleProtocolV2ProbeError(method, error)).toBe(expected);
|
|
@@ -631,10 +631,10 @@ describe('public device lifecycle events', () => {
|
|
|
631
631
|
test.each([
|
|
632
632
|
[HardwareErrorCode.BleConnectedError, true],
|
|
633
633
|
[HardwareErrorCode.BleTimeoutError, true],
|
|
634
|
+
[HardwareErrorCode.TransportProtocolProbeFailed, true],
|
|
635
|
+
[HardwareErrorCode.TransportProtocolMismatch, false],
|
|
634
636
|
[HardwareErrorCode.PollingTimeout, false],
|
|
635
637
|
[HardwareErrorCode.BleDeviceBondError, false],
|
|
636
|
-
[HardwareErrorCode.BlePeerRemovedPairingInformation, false],
|
|
637
|
-
[HardwareErrorCode.BleBondInvalid, false],
|
|
638
638
|
] as const)('retries a BLE connection error with error code %s: %s', (errorCode, expected) => {
|
|
639
639
|
const method = { payload: { connectProtocol: 'V2' } } as never;
|
|
640
640
|
const error = {
|
|
@@ -645,6 +645,21 @@ describe('public device lifecycle events', () => {
|
|
|
645
645
|
expect(isRetryableBleConnectionError(method, error)).toBe(expected);
|
|
646
646
|
});
|
|
647
647
|
|
|
648
|
+
test('does not retry a failed strict V1 probe as a V2 probe or after user cancellation', () => {
|
|
649
|
+
const error = ERRORS.TypedError(HardwareErrorCode.TransportProtocolProbeFailed, undefined, {
|
|
650
|
+
expectedProtocol: 'V1',
|
|
651
|
+
});
|
|
652
|
+
const method = { payload: { connectProtocol: 'V2' } } as never;
|
|
653
|
+
expect(isRetryableBleProtocolV2ProbeError(method, error)).toBe(false);
|
|
654
|
+
expect(isRetryableBleConnectionError(method, error)).toBe(false);
|
|
655
|
+
expect(
|
|
656
|
+
isRetryableBleConnectionError(
|
|
657
|
+
{ payload: {}, device: { wasInterruptedByUser: () => true } } as never,
|
|
658
|
+
ERRORS.TypedError(HardwareErrorCode.TransportProtocolProbeFailed)
|
|
659
|
+
)
|
|
660
|
+
).toBe(false);
|
|
661
|
+
});
|
|
662
|
+
|
|
648
663
|
test('does not retry the desktop acquire deadline fallback', () => {
|
|
649
664
|
const method = { payload: { connectProtocol: 'V2' } } as never;
|
|
650
665
|
const error = {
|
|
@@ -687,19 +702,6 @@ describe('public device lifecycle events', () => {
|
|
|
687
702
|
}
|
|
688
703
|
);
|
|
689
704
|
|
|
690
|
-
test.each([
|
|
691
|
-
[HardwareErrorCode.BleDeviceBondError, true],
|
|
692
|
-
[HardwareErrorCode.BlePeerRemovedPairingInformation, true],
|
|
693
|
-
[HardwareErrorCode.BleBondInvalid, true],
|
|
694
|
-
[HardwareErrorCode.DeviceNotFound, false],
|
|
695
|
-
] as const)('treats BLE stale-bond error code %s as terminal: %s', (errorCode, expected) => {
|
|
696
|
-
const error = {
|
|
697
|
-
errorCode,
|
|
698
|
-
};
|
|
699
|
-
|
|
700
|
-
expect(isTerminalBleStaleBondError(error)).toBe(expected);
|
|
701
|
-
});
|
|
702
|
-
|
|
703
705
|
test.each([
|
|
704
706
|
[HardwareErrorCode.DeviceCheckDeviceIdError, true],
|
|
705
707
|
[HardwareErrorCode.DeviceNotFound, false],
|
|
@@ -1058,6 +1060,28 @@ describe('public device lifecycle events', () => {
|
|
|
1058
1060
|
expect(device.wasInterruptedByUser()).toBe(false);
|
|
1059
1061
|
});
|
|
1060
1062
|
|
|
1063
|
+
test.each(['V1', 'V2'] as const)(
|
|
1064
|
+
'retires a failed USB %s session before the next user attempt',
|
|
1065
|
+
async protocol => {
|
|
1066
|
+
jest.spyOn(DataManager, 'getSettings').mockReturnValue('desktop-webusb' as never);
|
|
1067
|
+
const device = createInitializedDevice(protocol);
|
|
1068
|
+
device.originalDescriptor = { ...device.originalDescriptor, session: device.mainId };
|
|
1069
|
+
const disconnect = jest.fn().mockResolvedValue(undefined);
|
|
1070
|
+
const release = jest.fn().mockResolvedValue(undefined);
|
|
1071
|
+
device.deviceConnector = { disconnect, release } as never;
|
|
1072
|
+
device.commands = { disposed: false, dispose: jest.fn() } as never;
|
|
1073
|
+
(device as unknown as { deviceAcquired: boolean }).deviceAcquired = true;
|
|
1074
|
+
const error = ERRORS.TypedError(HardwareErrorCode.WebUsbDeviceAccessError);
|
|
1075
|
+
await expect(device.run(() => Promise.reject(error))).rejects.toBe(error);
|
|
1076
|
+
await new Promise(resolve => {
|
|
1077
|
+
setImmediate(resolve);
|
|
1078
|
+
});
|
|
1079
|
+
expect(disconnect).toHaveBeenCalledWith(device.mainId);
|
|
1080
|
+
expect(release).toHaveBeenCalledTimes(1);
|
|
1081
|
+
expect(device.hasDeviceAcquire()).toBe(false);
|
|
1082
|
+
}
|
|
1083
|
+
);
|
|
1084
|
+
|
|
1061
1085
|
test('waits for the canceled run to finish releasing before cancellation completes', async () => {
|
|
1062
1086
|
jest.spyOn(DataManager, 'getSettings').mockReturnValue('react-native' as never);
|
|
1063
1087
|
const device = createInitializedDevice('V2');
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
getLogBlockLabel,
|
|
6
6
|
getSafeLogPayload,
|
|
7
7
|
} from '../src/events';
|
|
8
|
+
import { getLog, initLog, setLoggerPostMessage } from '../src/utils/logger';
|
|
8
9
|
|
|
9
10
|
describe('getLogBlockLabel', () => {
|
|
10
11
|
it('keeps existing sensitive UI response blocking', () => {
|
|
@@ -96,4 +97,46 @@ describe('getLogBlockLabel', () => {
|
|
|
96
97
|
it('does not classify events without an API method as method calls', () => {
|
|
97
98
|
expect(getLogBlockLabel({ event: 'DEVICE_EVENT', type: 'device-connect' })).toBeUndefined();
|
|
98
99
|
});
|
|
100
|
+
|
|
101
|
+
it.each(['zcashGetAddress', 'zcashSignPczt'])(
|
|
102
|
+
'keeps %s wallet data out of logger caches and events with debug disabled',
|
|
103
|
+
method => {
|
|
104
|
+
const postMessage = jest.fn();
|
|
105
|
+
const prefix = `sensitive-zcash-${method}`;
|
|
106
|
+
const logger = initLog(prefix, false);
|
|
107
|
+
setLoggerPostMessage(postMessage);
|
|
108
|
+
const request = {
|
|
109
|
+
event: 'iframe-call',
|
|
110
|
+
payload: { method, pczt: 'synthetic-pczt-marker', includeUfvk: true },
|
|
111
|
+
};
|
|
112
|
+
const response = {
|
|
113
|
+
success: true,
|
|
114
|
+
payload: [
|
|
115
|
+
{
|
|
116
|
+
ufvk: 'synthetic-viewing-key-marker',
|
|
117
|
+
seedFingerprint: 'synthetic-fingerprint-marker',
|
|
118
|
+
pczt: 'synthetic-signed-pczt-marker',
|
|
119
|
+
},
|
|
120
|
+
],
|
|
121
|
+
};
|
|
122
|
+
const blockLabel = getLogBlockLabel(request);
|
|
123
|
+
|
|
124
|
+
expect(blockLabel).toBe(method);
|
|
125
|
+
expect(getLogBlockLabel(request.payload)).toBe(method);
|
|
126
|
+
logger.debug('request:', getSafeLogPayload(request, blockLabel));
|
|
127
|
+
logger.debug('response:', getSafeLogPayload(response, blockLabel));
|
|
128
|
+
|
|
129
|
+
const cached = getLog().filter(entry => entry.prefix === prefix);
|
|
130
|
+
expect(cached.map(entry => entry.message[1])).toEqual([
|
|
131
|
+
{ method, payload: '[REDACTED]' },
|
|
132
|
+
{ method, payload: '[REDACTED]' },
|
|
133
|
+
]);
|
|
134
|
+
expect(postMessage).toHaveBeenCalledTimes(2);
|
|
135
|
+
for (const output of [JSON.stringify(cached), JSON.stringify(postMessage.mock.calls)]) {
|
|
136
|
+
expect(output).not.toContain('synthetic-');
|
|
137
|
+
expect(output).toContain('[REDACTED]');
|
|
138
|
+
}
|
|
139
|
+
expect(response.payload[0].ufvk).toBe('synthetic-viewing-key-marker');
|
|
140
|
+
}
|
|
141
|
+
);
|
|
99
142
|
});
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { EDeviceType } from '@onekeyfe/hd-shared';
|
|
2
|
+
|
|
3
|
+
import DeviceReboot from '../src/api/protocol-v2/DeviceReboot';
|
|
1
4
|
import { Device } from '../src/device/Device';
|
|
2
5
|
import { UI_REQUEST } from '../src/events/ui-request';
|
|
3
6
|
|
|
@@ -23,12 +26,50 @@ const createBootloaderDevice = (protocolType: 'V1' | 'V2') => {
|
|
|
23
26
|
};
|
|
24
27
|
|
|
25
28
|
describe('Pro2 bootloader mode', () => {
|
|
26
|
-
test('
|
|
29
|
+
test('allows deviceReboot to return a Protocol V2 bootloader to normal mode', () => {
|
|
30
|
+
const device = createBootloaderDevice('V2');
|
|
31
|
+
const method = new DeviceReboot({
|
|
32
|
+
id: 1,
|
|
33
|
+
payload: { method: 'deviceReboot', rebootType: 'normal' },
|
|
34
|
+
});
|
|
35
|
+
method.init();
|
|
36
|
+
|
|
37
|
+
expect(device.hasUnexpectedMode(method.allowDeviceMode, method.requireDeviceMode)).toBeNull();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('blocks Protocol V2 methods that are not allowed in bootloader mode', () => {
|
|
41
|
+
const device = createBootloaderDevice('V2');
|
|
42
|
+
|
|
43
|
+
expect(device.hasUnexpectedMode([], [])).toBe(UI_REQUEST.BOOTLOADER);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('allows Protocol V2 methods that explicitly support bootloader mode', () => {
|
|
27
47
|
const device = createBootloaderDevice('V2');
|
|
28
48
|
|
|
29
|
-
expect(device.hasUnexpectedMode([], [])).toBeNull();
|
|
49
|
+
expect(device.hasUnexpectedMode([UI_REQUEST.BOOTLOADER], [])).toBeNull();
|
|
30
50
|
});
|
|
31
51
|
|
|
52
|
+
test.each([EDeviceType.Pro2, EDeviceType.Neo])(
|
|
53
|
+
'blocks unsupported Protocol V2 methods in %s romloader mode',
|
|
54
|
+
deviceType => {
|
|
55
|
+
const device = createBootloaderDevice('V2');
|
|
56
|
+
device.updateState(
|
|
57
|
+
{
|
|
58
|
+
protocol: 'V2',
|
|
59
|
+
identity: { deviceType },
|
|
60
|
+
status: { mode: 'romloader', initialized: null },
|
|
61
|
+
},
|
|
62
|
+
'initialize'
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
expect(device.hasUnexpectedMode([], [])).toBe(UI_REQUEST.BOOTLOADER);
|
|
66
|
+
expect(device.hasUnexpectedMode([UI_REQUEST.NOT_INITIALIZE], [])).toBe(UI_REQUEST.BOOTLOADER);
|
|
67
|
+
expect(
|
|
68
|
+
device.hasUnexpectedMode([UI_REQUEST.BOOTLOADER, UI_REQUEST.NOT_INITIALIZE], [])
|
|
69
|
+
).toBeNull();
|
|
70
|
+
}
|
|
71
|
+
);
|
|
72
|
+
|
|
32
73
|
test('keeps the Protocol V1 bootloader restriction', () => {
|
|
33
74
|
const device = createBootloaderDevice('V1');
|
|
34
75
|
|
|
@@ -4991,23 +4991,6 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
4991
4991
|
expect(typedCall.mock.calls.map(call => call[0])).toEqual(['DeviceInfoGet']);
|
|
4992
4992
|
});
|
|
4993
4993
|
|
|
4994
|
-
test('stops V4 bootloader reconnect polling on a stale BLE bond', async () => {
|
|
4995
|
-
const method = new FirmwareUpdateV4({
|
|
4996
|
-
id: 1,
|
|
4997
|
-
payload: {
|
|
4998
|
-
method: 'firmwareUpdateV4',
|
|
4999
|
-
},
|
|
5000
|
-
});
|
|
5001
|
-
const staleBondError = ERRORS.TypedError(HardwareErrorCode.BlePeerRemovedPairingInformation);
|
|
5002
|
-
const reconnectProtocolV2Device = jest.fn().mockRejectedValue(staleBondError);
|
|
5003
|
-
(method as any).reconnectProtocolV2Device = reconnectProtocolV2Device;
|
|
5004
|
-
|
|
5005
|
-
await expect((method as any).waitForProtocolV2BootloaderMode(60_000, 0)).rejects.toBe(
|
|
5006
|
-
staleBondError
|
|
5007
|
-
);
|
|
5008
|
-
expect(reconnectProtocolV2Device).toHaveBeenCalledTimes(1);
|
|
5009
|
-
});
|
|
5010
|
-
|
|
5011
4994
|
test('polls again when Protocol V2 bootloader serial is temporarily unavailable', async () => {
|
|
5012
4995
|
const method = new FirmwareUpdateV4({
|
|
5013
4996
|
id: 1,
|
|
@@ -9009,36 +8992,6 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
9009
8992
|
);
|
|
9010
8993
|
});
|
|
9011
8994
|
|
|
9012
|
-
test('does not retry or wrap a stale BLE bond during a V4 file transfer', async () => {
|
|
9013
|
-
const method = new FirmwareUpdateV4({
|
|
9014
|
-
id: 1,
|
|
9015
|
-
payload: {
|
|
9016
|
-
method: 'firmwareUpdateV4',
|
|
9017
|
-
},
|
|
9018
|
-
});
|
|
9019
|
-
const staleBondError = ERRORS.TypedError(HardwareErrorCode.BlePeerRemovedPairingInformation);
|
|
9020
|
-
(method as any).fileWriteChunk = jest.fn().mockRejectedValue(staleBondError);
|
|
9021
|
-
const recoverProtocolV2FileTransfer = jest.fn();
|
|
9022
|
-
(method as any).recoverProtocolV2FileTransfer = recoverProtocolV2FileTransfer;
|
|
9023
|
-
const source = await openFirmwareByteSource({
|
|
9024
|
-
binary: new Uint8Array([1]).buffer,
|
|
9025
|
-
});
|
|
9026
|
-
|
|
9027
|
-
try {
|
|
9028
|
-
await expect(
|
|
9029
|
-
(method as any).protocolV2SourceUpdateProcess({
|
|
9030
|
-
source,
|
|
9031
|
-
filePath: 'vol1:firmware.bin',
|
|
9032
|
-
processedSize: 0,
|
|
9033
|
-
totalSize: 1,
|
|
9034
|
-
})
|
|
9035
|
-
).rejects.toBe(staleBondError);
|
|
9036
|
-
} finally {
|
|
9037
|
-
await source?.close();
|
|
9038
|
-
}
|
|
9039
|
-
expect(recoverProtocolV2FileTransfer).not.toHaveBeenCalled();
|
|
9040
|
-
});
|
|
9041
|
-
|
|
9042
8995
|
test('does not recover or wrap a cancelled V4 file transfer', async () => {
|
|
9043
8996
|
const method = new FirmwareUpdateV4({
|
|
9044
8997
|
id: 1,
|
|
@@ -73,24 +73,26 @@ describe('live device state reads', () => {
|
|
|
73
73
|
}
|
|
74
74
|
);
|
|
75
75
|
|
|
76
|
-
test('
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
76
|
+
test.each(['bootloader', 'romloader'] as const)(
|
|
77
|
+
'reports %s mode for settings scope after minimal mode detection',
|
|
78
|
+
async mode => {
|
|
79
|
+
const getDeviceState = jest.fn().mockResolvedValue({ status: { mode } });
|
|
80
|
+
const method = new GetDeviceState({
|
|
81
|
+
id: 1,
|
|
82
|
+
payload: { method: 'getDeviceState', scope: 'settings' },
|
|
83
|
+
});
|
|
84
|
+
method.init();
|
|
85
|
+
(method as any).device = {
|
|
86
|
+
isProtocolV2: () => true,
|
|
87
|
+
getDeviceState,
|
|
88
|
+
};
|
|
88
89
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
90
|
+
await expect(method.run()).rejects.toMatchObject({
|
|
91
|
+
errorCode: HardwareErrorCode.NotAllowInBootloaderMode,
|
|
92
|
+
});
|
|
93
|
+
expect(getDeviceState).toHaveBeenCalledWith({ refreshSections: ['status', 'settings'] });
|
|
94
|
+
}
|
|
95
|
+
);
|
|
94
96
|
|
|
95
97
|
test('rejects an unknown scope', () => {
|
|
96
98
|
const method = new GetDeviceState({
|