@onekeyfe/hd-core 1.2.0-alpha.101 → 1.2.0-alpha.102
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__/bridgeBinaryPayload.test.ts +173 -0
- package/__tests__/check-all-firmware-release-protocol-v2.test.ts +2 -0
- package/__tests__/device-pool-state.test.ts +29 -0
- package/__tests__/get-device-state.test.ts +102 -13
- package/__tests__/protocol-v2-legacy-recovery.test.ts +29 -0
- package/__tests__/protocol-v2.test.ts +234 -1
- package/__tests__/refresh-device-state.test.ts +4 -1
- package/__tests__/search-devices.test.ts +2 -0
- package/__tests__/ton-sign-message.test.ts +84 -0
- package/dist/api/FirmwareUpdateV4.d.ts +3 -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.map +1 -1
- package/dist/api/firmware/protocolV2Release.d.ts.map +1 -1
- package/dist/api/ton/TonSignMessage.d.ts.map +1 -1
- package/dist/api/utils.d.ts.map +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/protocolV2LegacyRecovery.d.ts +5 -0
- package/dist/core/protocolV2LegacyRecovery.d.ts.map +1 -0
- package/dist/device/Device.d.ts +9 -2
- package/dist/device/Device.d.ts.map +1 -1
- package/dist/device/DevicePool.d.ts +1 -1
- package/dist/device/DevicePool.d.ts.map +1 -1
- package/dist/index.d.ts +12 -2
- package/dist/index.js +243 -60
- package/dist/protocols/protocol-v2/features.d.ts +8 -0
- package/dist/protocols/protocol-v2/features.d.ts.map +1 -1
- package/dist/protocols/protocol-v2/index.d.ts +2 -2
- package/dist/protocols/protocol-v2/index.d.ts.map +1 -1
- package/dist/topLevelInject.d.ts.map +1 -1
- package/dist/types/api/getDeviceState.d.ts +1 -0
- package/dist/types/api/getDeviceState.d.ts.map +1 -1
- package/dist/utils/bridgeBinaryPayload.d.ts +3 -0
- package/dist/utils/bridgeBinaryPayload.d.ts.map +1 -0
- package/package.json +4 -4
- package/src/api/FirmwareUpdateV4.ts +70 -23
- package/src/api/GetDeviceState.ts +1 -0
- package/src/api/SearchDevices.ts +2 -0
- package/src/api/firmware/protocolV2Release.ts +1 -0
- package/src/api/ton/TonSignMessage.ts +5 -3
- package/src/api/utils.ts +5 -2
- package/src/core/index.ts +4 -0
- package/src/core/protocolV2LegacyRecovery.ts +12 -0
- package/src/device/Device.ts +78 -20
- package/src/device/DevicePool.ts +17 -5
- package/src/protocols/protocol-v2/features.ts +10 -0
- package/src/protocols/protocol-v2/index.ts +2 -0
- package/src/topLevelInject.ts +4 -2
- package/src/types/api/getDeviceState.ts +2 -0
- package/src/utils/bridgeBinaryPayload.ts +137 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { HardwareTopLevelSdk } from '../src';
|
|
2
|
+
import { findMethod } from '../src/api/utils';
|
|
3
|
+
import {
|
|
4
|
+
decodeBridgeBinaryPayload,
|
|
5
|
+
encodeBridgeBinaryPayload,
|
|
6
|
+
} from '../src/utils/bridgeBinaryPayload';
|
|
7
|
+
|
|
8
|
+
import type { LowLevelCoreApi } from '../src';
|
|
9
|
+
|
|
10
|
+
jest.mock('../src/data/config', () => ({
|
|
11
|
+
DEFAULT_DOMAIN: 'https://example.com/',
|
|
12
|
+
getSDKVersion: () => '0.0.0-test',
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
const crossJsonOnlyBridge = (value: unknown): unknown => JSON.parse(JSON.stringify(value));
|
|
16
|
+
|
|
17
|
+
describe('bridge binary payload', () => {
|
|
18
|
+
test('preserves nested ArrayBuffer and sliced Uint8Array values', async () => {
|
|
19
|
+
const arrayBuffer = new Uint8Array([1, 2, 3]).buffer;
|
|
20
|
+
const backing = new Uint8Array([90, 4, 5, 6, 91]);
|
|
21
|
+
const encoded = await encodeBridgeBinaryPayload({
|
|
22
|
+
arrayBuffer,
|
|
23
|
+
bytes: backing.subarray(1, 4),
|
|
24
|
+
});
|
|
25
|
+
const decoded = decodeBridgeBinaryPayload(crossJsonOnlyBridge(encoded)) as {
|
|
26
|
+
arrayBuffer: ArrayBuffer;
|
|
27
|
+
bytes: Uint8Array;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
expect(decoded.arrayBuffer).toBeInstanceOf(ArrayBuffer);
|
|
31
|
+
expect(Array.from(new Uint8Array(decoded.arrayBuffer))).toEqual([1, 2, 3]);
|
|
32
|
+
expect(decoded.bytes).toBeInstanceOf(Uint8Array);
|
|
33
|
+
expect(Array.from(decoded.bytes)).toEqual([4, 5, 6]);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('encodes Blob input as byte data', async () => {
|
|
37
|
+
const encoded = await encodeBridgeBinaryPayload(new Blob([new Uint8Array([7, 8])]));
|
|
38
|
+
const decoded = decodeBridgeBinaryPayload(crossJsonOnlyBridge(encoded));
|
|
39
|
+
|
|
40
|
+
expect(decoded).toBeInstanceOf(Uint8Array);
|
|
41
|
+
expect(Array.from(decoded as Uint8Array)).toEqual([7, 8]);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('routes portfolio, wallpaper and NFT bytes through the top-level boundary', async () => {
|
|
45
|
+
const restoredPayloads: Array<Record<string, any>> = [];
|
|
46
|
+
const lowLevelApi = {
|
|
47
|
+
call: jest.fn(params => {
|
|
48
|
+
const wirePayload = crossJsonOnlyBridge(params);
|
|
49
|
+
const method = findMethod({ id: 1, payload: wirePayload } as any);
|
|
50
|
+
restoredPayloads.push(method.payload);
|
|
51
|
+
return Promise.resolve({ success: true, payload: {} });
|
|
52
|
+
}),
|
|
53
|
+
init: jest.fn(() => Promise.resolve(true)),
|
|
54
|
+
} as unknown as LowLevelCoreApi;
|
|
55
|
+
const sdk = HardwareTopLevelSdk();
|
|
56
|
+
await sdk.init({}, lowLevelApi);
|
|
57
|
+
|
|
58
|
+
const portfolioBytes = new Uint8Array([1, 2, 3]).buffer;
|
|
59
|
+
const wallpaperBacking = new Uint8Array([90, 4, 5, 6, 91]);
|
|
60
|
+
const nftBacking = new Uint8Array([80, 7, 8, 9, 10, 81]);
|
|
61
|
+
await sdk.uploadPortfolio('connect-id', { packageBytes: portfolioBytes });
|
|
62
|
+
await sdk.deviceUploadWallpaper('connect-id', {
|
|
63
|
+
width: 604,
|
|
64
|
+
height: 1024,
|
|
65
|
+
rgba: wallpaperBacking.subarray(1, 4),
|
|
66
|
+
});
|
|
67
|
+
await sdk.deviceUploadNft('connect-id', {
|
|
68
|
+
image: { width: 540, height: 540, rgba: nftBacking.subarray(1, 3) },
|
|
69
|
+
thumbnail: { width: 263, height: 263, rgba: nftBacking.subarray(3, 5) },
|
|
70
|
+
title: 'NFT',
|
|
71
|
+
subtitle: '',
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
expect(Array.from(new Uint8Array(restoredPayloads[0].packageBytes))).toEqual([1, 2, 3]);
|
|
75
|
+
expect(Array.from(restoredPayloads[1].rgba)).toEqual([4, 5, 6]);
|
|
76
|
+
expect(Array.from(restoredPayloads[2].image.rgba)).toEqual([7, 8]);
|
|
77
|
+
expect(Array.from(restoredPayloads[2].thumbnail.rgba)).toEqual([9, 10]);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('routes firmware update binaries through the top-level boundary', async () => {
|
|
81
|
+
const restoredPayloads: Array<Record<string, any>> = [];
|
|
82
|
+
const lowLevelApi = {
|
|
83
|
+
call: jest.fn(params => {
|
|
84
|
+
const wirePayload = crossJsonOnlyBridge(params);
|
|
85
|
+
const method = findMethod({ id: 1, payload: wirePayload } as any);
|
|
86
|
+
restoredPayloads.push(method.payload);
|
|
87
|
+
return Promise.resolve({ success: true, payload: {} });
|
|
88
|
+
}),
|
|
89
|
+
init: jest.fn(() => Promise.resolve(true)),
|
|
90
|
+
} as unknown as LowLevelCoreApi;
|
|
91
|
+
const sdk = HardwareTopLevelSdk();
|
|
92
|
+
await sdk.init({}, lowLevelApi);
|
|
93
|
+
const binary = (...bytes: number[]) => Uint8Array.from(bytes).buffer;
|
|
94
|
+
|
|
95
|
+
await sdk.firmwareUpdate('connect-id', {
|
|
96
|
+
binary: binary(1),
|
|
97
|
+
updateType: 'firmware',
|
|
98
|
+
});
|
|
99
|
+
await sdk.firmwareUpdateV2('connect-id', {
|
|
100
|
+
binary: binary(2),
|
|
101
|
+
updateType: 'firmware',
|
|
102
|
+
platform: 'ext',
|
|
103
|
+
});
|
|
104
|
+
await sdk.firmwareUpdateV3('connect-id', {
|
|
105
|
+
platform: 'ext',
|
|
106
|
+
bleBinary: binary(3),
|
|
107
|
+
firmwareBinary: binary(4),
|
|
108
|
+
bootloaderBinary: binary(5),
|
|
109
|
+
resourceBinary: binary(6),
|
|
110
|
+
});
|
|
111
|
+
await sdk.firmwareUpdateV4('connect-id', {
|
|
112
|
+
platform: 'ext',
|
|
113
|
+
targetsToUpdate: [
|
|
114
|
+
'boot',
|
|
115
|
+
'app_v1',
|
|
116
|
+
'app_v2',
|
|
117
|
+
'coprocessor',
|
|
118
|
+
'se01',
|
|
119
|
+
'se02',
|
|
120
|
+
'se03',
|
|
121
|
+
'se04',
|
|
122
|
+
'resource',
|
|
123
|
+
],
|
|
124
|
+
romloaderBinary: binary(7),
|
|
125
|
+
bootloaderBinary: binary(8),
|
|
126
|
+
applicationP1Binary: binary(9),
|
|
127
|
+
applicationP2Binary: binary(10),
|
|
128
|
+
coprocessorBinary: binary(11),
|
|
129
|
+
se01Binary: binary(12),
|
|
130
|
+
se02Binary: binary(13),
|
|
131
|
+
se03Binary: binary(14),
|
|
132
|
+
se04Binary: binary(15),
|
|
133
|
+
resourceArchiveBinary: binary(16),
|
|
134
|
+
});
|
|
135
|
+
await sdk.deviceUpdateBootloader('connect-id', { binary: binary(17) });
|
|
136
|
+
await sdk.deviceFullyUploadResource('connect-id', { binary: binary(18) });
|
|
137
|
+
|
|
138
|
+
expect(restoredPayloads).toHaveLength(6);
|
|
139
|
+
const restoredBinaries = [
|
|
140
|
+
restoredPayloads[0].binary,
|
|
141
|
+
restoredPayloads[1].binary,
|
|
142
|
+
restoredPayloads[2].bleBinary,
|
|
143
|
+
restoredPayloads[2].firmwareBinary,
|
|
144
|
+
restoredPayloads[2].bootloaderBinary,
|
|
145
|
+
restoredPayloads[2].resourceBinary,
|
|
146
|
+
restoredPayloads[3].romloaderBinary,
|
|
147
|
+
restoredPayloads[3].bootloaderBinary,
|
|
148
|
+
restoredPayloads[3].applicationP1Binary,
|
|
149
|
+
restoredPayloads[3].applicationP2Binary,
|
|
150
|
+
restoredPayloads[3].coprocessorBinary,
|
|
151
|
+
restoredPayloads[3].se01Binary,
|
|
152
|
+
restoredPayloads[3].se02Binary,
|
|
153
|
+
restoredPayloads[3].se03Binary,
|
|
154
|
+
restoredPayloads[3].se04Binary,
|
|
155
|
+
restoredPayloads[3].resourceArchiveBinary,
|
|
156
|
+
restoredPayloads[4].binary,
|
|
157
|
+
restoredPayloads[5].binary,
|
|
158
|
+
];
|
|
159
|
+
expect(restoredBinaries.map(value => Array.from(new Uint8Array(value)))).toEqual(
|
|
160
|
+
Array.from({ length: 18 }, (_, index) => [index + 1])
|
|
161
|
+
);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test('rejects malformed tagged binary data', () => {
|
|
165
|
+
expect(() =>
|
|
166
|
+
decodeBridgeBinaryPayload({
|
|
167
|
+
__onekey_hd_bridge_binary_payload__: 1,
|
|
168
|
+
data: 'not-base64',
|
|
169
|
+
type: 'uint8-array',
|
|
170
|
+
})
|
|
171
|
+
).toThrow('Invalid bridge binary payload data');
|
|
172
|
+
});
|
|
173
|
+
});
|
|
@@ -346,6 +346,7 @@ describe('checkAllFirmwareRelease Protocol V2 support', () => {
|
|
|
346
346
|
expect(typedCall).not.toHaveBeenCalled();
|
|
347
347
|
expect(getDeviceState).toHaveBeenCalledWith({
|
|
348
348
|
refreshSections: ['identity', 'versions'],
|
|
349
|
+
allowLegacyProtocolV2ProtocolInfo: true,
|
|
349
350
|
});
|
|
350
351
|
expect(method.getSupportedProtocols()).toEqual(['V1', 'V2']);
|
|
351
352
|
});
|
|
@@ -725,6 +726,7 @@ describe('checkAllFirmwareRelease Protocol V2 support', () => {
|
|
|
725
726
|
});
|
|
726
727
|
expect(getDeviceState).toHaveBeenCalledWith({
|
|
727
728
|
refreshSections: ['identity', 'versions', 'verification'],
|
|
729
|
+
allowLegacyProtocolV2ProtocolInfo: true,
|
|
728
730
|
});
|
|
729
731
|
});
|
|
730
732
|
|
|
@@ -38,6 +38,35 @@ describe('DevicePool state lifecycle', () => {
|
|
|
38
38
|
expect(getDeviceState).toHaveBeenNthCalledWith(2, { refreshSections: ['settings'] });
|
|
39
39
|
});
|
|
40
40
|
|
|
41
|
+
test('keeps legacy ProtocolInfo compatibility scoped to a discovery refresh', async () => {
|
|
42
|
+
const getDeviceState = jest.fn().mockResolvedValue({ status: { mode: 'bootloader' } });
|
|
43
|
+
const run = jest.fn(async (callback: () => Promise<void>) => callback());
|
|
44
|
+
const device = {
|
|
45
|
+
isProtocolV2: () => true,
|
|
46
|
+
getDeviceState,
|
|
47
|
+
run,
|
|
48
|
+
} as any;
|
|
49
|
+
|
|
50
|
+
await (DevicePool as any)._refreshRuntimeState(device, {
|
|
51
|
+
refreshRuntimeState: true,
|
|
52
|
+
allowLegacyProtocolV2ProtocolInfo: true,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
expect(run).toHaveBeenCalledWith(expect.any(Function), {
|
|
56
|
+
connectProtocol: undefined,
|
|
57
|
+
forceProtocolDetection: undefined,
|
|
58
|
+
allowLegacyProtocolV2ProtocolInfo: true,
|
|
59
|
+
});
|
|
60
|
+
expect(getDeviceState).toHaveBeenNthCalledWith(1, {
|
|
61
|
+
refreshSections: ['status'],
|
|
62
|
+
allowLegacyProtocolV2ProtocolInfo: true,
|
|
63
|
+
});
|
|
64
|
+
expect(getDeviceState).toHaveBeenNthCalledWith(2, {
|
|
65
|
+
refreshSections: ['settings'],
|
|
66
|
+
allowLegacyProtocolV2ProtocolInfo: true,
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
41
70
|
test('actively re-detects a cached device when protocol detection is forced', async () => {
|
|
42
71
|
const descriptor = { path: 'cached-path', protocolType: 'V1' } as any;
|
|
43
72
|
const device = Device.fromDescriptor(descriptor);
|
|
@@ -202,25 +202,111 @@ describe('getDeviceState', () => {
|
|
|
202
202
|
expect(state.versions.se01).toBe('1.0.0');
|
|
203
203
|
});
|
|
204
204
|
|
|
205
|
-
test.each([
|
|
206
|
-
'
|
|
207
|
-
|
|
205
|
+
test.each([
|
|
206
|
+
['bootloader', EDeviceType.Pro2, DeviceType.PRO2],
|
|
207
|
+
['romloader', EDeviceType.Pro2, DeviceType.PRO2],
|
|
208
|
+
['bootloader', EDeviceType.Neo, DeviceType.NEO],
|
|
209
|
+
['romloader', EDeviceType.Neo, DeviceType.NEO],
|
|
210
|
+
] as const)(
|
|
211
|
+
'refreshes cached %s state after %s reboots into the application',
|
|
212
|
+
async (mode, deviceType, protocolV2DeviceType) => {
|
|
208
213
|
const typedCall = jest.fn().mockImplementation((requestType: string) => {
|
|
209
|
-
if (requestType === '
|
|
214
|
+
if (requestType === 'ProtocolInfoRequest') {
|
|
215
|
+
return { message: protocolV2ApplicationInfo };
|
|
216
|
+
}
|
|
217
|
+
if (requestType === 'DeviceStatusGet') {
|
|
210
218
|
return {
|
|
211
|
-
message: {
|
|
212
|
-
|
|
219
|
+
message: { init_states: true, unlocked: true, device_id: 'wallet-1' },
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
throw new Error(`Unexpected request: ${requestType}`);
|
|
223
|
+
});
|
|
224
|
+
const device = createV2Device(typedCall);
|
|
225
|
+
device.updateState(
|
|
226
|
+
{
|
|
227
|
+
protocol: 'V2',
|
|
228
|
+
identity: { deviceType },
|
|
229
|
+
status: { mode },
|
|
230
|
+
raw: {
|
|
231
|
+
protocolV2DeviceInfo: {
|
|
232
|
+
hw: { Device_type: protocolV2DeviceType, serial_no: 'SERIAL-1' },
|
|
213
233
|
fw:
|
|
214
234
|
mode === 'romloader'
|
|
215
235
|
? { romloader: { version: '1.0.0' } }
|
|
216
236
|
: { bootloader: { version: '1.0.0' } },
|
|
217
237
|
},
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
238
|
+
protocolV2ProtocolInfo: getProtocolV2LoaderInfo(mode),
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
'initialize'
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
const state = await device.getDeviceState({ refreshSections: ['status'] });
|
|
245
|
+
|
|
246
|
+
expect(state.status.mode).toBe('normal');
|
|
247
|
+
expect(state.identity.deviceId).toBe('wallet-1');
|
|
248
|
+
expect(typedCall.mock.calls.map(call => call[0])).toEqual([
|
|
249
|
+
'ProtocolInfoRequest',
|
|
250
|
+
'DeviceStatusGet',
|
|
251
|
+
]);
|
|
252
|
+
}
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
test.each([
|
|
256
|
+
[EDeviceType.Pro2, DeviceType.PRO2],
|
|
257
|
+
[EDeviceType.Neo, DeviceType.NEO],
|
|
258
|
+
] as const)('invalidates cached loader state when %s is cancelled', async (deviceType, type) => {
|
|
259
|
+
const typedCall = jest.fn().mockImplementation((requestType: string) => {
|
|
260
|
+
if (requestType === 'DeviceInfoGet') {
|
|
261
|
+
return {
|
|
262
|
+
message: {
|
|
263
|
+
protocol_version: 2,
|
|
264
|
+
hw: { Device_type: type, serial_no: 'SERIAL-1' },
|
|
265
|
+
fw: { application: { version: '5.0.0' } },
|
|
266
|
+
},
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
if (requestType === 'ProtocolInfoRequest') {
|
|
270
|
+
return { message: protocolV2ApplicationInfo };
|
|
271
|
+
}
|
|
272
|
+
if (requestType === 'DeviceStatusGet') {
|
|
273
|
+
return {
|
|
274
|
+
message: { init_states: true, unlocked: true, device_id: 'wallet-1' },
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
throw new Error(`Unexpected request: ${requestType}`);
|
|
278
|
+
});
|
|
279
|
+
const cancel = jest.fn().mockResolvedValue(undefined);
|
|
280
|
+
const device = createV2Device(typedCall);
|
|
281
|
+
(device as any).commands = { cancel, typedCall };
|
|
282
|
+
device.updateState(
|
|
283
|
+
{
|
|
284
|
+
protocol: 'V2',
|
|
285
|
+
identity: { deviceType },
|
|
286
|
+
status: { mode: 'bootloader' },
|
|
287
|
+
raw: { protocolV2ProtocolInfo: getProtocolV2LoaderInfo('bootloader') },
|
|
288
|
+
},
|
|
289
|
+
'initialize'
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
await device.interruptionFromUser();
|
|
293
|
+
await device.initialize();
|
|
294
|
+
|
|
295
|
+
expect(cancel).toHaveBeenCalledTimes(1);
|
|
296
|
+
expect(device.state?.status.mode).toBe('normal');
|
|
297
|
+
expect(device.state?.identity.deviceId).toBe('wallet-1');
|
|
298
|
+
expect(typedCall.mock.calls.map(call => call[0])).toEqual([
|
|
299
|
+
'DeviceInfoGet',
|
|
300
|
+
'ProtocolInfoRequest',
|
|
301
|
+
'DeviceStatusGet',
|
|
302
|
+
]);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
test.each(['bootloader', 'romloader'] as const)(
|
|
306
|
+
'keeps live %s mode after refreshing cached loader state',
|
|
307
|
+
async mode => {
|
|
308
|
+
const typedCall = jest.fn().mockResolvedValue({
|
|
309
|
+
message: getProtocolV2LoaderInfo(mode),
|
|
224
310
|
});
|
|
225
311
|
const device = createV2Device(typedCall);
|
|
226
312
|
device.updateState(
|
|
@@ -236,7 +322,10 @@ describe('getDeviceState', () => {
|
|
|
236
322
|
const state = await device.getDeviceState({ refreshSections: ['status'] });
|
|
237
323
|
|
|
238
324
|
expect(state.status.mode).toBe(mode);
|
|
239
|
-
expect(typedCall).
|
|
325
|
+
expect(typedCall).toHaveBeenCalledTimes(1);
|
|
326
|
+
expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {
|
|
327
|
+
eventless_wallet_session: true,
|
|
328
|
+
});
|
|
240
329
|
}
|
|
241
330
|
);
|
|
242
331
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { isLegacyProtocolV2FirmwareRecoveryMethod } from '../src/core/protocolV2LegacyRecovery';
|
|
2
|
+
|
|
3
|
+
import type { BaseMethod } from '../src/api/BaseMethod';
|
|
4
|
+
|
|
5
|
+
const createMethod = (
|
|
6
|
+
name: string,
|
|
7
|
+
payload: Record<string, unknown>
|
|
8
|
+
): Pick<BaseMethod, 'name' | 'payload'> => ({
|
|
9
|
+
name,
|
|
10
|
+
payload: { method: name, ...payload },
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
describe('Protocol V2 legacy firmware recovery policy', () => {
|
|
14
|
+
test.each([
|
|
15
|
+
['firmware state read', 'getDeviceState', { scope: 'firmware' }],
|
|
16
|
+
['firmware release check', 'checkAllFirmwareRelease', {}],
|
|
17
|
+
['firmware update', 'firmwareUpdateV4', {}],
|
|
18
|
+
])('allows legacy ProtocolInfo for %s', (_label, name, payload) => {
|
|
19
|
+
expect(isLegacyProtocolV2FirmwareRecoveryMethod(createMethod(name, payload))).toBe(true);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test.each([
|
|
23
|
+
['runtime state read', 'getDeviceState', { scope: 'runtime' }],
|
|
24
|
+
['settings state read', 'getDeviceState', { scope: 'settings' }],
|
|
25
|
+
['ordinary API', 'getFeatures', {}],
|
|
26
|
+
])('rejects legacy ProtocolInfo for %s', (_label, name, payload) => {
|
|
27
|
+
expect(isLegacyProtocolV2FirmwareRecoveryMethod(createMethod(name, payload))).toBe(false);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
@@ -3194,6 +3194,125 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
3194
3194
|
expect(typedCall).toHaveBeenCalledTimes(1);
|
|
3195
3195
|
});
|
|
3196
3196
|
|
|
3197
|
+
test('rejects a cached legacy ProtocolInfo outside discovery and firmware recovery', async () => {
|
|
3198
|
+
const device = Device.fromDescriptor({
|
|
3199
|
+
path: 'usb-path',
|
|
3200
|
+
protocolType: 'V2',
|
|
3201
|
+
} as any);
|
|
3202
|
+
const typedCall = jest.fn().mockResolvedValue({
|
|
3203
|
+
type: 'ProtocolInfo',
|
|
3204
|
+
message: {
|
|
3205
|
+
version: 1,
|
|
3206
|
+
build_fingerprint: '',
|
|
3207
|
+
supported_messages: [],
|
|
3208
|
+
protobuf_definition: null,
|
|
3209
|
+
},
|
|
3210
|
+
});
|
|
3211
|
+
(device as any).commands = { typedCall };
|
|
3212
|
+
|
|
3213
|
+
await expect(
|
|
3214
|
+
device.probeProtocolV2RuntimeState({
|
|
3215
|
+
hw: { Device_type: DeviceType.PRO2, serial_no: 'PR2SERIAL' },
|
|
3216
|
+
fw: { application: { version: '1.2.3' } },
|
|
3217
|
+
})
|
|
3218
|
+
).rejects.toMatchObject({ errorCode: HardwareErrorCode.DeviceInitializeFailed });
|
|
3219
|
+
|
|
3220
|
+
expect(typedCall).toHaveBeenCalledTimes(1);
|
|
3221
|
+
});
|
|
3222
|
+
|
|
3223
|
+
test('uses DeviceStatusGet to identify App mode for an allowed legacy ProtocolInfo', async () => {
|
|
3224
|
+
const device = Device.fromDescriptor({
|
|
3225
|
+
path: 'usb-path',
|
|
3226
|
+
protocolType: 'V2',
|
|
3227
|
+
} as any);
|
|
3228
|
+
const typedCall = jest
|
|
3229
|
+
.fn()
|
|
3230
|
+
.mockResolvedValueOnce({
|
|
3231
|
+
type: 'ProtocolInfo',
|
|
3232
|
+
message: {
|
|
3233
|
+
version: 1,
|
|
3234
|
+
build_fingerprint: '',
|
|
3235
|
+
supported_messages: [],
|
|
3236
|
+
protobuf_definition: null,
|
|
3237
|
+
},
|
|
3238
|
+
})
|
|
3239
|
+
.mockResolvedValueOnce({
|
|
3240
|
+
type: 'DeviceStatus',
|
|
3241
|
+
message: { init_states: true, unlocked: true },
|
|
3242
|
+
});
|
|
3243
|
+
(device as any).commands = { typedCall };
|
|
3244
|
+
|
|
3245
|
+
await device.probeProtocolV2RuntimeState(
|
|
3246
|
+
{
|
|
3247
|
+
hw: { Device_type: DeviceType.PRO2, serial_no: 'PR2SERIAL' },
|
|
3248
|
+
fw: { application: { version: '1.2.3' } },
|
|
3249
|
+
},
|
|
3250
|
+
5000,
|
|
3251
|
+
{ allowLegacyProtocolV2ProtocolInfo: true }
|
|
3252
|
+
);
|
|
3253
|
+
|
|
3254
|
+
expect(typedCall).toHaveBeenNthCalledWith(
|
|
3255
|
+
1,
|
|
3256
|
+
'ProtocolInfoRequest',
|
|
3257
|
+
'ProtocolInfo',
|
|
3258
|
+
{ eventless_wallet_session: true },
|
|
3259
|
+
{ timeoutMs: 5000 }
|
|
3260
|
+
);
|
|
3261
|
+
expect(typedCall).toHaveBeenNthCalledWith(
|
|
3262
|
+
2,
|
|
3263
|
+
'DeviceStatusGet',
|
|
3264
|
+
'DeviceStatus',
|
|
3265
|
+
{},
|
|
3266
|
+
{ timeoutMs: 5000 }
|
|
3267
|
+
);
|
|
3268
|
+
expect(device.features).toMatchObject({
|
|
3269
|
+
mode: 'normal',
|
|
3270
|
+
bootloaderMode: false,
|
|
3271
|
+
initialized: true,
|
|
3272
|
+
unlocked: true,
|
|
3273
|
+
});
|
|
3274
|
+
});
|
|
3275
|
+
|
|
3276
|
+
test('maps an allowed legacy loader to bootloader only after an explicit unsupported status', async () => {
|
|
3277
|
+
const device = Device.fromDescriptor({
|
|
3278
|
+
path: 'usb-path',
|
|
3279
|
+
protocolType: 'V2',
|
|
3280
|
+
} as any);
|
|
3281
|
+
const typedCall = jest
|
|
3282
|
+
.fn()
|
|
3283
|
+
.mockResolvedValueOnce({
|
|
3284
|
+
type: 'ProtocolInfo',
|
|
3285
|
+
message: {
|
|
3286
|
+
version: 1,
|
|
3287
|
+
build_fingerprint: '',
|
|
3288
|
+
supported_messages: [],
|
|
3289
|
+
protobuf_definition: null,
|
|
3290
|
+
},
|
|
3291
|
+
})
|
|
3292
|
+
.mockRejectedValueOnce(new Error('Failure_UnexpectedMessage,Unknown message'));
|
|
3293
|
+
(device as any).commands = { typedCall };
|
|
3294
|
+
|
|
3295
|
+
await device.probeProtocolV2RuntimeState(
|
|
3296
|
+
{
|
|
3297
|
+
hw: { Device_type: DeviceType.NEO, serial_no: 'NEOSERIAL' },
|
|
3298
|
+
fw: { bootloader: { version: '0.2.0' } },
|
|
3299
|
+
},
|
|
3300
|
+
undefined,
|
|
3301
|
+
{ allowLegacyProtocolV2ProtocolInfo: true }
|
|
3302
|
+
);
|
|
3303
|
+
|
|
3304
|
+
expect(typedCall).toHaveBeenNthCalledWith(1, 'ProtocolInfoRequest', 'ProtocolInfo', {
|
|
3305
|
+
eventless_wallet_session: true,
|
|
3306
|
+
});
|
|
3307
|
+
expect(typedCall).toHaveBeenNthCalledWith(2, 'DeviceStatusGet', 'DeviceStatus', {});
|
|
3308
|
+
expect(device.features).toMatchObject({
|
|
3309
|
+
deviceType: 'neo',
|
|
3310
|
+
mode: 'bootloader',
|
|
3311
|
+
bootloaderMode: true,
|
|
3312
|
+
initialized: null,
|
|
3313
|
+
});
|
|
3314
|
+
});
|
|
3315
|
+
|
|
3197
3316
|
test('does not reinterpret a state update error as runtime detection failure', async () => {
|
|
3198
3317
|
const device = Device.fromDescriptor({
|
|
3199
3318
|
path: 'usb-path',
|
|
@@ -4249,6 +4368,43 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
4249
4368
|
expect(method.postTipMessage).toHaveBeenCalledWith('GoToBootloaderSuccess');
|
|
4250
4369
|
});
|
|
4251
4370
|
|
|
4371
|
+
test('keeps an ambiguous legacy runtime on the direct loader update path', async () => {
|
|
4372
|
+
const method = new FirmwareUpdateV4({
|
|
4373
|
+
id: 1,
|
|
4374
|
+
payload: {
|
|
4375
|
+
method: 'firmwareUpdateV4',
|
|
4376
|
+
},
|
|
4377
|
+
});
|
|
4378
|
+
(method as any).device = stubDevice({
|
|
4379
|
+
originalDescriptor: { id: 'usb-id', path: 'legacy-path', protocolType: 'V2' },
|
|
4380
|
+
state: {
|
|
4381
|
+
raw: {
|
|
4382
|
+
protocolV2ProtocolInfo: {
|
|
4383
|
+
version: 1,
|
|
4384
|
+
build_fingerprint: '',
|
|
4385
|
+
supported_messages: [],
|
|
4386
|
+
protobuf_definition: null,
|
|
4387
|
+
},
|
|
4388
|
+
},
|
|
4389
|
+
},
|
|
4390
|
+
features: {
|
|
4391
|
+
deviceType: 'pro2',
|
|
4392
|
+
mode: 'bootloader',
|
|
4393
|
+
bootloaderMode: true,
|
|
4394
|
+
capabilities: [],
|
|
4395
|
+
},
|
|
4396
|
+
isBootloader: () => true,
|
|
4397
|
+
isRomloader: () => false,
|
|
4398
|
+
});
|
|
4399
|
+
(method as any).protocolV2Reboot = jest.fn();
|
|
4400
|
+
method.postTipMessage = jest.fn();
|
|
4401
|
+
|
|
4402
|
+
await expect((method as any).enterProtocolV2BootloaderMode()).resolves.toBe(false);
|
|
4403
|
+
|
|
4404
|
+
expect((method as any).protocolV2LegacyDirectUpdate).toBe(true);
|
|
4405
|
+
expect((method as any).protocolV2Reboot).not.toHaveBeenCalled();
|
|
4406
|
+
});
|
|
4407
|
+
|
|
4252
4408
|
test('keeps Protocol V2 romloader active before firmware transfer', async () => {
|
|
4253
4409
|
const method = new FirmwareUpdateV4({
|
|
4254
4410
|
id: 1,
|
|
@@ -4881,6 +5037,81 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
4881
5037
|
expect(method.postProgressMessage).not.toHaveBeenCalled();
|
|
4882
5038
|
});
|
|
4883
5039
|
|
|
5040
|
+
test('reboots a legacy App only after the direct update endpoint is explicitly unavailable', async () => {
|
|
5041
|
+
const method = new FirmwareUpdateV4({
|
|
5042
|
+
id: 1,
|
|
5043
|
+
payload: {
|
|
5044
|
+
method: 'firmwareUpdateV4',
|
|
5045
|
+
},
|
|
5046
|
+
});
|
|
5047
|
+
const targets = [{ target_id: 4, path: 'vol0:/application_p1.bin' }];
|
|
5048
|
+
const typedCall = jest
|
|
5049
|
+
.fn()
|
|
5050
|
+
.mockRejectedValueOnce(new Error('Failure_InvalidMessage,Handler not registered'))
|
|
5051
|
+
.mockResolvedValueOnce({ type: 'Success', message: { message: '' } });
|
|
5052
|
+
|
|
5053
|
+
(method as any).device = stubDevice({
|
|
5054
|
+
getCommands: () => ({ typedCall }),
|
|
5055
|
+
});
|
|
5056
|
+
(method as any).protocolV2LegacyDirectUpdate = true;
|
|
5057
|
+
(method as any).rebootProtocolV2ToBootloader = jest.fn().mockResolvedValue(true);
|
|
5058
|
+
method.postTipMessage = jest.fn();
|
|
5059
|
+
method.postProgressMessage = jest.fn();
|
|
5060
|
+
|
|
5061
|
+
await expect((method as any).protocolV2StartFirmwareUpdate({ targets })).resolves.toEqual({
|
|
5062
|
+
type: 'Success',
|
|
5063
|
+
message: { message: '' },
|
|
5064
|
+
});
|
|
5065
|
+
|
|
5066
|
+
expect(typedCall).toHaveBeenCalledTimes(2);
|
|
5067
|
+
expect(typedCall).toHaveBeenNthCalledWith(
|
|
5068
|
+
1,
|
|
5069
|
+
'DeviceFirmwareUpdateRequest',
|
|
5070
|
+
'Success',
|
|
5071
|
+
{ targets },
|
|
5072
|
+
{ timeoutMs: 180000 }
|
|
5073
|
+
);
|
|
5074
|
+
expect(typedCall).toHaveBeenNthCalledWith(
|
|
5075
|
+
2,
|
|
5076
|
+
'DeviceFirmwareUpdateRequest',
|
|
5077
|
+
'Success',
|
|
5078
|
+
{ targets },
|
|
5079
|
+
{ timeoutMs: 180000 }
|
|
5080
|
+
);
|
|
5081
|
+
expect((method as any).rebootProtocolV2ToBootloader).toHaveBeenCalledTimes(1);
|
|
5082
|
+
expect(method.postTipMessage).toHaveBeenCalledWith('FirmwareUpdating');
|
|
5083
|
+
expect(method.postProgressMessage).toHaveBeenCalledWith(0, 'installingFirmware');
|
|
5084
|
+
});
|
|
5085
|
+
|
|
5086
|
+
test('does not replay a legacy direct update after a link failure', async () => {
|
|
5087
|
+
const method = new FirmwareUpdateV4({
|
|
5088
|
+
id: 1,
|
|
5089
|
+
payload: {
|
|
5090
|
+
method: 'firmwareUpdateV4',
|
|
5091
|
+
},
|
|
5092
|
+
});
|
|
5093
|
+
const typedCall = jest.fn().mockRejectedValue(new Error('LIBUSB_TRANSFER_TIMED_OUT'));
|
|
5094
|
+
|
|
5095
|
+
(method as any).device = stubDevice({
|
|
5096
|
+
getCommands: () => ({ typedCall }),
|
|
5097
|
+
});
|
|
5098
|
+
(method as any).protocolV2LegacyDirectUpdate = true;
|
|
5099
|
+
(method as any).rebootProtocolV2ToBootloader = jest.fn();
|
|
5100
|
+
method.postTipMessage = jest.fn();
|
|
5101
|
+
method.postProgressMessage = jest.fn();
|
|
5102
|
+
|
|
5103
|
+
await expect(
|
|
5104
|
+
(method as any).protocolV2StartFirmwareUpdate({
|
|
5105
|
+
targets: [{ target_id: 4, path: 'vol0:/application_p1.bin' }],
|
|
5106
|
+
})
|
|
5107
|
+
).rejects.toThrow('LIBUSB_TRANSFER_TIMED_OUT');
|
|
5108
|
+
|
|
5109
|
+
expect(typedCall).toHaveBeenCalledTimes(1);
|
|
5110
|
+
expect((method as any).rebootProtocolV2ToBootloader).not.toHaveBeenCalled();
|
|
5111
|
+
expect(method.postTipMessage).not.toHaveBeenCalled();
|
|
5112
|
+
expect(method.postProgressMessage).not.toHaveBeenCalled();
|
|
5113
|
+
});
|
|
5114
|
+
|
|
4884
5115
|
test('polls only firmware status while Protocol V2 bootloader is installing', async () => {
|
|
4885
5116
|
const method = new FirmwareUpdateV4({
|
|
4886
5117
|
id: 1,
|
|
@@ -5009,7 +5240,9 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
5009
5240
|
(method as any).device = stubDevice({ probeProtocolV2RuntimeState });
|
|
5010
5241
|
|
|
5011
5242
|
await expect((method as any).probeProtocolV2NormalMode(deviceInfo)).resolves.toBe(expected);
|
|
5012
|
-
expect(probeProtocolV2RuntimeState).toHaveBeenCalledWith(deviceInfo, 5000
|
|
5243
|
+
expect(probeProtocolV2RuntimeState).toHaveBeenCalledWith(deviceInfo, 5000, {
|
|
5244
|
+
allowLegacyProtocolV2ProtocolInfo: true,
|
|
5245
|
+
});
|
|
5013
5246
|
});
|
|
5014
5247
|
|
|
5015
5248
|
test('keeps polling when the firmware status handler is missing in loader mode', async () => {
|
|
@@ -48,7 +48,10 @@ describe('live device state reads', () => {
|
|
|
48
48
|
|
|
49
49
|
await method.run();
|
|
50
50
|
|
|
51
|
-
expect(getDeviceState).toHaveBeenCalledWith({
|
|
51
|
+
expect(getDeviceState).toHaveBeenCalledWith({
|
|
52
|
+
refreshSections: [...refreshSections],
|
|
53
|
+
...(scope === 'firmware' ? { allowLegacyProtocolV2ProtocolInfo: true } : {}),
|
|
54
|
+
});
|
|
52
55
|
});
|
|
53
56
|
|
|
54
57
|
test.each(['bootloader', 'romloader'] as const)(
|
|
@@ -94,6 +94,7 @@ describe('SearchDevices', () => {
|
|
|
94
94
|
connectProtocol: undefined,
|
|
95
95
|
forceProtocolDetection: true,
|
|
96
96
|
refreshRuntimeState: true,
|
|
97
|
+
allowLegacyProtocolV2ProtocolInfo: true,
|
|
97
98
|
}
|
|
98
99
|
);
|
|
99
100
|
expect(mockGetDevices).toHaveBeenNthCalledWith(
|
|
@@ -104,6 +105,7 @@ describe('SearchDevices', () => {
|
|
|
104
105
|
connectProtocol: undefined,
|
|
105
106
|
forceProtocolDetection: true,
|
|
106
107
|
refreshRuntimeState: true,
|
|
108
|
+
allowLegacyProtocolV2ProtocolInfo: true,
|
|
107
109
|
}
|
|
108
110
|
);
|
|
109
111
|
});
|