@onekeyfe/hd-core 1.2.0-alpha.10 → 1.2.0-alpha.11
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 +25 -0
- package/__tests__/RequestQueue.test.ts +34 -0
- package/__tests__/pro2Wallpaper.test.ts +50 -0
- package/__tests__/protocol-v2.test.ts +426 -60
- package/dist/api/BaseMethod.d.ts +4 -0
- package/dist/api/BaseMethod.d.ts.map +1 -1
- package/dist/api/FileWrite.d.ts.map +1 -1
- package/dist/api/FirmwareUpdateV4.d.ts +0 -4
- package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
- package/dist/api/GetPassphraseState.d.ts +1 -6
- package/dist/api/GetPassphraseState.d.ts.map +1 -1
- package/dist/api/UploadPortfolio.d.ts +18 -0
- package/dist/api/UploadPortfolio.d.ts.map +1 -0
- package/dist/api/index.d.ts +5 -0
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/protocol-v2/DeviceSessionGet.d.ts +1 -4
- package/dist/api/protocol-v2/DeviceSessionGet.d.ts.map +1 -1
- package/dist/api/protocol-v2/DeviceSettingsGet.d.ts +6 -0
- package/dist/api/protocol-v2/DeviceSettingsGet.d.ts.map +1 -0
- package/dist/api/protocol-v2/DeviceSettingsPageShow.d.ts +16 -0
- package/dist/api/protocol-v2/DeviceSettingsPageShow.d.ts.map +1 -0
- package/dist/api/protocol-v2/DeviceSettingsSet.d.ts +12 -0
- package/dist/api/protocol-v2/DeviceSettingsSet.d.ts.map +1 -0
- package/dist/api/protocol-v2/DeviceUploadWallpaper.d.ts +26 -0
- package/dist/api/protocol-v2/DeviceUploadWallpaper.d.ts.map +1 -0
- package/dist/core/RequestQueue.d.ts +2 -0
- package/dist/core/RequestQueue.d.ts.map +1 -1
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/device/Device.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/call.d.ts +7 -0
- package/dist/events/call.d.ts.map +1 -1
- package/dist/events/core.d.ts +2 -2
- package/dist/events/core.d.ts.map +1 -1
- package/dist/events/iframe.d.ts +1 -0
- package/dist/events/iframe.d.ts.map +1 -1
- package/dist/index.d.ts +76 -21
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +694 -253
- package/dist/inject.d.ts +3 -2
- package/dist/inject.d.ts.map +1 -1
- package/dist/lowLevelInject.d.ts +2 -1
- package/dist/lowLevelInject.d.ts.map +1 -1
- package/dist/protocols/protocol-v2/unlockRetry.d.ts +9 -0
- package/dist/protocols/protocol-v2/unlockRetry.d.ts.map +1 -0
- package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
- package/dist/topLevelInject.d.ts.map +1 -1
- package/dist/types/api/firmwareUpdate.d.ts +2 -0
- package/dist/types/api/firmwareUpdate.d.ts.map +1 -1
- package/dist/types/api/getPassphraseState.d.ts +2 -10
- package/dist/types/api/getPassphraseState.d.ts.map +1 -1
- package/dist/types/api/index.d.ts +8 -2
- package/dist/types/api/index.d.ts.map +1 -1
- package/dist/types/api/protocolV2.d.ts +19 -4
- package/dist/types/api/protocolV2.d.ts.map +1 -1
- package/dist/utils/deviceFeaturesUtils.d.ts +0 -2
- package/dist/utils/deviceFeaturesUtils.d.ts.map +1 -1
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/patch.d.ts +1 -1
- package/dist/utils/patch.d.ts.map +1 -1
- package/dist/utils/pro2Wallpaper.d.ts +12 -0
- package/dist/utils/pro2Wallpaper.d.ts.map +1 -0
- package/package.json +4 -4
- package/src/api/BaseMethod.ts +18 -0
- package/src/api/FileWrite.ts +9 -1
- package/src/api/FirmwareUpdateV4.ts +21 -91
- package/src/api/GetPassphraseState.ts +9 -18
- package/src/api/UploadPortfolio.ts +37 -0
- package/src/api/index.ts +5 -0
- package/src/api/protocol-v2/DeviceSessionGet.ts +3 -14
- package/src/api/protocol-v2/DeviceSettingsGet.ts +16 -0
- package/src/api/protocol-v2/DeviceSettingsPageShow.ts +59 -0
- package/src/api/protocol-v2/DeviceSettingsSet.ts +39 -0
- package/src/api/protocol-v2/DeviceUploadWallpaper.ts +160 -0
- package/src/core/RequestQueue.ts +19 -0
- package/src/core/index.ts +10 -1
- package/src/data/messages/messages-protocol-v2.json +92 -8
- package/src/device/Device.ts +17 -3
- package/src/device/DeviceCommands.ts +9 -3
- package/src/events/call.ts +6 -0
- package/src/events/core.ts +2 -0
- package/src/events/iframe.ts +1 -0
- package/src/index.ts +4 -0
- package/src/inject.ts +13 -0
- package/src/lowLevelInject.ts +3 -0
- package/src/protocols/protocol-v2/unlockRetry.ts +46 -0
- package/src/protocols/protocol-v2/walletSession.ts +14 -5
- package/src/topLevelInject.ts +2 -0
- package/src/types/api/firmwareUpdate.ts +12 -0
- package/src/types/api/getPassphraseState.ts +2 -11
- package/src/types/api/index.ts +12 -1
- package/src/types/api/protocolV2.ts +46 -3
- package/src/utils/deviceFeaturesUtils.ts +3 -8
- package/src/utils/index.ts +6 -0
- package/src/utils/pro2Wallpaper.ts +111 -0
|
@@ -7,12 +7,17 @@ import DnxSignTransaction from '../src/api/dynex/DnxSignTransaction';
|
|
|
7
7
|
import DirList from '../src/api/DirList';
|
|
8
8
|
import FileRead from '../src/api/FileRead';
|
|
9
9
|
import FileWrite from '../src/api/FileWrite';
|
|
10
|
+
import UploadPortfolio from '../src/api/UploadPortfolio';
|
|
10
11
|
import DeviceFactoryInfoGet from '../src/api/protocol-v2/DeviceFactoryInfoGet';
|
|
11
12
|
import DeviceFactoryInfoSet from '../src/api/protocol-v2/DeviceFactoryInfoSet';
|
|
12
13
|
import DeviceFirmwareUpdate from '../src/api/protocol-v2/DeviceFirmwareUpdate';
|
|
13
14
|
import DeviceGetFirmwareUpdateStatus from '../src/api/protocol-v2/DeviceGetFirmwareUpdateStatus';
|
|
14
15
|
import DeviceInfoGet from '../src/api/protocol-v2/DeviceInfoGet';
|
|
15
16
|
import DeviceReboot from '../src/api/protocol-v2/DeviceReboot';
|
|
17
|
+
import DeviceSettingsGet from '../src/api/protocol-v2/DeviceSettingsGet';
|
|
18
|
+
import DeviceSettingsPageShow from '../src/api/protocol-v2/DeviceSettingsPageShow';
|
|
19
|
+
import DeviceSettingsSet from '../src/api/protocol-v2/DeviceSettingsSet';
|
|
20
|
+
import DeviceUploadWallpaper from '../src/api/protocol-v2/DeviceUploadWallpaper';
|
|
16
21
|
import DeviceSessionGet from '../src/api/protocol-v2/DeviceSessionGet';
|
|
17
22
|
import DeviceStatusGet from '../src/api/protocol-v2/DeviceStatusGet';
|
|
18
23
|
import FilesystemFormat from '../src/api/protocol-v2/FilesystemFormat';
|
|
@@ -56,6 +61,7 @@ import {
|
|
|
56
61
|
getProtocolV2WalletSession,
|
|
57
62
|
refreshProtocolV2DeviceStatus,
|
|
58
63
|
} from '../src/protocols/protocol-v2/walletSession';
|
|
64
|
+
import { runMethodWithUnlockRetry } from '../src/protocols/protocol-v2/unlockRetry';
|
|
59
65
|
import { buildProfileFromProtocolV2, buildProtocolV2FeaturesPayload } from '../src/deviceProfile';
|
|
60
66
|
import {
|
|
61
67
|
getDeviceType,
|
|
@@ -80,6 +86,153 @@ jest.mock('../src/data/config', () => ({
|
|
|
80
86
|
DEFAULT_DOMAIN: 'https://jssdk.onekey.so/1.0.0/',
|
|
81
87
|
}));
|
|
82
88
|
|
|
89
|
+
describe('DeviceUploadWallpaper', () => {
|
|
90
|
+
test('encodes, uploads and applies a Pro2 wallpaper', async () => {
|
|
91
|
+
const rgba = new Uint8Array(604 * 1024 * 4).fill(255);
|
|
92
|
+
const typedCall = jest.fn().mockImplementation((request, _response, params) => {
|
|
93
|
+
if (request === 'FilesystemDirMake') return { message: { message: 'directory ready' } };
|
|
94
|
+
if (request === 'FilesystemFileWrite') {
|
|
95
|
+
return { message: { processed_byte: params.file.offset + params.file.data.byteLength } };
|
|
96
|
+
}
|
|
97
|
+
if (request === 'SetWallpaper') return { message: { message: 'wallpaper applied' } };
|
|
98
|
+
throw new Error(`Unexpected request: ${request}`);
|
|
99
|
+
});
|
|
100
|
+
const method = new DeviceUploadWallpaper({
|
|
101
|
+
id: 1,
|
|
102
|
+
payload: { method: 'deviceUploadWallpaper', width: 604, height: 1024, rgba },
|
|
103
|
+
});
|
|
104
|
+
(method as any).device = stubDevice({ commands: { typedCall } });
|
|
105
|
+
|
|
106
|
+
method.init();
|
|
107
|
+
const result = await method.run();
|
|
108
|
+
|
|
109
|
+
expect(method.requireProtocolV2).toBe(true);
|
|
110
|
+
expect(method.unlockPolicy).toBe('retry-on-locked');
|
|
111
|
+
expect(typedCall).toHaveBeenNthCalledWith(
|
|
112
|
+
1,
|
|
113
|
+
'FilesystemDirMake',
|
|
114
|
+
'Success',
|
|
115
|
+
{ path: 'vol0:/wallpapers/user' }
|
|
116
|
+
);
|
|
117
|
+
const fileWriteCall = typedCall.mock.calls.find(call => call[0] === 'FilesystemFileWrite');
|
|
118
|
+
expect(fileWriteCall?.[2]).toMatchObject({
|
|
119
|
+
file: { path: expect.stringMatching(/^vol0:\/wallpapers\/user\/wallpaper-[a-f0-9]+\.bin$/) },
|
|
120
|
+
overwrite: true,
|
|
121
|
+
append: false,
|
|
122
|
+
});
|
|
123
|
+
expect(typedCall).toHaveBeenLastCalledWith('SetWallpaper', 'Success', {
|
|
124
|
+
target: 1,
|
|
125
|
+
path: result.path,
|
|
126
|
+
});
|
|
127
|
+
expect(result).toMatchObject({ colorFormat: 'RGB565', message: 'wallpaper applied' });
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('rejects unsafe filenames before device communication', async () => {
|
|
131
|
+
const method = new DeviceUploadWallpaper({
|
|
132
|
+
id: 1,
|
|
133
|
+
payload: {
|
|
134
|
+
method: 'deviceUploadWallpaper',
|
|
135
|
+
width: 604,
|
|
136
|
+
height: 1024,
|
|
137
|
+
rgba: new Uint8Array(604 * 1024 * 4),
|
|
138
|
+
fileName: '../wallpaper.bin',
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
expect(() => method.init()).toThrow('fileName');
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe('UploadPortfolio', () => {
|
|
147
|
+
test('stages the complete package before applying PortfolioUpdate', async () => {
|
|
148
|
+
const packageBytes = new Uint8Array([1, 2, 3]);
|
|
149
|
+
const typedCall = jest
|
|
150
|
+
.fn()
|
|
151
|
+
.mockResolvedValueOnce({ message: { processed_byte: 3 } })
|
|
152
|
+
.mockResolvedValueOnce({ message: { message: 'Portfolio updated' } });
|
|
153
|
+
const method = new UploadPortfolio({
|
|
154
|
+
id: 1,
|
|
155
|
+
payload: {
|
|
156
|
+
method: 'uploadPortfolio',
|
|
157
|
+
packageBytes,
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
(method as any).device = stubDevice({ commands: { typedCall } });
|
|
161
|
+
method.postMessage = jest.fn();
|
|
162
|
+
|
|
163
|
+
method.init();
|
|
164
|
+
const result = await method.run();
|
|
165
|
+
|
|
166
|
+
expect(typedCall).toHaveBeenNthCalledWith(
|
|
167
|
+
1,
|
|
168
|
+
'FilesystemFileWrite',
|
|
169
|
+
'FilesystemFile',
|
|
170
|
+
{
|
|
171
|
+
file: {
|
|
172
|
+
path: 'vol1:/portfolio/portfolio.pfol.pending',
|
|
173
|
+
offset: 0,
|
|
174
|
+
total_size: 3,
|
|
175
|
+
data: packageBytes,
|
|
176
|
+
},
|
|
177
|
+
overwrite: true,
|
|
178
|
+
append: false,
|
|
179
|
+
ui_percentage: 100,
|
|
180
|
+
},
|
|
181
|
+
{ timeoutMs: undefined }
|
|
182
|
+
);
|
|
183
|
+
expect(typedCall).toHaveBeenNthCalledWith(2, 'PortfolioUpdate', 'Success', {});
|
|
184
|
+
expect(result).toMatchObject({
|
|
185
|
+
path: 'vol1:/portfolio/portfolio.pfol.pending',
|
|
186
|
+
processed_byte: 3,
|
|
187
|
+
portfolioUpdated: true,
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test('does not apply PortfolioUpdate when staging fails', async () => {
|
|
192
|
+
const typedCall = jest.fn().mockRejectedValue(new Error('write failed'));
|
|
193
|
+
const method = new UploadPortfolio({
|
|
194
|
+
id: 1,
|
|
195
|
+
payload: {
|
|
196
|
+
method: 'uploadPortfolio',
|
|
197
|
+
packageBytes: new Uint8Array([1]),
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
(method as any).device = stubDevice({ commands: { typedCall } });
|
|
201
|
+
|
|
202
|
+
method.init();
|
|
203
|
+
|
|
204
|
+
await expect(method.run()).rejects.toThrow('write failed');
|
|
205
|
+
expect(typedCall).toHaveBeenCalledTimes(1);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test('stops after the acknowledged chunk when the operation is aborted', async () => {
|
|
209
|
+
const packageBytes = new Uint8Array(4001);
|
|
210
|
+
const abortController = new AbortController();
|
|
211
|
+
const typedCall = jest.fn().mockImplementationOnce(async () => {
|
|
212
|
+
abortController.abort();
|
|
213
|
+
return { message: { processed_byte: 2048 } };
|
|
214
|
+
});
|
|
215
|
+
const method = new UploadPortfolio({
|
|
216
|
+
id: 1,
|
|
217
|
+
payload: {
|
|
218
|
+
method: 'uploadPortfolio',
|
|
219
|
+
packageBytes,
|
|
220
|
+
operationId: 'portfolio-1',
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
method.abortSignal = abortController.signal;
|
|
224
|
+
(method as any).device = stubDevice({ commands: { typedCall } });
|
|
225
|
+
|
|
226
|
+
method.init();
|
|
227
|
+
|
|
228
|
+
await expect(method.run()).rejects.toMatchObject({
|
|
229
|
+
errorCode: HardwareErrorCode.CallQueueActionCancelled,
|
|
230
|
+
});
|
|
231
|
+
expect(typedCall).toHaveBeenCalledTimes(1);
|
|
232
|
+
expect(typedCall).not.toHaveBeenCalledWith('PortfolioUpdate', 'Success', {});
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
83
236
|
const descriptor = {
|
|
84
237
|
id: 'ble-id',
|
|
85
238
|
path: 'usb-path',
|
|
@@ -272,7 +425,6 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
272
425
|
await expect(
|
|
273
426
|
getPassphraseState(device, {
|
|
274
427
|
expectPassphraseState: 'state-2',
|
|
275
|
-
allowCreateAttachPin: true,
|
|
276
428
|
})
|
|
277
429
|
).rejects.toEqual(
|
|
278
430
|
expect.objectContaining({
|
|
@@ -308,7 +460,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
308
460
|
expect(updateProtocolV2Features).not.toHaveBeenCalled();
|
|
309
461
|
});
|
|
310
462
|
|
|
311
|
-
test('deviceSessionGet
|
|
463
|
+
test('deviceSessionGet sends an empty request and does not mutate wallet cache', async () => {
|
|
312
464
|
const typedCall = jest.fn().mockResolvedValue({
|
|
313
465
|
type: 'DeviceSession',
|
|
314
466
|
message: { session_id: 'new-session', btc_test_address: 'state-a' },
|
|
@@ -318,7 +470,6 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
318
470
|
payload: {
|
|
319
471
|
method: 'deviceSessionGet',
|
|
320
472
|
connectId: 'connect-id',
|
|
321
|
-
sessionId: 'cached-session',
|
|
322
473
|
},
|
|
323
474
|
});
|
|
324
475
|
method.init();
|
|
@@ -332,9 +483,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
332
483
|
session_id: 'new-session',
|
|
333
484
|
btc_test_address: 'state-a',
|
|
334
485
|
});
|
|
335
|
-
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
|
|
336
|
-
session_id: 'cached-session',
|
|
337
|
-
});
|
|
486
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
|
|
338
487
|
expect(updateInternalState).not.toHaveBeenCalled();
|
|
339
488
|
});
|
|
340
489
|
|
|
@@ -343,7 +492,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
343
492
|
const api = createCoreApi(call as any);
|
|
344
493
|
|
|
345
494
|
await api.deviceStatusGet('connect-id', { retryCount: 1 });
|
|
346
|
-
await api.deviceSessionGet('connect-id', {
|
|
495
|
+
await api.deviceSessionGet('connect-id', { retryCount: 1 });
|
|
347
496
|
|
|
348
497
|
expect(call).toHaveBeenNthCalledWith(1, {
|
|
349
498
|
method: 'deviceStatusGet',
|
|
@@ -353,7 +502,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
353
502
|
expect(call).toHaveBeenNthCalledWith(2, {
|
|
354
503
|
method: 'deviceSessionGet',
|
|
355
504
|
connectId: 'connect-id',
|
|
356
|
-
|
|
505
|
+
retryCount: 1,
|
|
357
506
|
});
|
|
358
507
|
});
|
|
359
508
|
|
|
@@ -415,7 +564,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
415
564
|
expect(device.getInternalState()).toBeUndefined();
|
|
416
565
|
});
|
|
417
566
|
|
|
418
|
-
test('
|
|
567
|
+
test('retries without the selected Pro2 cache entry after invalid session rejection', async () => {
|
|
419
568
|
const device = Device.fromDescriptor({
|
|
420
569
|
id: 'cache-device-3',
|
|
421
570
|
path: 'cache-path-3',
|
|
@@ -433,12 +582,49 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
433
582
|
);
|
|
434
583
|
device.passphraseState = 'state-a';
|
|
435
584
|
preloadSessionCache('stable-device-3', 'state-a', 'session-a');
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
585
|
+
const typedCall = jest
|
|
586
|
+
.fn()
|
|
587
|
+
.mockRejectedValueOnce(new Error('Failure_ProcessError,Failure_InvalidSession'))
|
|
588
|
+
.mockResolvedValueOnce({
|
|
589
|
+
type: 'DeviceSession',
|
|
590
|
+
message: { session_id: 'session-b', btc_test_address: 'state-a' },
|
|
591
|
+
});
|
|
592
|
+
(device as any).commands = { typedCall };
|
|
593
|
+
|
|
594
|
+
await expect(getProtocolV2WalletSession(device)).resolves.toMatchObject({
|
|
595
|
+
passphraseState: 'state-a',
|
|
596
|
+
newSession: 'session-b',
|
|
597
|
+
});
|
|
598
|
+
expect(typedCall.mock.calls).toEqual([
|
|
599
|
+
['DeviceSessionGet', 'DeviceSession', { session_id: 'session-a' }],
|
|
600
|
+
['DeviceSessionGet', 'DeviceSession', {}],
|
|
601
|
+
]);
|
|
602
|
+
expect(device.getInternalState()).toBe('session-b');
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
test('does not retry an invalid Pro2 session when no cached session was sent', async () => {
|
|
606
|
+
const device = Device.fromDescriptor({
|
|
607
|
+
id: 'cache-device-no-session',
|
|
608
|
+
path: 'cache-path-no-session',
|
|
609
|
+
protocolType: 'V2',
|
|
610
|
+
} as any);
|
|
611
|
+
(device as any).features = normalizeProtocolV2Features(
|
|
612
|
+
{ ...descriptor, protocolType: 'V2' } as any,
|
|
613
|
+
{
|
|
614
|
+
status: {
|
|
615
|
+
device_id: 'stable-device-no-session',
|
|
616
|
+
unlocked: true,
|
|
617
|
+
passphrase_enabled: true,
|
|
618
|
+
},
|
|
619
|
+
}
|
|
620
|
+
);
|
|
621
|
+
const typedCall = jest
|
|
622
|
+
.fn()
|
|
623
|
+
.mockRejectedValue(new Error('Failure_ProcessError,Failure_InvalidSession'));
|
|
624
|
+
(device as any).commands = { typedCall };
|
|
439
625
|
|
|
440
626
|
await expect(getProtocolV2WalletSession(device)).rejects.toThrow('Failure_InvalidSession');
|
|
441
|
-
expect(
|
|
627
|
+
expect(typedCall.mock.calls).toEqual([['DeviceSessionGet', 'DeviceSession', {}]]);
|
|
442
628
|
});
|
|
443
629
|
|
|
444
630
|
test('rejects a mismatched Pro2 wallet state and clears the selected cache entry', async () => {
|
|
@@ -547,7 +733,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
547
733
|
expect(typedCall).toHaveBeenCalledWith('DeviceStatusGet', 'DeviceStatus', {});
|
|
548
734
|
});
|
|
549
735
|
|
|
550
|
-
test('returns
|
|
736
|
+
test('returns passphrase state string for existing Pro devices', async () => {
|
|
551
737
|
const features = {
|
|
552
738
|
deviceId: 'pro-device-id',
|
|
553
739
|
deviceType: 'pro',
|
|
@@ -580,12 +766,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
580
766
|
getCurrentPassphraseProtection: () => true,
|
|
581
767
|
}) as any;
|
|
582
768
|
|
|
583
|
-
await expect(method.run()).resolves.
|
|
584
|
-
passphraseState: 'state-pro',
|
|
585
|
-
sessionId: 'session-pro',
|
|
586
|
-
unlockedAttachPin: false,
|
|
587
|
-
passphraseProtection: true,
|
|
588
|
-
});
|
|
769
|
+
await expect(method.run()).resolves.toBe('state-pro');
|
|
589
770
|
expect(updateInternalState).toHaveBeenCalledWith(
|
|
590
771
|
true,
|
|
591
772
|
'state-pro',
|
|
@@ -595,7 +776,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
595
776
|
);
|
|
596
777
|
});
|
|
597
778
|
|
|
598
|
-
test('
|
|
779
|
+
test('returns passphrase state string for Pro2 devices', async () => {
|
|
599
780
|
const features = {
|
|
600
781
|
deviceId: null,
|
|
601
782
|
deviceType: 'pro2',
|
|
@@ -630,12 +811,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
630
811
|
getCurrentPassphraseProtection: () => true,
|
|
631
812
|
}) as any;
|
|
632
813
|
|
|
633
|
-
await expect(method.run()).resolves.
|
|
634
|
-
passphraseState: 'state-pro2',
|
|
635
|
-
sessionId: 'session-pro2',
|
|
636
|
-
unlockedAttachPin: true,
|
|
637
|
-
passphraseProtection: true,
|
|
638
|
-
});
|
|
814
|
+
await expect(method.run()).resolves.toBe('state-pro2');
|
|
639
815
|
expect(getFeatures).not.toHaveBeenCalled();
|
|
640
816
|
});
|
|
641
817
|
|
|
@@ -673,12 +849,7 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
673
849
|
getCurrentPassphraseProtection: () => true,
|
|
674
850
|
}) as any;
|
|
675
851
|
|
|
676
|
-
await expect(method.run()).resolves.
|
|
677
|
-
passphraseState: 'state-pro2-new',
|
|
678
|
-
sessionId: undefined,
|
|
679
|
-
unlockedAttachPin: false,
|
|
680
|
-
passphraseProtection: true,
|
|
681
|
-
});
|
|
852
|
+
await expect(method.run()).resolves.toBe('state-pro2-new');
|
|
682
853
|
expect(clearInternalState).toHaveBeenCalledTimes(1);
|
|
683
854
|
expect(updateInternalState).toHaveBeenCalledWith(
|
|
684
855
|
true,
|
|
@@ -1578,16 +1749,10 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1578
1749
|
if (requestType === 'DeviceSessionAskPin') {
|
|
1579
1750
|
return {
|
|
1580
1751
|
type: 'DeviceSessionPinResult',
|
|
1581
|
-
message: { unlocked: false, passphrase_protection: false },
|
|
1582
|
-
};
|
|
1583
|
-
}
|
|
1584
|
-
if (requestType === 'DeviceStatusGet') {
|
|
1585
|
-
return {
|
|
1586
|
-
type: 'DeviceStatus',
|
|
1587
1752
|
message: {
|
|
1588
|
-
device_id: 'PRO2-DEVICE-ID',
|
|
1589
1753
|
unlocked: true,
|
|
1590
|
-
|
|
1754
|
+
unlocked_attach_pin: true,
|
|
1755
|
+
passphrase_protection: true,
|
|
1591
1756
|
},
|
|
1592
1757
|
};
|
|
1593
1758
|
}
|
|
@@ -1608,22 +1773,24 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1608
1773
|
|
|
1609
1774
|
const features = await device.unlockDevice();
|
|
1610
1775
|
|
|
1611
|
-
expect(typedCall.mock.calls).toEqual([
|
|
1612
|
-
['DeviceSessionAskPin', 'DeviceSessionPinResult'],
|
|
1613
|
-
['DeviceStatusGet', 'DeviceStatus', {}],
|
|
1614
|
-
]);
|
|
1776
|
+
expect(typedCall.mock.calls).toEqual([['DeviceSessionAskPin', 'DeviceSessionPinResult']]);
|
|
1615
1777
|
expect(typedCall).not.toHaveBeenCalledWith('GetAddress', 'Address', expect.anything());
|
|
1616
1778
|
expect(typedCall).not.toHaveBeenCalledWith('GetFeatures', 'Features', {});
|
|
1617
1779
|
expect(features).toMatchObject({
|
|
1618
1780
|
deviceType: 'pro2',
|
|
1619
|
-
deviceId: 'PRO2-DEVICE-ID',
|
|
1620
1781
|
firmwareVersion: '1.2.3',
|
|
1621
1782
|
unlocked: true,
|
|
1783
|
+
unlockedAttachPin: true,
|
|
1622
1784
|
passphraseProtection: true,
|
|
1623
1785
|
});
|
|
1786
|
+
expect(features.raw?.protocolV2DeviceInfo?.status).toMatchObject({
|
|
1787
|
+
unlocked: true,
|
|
1788
|
+
unlocked_by_attach_to_pin: true,
|
|
1789
|
+
passphrase_enabled: true,
|
|
1790
|
+
});
|
|
1624
1791
|
});
|
|
1625
1792
|
|
|
1626
|
-
test('syncs Protocol V2 features passphrase state from
|
|
1793
|
+
test('syncs Protocol V2 features passphrase state from DeviceSessionPinResult after unlock', async () => {
|
|
1627
1794
|
const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
|
|
1628
1795
|
(device as any).features = normalizeProtocolV2Features(
|
|
1629
1796
|
{ ...descriptor, protocolType: 'V2' } as any,
|
|
@@ -1637,20 +1804,10 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1637
1804
|
if (requestType === 'DeviceSessionAskPin') {
|
|
1638
1805
|
return {
|
|
1639
1806
|
type: 'DeviceSessionPinResult',
|
|
1640
|
-
message: {
|
|
1641
|
-
unlocked: false,
|
|
1642
|
-
unlocked_attach_pin: false,
|
|
1643
|
-
passphrase_protection: false,
|
|
1644
|
-
},
|
|
1645
|
-
};
|
|
1646
|
-
}
|
|
1647
|
-
if (requestType === 'DeviceStatusGet') {
|
|
1648
|
-
return {
|
|
1649
|
-
type: 'DeviceStatus',
|
|
1650
1807
|
message: {
|
|
1651
1808
|
unlocked: true,
|
|
1652
|
-
|
|
1653
|
-
|
|
1809
|
+
unlocked_attach_pin: true,
|
|
1810
|
+
passphrase_protection: true,
|
|
1654
1811
|
},
|
|
1655
1812
|
};
|
|
1656
1813
|
}
|
|
@@ -1660,7 +1817,9 @@ describe('Protocol V2 feature adapter', () => {
|
|
|
1660
1817
|
|
|
1661
1818
|
await device.unlockDevice();
|
|
1662
1819
|
|
|
1820
|
+
expect(typedCall.mock.calls).toEqual([['DeviceSessionAskPin', 'DeviceSessionPinResult']]);
|
|
1663
1821
|
expect((device as any).profile).toBeUndefined();
|
|
1822
|
+
expect(device.features?.unlocked).toBe(true);
|
|
1664
1823
|
expect(device.features?.passphraseProtection).toBe(true);
|
|
1665
1824
|
expect(device.features?.unlockedAttachPin).toBe(true);
|
|
1666
1825
|
});
|
|
@@ -2818,6 +2977,16 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
2818
2977
|
payload: {
|
|
2819
2978
|
method: 'firmwareUpdateV4',
|
|
2820
2979
|
platform: 'web',
|
|
2980
|
+
targetsToUpdate: [
|
|
2981
|
+
'boot',
|
|
2982
|
+
'app_v1',
|
|
2983
|
+
'app_v2',
|
|
2984
|
+
'coprocessor',
|
|
2985
|
+
'se01',
|
|
2986
|
+
'se02',
|
|
2987
|
+
'se03',
|
|
2988
|
+
'se04',
|
|
2989
|
+
],
|
|
2821
2990
|
},
|
|
2822
2991
|
});
|
|
2823
2992
|
method.init();
|
|
@@ -2931,6 +3100,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
2931
3100
|
payload: {
|
|
2932
3101
|
method: 'firmwareUpdateV4',
|
|
2933
3102
|
platform: 'web',
|
|
3103
|
+
targetsToUpdate: ['resource'],
|
|
2934
3104
|
},
|
|
2935
3105
|
});
|
|
2936
3106
|
method.init();
|
|
@@ -2980,7 +3150,7 @@ describe('Protocol V2 firmware update targets', () => {
|
|
|
2980
3150
|
getFirmwareLatestReleaseSpy.mockRestore();
|
|
2981
3151
|
});
|
|
2982
3152
|
|
|
2983
|
-
test('
|
|
3153
|
+
test('does not download Pro2 firmware components that App did not select', async () => {
|
|
2984
3154
|
const method = new FirmwareUpdateV4({
|
|
2985
3155
|
id: 1,
|
|
2986
3156
|
payload: {
|
|
@@ -3671,6 +3841,102 @@ describe('Protocol V2 reboot methods', () => {
|
|
|
3671
3841
|
});
|
|
3672
3842
|
});
|
|
3673
3843
|
|
|
3844
|
+
describe('Protocol V2 protected method execution', () => {
|
|
3845
|
+
const deviceLockedError = () =>
|
|
3846
|
+
Object.assign(new Error('Device locked'), {
|
|
3847
|
+
errorCode: HardwareErrorCode.DeviceLocked,
|
|
3848
|
+
});
|
|
3849
|
+
|
|
3850
|
+
test('unlocks and retries an opted-in Protocol V2 method once', async () => {
|
|
3851
|
+
const calls: string[] = [];
|
|
3852
|
+
const method = {
|
|
3853
|
+
unlockPolicy: 'retry-on-locked',
|
|
3854
|
+
run: jest
|
|
3855
|
+
.fn()
|
|
3856
|
+
.mockImplementationOnce(async () => {
|
|
3857
|
+
calls.push('run-1');
|
|
3858
|
+
throw deviceLockedError();
|
|
3859
|
+
})
|
|
3860
|
+
.mockImplementationOnce(async () => {
|
|
3861
|
+
calls.push('run-2');
|
|
3862
|
+
return { message: 'ok' };
|
|
3863
|
+
}),
|
|
3864
|
+
};
|
|
3865
|
+
const device = {
|
|
3866
|
+
isProtocolV2: () => true,
|
|
3867
|
+
unlockDevice: jest.fn(async () => {
|
|
3868
|
+
calls.push('unlock');
|
|
3869
|
+
}),
|
|
3870
|
+
};
|
|
3871
|
+
|
|
3872
|
+
await expect(runMethodWithUnlockRetry(method as any, device as any)).resolves.toEqual({
|
|
3873
|
+
message: 'ok',
|
|
3874
|
+
});
|
|
3875
|
+
expect(calls).toEqual(['run-1', 'unlock', 'run-2']);
|
|
3876
|
+
});
|
|
3877
|
+
|
|
3878
|
+
test('does not unlock a Protocol V1 device or a method without the policy', async () => {
|
|
3879
|
+
for (const [isProtocolV2, unlockPolicy] of [
|
|
3880
|
+
[false, 'retry-on-locked'],
|
|
3881
|
+
[true, 'none'],
|
|
3882
|
+
] as const) {
|
|
3883
|
+
const error = deviceLockedError();
|
|
3884
|
+
const method = {
|
|
3885
|
+
unlockPolicy,
|
|
3886
|
+
run: jest.fn(async () => {
|
|
3887
|
+
throw error;
|
|
3888
|
+
}),
|
|
3889
|
+
};
|
|
3890
|
+
const device = {
|
|
3891
|
+
isProtocolV2: () => isProtocolV2,
|
|
3892
|
+
unlockDevice: jest.fn(),
|
|
3893
|
+
};
|
|
3894
|
+
|
|
3895
|
+
await expect(runMethodWithUnlockRetry(method as any, device as any)).rejects.toBe(error);
|
|
3896
|
+
expect(device.unlockDevice).not.toHaveBeenCalled();
|
|
3897
|
+
expect(method.run).toHaveBeenCalledTimes(1);
|
|
3898
|
+
}
|
|
3899
|
+
});
|
|
3900
|
+
|
|
3901
|
+
test('does not retry when unlock fails or when the retry is still locked', async () => {
|
|
3902
|
+
const initialError = deviceLockedError();
|
|
3903
|
+
const unlockError = new Error('PIN cancelled');
|
|
3904
|
+
const unlockFailMethod = {
|
|
3905
|
+
unlockPolicy: 'retry-on-locked',
|
|
3906
|
+
run: jest.fn(async () => {
|
|
3907
|
+
throw initialError;
|
|
3908
|
+
}),
|
|
3909
|
+
};
|
|
3910
|
+
const unlockFailDevice = {
|
|
3911
|
+
isProtocolV2: () => true,
|
|
3912
|
+
unlockDevice: jest.fn(async () => {
|
|
3913
|
+
throw unlockError;
|
|
3914
|
+
}),
|
|
3915
|
+
};
|
|
3916
|
+
|
|
3917
|
+
await expect(
|
|
3918
|
+
runMethodWithUnlockRetry(unlockFailMethod as any, unlockFailDevice as any)
|
|
3919
|
+
).rejects.toBe(unlockError);
|
|
3920
|
+
expect(unlockFailMethod.run).toHaveBeenCalledTimes(1);
|
|
3921
|
+
|
|
3922
|
+
const retryError = deviceLockedError();
|
|
3923
|
+
const retryFailMethod = {
|
|
3924
|
+
unlockPolicy: 'retry-on-locked',
|
|
3925
|
+
run: jest.fn().mockRejectedValueOnce(initialError).mockRejectedValueOnce(retryError),
|
|
3926
|
+
};
|
|
3927
|
+
const retryFailDevice = {
|
|
3928
|
+
isProtocolV2: () => true,
|
|
3929
|
+
unlockDevice: jest.fn(async () => undefined),
|
|
3930
|
+
};
|
|
3931
|
+
|
|
3932
|
+
await expect(
|
|
3933
|
+
runMethodWithUnlockRetry(retryFailMethod as any, retryFailDevice as any)
|
|
3934
|
+
).rejects.toBe(retryError);
|
|
3935
|
+
expect(retryFailMethod.run).toHaveBeenCalledTimes(2);
|
|
3936
|
+
expect(retryFailDevice.unlockDevice).toHaveBeenCalledTimes(1);
|
|
3937
|
+
});
|
|
3938
|
+
});
|
|
3939
|
+
|
|
3674
3940
|
describe('Protocol V2 current low-level methods', () => {
|
|
3675
3941
|
test('sends ProtocolInfoRequest from protocolInfoRequest', async () => {
|
|
3676
3942
|
const typedCall = jest.fn().mockResolvedValue({ message: { version: 1 } });
|
|
@@ -3724,6 +3990,106 @@ describe('Protocol V2 current low-level methods', () => {
|
|
|
3724
3990
|
expect(typedCall).toHaveBeenLastCalledWith('DeviceFactoryInfoGet', 'DeviceFactoryInfo', {});
|
|
3725
3991
|
});
|
|
3726
3992
|
|
|
3993
|
+
test('gets Protocol V2 device settings', async () => {
|
|
3994
|
+
const typedCall = jest.fn().mockResolvedValue({ message: { brightness: 80 } });
|
|
3995
|
+
const method = new DeviceSettingsGet({
|
|
3996
|
+
id: 1,
|
|
3997
|
+
payload: { method: 'deviceSettingsGet' },
|
|
3998
|
+
});
|
|
3999
|
+
method.init();
|
|
4000
|
+
(method as any).device = stubDevice({ commands: { typedCall } });
|
|
4001
|
+
|
|
4002
|
+
await expect(method.run()).resolves.toEqual({ brightness: 80 });
|
|
4003
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceSettingsGet', 'DeviceSettings', {});
|
|
4004
|
+
});
|
|
4005
|
+
|
|
4006
|
+
test('sets Protocol V2 device settings without passphrase fields', async () => {
|
|
4007
|
+
const typedCall = jest.fn().mockResolvedValue({ message: { message: 'ok' } });
|
|
4008
|
+
const method = new DeviceSettingsSet({
|
|
4009
|
+
id: 1,
|
|
4010
|
+
payload: {
|
|
4011
|
+
method: 'deviceSettingsSet',
|
|
4012
|
+
settings: {
|
|
4013
|
+
label: 'My Pro 2',
|
|
4014
|
+
brightness: 80,
|
|
4015
|
+
airgap_mode: true,
|
|
4016
|
+
passphrase_enable: true,
|
|
4017
|
+
},
|
|
4018
|
+
},
|
|
4019
|
+
});
|
|
4020
|
+
method.init();
|
|
4021
|
+
(method as any).device = stubDevice({ commands: { typedCall } });
|
|
4022
|
+
|
|
4023
|
+
await method.run();
|
|
4024
|
+
|
|
4025
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceSettingsSet', 'Success', {
|
|
4026
|
+
settings: {
|
|
4027
|
+
label: 'My Pro 2',
|
|
4028
|
+
brightness: 80,
|
|
4029
|
+
},
|
|
4030
|
+
});
|
|
4031
|
+
});
|
|
4032
|
+
|
|
4033
|
+
test('opens non-passphrase Protocol V2 settings pages', async () => {
|
|
4034
|
+
const typedCall = jest.fn().mockResolvedValue({ message: { message: 'ok' } });
|
|
4035
|
+
const method = new DeviceSettingsPageShow({
|
|
4036
|
+
id: 1,
|
|
4037
|
+
payload: {
|
|
4038
|
+
method: 'deviceSettingsPageShow',
|
|
4039
|
+
page: 'DeviceAirgap',
|
|
4040
|
+
fieldName: 'airgap_mode',
|
|
4041
|
+
},
|
|
4042
|
+
});
|
|
4043
|
+
method.init();
|
|
4044
|
+
(method as any).device = stubDevice({ commands: { typedCall } });
|
|
4045
|
+
|
|
4046
|
+
await method.run();
|
|
4047
|
+
|
|
4048
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceSettingsPageShow', 'Success', {
|
|
4049
|
+
page: 3,
|
|
4050
|
+
field_name: 'airgap_mode',
|
|
4051
|
+
});
|
|
4052
|
+
});
|
|
4053
|
+
|
|
4054
|
+
test('opens the Protocol V2 passphrase settings page', async () => {
|
|
4055
|
+
const typedCall = jest.fn().mockResolvedValue({ message: { message: 'ok' } });
|
|
4056
|
+
const method = new DeviceSettingsPageShow({
|
|
4057
|
+
id: 1,
|
|
4058
|
+
payload: {
|
|
4059
|
+
method: 'deviceSettingsPageShow',
|
|
4060
|
+
page: 'DevicePassphrase',
|
|
4061
|
+
},
|
|
4062
|
+
});
|
|
4063
|
+
method.init();
|
|
4064
|
+
(method as any).device = stubDevice({ commands: { typedCall } });
|
|
4065
|
+
|
|
4066
|
+
await method.run();
|
|
4067
|
+
|
|
4068
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceSettingsPageShow', 'Success', {
|
|
4069
|
+
page: 2,
|
|
4070
|
+
field_name: undefined,
|
|
4071
|
+
});
|
|
4072
|
+
});
|
|
4073
|
+
|
|
4074
|
+
test('marks Protocol V2 settings methods for unlock-on-locked retry', () => {
|
|
4075
|
+
const methods = [
|
|
4076
|
+
new DeviceSettingsGet({ id: 1, payload: { method: 'deviceSettingsGet' } }),
|
|
4077
|
+
new DeviceSettingsSet({
|
|
4078
|
+
id: 2,
|
|
4079
|
+
payload: { method: 'deviceSettingsSet', settings: { brightness: 80 } },
|
|
4080
|
+
}),
|
|
4081
|
+
new DeviceSettingsPageShow({
|
|
4082
|
+
id: 3,
|
|
4083
|
+
payload: { method: 'deviceSettingsPageShow', page: 'DevicePassphrase' },
|
|
4084
|
+
}),
|
|
4085
|
+
];
|
|
4086
|
+
|
|
4087
|
+
methods.forEach(method => {
|
|
4088
|
+
method.init();
|
|
4089
|
+
expect(method.unlockPolicy).toBe('retry-on-locked');
|
|
4090
|
+
});
|
|
4091
|
+
});
|
|
4092
|
+
|
|
3727
4093
|
test('sends FilesystemPermissionFix from filesystemPermissionFix', async () => {
|
|
3728
4094
|
const typedCall = jest.fn().mockResolvedValue({ message: {} });
|
|
3729
4095
|
const method = new FilesystemPermissionFix({
|
package/dist/api/BaseMethod.d.ts
CHANGED
|
@@ -4,7 +4,9 @@ import type { DeviceFirmwareRange } from '../types';
|
|
|
4
4
|
import type { CoreMessage } from '../events';
|
|
5
5
|
import type { RequestContext } from '../utils/tracing';
|
|
6
6
|
import type { CoreContext } from '../core';
|
|
7
|
+
export type UnlockPolicy = 'none' | 'retry-on-locked';
|
|
7
8
|
export declare abstract class BaseMethod<Params = undefined> {
|
|
9
|
+
abortSignal?: AbortSignal;
|
|
8
10
|
responseID: number;
|
|
9
11
|
device: Device;
|
|
10
12
|
params: Params;
|
|
@@ -30,6 +32,8 @@ export declare abstract class BaseMethod<Params = undefined> {
|
|
|
30
32
|
getPreWarmKey(): string;
|
|
31
33
|
strictCheckDeviceSupport: boolean;
|
|
32
34
|
requireProtocolV2: boolean;
|
|
35
|
+
unlockPolicy: UnlockPolicy;
|
|
36
|
+
protected throwIfAborted(): void;
|
|
33
37
|
postMessage: (message: CoreMessage) => void;
|
|
34
38
|
context?: CoreContext;
|
|
35
39
|
temporarySafetyCheckPrompted: boolean;
|