@onekeyfe/hd-core 1.2.0-alpha.21 → 1.2.0-alpha.22
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 +87 -2
- package/__tests__/connectSettings.test.ts +47 -5
- package/__tests__/device-identity.test.ts +24 -2
- package/__tests__/device-settings.test.ts +1 -1
- package/__tests__/device-wallet-session-store.test.ts +111 -7
- package/__tests__/firmware-update/firmware-artifact-source.test.ts +119 -0
- package/__tests__/firmware-update/firmware-host-binding.test.ts +70 -0
- package/__tests__/firmware-update/firmware-update-capabilities.test.ts +13 -0
- package/__tests__/firmware-update/firmware-update-plan.test.ts +326 -0
- package/__tests__/firmware-update/firmware-update-prepared-plan.test.ts +200 -0
- package/__tests__/firmware-update/firmware-update-v2-download-before-boot.test.ts +39 -30
- package/__tests__/firmware-update/firmware-upload-source.test.ts +70 -0
- package/__tests__/open-wallet-session.test.ts +253 -13
- package/__tests__/pro2Wallpaper.test.ts +7 -18
- package/__tests__/protocol-v2-unlock-policy.test.ts +45 -0
- package/__tests__/protocol-v2.test.ts +415 -145
- package/__tests__/public-device-state-api.test.ts +69 -62
- package/__tests__/public-pro2-api-boundary.test.ts +8 -34
- 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/ClearSessionCache.d.ts.map +1 -1
- package/dist/api/FirmwareUpdate.d.ts.map +1 -1
- package/dist/api/FirmwareUpdateV2.d.ts +11 -0
- package/dist/api/FirmwareUpdateV2.d.ts.map +1 -1
- package/dist/api/FirmwareUpdateV3.d.ts +7 -3
- package/dist/api/FirmwareUpdateV3.d.ts.map +1 -1
- package/dist/api/FirmwareUpdateV4.d.ts +29 -10
- package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
- package/dist/api/OpenWalletSession.d.ts.map +1 -1
- package/dist/api/conflux/ConfluxGetAddress.d.ts +1 -0
- package/dist/api/conflux/ConfluxGetAddress.d.ts.map +1 -1
- package/dist/api/conflux/ConfluxSignMessage.d.ts +1 -0
- package/dist/api/conflux/ConfluxSignMessage.d.ts.map +1 -1
- package/dist/api/conflux/ConfluxSignMessageCIP23.d.ts +1 -0
- package/dist/api/conflux/ConfluxSignMessageCIP23.d.ts.map +1 -1
- package/dist/api/conflux/ConfluxSignTransaction.d.ts +1 -0
- package/dist/api/conflux/ConfluxSignTransaction.d.ts.map +1 -1
- package/dist/api/device/DeviceUnlock.d.ts +1 -1
- package/dist/api/device/DeviceUnlock.d.ts.map +1 -1
- package/dist/api/device/DeviceUpdateBootloader.d.ts +5 -1
- package/dist/api/device/DeviceUpdateBootloader.d.ts.map +1 -1
- package/dist/api/firmware/FirmwareArtifactSource.d.ts +26 -0
- package/dist/api/firmware/FirmwareArtifactSource.d.ts.map +1 -0
- package/dist/api/firmware/FirmwareHostBinding.d.ts +6 -0
- package/dist/api/firmware/FirmwareHostBinding.d.ts.map +1 -0
- package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts +7 -0
- package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts.map +1 -1
- package/dist/api/firmware/FirmwareUpdateCapabilities.d.ts +3 -0
- package/dist/api/firmware/FirmwareUpdateCapabilities.d.ts.map +1 -0
- package/dist/api/firmware/FirmwareUpdatePlan.d.ts +24 -0
- package/dist/api/firmware/FirmwareUpdatePlan.d.ts.map +1 -0
- package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts +25 -0
- package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts.map +1 -0
- package/dist/api/firmware/getBinary.d.ts +7 -0
- package/dist/api/firmware/getBinary.d.ts.map +1 -1
- package/dist/api/firmware/updateBootloader.d.ts +2 -0
- package/dist/api/firmware/updateBootloader.d.ts.map +1 -1
- package/dist/api/firmware/uploadFirmware.d.ts +9 -1
- package/dist/api/firmware/uploadFirmware.d.ts.map +1 -1
- package/dist/api/helpers/protocolV2FileWrite.d.ts.map +1 -1
- package/dist/api/index.d.ts +0 -17
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/nexa/NexaSignTransaction.d.ts +1 -0
- package/dist/api/nexa/NexaSignTransaction.d.ts.map +1 -1
- package/dist/api/stellar/StellarSignTransaction.d.ts +1 -1
- package/dist/data-manager/DataManager.d.ts +1 -0
- package/dist/data-manager/DataManager.d.ts.map +1 -1
- package/dist/data-manager/connectSettings.d.ts.map +1 -1
- package/dist/device/Device.d.ts.map +1 -1
- package/dist/device/DeviceCommands.d.ts.map +1 -1
- package/dist/device/DeviceStateMapper.d.ts.map +1 -1
- package/dist/device/DeviceStateProjector.d.ts.map +1 -1
- package/dist/events/logBlockEvent.d.ts.map +1 -1
- package/dist/index.d.ts +265 -226
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3045 -1550
- package/dist/inject.d.ts.map +1 -1
- package/dist/lowLevelInject.d.ts.map +1 -1
- package/dist/protocols/protocol-v2/settingsUnlockPolicy.d.ts.map +1 -1
- package/dist/protocols/protocol-v2/unlockPolicy.d.ts +4 -0
- package/dist/protocols/protocol-v2/unlockPolicy.d.ts.map +1 -0
- package/dist/protocols/protocol-v2/unlockRetry.d.ts.map +1 -1
- package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
- package/dist/topLevelInject.d.ts.map +1 -1
- package/dist/types/api/checkAllFirmwareRelease.d.ts +3 -0
- package/dist/types/api/checkAllFirmwareRelease.d.ts.map +1 -1
- package/dist/types/api/deviceUnlock.d.ts +2 -2
- package/dist/types/api/deviceUnlock.d.ts.map +1 -1
- package/dist/types/api/deviceUpdateBootloader.d.ts +6 -0
- package/dist/types/api/deviceUpdateBootloader.d.ts.map +1 -1
- package/dist/types/api/export.d.ts +4 -1
- package/dist/types/api/export.d.ts.map +1 -1
- package/dist/types/api/firmwareUpdate.d.ts +69 -0
- package/dist/types/api/firmwareUpdate.d.ts.map +1 -1
- package/dist/types/api/firmwareUpdateCapabilities.d.ts +9 -0
- package/dist/types/api/firmwareUpdateCapabilities.d.ts.map +1 -0
- package/dist/types/api/firmwareUpdatePlan.d.ts +27 -0
- package/dist/types/api/firmwareUpdatePlan.d.ts.map +1 -0
- package/dist/types/api/firmwareUpdatePreparedPlan.d.ts +42 -0
- package/dist/types/api/firmwareUpdatePreparedPlan.d.ts.map +1 -0
- package/dist/types/api/index.d.ts +10 -19
- package/dist/types/api/index.d.ts.map +1 -1
- package/dist/types/api/openWalletSession.d.ts +1 -0
- package/dist/types/api/openWalletSession.d.ts.map +1 -1
- package/dist/types/api/protocolV2.d.ts +3 -76
- package/dist/types/api/protocolV2.d.ts.map +1 -1
- package/dist/types/api/sessionCache.d.ts +4 -1
- package/dist/types/api/sessionCache.d.ts.map +1 -1
- package/dist/types/settings.d.ts +13 -0
- package/dist/types/settings.d.ts.map +1 -1
- package/dist/utils/deviceFeaturesUtils.d.ts.map +1 -1
- package/dist/utils/patch.d.ts +1 -1
- package/dist/utils/patch.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/api/BaseMethod.ts +8 -3
- package/src/api/CheckAllFirmwareRelease.ts +16 -3
- package/src/api/ClearSessionCache.ts +4 -0
- package/src/api/FirmwareUpdate.ts +8 -1
- package/src/api/FirmwareUpdateV2.ts +284 -119
- package/src/api/FirmwareUpdateV3.ts +248 -55
- package/src/api/FirmwareUpdateV4.ts +816 -328
- package/src/api/OpenWalletSession.ts +74 -25
- package/src/api/UploadPortfolio.ts +1 -1
- package/src/api/conflux/ConfluxGetAddress.ts +2 -0
- package/src/api/conflux/ConfluxSignMessage.ts +2 -0
- package/src/api/conflux/ConfluxSignMessageCIP23.ts +2 -0
- package/src/api/conflux/ConfluxSignTransaction.ts +2 -0
- package/src/api/device/DeviceChangePin.ts +1 -1
- package/src/api/device/DeviceUnlock.ts +1 -2
- package/src/api/device/DeviceUpdateBootloader.ts +114 -6
- package/src/api/device/DeviceWipe.ts +1 -1
- package/src/api/firmware/FirmwareArtifactSource.ts +278 -0
- package/src/api/firmware/FirmwareHostBinding.ts +100 -0
- package/src/api/firmware/FirmwareUpdateBaseMethod.ts +50 -0
- package/src/api/firmware/FirmwareUpdateCapabilities.ts +9 -0
- package/src/api/firmware/FirmwareUpdatePlan.ts +621 -0
- package/src/api/firmware/FirmwareUpdatePreparedPlan.ts +378 -0
- package/src/api/firmware/updateBootloader.ts +19 -4
- package/src/api/firmware/uploadFirmware.ts +140 -22
- package/src/api/helpers/protocolV2FileWrite.ts +6 -0
- package/src/api/index.ts +0 -18
- package/src/api/nexa/NexaSignTransaction.ts +2 -0
- package/src/api/protocol-v2/DeviceUploadWallpaper.ts +1 -1
- package/src/data/messages/messages-protocol-v2.json +77 -4
- package/src/data-manager/DataManager.ts +33 -33
- package/src/data-manager/connectSettings.ts +11 -0
- package/src/device/Device.ts +50 -60
- package/src/device/DeviceCommands.ts +58 -14
- package/src/device/DeviceStateMapper.ts +18 -1
- package/src/device/DeviceStateProjector.ts +7 -1
- package/src/events/logBlockEvent.ts +1 -0
- package/src/index.ts +5 -0
- package/src/inject.ts +22 -25
- package/src/lowLevelInject.ts +5 -1
- package/src/protocols/protocol-v2/deviceSession.ts +1 -1
- package/src/protocols/protocol-v2/settingsUnlockPolicy.ts +2 -2
- package/src/protocols/protocol-v2/unlockPolicy.ts +105 -0
- package/src/protocols/protocol-v2/unlockRetry.ts +1 -3
- package/src/protocols/protocol-v2/walletSession.ts +68 -12
- package/src/topLevelInject.ts +5 -1
- package/src/types/api/checkAllFirmwareRelease.ts +3 -0
- package/src/types/api/deviceUnlock.ts +2 -5
- package/src/types/api/deviceUpdateBootloader.ts +6 -0
- package/src/types/api/export.ts +18 -0
- package/src/types/api/firmwareUpdate.ts +76 -0
- package/src/types/api/firmwareUpdateCapabilities.ts +9 -0
- package/src/types/api/firmwareUpdatePlan.ts +36 -0
- package/src/types/api/firmwareUpdatePreparedPlan.ts +53 -0
- package/src/types/api/index.ts +14 -34
- package/src/types/api/openWalletSession.ts +2 -0
- package/src/types/api/protocolV2.ts +3 -156
- package/src/types/api/sessionCache.ts +9 -4
- package/src/types/settings.ts +13 -0
- package/src/utils/deviceFeaturesUtils.ts +5 -1
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { EDeviceType } from '@onekeyfe/hd-shared';
|
|
2
|
+
|
|
3
|
+
import { uploadFirmwareFromSource } from '../../src/api/firmware/uploadFirmware';
|
|
4
|
+
|
|
5
|
+
import type { Device } from '../../src/device/Device';
|
|
6
|
+
import type { FirmwareByteSource } from '../../src/api/firmware/FirmwareArtifactSource';
|
|
7
|
+
|
|
8
|
+
jest.mock('../../src/data/config', () => ({
|
|
9
|
+
getSDKVersion: jest.fn(() => '1.0.0'),
|
|
10
|
+
DEFAULT_DOMAIN: 'https://jssdk.onekey.so/1.0.0/',
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
describe('uploadFirmwareFromSource', () => {
|
|
14
|
+
it('serves legacy Touch firmware requests without materializing the complete artifact', async () => {
|
|
15
|
+
const size = 700 * 1024;
|
|
16
|
+
const requestedRanges: Array<{ offset: number; length: number }> = [];
|
|
17
|
+
const source: FirmwareByteSource = {
|
|
18
|
+
size,
|
|
19
|
+
readAt: jest.fn((offset: number, length: number) => {
|
|
20
|
+
requestedRanges.push({ offset, length });
|
|
21
|
+
return Promise.resolve(new ArrayBuffer(length));
|
|
22
|
+
}),
|
|
23
|
+
close: jest.fn(() => Promise.resolve()),
|
|
24
|
+
};
|
|
25
|
+
let nextOffset = 0;
|
|
26
|
+
const requestedLength = 128 * 1024;
|
|
27
|
+
const typedCall = jest.fn((type: string) => {
|
|
28
|
+
if (type === 'FirmwareErase') {
|
|
29
|
+
return Promise.resolve({
|
|
30
|
+
type: 'FirmwareRequest',
|
|
31
|
+
message: {
|
|
32
|
+
offset: nextOffset,
|
|
33
|
+
length: Math.min(requestedLength, size - nextOffset),
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
if (type === 'FirmwareUpload') {
|
|
38
|
+
nextOffset += Math.min(requestedLength, size - nextOffset);
|
|
39
|
+
if (nextOffset < size) {
|
|
40
|
+
return Promise.resolve({
|
|
41
|
+
type: 'FirmwareRequest',
|
|
42
|
+
message: {
|
|
43
|
+
offset: nextOffset,
|
|
44
|
+
length: Math.min(requestedLength, size - nextOffset),
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return Promise.resolve({
|
|
49
|
+
type: 'Success',
|
|
50
|
+
message: { message: 'installed' },
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
return Promise.reject(new Error(`Unexpected command: ${type}`));
|
|
54
|
+
});
|
|
55
|
+
const device = {
|
|
56
|
+
features: undefined,
|
|
57
|
+
emit: jest.fn(),
|
|
58
|
+
getCurrentDeviceType: () => EDeviceType.Touch,
|
|
59
|
+
toMessageObject: () => ({}),
|
|
60
|
+
} as unknown as Device;
|
|
61
|
+
|
|
62
|
+
await expect(
|
|
63
|
+
uploadFirmwareFromSource('firmware', typedCall as never, jest.fn(), device, source)
|
|
64
|
+
).resolves.toEqual({ message: 'installed' });
|
|
65
|
+
|
|
66
|
+
expect(requestedRanges).toHaveLength(Math.ceil(size / requestedLength));
|
|
67
|
+
expect(requestedRanges.every(({ length }) => length <= 256 * 1024)).toBe(true);
|
|
68
|
+
expect(requestedRanges.some(({ length }) => length === size)).toBe(false);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -28,6 +28,7 @@ const createDevice = ({
|
|
|
28
28
|
commands: { typedCall, promptPassphrase },
|
|
29
29
|
getDeviceState: jest.fn().mockResolvedValue({
|
|
30
30
|
identity: { deviceId: 'device-1' },
|
|
31
|
+
status: { passphraseProtection },
|
|
31
32
|
}),
|
|
32
33
|
getCurrentDeviceId: () => 'device-1',
|
|
33
34
|
getCurrentPassphraseProtection: () => passphraseProtection,
|
|
@@ -145,6 +146,7 @@ describe('openWalletSession', () => {
|
|
|
145
146
|
walletType: 'standard',
|
|
146
147
|
deviceId: 'device-1',
|
|
147
148
|
passphraseState: null,
|
|
149
|
+
sessionId: 'main-wallet-session',
|
|
148
150
|
resumed: false,
|
|
149
151
|
});
|
|
150
152
|
expect(method.payload.useEmptyPassphrase).toBe(true);
|
|
@@ -175,6 +177,43 @@ describe('openWalletSession', () => {
|
|
|
175
177
|
});
|
|
176
178
|
});
|
|
177
179
|
|
|
180
|
+
test('classifies the selected Pro2 wallet from the post-unlock device state', async () => {
|
|
181
|
+
const typedCall = jest
|
|
182
|
+
.fn()
|
|
183
|
+
.mockResolvedValueOnce({ message: { version: 2 } })
|
|
184
|
+
.mockResolvedValueOnce({
|
|
185
|
+
message: {
|
|
186
|
+
btc_test_address: 'hidden-state-after-unlock',
|
|
187
|
+
session_id: 'hidden-session-after-unlock',
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
const promptPassphrase = jest.fn().mockResolvedValue({ passphrase: 'hidden secret' });
|
|
191
|
+
const method = new OpenWalletSession({
|
|
192
|
+
payload: { method: 'openWalletSession', connectId: 'connect-id', mode: 'select-hidden' },
|
|
193
|
+
});
|
|
194
|
+
method.init();
|
|
195
|
+
const device = createDevice({ passphraseProtection: false, typedCall, promptPassphrase });
|
|
196
|
+
device.features.unlocked = false;
|
|
197
|
+
device.getCurrentPassphraseProtection = () => device.features.passphraseProtection;
|
|
198
|
+
device.unlockDevice = jest.fn().mockImplementation(() => {
|
|
199
|
+
device.features.unlocked = true;
|
|
200
|
+
device.features.passphraseProtection = true;
|
|
201
|
+
return Promise.resolve(device.features);
|
|
202
|
+
});
|
|
203
|
+
method.device = device as any;
|
|
204
|
+
|
|
205
|
+
await expect(method.run()).resolves.toEqual({
|
|
206
|
+
protocol: 'V2',
|
|
207
|
+
walletType: 'hidden',
|
|
208
|
+
deviceId: 'device-1',
|
|
209
|
+
passphraseState: 'hidden-state-after-unlock',
|
|
210
|
+
sessionId: 'hidden-session-after-unlock',
|
|
211
|
+
resumed: false,
|
|
212
|
+
});
|
|
213
|
+
expect(device.unlockDevice).toHaveBeenCalledTimes(1);
|
|
214
|
+
expect(promptPassphrase).toHaveBeenCalledTimes(1);
|
|
215
|
+
});
|
|
216
|
+
|
|
178
217
|
test('uses legacy initSession to replace a hidden-wallet session on Protocol V2', async () => {
|
|
179
218
|
const typedCall = jest.fn().mockResolvedValue({
|
|
180
219
|
message: {
|
|
@@ -201,10 +240,16 @@ describe('openWalletSession', () => {
|
|
|
201
240
|
walletType: 'hidden',
|
|
202
241
|
deviceId: 'device-1',
|
|
203
242
|
passphraseState: 'new-hidden-state',
|
|
243
|
+
sessionId: 'new-hidden-session',
|
|
204
244
|
resumed: false,
|
|
205
245
|
});
|
|
206
|
-
expect(typedCall).toHaveBeenCalledWith('
|
|
207
|
-
|
|
246
|
+
expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {
|
|
247
|
+
eventless_wallet_session: true,
|
|
248
|
+
});
|
|
249
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceSessionOpen', 'DeviceSession', {
|
|
250
|
+
select: { host_passphrase: { passphrase: 'new hidden secret' } },
|
|
251
|
+
});
|
|
252
|
+
expect(promptPassphrase).toHaveBeenCalled();
|
|
208
253
|
expect(deviceWalletSessionStore.get('device-1', 'stale-hidden-state')).toBeUndefined();
|
|
209
254
|
expect(deviceWalletSessionStore.get('device-1', 'new-hidden-state')).toBe('new-hidden-session');
|
|
210
255
|
});
|
|
@@ -266,10 +311,11 @@ describe('openWalletSession', () => {
|
|
|
266
311
|
walletType: 'hidden',
|
|
267
312
|
deviceId: 'device-1',
|
|
268
313
|
passphraseState: 'hidden-state',
|
|
314
|
+
sessionId: 'renewed-session',
|
|
269
315
|
resumed: true,
|
|
270
316
|
});
|
|
271
|
-
expect(typedCall).toHaveBeenCalledWith('
|
|
272
|
-
session_id: 'known-session',
|
|
317
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceSessionOpen', 'DeviceSession', {
|
|
318
|
+
resume: { session_id: 'known-session' },
|
|
273
319
|
});
|
|
274
320
|
expect(promptPassphrase).not.toHaveBeenCalled();
|
|
275
321
|
});
|
|
@@ -378,7 +424,7 @@ describe('openWalletSession', () => {
|
|
|
378
424
|
});
|
|
379
425
|
|
|
380
426
|
test('opens the standard wallet without selecting a hidden wallet', async () => {
|
|
381
|
-
const typedCall = jest.fn();
|
|
427
|
+
const typedCall = jest.fn().mockResolvedValue({ message: { version: 2 } });
|
|
382
428
|
const promptPassphrase = jest.fn();
|
|
383
429
|
const method = new OpenWalletSession({
|
|
384
430
|
payload: { method: 'openWalletSession', connectId: 'connect-id', mode: 'standard' },
|
|
@@ -395,17 +441,61 @@ describe('openWalletSession', () => {
|
|
|
395
441
|
passphraseState: null,
|
|
396
442
|
resumed: false,
|
|
397
443
|
});
|
|
398
|
-
expect(typedCall).
|
|
444
|
+
expect(typedCall).toHaveBeenCalledTimes(1);
|
|
445
|
+
expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {
|
|
446
|
+
eventless_wallet_session: true,
|
|
447
|
+
});
|
|
399
448
|
expect(promptPassphrase).not.toHaveBeenCalled();
|
|
400
449
|
expect(device.passphraseState).toBeUndefined();
|
|
401
450
|
});
|
|
402
451
|
|
|
452
|
+
test('opens a locked Protocol V2 standard wallet after deviceId becomes available', async () => {
|
|
453
|
+
const typedCall = jest.fn().mockResolvedValue({ message: { version: 2 } });
|
|
454
|
+
const method = new OpenWalletSession({
|
|
455
|
+
payload: { method: 'openWalletSession', connectId: 'connect-id', mode: 'standard' },
|
|
456
|
+
});
|
|
457
|
+
method.init();
|
|
458
|
+
const device = createDevice({ typedCall });
|
|
459
|
+
device.features.unlocked = false;
|
|
460
|
+
device.getDeviceState = jest
|
|
461
|
+
.fn()
|
|
462
|
+
.mockResolvedValueOnce({
|
|
463
|
+
identity: { deviceId: undefined },
|
|
464
|
+
status: { passphraseProtection: true },
|
|
465
|
+
})
|
|
466
|
+
.mockResolvedValueOnce({
|
|
467
|
+
identity: { deviceId: 'device-1' },
|
|
468
|
+
status: { passphraseProtection: true },
|
|
469
|
+
});
|
|
470
|
+
device.unlockDevice = jest.fn().mockImplementation(() => {
|
|
471
|
+
device.features.unlocked = true;
|
|
472
|
+
return Promise.resolve(device.features);
|
|
473
|
+
});
|
|
474
|
+
method.device = device as any;
|
|
475
|
+
|
|
476
|
+
await expect(method.run()).resolves.toEqual({
|
|
477
|
+
protocol: 'V2',
|
|
478
|
+
walletType: 'standard',
|
|
479
|
+
deviceId: 'device-1',
|
|
480
|
+
passphraseState: null,
|
|
481
|
+
resumed: false,
|
|
482
|
+
});
|
|
483
|
+
expect(device.unlockDevice).toHaveBeenCalledTimes(1);
|
|
484
|
+
expect(device.getDeviceState).toHaveBeenCalledTimes(2);
|
|
485
|
+
});
|
|
486
|
+
|
|
403
487
|
test('rejects a standard-wallet request when Protocol V2 is unlocked by Attach PIN', async () => {
|
|
488
|
+
const typedCall = jest.fn().mockResolvedValue({
|
|
489
|
+
message: {
|
|
490
|
+
btc_test_address: 'attach-wallet-state',
|
|
491
|
+
session_id: 'attach-wallet-session',
|
|
492
|
+
},
|
|
493
|
+
});
|
|
404
494
|
const method = new OpenWalletSession({
|
|
405
495
|
payload: { method: 'openWalletSession', connectId: 'connect-id', mode: 'standard' },
|
|
406
496
|
});
|
|
407
497
|
method.init();
|
|
408
|
-
const device = createDevice();
|
|
498
|
+
const device = createDevice({ typedCall });
|
|
409
499
|
device.features.unlockedAttachPin = true;
|
|
410
500
|
method.device = device as any;
|
|
411
501
|
|
|
@@ -416,7 +506,7 @@ describe('openWalletSession', () => {
|
|
|
416
506
|
expect(device.clearInternalState).toHaveBeenCalled();
|
|
417
507
|
});
|
|
418
508
|
|
|
419
|
-
test('selects a hidden wallet and
|
|
509
|
+
test('selects a hidden wallet and returns the CLI compatibility session', async () => {
|
|
420
510
|
const typedCall = jest.fn().mockResolvedValue({
|
|
421
511
|
message: {
|
|
422
512
|
btc_test_address: 'hidden-state',
|
|
@@ -437,14 +527,63 @@ describe('openWalletSession', () => {
|
|
|
437
527
|
walletType: 'hidden',
|
|
438
528
|
deviceId: 'device-1',
|
|
439
529
|
passphraseState: 'hidden-state',
|
|
530
|
+
sessionId: 'hidden-session',
|
|
440
531
|
resumed: false,
|
|
441
532
|
});
|
|
442
|
-
expect(typedCall).toHaveBeenCalledWith('
|
|
443
|
-
|
|
533
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceSessionOpen', 'DeviceSession', {
|
|
534
|
+
select: { host_passphrase: { passphrase: 'hidden secret' } },
|
|
535
|
+
});
|
|
536
|
+
expect(promptPassphrase).toHaveBeenCalled();
|
|
444
537
|
expect(device.passphraseState).toBeUndefined();
|
|
445
538
|
expect(deviceWalletSessionStore.get('device-1', 'hidden-state')).toBe('hidden-session');
|
|
446
539
|
});
|
|
447
540
|
|
|
541
|
+
test('selects on-device passphrase entry through DeviceSessionOpen', async () => {
|
|
542
|
+
const typedCall = jest.fn().mockResolvedValue({
|
|
543
|
+
message: { btc_test_address: 'device-state', session_id: 'device-session' },
|
|
544
|
+
});
|
|
545
|
+
const promptPassphrase = jest.fn().mockResolvedValue({ passphraseOnDevice: true });
|
|
546
|
+
const method = new OpenWalletSession({
|
|
547
|
+
payload: { method: 'openWalletSession', connectId: 'connect-id', mode: 'select-hidden' },
|
|
548
|
+
});
|
|
549
|
+
method.init();
|
|
550
|
+
method.device = createDevice({ typedCall, promptPassphrase }) as any;
|
|
551
|
+
|
|
552
|
+
await expect(method.run()).resolves.toMatchObject({
|
|
553
|
+
walletType: 'hidden',
|
|
554
|
+
passphraseState: 'device-state',
|
|
555
|
+
});
|
|
556
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceSessionOpen', 'DeviceSession', {
|
|
557
|
+
select: { passphrase_on_device: {} },
|
|
558
|
+
});
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
test('selects Attach PIN only when the device reports an existing binding', async () => {
|
|
562
|
+
const typedCall = jest.fn().mockResolvedValue({
|
|
563
|
+
message: { btc_test_address: 'attach-state', session_id: 'attach-session' },
|
|
564
|
+
});
|
|
565
|
+
const promptPassphrase = jest.fn().mockResolvedValue({ attachPinOnDevice: true });
|
|
566
|
+
const method = new OpenWalletSession({
|
|
567
|
+
payload: { method: 'openWalletSession', connectId: 'connect-id', mode: 'select-hidden' },
|
|
568
|
+
});
|
|
569
|
+
method.init();
|
|
570
|
+
const device = createDevice({ typedCall, promptPassphrase });
|
|
571
|
+
device.features.attachToPinEnabled = true;
|
|
572
|
+
method.device = device as any;
|
|
573
|
+
|
|
574
|
+
await expect(method.run()).resolves.toMatchObject({
|
|
575
|
+
walletType: 'hidden',
|
|
576
|
+
passphraseState: 'attach-state',
|
|
577
|
+
});
|
|
578
|
+
expect(promptPassphrase).toHaveBeenCalledWith(
|
|
579
|
+
expect.objectContaining({ existsAttachPinUser: true }),
|
|
580
|
+
{ cancelDeviceOnReject: false }
|
|
581
|
+
);
|
|
582
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceSessionOpen', 'DeviceSession', {
|
|
583
|
+
select: { attach_pin_on_device: {} },
|
|
584
|
+
});
|
|
585
|
+
});
|
|
586
|
+
|
|
448
587
|
test.each([
|
|
449
588
|
{
|
|
450
589
|
missingField: 'session_id',
|
|
@@ -468,7 +607,7 @@ describe('openWalletSession', () => {
|
|
|
468
607
|
|
|
469
608
|
await expect(method.run()).rejects.toMatchObject({
|
|
470
609
|
errorCode: HardwareErrorCode.RuntimeError,
|
|
471
|
-
message: '
|
|
610
|
+
message: 'DeviceSessionOpen returned an incomplete DeviceSession response.',
|
|
472
611
|
});
|
|
473
612
|
expect(device.clearInternalState).toHaveBeenCalled();
|
|
474
613
|
expect(device.updateInternalState).not.toHaveBeenCalled();
|
|
@@ -501,14 +640,115 @@ describe('openWalletSession', () => {
|
|
|
501
640
|
walletType: 'hidden',
|
|
502
641
|
deviceId: 'device-1',
|
|
503
642
|
passphraseState: 'hidden-state',
|
|
643
|
+
sessionId: 'renewed-session',
|
|
504
644
|
resumed: true,
|
|
505
645
|
});
|
|
506
|
-
expect(typedCall).toHaveBeenCalledWith('
|
|
507
|
-
session_id: 'known-session',
|
|
646
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceSessionOpen', 'DeviceSession', {
|
|
647
|
+
resume: { session_id: 'known-session' },
|
|
508
648
|
});
|
|
509
649
|
expect(promptPassphrase).not.toHaveBeenCalled();
|
|
510
650
|
});
|
|
511
651
|
|
|
652
|
+
test('validates a locked Protocol V2 resume binding after deviceId refresh', async () => {
|
|
653
|
+
const typedCall = jest.fn().mockResolvedValue({
|
|
654
|
+
message: {
|
|
655
|
+
btc_test_address: 'hidden-state',
|
|
656
|
+
session_id: 'renewed-session',
|
|
657
|
+
},
|
|
658
|
+
});
|
|
659
|
+
const method = new OpenWalletSession({
|
|
660
|
+
payload: {
|
|
661
|
+
method: 'openWalletSession',
|
|
662
|
+
connectId: 'connect-id',
|
|
663
|
+
mode: 'resume-hidden',
|
|
664
|
+
deviceId: 'device-1',
|
|
665
|
+
passphraseState: 'hidden-state',
|
|
666
|
+
},
|
|
667
|
+
});
|
|
668
|
+
method.init();
|
|
669
|
+
deviceWalletSessionStore.set('device-1', 'hidden-state', 'known-session');
|
|
670
|
+
const device = createDevice({ typedCall });
|
|
671
|
+
device.features.unlocked = false;
|
|
672
|
+
device.getDeviceState = jest
|
|
673
|
+
.fn()
|
|
674
|
+
.mockResolvedValueOnce({
|
|
675
|
+
identity: { deviceId: undefined },
|
|
676
|
+
status: { passphraseProtection: true },
|
|
677
|
+
})
|
|
678
|
+
.mockResolvedValueOnce({
|
|
679
|
+
identity: { deviceId: 'device-1' },
|
|
680
|
+
status: { passphraseProtection: true },
|
|
681
|
+
});
|
|
682
|
+
device.unlockDevice = jest.fn().mockImplementation(() => {
|
|
683
|
+
device.features.unlocked = true;
|
|
684
|
+
return Promise.resolve(device.features);
|
|
685
|
+
});
|
|
686
|
+
method.device = device as any;
|
|
687
|
+
|
|
688
|
+
await expect(method.run()).resolves.toMatchObject({
|
|
689
|
+
protocol: 'V2',
|
|
690
|
+
walletType: 'hidden',
|
|
691
|
+
deviceId: 'device-1',
|
|
692
|
+
passphraseState: 'hidden-state',
|
|
693
|
+
resumed: true,
|
|
694
|
+
});
|
|
695
|
+
expect(device.unlockDevice).toHaveBeenCalledTimes(1);
|
|
696
|
+
expect(device.getDeviceState).toHaveBeenCalledTimes(2);
|
|
697
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceSessionOpen', 'DeviceSession', {
|
|
698
|
+
resume: { session_id: 'known-session' },
|
|
699
|
+
});
|
|
700
|
+
const sessionOpenCall = typedCall.mock.calls.findIndex(
|
|
701
|
+
([requestName]) => requestName === 'DeviceSessionOpen'
|
|
702
|
+
);
|
|
703
|
+
expect(device.getDeviceState.mock.invocationCallOrder[1]).toBeLessThan(
|
|
704
|
+
typedCall.mock.invocationCallOrder[sessionOpenCall]
|
|
705
|
+
);
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
test('rejects a Protocol V2 resume when the refreshed deviceId does not match', async () => {
|
|
709
|
+
const typedCall = jest.fn().mockResolvedValue({
|
|
710
|
+
message: {
|
|
711
|
+
btc_test_address: 'hidden-state',
|
|
712
|
+
session_id: 'renewed-session',
|
|
713
|
+
},
|
|
714
|
+
});
|
|
715
|
+
const method = new OpenWalletSession({
|
|
716
|
+
payload: {
|
|
717
|
+
method: 'openWalletSession',
|
|
718
|
+
connectId: 'connect-id',
|
|
719
|
+
mode: 'resume-hidden',
|
|
720
|
+
deviceId: 'device-1',
|
|
721
|
+
passphraseState: 'hidden-state',
|
|
722
|
+
},
|
|
723
|
+
});
|
|
724
|
+
method.init();
|
|
725
|
+
deviceWalletSessionStore.set('device-1', 'hidden-state', 'known-session');
|
|
726
|
+
const device = createDevice({ typedCall });
|
|
727
|
+
device.features.unlocked = false;
|
|
728
|
+
device.getDeviceState = jest
|
|
729
|
+
.fn()
|
|
730
|
+
.mockResolvedValueOnce({
|
|
731
|
+
identity: { deviceId: undefined },
|
|
732
|
+
status: { passphraseProtection: true },
|
|
733
|
+
})
|
|
734
|
+
.mockResolvedValueOnce({
|
|
735
|
+
identity: { deviceId: 'other-device' },
|
|
736
|
+
status: { passphraseProtection: true },
|
|
737
|
+
});
|
|
738
|
+
device.unlockDevice = jest.fn().mockImplementation(() => {
|
|
739
|
+
device.features.unlocked = true;
|
|
740
|
+
return Promise.resolve(device.features);
|
|
741
|
+
});
|
|
742
|
+
method.device = device as any;
|
|
743
|
+
|
|
744
|
+
await expect(method.run()).rejects.toMatchObject({
|
|
745
|
+
errorCode: HardwareErrorCode.DeviceCheckDeviceIdError,
|
|
746
|
+
});
|
|
747
|
+
expect(typedCall).not.toHaveBeenCalled();
|
|
748
|
+
expect(device.clearInternalState).not.toHaveBeenCalled();
|
|
749
|
+
expect(deviceWalletSessionStore.get('device-1', 'hidden-state')).toBeUndefined();
|
|
750
|
+
});
|
|
751
|
+
|
|
512
752
|
test('does not fall back to wallet selection when a cached resume is invalid', async () => {
|
|
513
753
|
const typedCall = jest.fn().mockRejectedValue(
|
|
514
754
|
Object.assign(new Error('Invalid session'), {
|
|
@@ -10,18 +10,7 @@ describe('encodePro2Wallpaper', () => {
|
|
|
10
10
|
|
|
11
11
|
expect(result.colorFormat).toBe('RGB565');
|
|
12
12
|
expect(Array.from(result.data.slice(0, 12))).toEqual([
|
|
13
|
-
0x19,
|
|
14
|
-
0x12,
|
|
15
|
-
0,
|
|
16
|
-
0,
|
|
17
|
-
2,
|
|
18
|
-
0,
|
|
19
|
-
1,
|
|
20
|
-
0,
|
|
21
|
-
4,
|
|
22
|
-
0,
|
|
23
|
-
0,
|
|
24
|
-
0,
|
|
13
|
+
0x19, 0x12, 0, 0, 2, 0, 1, 0, 4, 0, 0, 0,
|
|
25
14
|
]);
|
|
26
15
|
expect(Array.from(result.data.slice(12))).toEqual([0x00, 0xf0, 0xc0, 0x07]);
|
|
27
16
|
});
|
|
@@ -40,11 +29,11 @@ describe('encodePro2Wallpaper', () => {
|
|
|
40
29
|
});
|
|
41
30
|
|
|
42
31
|
test('rejects invalid dimensions and RGBA byte length', () => {
|
|
43
|
-
expect(() =>
|
|
44
|
-
|
|
45
|
-
)
|
|
46
|
-
expect(() =>
|
|
47
|
-
|
|
48
|
-
)
|
|
32
|
+
expect(() => encodePro2Wallpaper({ width: 0, height: 1, rgba: new Uint8Array() })).toThrow(
|
|
33
|
+
'width'
|
|
34
|
+
);
|
|
35
|
+
expect(() => encodePro2Wallpaper({ width: 2, height: 1, rgba: new Uint8Array(4) })).toThrow(
|
|
36
|
+
'8'
|
|
37
|
+
);
|
|
49
38
|
});
|
|
50
39
|
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import * as publicMethods from '../src/api';
|
|
2
|
+
import {
|
|
3
|
+
PROTOCOL_V2_RETRY_ON_LOCKED_METHODS,
|
|
4
|
+
getProtocolV2UnlockPolicy,
|
|
5
|
+
} from '../src/protocols/protocol-v2/unlockPolicy';
|
|
6
|
+
|
|
7
|
+
jest.mock('../src/data/config', () => ({
|
|
8
|
+
DEFAULT_DOMAIN: 'https://example.com/',
|
|
9
|
+
getSDKVersion: () => '0.0.0-test',
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
describe('Protocol V2 public method unlock policy', () => {
|
|
13
|
+
test('classifies the complete public method table with an explicit replay allowlist', () => {
|
|
14
|
+
const publicMethodNames = Object.keys(publicMethods);
|
|
15
|
+
const retryMethodNames = new Set<string>(PROTOCOL_V2_RETRY_ON_LOCKED_METHODS);
|
|
16
|
+
|
|
17
|
+
expect(publicMethodNames.length).toBeGreaterThan(100);
|
|
18
|
+
expect(new Set(PROTOCOL_V2_RETRY_ON_LOCKED_METHODS).size).toBe(
|
|
19
|
+
PROTOCOL_V2_RETRY_ON_LOCKED_METHODS.length
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
for (const methodName of PROTOCOL_V2_RETRY_ON_LOCKED_METHODS) {
|
|
23
|
+
expect(publicMethods).toHaveProperty(methodName);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
for (const methodName of publicMethodNames) {
|
|
27
|
+
expect(getProtocolV2UnlockPolicy(methodName)).toBe(
|
|
28
|
+
retryMethodNames.has(methodName) ? 'retry-on-locked' : 'none'
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test.each([
|
|
34
|
+
'deviceUnlock',
|
|
35
|
+
'deviceLock',
|
|
36
|
+
'deviceWipe',
|
|
37
|
+
'deviceChangePin',
|
|
38
|
+
'deviceSettings',
|
|
39
|
+
'firmwareUpdateV4',
|
|
40
|
+
'deviceUploadWallpaper',
|
|
41
|
+
'uploadPortfolio',
|
|
42
|
+
])('does not make the state-changing method %s replayable by default', methodName => {
|
|
43
|
+
expect(getProtocolV2UnlockPolicy(methodName)).toBe('none');
|
|
44
|
+
});
|
|
45
|
+
});
|