@onekeyfe/hd-core 1.2.0-alpha.99 → 1.2.1-alpha.0
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__/AllNetworkGetAddressBase.tracing.test.ts +20 -4
- package/__tests__/DeviceCommands.test.ts +203 -27
- package/__tests__/base64Data.test.ts +69 -0
- package/__tests__/check-all-firmware-release-protocol-v2.test.ts +48 -2
- package/__tests__/check-firmware-release-protocol-v2.test.ts +82 -13
- package/__tests__/device-connector-protocol.test.ts +21 -0
- package/__tests__/device-lifecycle-events.test.ts +427 -7
- package/__tests__/device-pool-state.test.ts +25 -0
- package/__tests__/device-settings.test.ts +237 -9
- package/__tests__/device-state-events.test.ts +2 -2
- package/__tests__/device-state-mapper.test.ts +61 -5
- package/__tests__/device-utils.test.ts +15 -1
- package/__tests__/device-wallet-session-store.test.ts +147 -21
- package/__tests__/deviceSettings.test.ts +7 -3
- package/__tests__/deviceUploadNft.test.ts +36 -4
- package/__tests__/firmware-update/device-update-bootloader.test.ts +13 -3
- package/__tests__/firmware-update/firmware-artifact-source.test.ts +61 -9
- package/__tests__/firmware-update/firmware-prepared-host-digest.test.ts +19 -9
- package/__tests__/firmware-update/firmware-prepared-resource-archive.test.ts +21 -0
- package/__tests__/firmware-update/firmware-update-bootloader-poll.test.ts +63 -0
- package/__tests__/firmware-update/firmware-update-plan.test.ts +59 -55
- package/__tests__/firmware-update/firmware-update-prepared-plan.test.ts +19 -5
- package/__tests__/firmware-update/firmware-update-v3-reconnect.test.ts +116 -0
- package/__tests__/firmware-update/firmware-update-v4-install-poll.test.ts +979 -0
- package/__tests__/firmware-update/firmware-update-v4-parameters.test.ts +85 -0
- package/__tests__/get-device-state.test.ts +60 -21
- package/__tests__/logBlockEvent.test.ts +13 -1
- package/__tests__/open-wallet-session.test.ts +127 -1
- package/__tests__/protocol-v2-firmware-targets.test.ts +23 -0
- package/__tests__/protocol-v2-resources.test.ts +185 -88
- package/__tests__/protocol-v2-unlock-policy.test.ts +235 -2
- package/__tests__/protocol-v2.test.ts +2199 -486
- package/__tests__/refresh-device-state.test.ts +3 -1
- package/__tests__/resourceBase64Boundary.test.ts +48 -0
- package/__tests__/search-devices.test.ts +79 -32
- package/__tests__/ton-sign-message.test.ts +84 -0
- package/dist/api/BaseMethod.d.ts +2 -1
- package/dist/api/BaseMethod.d.ts.map +1 -1
- package/dist/api/CheckAllFirmwareRelease.d.ts.map +1 -1
- package/dist/api/CheckBootloaderRelease.d.ts.map +1 -1
- package/dist/api/FirmwareUpdateV2.d.ts.map +1 -1
- package/dist/api/FirmwareUpdateV3.d.ts.map +1 -1
- package/dist/api/FirmwareUpdateV4.d.ts +20 -6
- package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
- package/dist/api/OpenWalletSession.d.ts.map +1 -1
- package/dist/api/SearchDevices.d.ts.map +1 -1
- package/dist/api/UploadPortfolio.d.ts +1 -1
- package/dist/api/UploadPortfolio.d.ts.map +1 -1
- package/dist/api/allnetwork/AllNetworkGetAddressBase.d.ts.map +1 -1
- package/dist/api/device/DeviceChangePin.d.ts.map +1 -1
- package/dist/api/device/DeviceRebootToBoardloader.d.ts.map +1 -1
- package/dist/api/device/DeviceRebootToBootloader.d.ts.map +1 -1
- package/dist/api/device/DeviceSettings.d.ts.map +1 -1
- package/dist/api/device/DeviceVerify.d.ts +1 -0
- package/dist/api/device/DeviceVerify.d.ts.map +1 -1
- package/dist/api/device/DeviceWipe.d.ts.map +1 -1
- package/dist/api/firmware/FirmwareArtifactSource.d.ts +5 -1
- package/dist/api/firmware/FirmwareArtifactSource.d.ts.map +1 -1
- package/dist/api/firmware/FirmwarePreparedResourceArchive.d.ts.map +1 -1
- package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts +2 -2
- package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts.map +1 -1
- package/dist/api/firmware/FirmwareUpdatePlan.d.ts +0 -15
- package/dist/api/firmware/FirmwareUpdatePlan.d.ts.map +1 -1
- package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts.map +1 -1
- package/dist/api/firmware/getBinary.d.ts +3 -2
- package/dist/api/firmware/getBinary.d.ts.map +1 -1
- package/dist/api/firmware/protocolV2Release.d.ts.map +1 -1
- package/dist/api/helpers/base64Data.d.ts +16 -0
- package/dist/api/helpers/base64Data.d.ts.map +1 -0
- package/dist/api/protocol-v2/DeviceFirmwareUpdate.d.ts +3 -1
- package/dist/api/protocol-v2/DeviceFirmwareUpdate.d.ts.map +1 -1
- package/dist/api/protocol-v2/DeviceGetFirmwareUpdateStatus.d.ts.map +1 -1
- package/dist/api/protocol-v2/DeviceInfoGet.d.ts +1 -1
- package/dist/api/protocol-v2/DeviceInfoGet.d.ts.map +1 -1
- package/dist/api/protocol-v2/DeviceReboot.d.ts +1 -1
- package/dist/api/protocol-v2/DeviceReboot.d.ts.map +1 -1
- package/dist/api/protocol-v2/DeviceUploadNft.d.ts +2 -3
- package/dist/api/protocol-v2/DeviceUploadNft.d.ts.map +1 -1
- package/dist/api/protocol-v2/DeviceUploadWallpaper.d.ts +2 -3
- package/dist/api/protocol-v2/DeviceUploadWallpaper.d.ts.map +1 -1
- package/dist/api/protocol-v2/helpers.d.ts.map +1 -1
- package/dist/api/ton/TonSignMessage.d.ts.map +1 -1
- package/dist/core/index.d.ts +6 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/data-manager/DataManager.d.ts +4 -3
- package/dist/data-manager/DataManager.d.ts.map +1 -1
- package/dist/data-manager/TransportManager.d.ts.map +1 -1
- package/dist/device/Device.d.ts +16 -3
- 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/device/DeviceConnector.d.ts +1 -1
- package/dist/device/DeviceConnector.d.ts.map +1 -1
- package/dist/device/DevicePool.d.ts.map +1 -1
- package/dist/device/DeviceStateMapper.d.ts.map +1 -1
- package/dist/deviceProfile/buildDeviceFeatures.d.ts.map +1 -1
- package/dist/events/logBlockEvent.d.ts.map +1 -1
- package/dist/events/ui-request.d.ts +3 -0
- package/dist/events/ui-request.d.ts.map +1 -1
- package/dist/index.d.ts +54 -84
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2150 -1469
- package/dist/protocols/protocol-v2/features.d.ts +13 -5
- 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/protocols/protocol-v2/resources.d.ts +12 -7
- package/dist/protocols/protocol-v2/resources.d.ts.map +1 -1
- package/dist/protocols/protocol-v2/unlockPolicyRunner.d.ts +3 -1
- package/dist/protocols/protocol-v2/unlockPolicyRunner.d.ts.map +1 -1
- package/dist/protocols/protocol-v2/walletSession.d.ts +3 -0
- package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
- package/dist/types/api/firmwareUpdatePlan.d.ts +2 -2
- package/dist/types/api/firmwareUpdatePlan.d.ts.map +1 -1
- package/dist/types/api/protocolV2.d.ts +1 -1
- package/dist/types/api/protocolV2.d.ts.map +1 -1
- package/dist/types/device.d.ts.map +1 -1
- package/dist/types/settings.d.ts +3 -31
- package/dist/types/settings.d.ts.map +1 -1
- package/dist/utils/deviceFeaturesCompat.d.ts.map +1 -1
- package/dist/utils/deviceFeaturesUtils.d.ts +3 -0
- package/dist/utils/deviceFeaturesUtils.d.ts.map +1 -1
- package/dist/utils/deviceInfoUtils.d.ts.map +1 -1
- package/dist/utils/deviceSettings.d.ts +1 -0
- package/dist/utils/deviceSettings.d.ts.map +1 -1
- package/dist/utils/index.d.ts +1 -1
- 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/pro2Nft.d.ts +7 -0
- package/dist/utils/pro2Nft.d.ts.map +1 -1
- package/package.json +6 -4
- package/src/api/BaseMethod.ts +8 -1
- package/src/api/CheckAllFirmwareRelease.ts +2 -4
- package/src/api/CheckBLEFirmwareRelease.ts +2 -2
- package/src/api/CheckBootloaderRelease.ts +3 -2
- package/src/api/CheckFirmwareRelease.ts +2 -2
- package/src/api/FirmwareUpdateV2.ts +12 -9
- package/src/api/FirmwareUpdateV3.ts +29 -19
- package/src/api/FirmwareUpdateV4.ts +934 -739
- package/src/api/OpenWalletSession.ts +39 -9
- package/src/api/PromptWebDeviceAccess.ts +2 -2
- package/src/api/SearchDevices.ts +6 -3
- package/src/api/UploadPortfolio.ts +9 -2
- package/src/api/allnetwork/AllNetworkGetAddressBase.ts +2 -1
- package/src/api/device/DeviceChangePin.ts +4 -1
- package/src/api/device/DeviceRebootToBoardloader.ts +3 -1
- package/src/api/device/DeviceRebootToBootloader.ts +3 -1
- package/src/api/device/DeviceSettings.ts +34 -20
- package/src/api/device/DeviceVerify.ts +25 -0
- package/src/api/device/DeviceWipe.ts +4 -1
- package/src/api/firmware/FirmwareArtifactSource.ts +41 -14
- package/src/api/firmware/FirmwarePreparedResourceArchive.ts +16 -27
- package/src/api/firmware/FirmwareUpdateBaseMethod.ts +9 -3
- package/src/api/firmware/FirmwareUpdatePlan.ts +37 -77
- package/src/api/firmware/FirmwareUpdatePreparedPlan.ts +10 -6
- package/src/api/firmware/protocolV2Release.ts +7 -2
- package/src/api/helpers/base64Data.ts +85 -0
- package/src/api/protocol-v2/DeviceFactoryInfoSet.ts +1 -1
- package/src/api/protocol-v2/DeviceFirmwareUpdate.ts +6 -28
- package/src/api/protocol-v2/DeviceGetFirmwareUpdateStatus.ts +7 -1
- package/src/api/protocol-v2/DeviceInfoGet.ts +3 -3
- package/src/api/protocol-v2/DeviceReboot.ts +5 -0
- package/src/api/protocol-v2/DeviceUploadNft.ts +61 -9
- package/src/api/protocol-v2/DeviceUploadWallpaper.ts +53 -19
- package/src/api/protocol-v2/helpers.ts +8 -0
- package/src/api/ton/TonSignMessage.ts +5 -3
- package/src/core/index.ts +264 -55
- package/src/data/messages/messages-protocol-v2.json +140 -13
- package/src/data-manager/DataManager.ts +46 -39
- package/src/data-manager/TransportManager.ts +3 -1
- package/src/device/Device.ts +210 -44
- package/src/device/DeviceCommands.ts +27 -18
- package/src/device/DeviceConnector.ts +17 -3
- package/src/device/DevicePool.ts +9 -2
- package/src/device/DeviceStateMapper.ts +30 -24
- package/src/deviceProfile/buildDeviceFeatures.ts +10 -5
- package/src/events/logBlockEvent.ts +14 -1
- package/src/events/ui-request.ts +3 -0
- package/src/index.ts +0 -10
- package/src/protocols/protocol-v2/features.ts +22 -5
- package/src/protocols/protocol-v2/index.ts +2 -0
- package/src/protocols/protocol-v2/resources.ts +110 -147
- package/src/protocols/protocol-v2/unlockPolicyRunner.ts +13 -1
- package/src/protocols/protocol-v2/walletSession.ts +65 -22
- package/src/types/api/firmwareUpdate.ts +1 -1
- package/src/types/api/firmwareUpdatePlan.ts +2 -2
- package/src/types/api/protocolV2.ts +1 -1
- package/src/types/device.ts +2 -1
- package/src/types/settings.ts +6 -34
- package/src/utils/deviceFeaturesCompat.ts +9 -4
- package/src/utils/deviceFeaturesUtils.ts +4 -0
- package/src/utils/deviceInfoUtils.ts +9 -2
- package/src/utils/deviceSettings.ts +11 -4
- package/src/utils/index.ts +1 -0
- package/src/utils/pro2Nft.ts +31 -8
- package/__tests__/firmware-memory-host.test.ts +0 -126
- package/dist/api/firmware/FirmwareMemoryHost.d.ts +0 -22
- package/dist/api/firmware/FirmwareMemoryHost.d.ts.map +0 -1
- package/src/api/firmware/FirmwareMemoryHost.ts +0 -143
|
@@ -352,7 +352,75 @@ describe('Protocol V1 wallet identity initialization', () => {
|
|
|
352
352
|
jest.restoreAllMocks();
|
|
353
353
|
});
|
|
354
354
|
|
|
355
|
-
test('
|
|
355
|
+
test('sends no wallet context before identity is proven on first contact', async () => {
|
|
356
|
+
const device = Device.fromDescriptor({ id: 'connect-a', path: 'connect-a' } as never);
|
|
357
|
+
// No cached features: there is no local evidence which device sits here.
|
|
358
|
+
deviceWalletSessionStore.set('device-a', 'hidden-a', 'session-a');
|
|
359
|
+
const typedCall = jest.fn().mockResolvedValue({
|
|
360
|
+
type: 'Features',
|
|
361
|
+
message: { device_id: 'intruder-device', session_id: 'intruder-session' },
|
|
362
|
+
});
|
|
363
|
+
device.commands = { typedCall } as never;
|
|
364
|
+
jest.spyOn(TransportManager, 'reconfigure').mockResolvedValue(undefined);
|
|
365
|
+
|
|
366
|
+
await expect(
|
|
367
|
+
device.initialize({ deviceId: 'device-a', passphraseState: 'hidden-a' })
|
|
368
|
+
).rejects.toMatchObject({ errorCode: HardwareErrorCode.DeviceCheckDeviceIdError });
|
|
369
|
+
|
|
370
|
+
// Exactly one context-free Initialize: neither the cached session_id nor
|
|
371
|
+
// the passphrase_state may reach an unproven device.
|
|
372
|
+
expect(typedCall).toHaveBeenCalledTimes(1);
|
|
373
|
+
expect(typedCall).toHaveBeenCalledWith(
|
|
374
|
+
'Initialize',
|
|
375
|
+
'Features',
|
|
376
|
+
{ is_contains_attach: true },
|
|
377
|
+
expect.any(Object)
|
|
378
|
+
);
|
|
379
|
+
// The expected device's cached session is untouched.
|
|
380
|
+
expect(deviceWalletSessionStore.get('device-a', 'hidden-a')).toBe('session-a');
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
test('proves identity first, then resumes the wallet session on first contact', async () => {
|
|
384
|
+
const device = Device.fromDescriptor({ id: 'connect-a', path: 'connect-a' } as never);
|
|
385
|
+
deviceWalletSessionStore.set('device-a', 'hidden-a', 'session-a');
|
|
386
|
+
const typedCall = jest
|
|
387
|
+
.fn()
|
|
388
|
+
.mockResolvedValueOnce({
|
|
389
|
+
type: 'Features',
|
|
390
|
+
message: { device_id: 'device-a', session_id: 'standard-session' },
|
|
391
|
+
})
|
|
392
|
+
.mockResolvedValueOnce({
|
|
393
|
+
type: 'Features',
|
|
394
|
+
message: { device_id: 'device-a', session_id: 'session-a' },
|
|
395
|
+
});
|
|
396
|
+
device.commands = { typedCall } as never;
|
|
397
|
+
jest.spyOn(TransportManager, 'reconfigure').mockResolvedValue(undefined);
|
|
398
|
+
|
|
399
|
+
await device.initialize({ deviceId: 'device-a', passphraseState: 'hidden-a' });
|
|
400
|
+
|
|
401
|
+
expect(typedCall).toHaveBeenCalledTimes(2);
|
|
402
|
+
expect(typedCall).toHaveBeenNthCalledWith(
|
|
403
|
+
1,
|
|
404
|
+
'Initialize',
|
|
405
|
+
'Features',
|
|
406
|
+
{ is_contains_attach: true },
|
|
407
|
+
expect.any(Object)
|
|
408
|
+
);
|
|
409
|
+
expect(typedCall).toHaveBeenNthCalledWith(
|
|
410
|
+
2,
|
|
411
|
+
'Initialize',
|
|
412
|
+
'Features',
|
|
413
|
+
expect.objectContaining({
|
|
414
|
+
session_id: 'session-a',
|
|
415
|
+
passphrase_state: 'hidden-a',
|
|
416
|
+
}),
|
|
417
|
+
expect.any(Object)
|
|
418
|
+
);
|
|
419
|
+
// The context-free identity call must not overwrite the cached session.
|
|
420
|
+
expect(deviceWalletSessionStore.get('device-a', 'hidden-a')).toBe('session-a');
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
test('purges cached sessions and rejects when the live device identity changes', async () => {
|
|
356
424
|
const device = Device.fromDescriptor({ id: 'connect-b', path: 'connect-b' } as never);
|
|
357
425
|
device.features = {
|
|
358
426
|
protocol: 'V1',
|
|
@@ -363,7 +431,7 @@ describe('Protocol V1 wallet identity initialization', () => {
|
|
|
363
431
|
deviceWalletSessionStore.set('cached-device-a', 'hidden-a', 'session-a');
|
|
364
432
|
const typedCall = jest.fn().mockResolvedValue({
|
|
365
433
|
type: 'Features',
|
|
366
|
-
message: { device_id: 'live-device-b' },
|
|
434
|
+
message: { device_id: 'live-device-b', session_id: 'wrong-device-session' },
|
|
367
435
|
});
|
|
368
436
|
device.commands = { typedCall } as never;
|
|
369
437
|
jest.spyOn(TransportManager, 'reconfigure').mockResolvedValue(undefined);
|
|
@@ -371,11 +439,76 @@ describe('Protocol V1 wallet identity initialization', () => {
|
|
|
371
439
|
await expect(
|
|
372
440
|
device.initialize({ deviceId: 'cached-device-a', passphraseState: 'hidden-a' })
|
|
373
441
|
).rejects.toMatchObject({ errorCode: HardwareErrorCode.DeviceCheckDeviceIdError });
|
|
442
|
+
// Identity is validated from the single Initialize round trip; no separate
|
|
443
|
+
// GetFeatures preflight.
|
|
374
444
|
expect(typedCall).toHaveBeenCalledTimes(1);
|
|
375
|
-
expect(typedCall).toHaveBeenCalledWith(
|
|
445
|
+
expect(typedCall).toHaveBeenCalledWith(
|
|
446
|
+
'Initialize',
|
|
447
|
+
'Features',
|
|
448
|
+
expect.objectContaining({ passphrase_state: 'hidden-a' }),
|
|
449
|
+
expect.any(Object)
|
|
450
|
+
);
|
|
451
|
+
// Identity change purges the previous device's cached sessions (pre-existing
|
|
452
|
+
// reconcileDeviceIdentity behavior — never carry sessions across devices)…
|
|
453
|
+
expect(deviceWalletSessionStore.get('cached-device-a', 'hidden-a')).toBeUndefined();
|
|
454
|
+
// …and the session the mismatched Initialize cached under the wrong
|
|
455
|
+
// device's identity is dropped as well.
|
|
456
|
+
expect(deviceWalletSessionStore.get('live-device-b', 'hidden-a')).toBeUndefined();
|
|
376
457
|
});
|
|
377
458
|
|
|
378
|
-
test('
|
|
459
|
+
test('drops the wrong-device session even when reconfigure fails after Initialize', async () => {
|
|
460
|
+
const device = Device.fromDescriptor({ id: 'connect-b', path: 'connect-b' } as never);
|
|
461
|
+
device.features = {
|
|
462
|
+
protocol: 'V1',
|
|
463
|
+
deviceId: 'cached-device-a',
|
|
464
|
+
unlocked: true,
|
|
465
|
+
passphraseProtection: true,
|
|
466
|
+
} as never;
|
|
467
|
+
deviceWalletSessionStore.set('cached-device-a', 'hidden-a', 'session-a');
|
|
468
|
+
const typedCall = jest.fn().mockResolvedValue({
|
|
469
|
+
type: 'Features',
|
|
470
|
+
message: { device_id: 'live-device-b', session_id: 'wrong-device-session' },
|
|
471
|
+
});
|
|
472
|
+
device.commands = { typedCall } as never;
|
|
473
|
+
// Pre-encode resync succeeds; the post-response reconfigure inside
|
|
474
|
+
// callInitialize rejects — the session write has already happened by then.
|
|
475
|
+
jest
|
|
476
|
+
.spyOn(TransportManager, 'reconfigure')
|
|
477
|
+
.mockResolvedValueOnce(undefined)
|
|
478
|
+
.mockRejectedValueOnce(new Error('configure failed'));
|
|
479
|
+
|
|
480
|
+
await expect(
|
|
481
|
+
device.initialize({ deviceId: 'cached-device-a', passphraseState: 'hidden-a' })
|
|
482
|
+
).rejects.toMatchObject({ errorCode: HardwareErrorCode.DeviceCheckDeviceIdError });
|
|
483
|
+
// The wrong-device session must not survive the error path either.
|
|
484
|
+
expect(deviceWalletSessionStore.get('live-device-b', 'hidden-a')).toBeUndefined();
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
test('re-syncs the V1 message schema for this device before encoding Initialize', async () => {
|
|
488
|
+
const device = Device.fromDescriptor({ id: 'connect-a', path: 'connect-a' } as never);
|
|
489
|
+
device.features = {
|
|
490
|
+
protocol: 'V1',
|
|
491
|
+
deviceId: 'device-a',
|
|
492
|
+
unlocked: true,
|
|
493
|
+
passphraseProtection: true,
|
|
494
|
+
} as never;
|
|
495
|
+
const typedCall = jest.fn().mockResolvedValue({
|
|
496
|
+
type: 'Features',
|
|
497
|
+
message: { device_id: 'device-a' },
|
|
498
|
+
});
|
|
499
|
+
device.commands = { typedCall } as never;
|
|
500
|
+
const reconfigure = jest.spyOn(TransportManager, 'reconfigure').mockResolvedValue(undefined);
|
|
501
|
+
|
|
502
|
+
await device.initialize({ deviceId: 'device-a', passphraseState: 'hidden-a' });
|
|
503
|
+
|
|
504
|
+
// A stale process-global schema (from another device) would silently strip
|
|
505
|
+
// passphrase_state from the wire message, so the resync must come first.
|
|
506
|
+
expect(reconfigure.mock.invocationCallOrder[0]).toBeLessThan(
|
|
507
|
+
typedCall.mock.invocationCallOrder[0]
|
|
508
|
+
);
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
test('resumes a cached V1 wallet with a single identity-validated Initialize', async () => {
|
|
379
512
|
const device = Device.fromDescriptor({ id: 'connect-a', path: 'connect-a' } as never);
|
|
380
513
|
device.features = {
|
|
381
514
|
protocol: 'V1',
|
|
@@ -384,22 +517,17 @@ describe('Protocol V1 wallet identity initialization', () => {
|
|
|
384
517
|
passphraseProtection: true,
|
|
385
518
|
} as never;
|
|
386
519
|
deviceWalletSessionStore.set('device-a', 'hidden-a', 'session-a');
|
|
387
|
-
const typedCall = jest
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
type: 'Features',
|
|
392
|
-
message: { device_id: 'device-a', session_id: 'session-a' },
|
|
393
|
-
});
|
|
520
|
+
const typedCall = jest.fn().mockResolvedValueOnce({
|
|
521
|
+
type: 'Features',
|
|
522
|
+
message: { device_id: 'device-a', session_id: 'session-a' },
|
|
523
|
+
});
|
|
394
524
|
device.commands = { typedCall } as never;
|
|
395
525
|
jest.spyOn(TransportManager, 'reconfigure').mockResolvedValue(undefined);
|
|
396
526
|
|
|
397
527
|
await device.initialize({ deviceId: 'device-a', passphraseState: 'hidden-a' });
|
|
398
528
|
|
|
399
|
-
expect(typedCall).toHaveBeenCalledTimes(
|
|
400
|
-
expect(typedCall).
|
|
401
|
-
expect(typedCall).toHaveBeenNthCalledWith(
|
|
402
|
-
2,
|
|
529
|
+
expect(typedCall).toHaveBeenCalledTimes(1);
|
|
530
|
+
expect(typedCall).toHaveBeenCalledWith(
|
|
403
531
|
'Initialize',
|
|
404
532
|
'Features',
|
|
405
533
|
expect.objectContaining({
|
|
@@ -408,9 +536,10 @@ describe('Protocol V1 wallet identity initialization', () => {
|
|
|
408
536
|
}),
|
|
409
537
|
expect.any(Object)
|
|
410
538
|
);
|
|
539
|
+
expect(deviceWalletSessionStore.get('device-a', 'hidden-a')).toBe('session-a');
|
|
411
540
|
});
|
|
412
541
|
|
|
413
|
-
test('selects the standard V1 wallet
|
|
542
|
+
test('selects the standard V1 wallet with a single identity-validated Initialize', async () => {
|
|
414
543
|
const device = Device.fromDescriptor({ id: 'connect-a', path: 'connect-a' } as never);
|
|
415
544
|
device.features = {
|
|
416
545
|
protocol: 'V1',
|
|
@@ -420,17 +549,14 @@ describe('Protocol V1 wallet identity initialization', () => {
|
|
|
420
549
|
} as never;
|
|
421
550
|
const typedCall = jest
|
|
422
551
|
.fn()
|
|
423
|
-
.mockResolvedValueOnce({ type: 'Features', message: { device_id: 'device-a' } })
|
|
424
552
|
.mockResolvedValueOnce({ type: 'Features', message: { device_id: 'device-a' } });
|
|
425
553
|
device.commands = { typedCall } as never;
|
|
426
554
|
jest.spyOn(TransportManager, 'reconfigure').mockResolvedValue(undefined);
|
|
427
555
|
|
|
428
556
|
await device.initialize({ deviceId: 'device-a' });
|
|
429
557
|
|
|
430
|
-
expect(typedCall).toHaveBeenCalledTimes(
|
|
431
|
-
expect(typedCall).
|
|
432
|
-
expect(typedCall).toHaveBeenNthCalledWith(
|
|
433
|
-
2,
|
|
558
|
+
expect(typedCall).toHaveBeenCalledTimes(1);
|
|
559
|
+
expect(typedCall).toHaveBeenCalledWith(
|
|
434
560
|
'Initialize',
|
|
435
561
|
'Features',
|
|
436
562
|
{
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { EDeviceType } from '@onekeyfe/hd-shared';
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
+
DEVICE_SETTINGS_NEVER_TIMEOUT_MS,
|
|
4
5
|
PROTOCOL_V2_NEVER_TIMEOUT_MS,
|
|
5
6
|
getAutoLockOptions,
|
|
6
7
|
getAutoShutDownOptions,
|
|
@@ -67,7 +68,6 @@ describe('Pro 2 device settings options', () => {
|
|
|
67
68
|
{ label: '2 minutes', valueMs: 120_000 },
|
|
68
69
|
{ label: '5 minutes', valueMs: 300_000 },
|
|
69
70
|
{ label: '10 minutes', valueMs: 600_000 },
|
|
70
|
-
{ label: '30 minutes', valueMs: 1_800_000 },
|
|
71
71
|
{ label: 'Never', valueMs: PROTOCOL_V2_NEVER_TIMEOUT_MS },
|
|
72
72
|
]);
|
|
73
73
|
});
|
|
@@ -82,10 +82,14 @@ describe('Pro 2 device settings options', () => {
|
|
|
82
82
|
);
|
|
83
83
|
});
|
|
84
84
|
|
|
85
|
-
test('
|
|
85
|
+
test('returns the firmware-supported never value for both protocols', () => {
|
|
86
86
|
expect(getAutoLockOptions(EDeviceType.Pro, 'V1').slice(-1)[0]).toEqual({
|
|
87
87
|
label: 'Never',
|
|
88
|
-
valueMs:
|
|
88
|
+
valueMs: DEVICE_SETTINGS_NEVER_TIMEOUT_MS,
|
|
89
|
+
});
|
|
90
|
+
expect(getAutoShutDownOptions(EDeviceType.Pro, 'V1').slice(-1)[0]).toEqual({
|
|
91
|
+
label: 'Never',
|
|
92
|
+
valueMs: DEVICE_SETTINGS_NEVER_TIMEOUT_MS,
|
|
89
93
|
});
|
|
90
94
|
expect(getAutoLockOptions(EDeviceType.Pro2, 'V2').slice(-1)[0]).toEqual({
|
|
91
95
|
label: 'Never',
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
2
|
+
import { DeviceSessionPinType } from '@onekeyfe/hd-transport';
|
|
3
|
+
import { encode as encodeJpeg } from 'jpeg-js';
|
|
2
4
|
|
|
3
5
|
import DeviceUploadNft from '../src/api/protocol-v2/DeviceUploadNft';
|
|
4
6
|
|
|
@@ -13,6 +15,19 @@ const createRgba = (width: number, height: number) => {
|
|
|
13
15
|
return data;
|
|
14
16
|
};
|
|
15
17
|
|
|
18
|
+
const jpegBase64Cache = new Map<string, string>();
|
|
19
|
+
|
|
20
|
+
const createJpegBase64 = (width: number, height: number) => {
|
|
21
|
+
const key = `${width}x${height}`;
|
|
22
|
+
const cached = jpegBase64Cache.get(key);
|
|
23
|
+
if (cached) return cached;
|
|
24
|
+
const value = encodeJpeg({ width, height, data: createRgba(width, height) }, 80).data.toString(
|
|
25
|
+
'base64'
|
|
26
|
+
);
|
|
27
|
+
jpegBase64Cache.set(key, value);
|
|
28
|
+
return value;
|
|
29
|
+
};
|
|
30
|
+
|
|
16
31
|
const createMethod = ({
|
|
17
32
|
typedCall,
|
|
18
33
|
supportedMessages = [60802, 60805, 60808, 61500],
|
|
@@ -26,8 +41,8 @@ const createMethod = ({
|
|
|
26
41
|
id: 1,
|
|
27
42
|
payload: {
|
|
28
43
|
method: 'deviceUploadNft',
|
|
29
|
-
|
|
30
|
-
|
|
44
|
+
imageJpegBase64: createJpegBase64(540, 540),
|
|
45
|
+
thumbnailJpegBase64: createJpegBase64(263, 263),
|
|
31
46
|
title: 'CryptoPunk #3100',
|
|
32
47
|
subtitle: 'CryptoPunks',
|
|
33
48
|
timestampMs: 1_760_000_000_000,
|
|
@@ -86,8 +101,8 @@ describe('DeviceUploadNft', () => {
|
|
|
86
101
|
id: 1,
|
|
87
102
|
payload: {
|
|
88
103
|
method: 'deviceUploadNft',
|
|
89
|
-
|
|
90
|
-
|
|
104
|
+
imageJpegBase64: createJpegBase64(540, 540),
|
|
105
|
+
thumbnailJpegBase64: createJpegBase64(263, 263),
|
|
91
106
|
title: 'CryptoPunk #3100',
|
|
92
107
|
subtitle: 'CryptoPunks',
|
|
93
108
|
timestampMs: 1_760_000_000_000,
|
|
@@ -96,12 +111,29 @@ describe('DeviceUploadNft', () => {
|
|
|
96
111
|
|
|
97
112
|
method.init();
|
|
98
113
|
|
|
114
|
+
expect(method.unlockPolicy).toBe('unlock-before-run');
|
|
115
|
+
expect(method.protocolV2PreUnlockPinType).toBe(DeviceSessionPinType.Any);
|
|
99
116
|
expect((method as any).params).toMatchObject({
|
|
100
117
|
chunkSize: 2048,
|
|
101
118
|
paceMs: 0,
|
|
102
119
|
});
|
|
103
120
|
});
|
|
104
121
|
|
|
122
|
+
test('rejects invalid image Base64 before device communication', () => {
|
|
123
|
+
const method = new DeviceUploadNft({
|
|
124
|
+
id: 1,
|
|
125
|
+
payload: {
|
|
126
|
+
method: 'deviceUploadNft',
|
|
127
|
+
imageJpegBase64: 'not-base64',
|
|
128
|
+
thumbnailJpegBase64: createJpegBase64(263, 263),
|
|
129
|
+
title: 'NFT',
|
|
130
|
+
subtitle: '',
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
expect(() => method.init()).toThrow('canonical Base64');
|
|
135
|
+
});
|
|
136
|
+
|
|
105
137
|
test('uploads the triplet in order without creating the firmware-owned directory', async () => {
|
|
106
138
|
const typedCall = jest.fn((request: string, _response: string, params: any) => {
|
|
107
139
|
if (request === 'FilesystemPathInfoQuery') {
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { EDeviceType, EFirmwareType, ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
2
|
+
import { sha256 } from '@noble/hashes/sha256';
|
|
3
|
+
import { bytesToHex } from '@noble/hashes/utils';
|
|
2
4
|
|
|
3
5
|
import DeviceUpdateBootloader from '../../src/api/device/DeviceUpdateBootloader';
|
|
4
6
|
import {
|
|
@@ -27,10 +29,11 @@ jest.mock('../../src/device/DevicePool', () => ({
|
|
|
27
29
|
DevicePool: {},
|
|
28
30
|
}));
|
|
29
31
|
|
|
32
|
+
const artifactBytes = Uint8Array.from([1, 2, 3, 4]);
|
|
30
33
|
const artifact = {
|
|
31
34
|
artifactRef: `fw:${'a'.repeat(64)}`,
|
|
32
|
-
size:
|
|
33
|
-
sha256:
|
|
35
|
+
size: artifactBytes.byteLength,
|
|
36
|
+
sha256: bytesToHex(sha256(artifactBytes)),
|
|
34
37
|
};
|
|
35
38
|
|
|
36
39
|
const createMethod = () => {
|
|
@@ -38,7 +41,14 @@ const createMethod = () => {
|
|
|
38
41
|
const close = jest.fn().mockRejectedValue(closeError);
|
|
39
42
|
const artifactReader = {
|
|
40
43
|
open: jest.fn().mockResolvedValue({ readerId: 'reader-1', size: artifact.size }),
|
|
41
|
-
read: jest.fn()
|
|
44
|
+
read: jest.fn(({ offset, length }: { offset: number; length: number }) => {
|
|
45
|
+
const data = artifactBytes.slice(offset, offset + length).buffer;
|
|
46
|
+
return Promise.resolve({
|
|
47
|
+
data,
|
|
48
|
+
bytesRead: data.byteLength,
|
|
49
|
+
eof: offset + length === artifactBytes.byteLength,
|
|
50
|
+
});
|
|
51
|
+
}),
|
|
42
52
|
close,
|
|
43
53
|
};
|
|
44
54
|
const hostBindingGeneration = registerFirmwareUpdateHostBinding({
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { sha256 } from '@noble/hashes/sha256';
|
|
2
|
+
import { bytesToHex } from '@noble/hashes/utils';
|
|
3
|
+
|
|
1
4
|
import {
|
|
2
5
|
MAX_FIRMWARE_ARTIFACT_READ_BYTES,
|
|
3
6
|
openFirmwareByteSource,
|
|
@@ -6,19 +9,20 @@ import {
|
|
|
6
9
|
|
|
7
10
|
import type { FirmwareArtifactReader } from '../../src/types/api/firmwareUpdate';
|
|
8
11
|
|
|
12
|
+
const artifactBytes = Uint8Array.from([1, 2, 3, 4, 5, 6]);
|
|
9
13
|
const artifact = {
|
|
10
14
|
artifactRef: `fw:${'a'.repeat(64)}`,
|
|
11
|
-
size:
|
|
12
|
-
sha256:
|
|
15
|
+
size: artifactBytes.byteLength,
|
|
16
|
+
sha256: bytesToHex(sha256(artifactBytes)),
|
|
13
17
|
};
|
|
14
18
|
|
|
15
|
-
const createReader = () => {
|
|
16
|
-
const open = jest.fn(() => Promise.resolve({ readerId: 'reader-1', size:
|
|
19
|
+
const createReader = (bytes = artifactBytes) => {
|
|
20
|
+
const open = jest.fn(() => Promise.resolve({ readerId: 'reader-1', size: bytes.byteLength }));
|
|
17
21
|
const read = jest.fn(({ offset, length }: { readerId: string; offset: number; length: number }) =>
|
|
18
22
|
Promise.resolve({
|
|
19
|
-
data:
|
|
23
|
+
data: bytes.slice(offset, offset + length).buffer,
|
|
20
24
|
bytesRead: length,
|
|
21
|
-
eof: offset + length ===
|
|
25
|
+
eof: offset + length === bytes.byteLength,
|
|
22
26
|
})
|
|
23
27
|
);
|
|
24
28
|
const close = jest.fn(() => Promise.resolve());
|
|
@@ -27,12 +31,18 @@ const createReader = () => {
|
|
|
27
31
|
};
|
|
28
32
|
|
|
29
33
|
describe('FirmwareArtifactSource', () => {
|
|
30
|
-
it('
|
|
31
|
-
const
|
|
34
|
+
it('materializes verified bytes before exposing the source', async () => {
|
|
35
|
+
const backingBytes = Uint8Array.from(artifactBytes);
|
|
36
|
+
const { reader, read, close } = createReader(backingBytes);
|
|
32
37
|
const source = await openFirmwareByteSource({ artifact, reader });
|
|
33
38
|
if (!source) {
|
|
34
39
|
throw new Error('Expected a firmware byte source');
|
|
35
40
|
}
|
|
41
|
+
expect(read).toHaveBeenCalledWith({ readerId: 'reader-1', offset: 0, length: artifact.size });
|
|
42
|
+
expect(close).toHaveBeenCalledWith({ readerId: 'reader-1' });
|
|
43
|
+
|
|
44
|
+
backingBytes.fill(9);
|
|
45
|
+
read.mockClear();
|
|
36
46
|
|
|
37
47
|
const writes: Array<{ offset: number; data: number[] }> = [];
|
|
38
48
|
await writeFirmwareByteSource({
|
|
@@ -52,9 +62,50 @@ describe('FirmwareArtifactSource', () => {
|
|
|
52
62
|
{ offset: 0, data: [1, 2, 3, 4] },
|
|
53
63
|
{ offset: 4, data: [5, 6] },
|
|
54
64
|
]);
|
|
65
|
+
expect(read).not.toHaveBeenCalled();
|
|
66
|
+
expect(close).toHaveBeenCalledTimes(1);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('rejects same-sized bytes that do not match the receipt digest', async () => {
|
|
70
|
+
const { reader, close } = createReader(Uint8Array.from([6, 5, 4, 3, 2, 1]));
|
|
71
|
+
|
|
72
|
+
await expect(openFirmwareByteSource({ artifact, reader })).rejects.toMatchObject({
|
|
73
|
+
params: {
|
|
74
|
+
firmwareUpdateCode: 'FirmwareArtifactReceiptMismatch',
|
|
75
|
+
},
|
|
76
|
+
});
|
|
55
77
|
expect(close).toHaveBeenCalledWith({ readerId: 'reader-1' });
|
|
56
78
|
});
|
|
57
79
|
|
|
80
|
+
it('verifies large artifacts through bounded sequential reads', async () => {
|
|
81
|
+
const bytes = new Uint8Array(MAX_FIRMWARE_ARTIFACT_READ_BYTES + 3);
|
|
82
|
+
bytes.fill(7);
|
|
83
|
+
const largeArtifact = {
|
|
84
|
+
...artifact,
|
|
85
|
+
size: bytes.byteLength,
|
|
86
|
+
sha256: bytesToHex(sha256(bytes)),
|
|
87
|
+
};
|
|
88
|
+
const { reader, read } = createReader(bytes);
|
|
89
|
+
|
|
90
|
+
const source = await openFirmwareByteSource({ artifact: largeArtifact, reader });
|
|
91
|
+
if (!source) {
|
|
92
|
+
throw new Error('Expected a firmware byte source');
|
|
93
|
+
}
|
|
94
|
+
expect(read.mock.calls.map(([request]) => request)).toEqual([
|
|
95
|
+
{
|
|
96
|
+
readerId: 'reader-1',
|
|
97
|
+
offset: 0,
|
|
98
|
+
length: MAX_FIRMWARE_ARTIFACT_READ_BYTES,
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
readerId: 'reader-1',
|
|
102
|
+
offset: MAX_FIRMWARE_ARTIFACT_READ_BYTES,
|
|
103
|
+
length: 3,
|
|
104
|
+
},
|
|
105
|
+
]);
|
|
106
|
+
await source.close();
|
|
107
|
+
});
|
|
108
|
+
|
|
58
109
|
it('rejects a reader whose opened size differs from the receipt', async () => {
|
|
59
110
|
const { reader, open, close } = createReader();
|
|
60
111
|
open.mockResolvedValue({
|
|
@@ -70,7 +121,7 @@ describe('FirmwareArtifactSource', () => {
|
|
|
70
121
|
expect(close).toHaveBeenCalledWith({ readerId: 'reader-1' });
|
|
71
122
|
});
|
|
72
123
|
|
|
73
|
-
it('rejects paths and over-limit
|
|
124
|
+
it('rejects paths and over-limit writes before invoking the host again', async () => {
|
|
74
125
|
const { reader, read } = createReader();
|
|
75
126
|
await expect(
|
|
76
127
|
openFirmwareByteSource({
|
|
@@ -87,6 +138,7 @@ describe('FirmwareArtifactSource', () => {
|
|
|
87
138
|
if (!source) {
|
|
88
139
|
throw new Error('Expected a firmware byte source');
|
|
89
140
|
}
|
|
141
|
+
read.mockClear();
|
|
90
142
|
await expect(
|
|
91
143
|
writeFirmwareByteSource({
|
|
92
144
|
source,
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { EDeviceType, EFirmwareType } from '@onekeyfe/hd-shared';
|
|
2
|
+
import { sha256 } from '@noble/hashes/sha256';
|
|
3
|
+
import { bytesToHex } from '@noble/hashes/utils';
|
|
2
4
|
|
|
3
5
|
import FirmwareUpdateV2 from '../../src/api/FirmwareUpdateV2';
|
|
4
6
|
import FirmwareUpdateV3 from '../../src/api/FirmwareUpdateV3';
|
|
@@ -24,6 +26,9 @@ const noUpdate = {
|
|
|
24
26
|
release: undefined,
|
|
25
27
|
};
|
|
26
28
|
|
|
29
|
+
const preparedArtifactBytes = Uint8Array.from([1, 2, 3, 4]);
|
|
30
|
+
const preparedArtifactSha256 = bytesToHex(sha256(preparedArtifactBytes));
|
|
31
|
+
|
|
27
32
|
const createPreparedPlan = ({
|
|
28
33
|
executor,
|
|
29
34
|
target,
|
|
@@ -31,11 +36,10 @@ const createPreparedPlan = ({
|
|
|
31
36
|
executor: 'v2' | 'v3';
|
|
32
37
|
target: 'firmware' | 'bootloader';
|
|
33
38
|
}) => {
|
|
34
|
-
const sha256 = target === 'firmware' ? 'a'.repeat(64) : 'b'.repeat(64);
|
|
35
39
|
const artifact: FirmwareArtifactReference = {
|
|
36
|
-
artifactRef: `fw:${
|
|
37
|
-
size:
|
|
38
|
-
sha256,
|
|
40
|
+
artifactRef: `fw:${preparedArtifactSha256}`,
|
|
41
|
+
size: preparedArtifactBytes.byteLength,
|
|
42
|
+
sha256: preparedArtifactSha256,
|
|
39
43
|
};
|
|
40
44
|
const isV3 = executor === 'v3';
|
|
41
45
|
const plan = buildFirmwareUpdatePlan({
|
|
@@ -97,11 +101,10 @@ const registerMismatchedHost = () => {
|
|
|
97
101
|
};
|
|
98
102
|
|
|
99
103
|
const createPreparedV4Plan = () => {
|
|
100
|
-
const sha256 = 'c'.repeat(64);
|
|
101
104
|
const artifact: FirmwareArtifactReference = {
|
|
102
|
-
artifactRef: `fw:${
|
|
103
|
-
size:
|
|
104
|
-
sha256,
|
|
105
|
+
artifactRef: `fw:${preparedArtifactSha256}`,
|
|
106
|
+
size: preparedArtifactBytes.byteLength,
|
|
107
|
+
sha256: preparedArtifactSha256,
|
|
105
108
|
};
|
|
106
109
|
const plan = buildFirmwareUpdatePlan({
|
|
107
110
|
features: {
|
|
@@ -424,7 +427,14 @@ describe('prepared firmware host digest binding', () => {
|
|
|
424
427
|
});
|
|
425
428
|
const artifactReader = {
|
|
426
429
|
open: jest.fn().mockResolvedValue({ readerId: 'bootloader-reader', size: artifact.size }),
|
|
427
|
-
read: jest.fn()
|
|
430
|
+
read: jest.fn(({ offset, length }: { offset: number; length: number }) => {
|
|
431
|
+
const data = preparedArtifactBytes.slice(offset, offset + length).buffer;
|
|
432
|
+
return Promise.resolve({
|
|
433
|
+
data,
|
|
434
|
+
bytesRead: data.byteLength,
|
|
435
|
+
eof: offset + length === preparedArtifactBytes.byteLength,
|
|
436
|
+
});
|
|
437
|
+
}),
|
|
428
438
|
close: jest.fn().mockResolvedValue(undefined),
|
|
429
439
|
};
|
|
430
440
|
const hostBindingGeneration = registerFirmwareUpdateHostBinding({
|
|
@@ -105,6 +105,27 @@ const createPreparedResourcePlan = ({
|
|
|
105
105
|
};
|
|
106
106
|
|
|
107
107
|
describe.each(['v2', 'v3'] as const)('%s prepared resource archive binding', executor => {
|
|
108
|
+
test('rejects a missing artifact reader', async () => {
|
|
109
|
+
const entryBinary = Uint8Array.from([1, 2, 3, 4]).buffer;
|
|
110
|
+
const zip = new JSZip();
|
|
111
|
+
zip.file('images/logo.bin', entryBinary);
|
|
112
|
+
const archiveBinary = await zip.generateAsync({ type: 'arraybuffer' });
|
|
113
|
+
const { preparedPlan } = createPreparedResourcePlan({
|
|
114
|
+
executor,
|
|
115
|
+
archiveBinary,
|
|
116
|
+
entryBinary,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
await expect(
|
|
120
|
+
readVerifiedPreparedResourceArchive({
|
|
121
|
+
preparedPlan,
|
|
122
|
+
reader: undefined,
|
|
123
|
+
})
|
|
124
|
+
).rejects.toMatchObject({
|
|
125
|
+
params: { firmwareUpdateCode: 'FirmwareArtifactReaderInvalid' },
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
108
129
|
test('uses canonical bytes extracted from the approved ZIP', async () => {
|
|
109
130
|
const entryBinary = Uint8Array.from([1, 2, 3, 4]).buffer;
|
|
110
131
|
const zip = new JSZip();
|
|
@@ -203,6 +203,69 @@ describe.each([
|
|
|
203
203
|
});
|
|
204
204
|
});
|
|
205
205
|
|
|
206
|
+
describe('FirmwareUpdateV2 WebUSB bootloader polling', () => {
|
|
207
|
+
beforeEach(() => {
|
|
208
|
+
jest.useFakeTimers({ doNotFake: ['setImmediate', 'performance'] });
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
afterEach(() => {
|
|
212
|
+
jest.clearAllTimers();
|
|
213
|
+
jest.useRealTimers();
|
|
214
|
+
jest.restoreAllMocks();
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test('skips interval ticks while a bootloader probe is in flight and resumes afterward', async () => {
|
|
218
|
+
jest.spyOn(DataManager, 'getSettings').mockReturnValue('desktop' as never);
|
|
219
|
+
jest.spyOn(DataManager, 'isBleConnect').mockReturnValue(false);
|
|
220
|
+
jest.spyOn(DataManager, 'isBrowserWebUsb').mockReturnValue(false);
|
|
221
|
+
|
|
222
|
+
const method = new FirmwareUpdateV2({
|
|
223
|
+
id: 1,
|
|
224
|
+
payload: { method: 'firmwareUpdateV2', connectId: WEBUSB_ID },
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
let inFlight = 0;
|
|
228
|
+
let maxInFlight = 0;
|
|
229
|
+
const resolveProbes: Array<(found: boolean) => void> = [];
|
|
230
|
+
const probe = jest.spyOn(method as any, '_checkDeviceInBootloaderMode').mockImplementation(
|
|
231
|
+
() =>
|
|
232
|
+
new Promise<boolean>(resolve => {
|
|
233
|
+
inFlight += 1;
|
|
234
|
+
maxInFlight = Math.max(maxInFlight, inFlight);
|
|
235
|
+
resolveProbes.push(found => {
|
|
236
|
+
inFlight -= 1;
|
|
237
|
+
resolve(found);
|
|
238
|
+
});
|
|
239
|
+
})
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
method.device = {
|
|
243
|
+
getCurrentDeviceType: jest.fn(() => EDeviceType.Classic),
|
|
244
|
+
} as unknown as Device;
|
|
245
|
+
|
|
246
|
+
method.checkDeviceToBootloader(WEBUSB_ID);
|
|
247
|
+
|
|
248
|
+
for (let elapsed = 0; elapsed < 6500; elapsed += 500) {
|
|
249
|
+
jest.advanceTimersByTime(500);
|
|
250
|
+
// eslint-disable-next-line no-await-in-loop
|
|
251
|
+
await flush();
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
expect(probe).toHaveBeenCalled();
|
|
255
|
+
expect(maxInFlight).toBe(1);
|
|
256
|
+
|
|
257
|
+
resolveProbes[0]?.(false);
|
|
258
|
+
await flush();
|
|
259
|
+
jest.advanceTimersByTime(2000);
|
|
260
|
+
await flush();
|
|
261
|
+
|
|
262
|
+
expect(probe).toHaveBeenCalledTimes(2);
|
|
263
|
+
expect(maxInFlight).toBe(1);
|
|
264
|
+
resolveProbes[1]?.(false);
|
|
265
|
+
await flush();
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
206
269
|
describe.each([
|
|
207
270
|
[
|
|
208
271
|
'FirmwareUpdateV2',
|