@onekeyfe/hd-core 1.2.0-alpha.32 → 1.2.0-alpha.34
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__/check-all-firmware-release-protocol-v2.test.ts +31 -0
- package/__tests__/core-dispose.test.ts +3 -3
- package/__tests__/device-lifecycle-events.test.ts +52 -0
- package/__tests__/device-pool-state.test.ts +14 -2
- package/__tests__/device-settings.test.ts +22 -0
- package/__tests__/device-state-mapper.test.ts +13 -2
- package/__tests__/device-state-projector.test.ts +2 -2
- package/__tests__/device-utils.test.ts +1 -0
- package/__tests__/device-wallet-session-store.test.ts +64 -17
- package/__tests__/kaspaSignTransaction.test.ts +9 -0
- package/__tests__/known-device-public-types.type-test.ts +3 -0
- package/__tests__/logBlockEvent.test.ts +89 -5
- package/__tests__/open-wallet-session.test.ts +334 -30
- package/__tests__/pro2Wallpaper.test.ts +1 -1
- package/__tests__/protocol-v2.test.ts +708 -75
- package/dist/api/CheckAllFirmwareRelease.d.ts.map +1 -1
- package/dist/api/FirmwareUpdateV4.d.ts +1 -0
- package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
- package/dist/api/GetPassphraseState.d.ts.map +1 -1
- package/dist/api/device/DeviceSettings.d.ts.map +1 -1
- package/dist/api/kaspa/KaspaSignTransaction.d.ts.map +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/device/Device.d.ts +7 -2
- package/dist/device/Device.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 +1 -0
- package/dist/device/DevicePool.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/device/DeviceWalletSessionStore.d.ts +8 -0
- package/dist/device/DeviceWalletSessionStore.d.ts.map +1 -1
- package/dist/deviceProfile/buildDeviceFeatures.d.ts.map +1 -1
- package/dist/deviceProfile/protocolV2DeviceIdentity.d.ts +7 -0
- package/dist/deviceProfile/protocolV2DeviceIdentity.d.ts.map +1 -0
- package/dist/events/logBlockEvent.d.ts +2 -0
- package/dist/events/logBlockEvent.d.ts.map +1 -1
- package/dist/index.d.ts +19 -7
- package/dist/index.js +456 -222
- package/dist/protocols/protocol-v2/walletSession.d.ts +0 -10
- package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
- package/dist/types/api/openWalletSession.d.ts +1 -1
- package/dist/types/api/openWalletSession.d.ts.map +1 -1
- package/dist/types/device.d.ts +4 -2
- package/dist/types/device.d.ts.map +1 -1
- package/dist/types/params.d.ts.map +1 -1
- package/dist/utils/deviceFeaturesCompat.d.ts.map +1 -1
- package/dist/utils/deviceFeaturesUtils.d.ts +0 -5
- package/dist/utils/deviceFeaturesUtils.d.ts.map +1 -1
- package/dist/utils/pro2Wallpaper.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/api/CheckAllFirmwareRelease.ts +11 -81
- package/src/api/FirmwareUpdateV4.ts +33 -17
- package/src/api/GetPassphraseState.ts +1 -3
- package/src/api/OpenWalletSession.ts +3 -3
- package/src/api/device/DeviceSettings.ts +8 -1
- package/src/api/kaspa/KaspaSignTransaction.ts +7 -0
- package/src/core/index.ts +7 -5
- package/src/device/Device.ts +37 -9
- package/src/device/DeviceConnector.ts +10 -7
- package/src/device/DevicePool.ts +5 -3
- package/src/device/DeviceStateMapper.ts +5 -3
- package/src/device/DeviceStateProjector.ts +0 -7
- package/src/device/DeviceWalletSessionStore.ts +57 -4
- package/src/deviceProfile/buildDeviceFeatures.ts +10 -3
- package/src/deviceProfile/protocolV2DeviceIdentity.ts +32 -0
- package/src/events/logBlockEvent.ts +84 -1
- package/src/protocols/protocol-v2/deviceSession.ts +1 -1
- package/src/protocols/protocol-v2/walletSession.ts +116 -34
- package/src/types/api/openWalletSession.ts +1 -1
- package/src/types/device.ts +6 -2
- package/src/types/params.ts +2 -1
- package/src/utils/deviceFeaturesCompat.ts +8 -1
- package/src/utils/pro2Wallpaper.ts +3 -9
- package/tsconfig.type-tests.json +5 -1
|
@@ -3,6 +3,7 @@ import { DeviceSessionPinType } from '@onekeyfe/hd-transport';
|
|
|
3
3
|
|
|
4
4
|
import GetPassphraseState from '../src/api/GetPassphraseState';
|
|
5
5
|
import OpenWalletSession from '../src/api/OpenWalletSession';
|
|
6
|
+
import { Device } from '../src/device/Device';
|
|
6
7
|
import { deviceWalletSessionStore } from '../src/device/DeviceWalletSessionStore';
|
|
7
8
|
|
|
8
9
|
jest.mock('../src/data/config', () => ({
|
|
@@ -26,7 +27,22 @@ const createDevice = ({
|
|
|
26
27
|
passphraseProtection,
|
|
27
28
|
attachToPinEnabled: false,
|
|
28
29
|
},
|
|
29
|
-
commands: {
|
|
30
|
+
commands: {
|
|
31
|
+
typedCall: jest.fn((request: string, ...args: unknown[]) => {
|
|
32
|
+
if (request === 'DeviceStatusGet') {
|
|
33
|
+
return {
|
|
34
|
+
message: {
|
|
35
|
+
device_id: 'device-1',
|
|
36
|
+
unlocked: true,
|
|
37
|
+
unlocked_attach_pin: false,
|
|
38
|
+
passphrase_enabled: passphraseProtection,
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
return typedCall(request, ...args);
|
|
43
|
+
}),
|
|
44
|
+
promptPassphrase,
|
|
45
|
+
},
|
|
30
46
|
getDeviceState: jest.fn().mockResolvedValue({
|
|
31
47
|
identity: { deviceId: 'device-1' },
|
|
32
48
|
status: { passphraseProtection },
|
|
@@ -47,6 +63,12 @@ const createDevice = ({
|
|
|
47
63
|
}),
|
|
48
64
|
lockDevice: jest.fn(),
|
|
49
65
|
unlockDevice: jest.fn(),
|
|
66
|
+
updateProtocolV2Status: jest.fn((status: Record<string, unknown>) => {
|
|
67
|
+
device.features.unlocked = status.unlocked ?? device.features.unlocked;
|
|
68
|
+
device.features.unlockedAttachPin =
|
|
69
|
+
status.unlocked_attach_pin ?? device.features.unlockedAttachPin;
|
|
70
|
+
return device.features;
|
|
71
|
+
}),
|
|
50
72
|
initialize: jest.fn(),
|
|
51
73
|
emit: jest.fn(),
|
|
52
74
|
isProtocolV2: () => true,
|
|
@@ -79,7 +101,7 @@ describe('openWalletSession', () => {
|
|
|
79
101
|
const typedCall = jest
|
|
80
102
|
.fn()
|
|
81
103
|
.mockResolvedValueOnce({ message: { version: 2 } })
|
|
82
|
-
.mockResolvedValueOnce({ message: {
|
|
104
|
+
.mockResolvedValueOnce({ message: {} })
|
|
83
105
|
.mockResolvedValueOnce({
|
|
84
106
|
message: {
|
|
85
107
|
btc_test_address: 'pro2-wallet-state',
|
|
@@ -101,7 +123,7 @@ describe('openWalletSession', () => {
|
|
|
101
123
|
expect(promptPassphrase).toHaveBeenCalledWith(
|
|
102
124
|
{
|
|
103
125
|
existsAttachPinUser: false,
|
|
104
|
-
deviceOnly:
|
|
126
|
+
deviceOnly: false,
|
|
105
127
|
source: 'wallet-session-coordinator',
|
|
106
128
|
reason: 'open-wallet',
|
|
107
129
|
},
|
|
@@ -186,7 +208,7 @@ describe('openWalletSession', () => {
|
|
|
186
208
|
protocol: 'V1',
|
|
187
209
|
walletType: 'standard',
|
|
188
210
|
deviceId: 'device-1',
|
|
189
|
-
passphraseState:
|
|
211
|
+
passphraseState: 'main-wallet-state',
|
|
190
212
|
resumed: false,
|
|
191
213
|
});
|
|
192
214
|
expect(method.payload.useEmptyPassphrase).toBe(true);
|
|
@@ -200,7 +222,7 @@ describe('openWalletSession', () => {
|
|
|
200
222
|
const typedCall = jest
|
|
201
223
|
.fn()
|
|
202
224
|
.mockResolvedValueOnce({ message: { version: 2 } })
|
|
203
|
-
.mockResolvedValueOnce({ message: {
|
|
225
|
+
.mockResolvedValueOnce({ message: {} })
|
|
204
226
|
.mockResolvedValueOnce({
|
|
205
227
|
message: {
|
|
206
228
|
btc_test_address: 'hidden-state',
|
|
@@ -234,7 +256,7 @@ describe('openWalletSession', () => {
|
|
|
234
256
|
const typedCall = jest
|
|
235
257
|
.fn()
|
|
236
258
|
.mockResolvedValueOnce({ message: { version: 2 } })
|
|
237
|
-
.mockResolvedValueOnce({ message: {
|
|
259
|
+
.mockResolvedValueOnce({ message: {} })
|
|
238
260
|
.mockResolvedValueOnce({
|
|
239
261
|
message: {
|
|
240
262
|
btc_test_address: 'hidden-state-after-unlock',
|
|
@@ -267,7 +289,7 @@ describe('openWalletSession', () => {
|
|
|
267
289
|
passphraseState: 'hidden-state-after-unlock',
|
|
268
290
|
resumed: false,
|
|
269
291
|
});
|
|
270
|
-
expect(device.unlockDevice).
|
|
292
|
+
expect(device.unlockDevice).not.toHaveBeenCalled();
|
|
271
293
|
expect(promptPassphrase).toHaveBeenCalledTimes(1);
|
|
272
294
|
});
|
|
273
295
|
|
|
@@ -275,7 +297,7 @@ describe('openWalletSession', () => {
|
|
|
275
297
|
const typedCall = jest
|
|
276
298
|
.fn()
|
|
277
299
|
.mockResolvedValueOnce({ message: { version: 2 } })
|
|
278
|
-
.mockResolvedValueOnce({ message: {
|
|
300
|
+
.mockResolvedValueOnce({ message: {} })
|
|
279
301
|
.mockResolvedValueOnce({
|
|
280
302
|
message: {
|
|
281
303
|
btc_test_address: 'new-hidden-state',
|
|
@@ -315,7 +337,7 @@ describe('openWalletSession', () => {
|
|
|
315
337
|
const typedCall = jest
|
|
316
338
|
.fn()
|
|
317
339
|
.mockResolvedValueOnce({ message: { version: 2 } })
|
|
318
|
-
.mockResolvedValueOnce({ message: {
|
|
340
|
+
.mockResolvedValueOnce({ message: {} })
|
|
319
341
|
.mockResolvedValueOnce({
|
|
320
342
|
message: {
|
|
321
343
|
btc_test_address: 'current-device-state',
|
|
@@ -484,8 +506,56 @@ describe('openWalletSession', () => {
|
|
|
484
506
|
});
|
|
485
507
|
});
|
|
486
508
|
|
|
509
|
+
test('rejects a Protocol V1 resume when the device returns another hidden wallet', async () => {
|
|
510
|
+
const typedCall = jest.fn().mockResolvedValue({
|
|
511
|
+
type: 'PassphraseState',
|
|
512
|
+
message: {
|
|
513
|
+
passphrase_state: 'other-hidden-state',
|
|
514
|
+
session_id: 'other-session',
|
|
515
|
+
unlocked_attach_pin: false,
|
|
516
|
+
},
|
|
517
|
+
});
|
|
518
|
+
const method = new OpenWalletSession({
|
|
519
|
+
payload: {
|
|
520
|
+
method: 'openWalletSession',
|
|
521
|
+
connectId: 'connect-id',
|
|
522
|
+
mode: 'resume-hidden',
|
|
523
|
+
deviceId: 'device-1',
|
|
524
|
+
passphraseState: 'hidden-state',
|
|
525
|
+
},
|
|
526
|
+
});
|
|
527
|
+
method.init();
|
|
528
|
+
deviceWalletSessionStore.set('device-1', 'hidden-state', 'known-session');
|
|
529
|
+
const device = createDevice({ typedCall });
|
|
530
|
+
device.isProtocolV2 = () => false;
|
|
531
|
+
device.features = {
|
|
532
|
+
...device.features,
|
|
533
|
+
deviceId: 'device-1',
|
|
534
|
+
unlocked: true,
|
|
535
|
+
sessionId: null,
|
|
536
|
+
};
|
|
537
|
+
device.getCurrentFirmwareVersionString = () => '4.15.0';
|
|
538
|
+
device.getCurrentDeviceType = () => EDeviceType.Pro;
|
|
539
|
+
device.getFeatures = jest.fn();
|
|
540
|
+
method.device = device as any;
|
|
541
|
+
|
|
542
|
+
await expect(method.run()).rejects.toMatchObject({
|
|
543
|
+
errorCode: HardwareErrorCode.DeviceCheckPassphraseStateError,
|
|
544
|
+
});
|
|
545
|
+
expect(device.clearInternalState).toHaveBeenCalledTimes(1);
|
|
546
|
+
expect(deviceWalletSessionStore.get('device-1', 'hidden-state')).toBeUndefined();
|
|
547
|
+
});
|
|
548
|
+
|
|
487
549
|
test('opens the standard wallet without selecting a hidden wallet', async () => {
|
|
488
|
-
const typedCall = jest
|
|
550
|
+
const typedCall = jest
|
|
551
|
+
.fn()
|
|
552
|
+
.mockResolvedValueOnce({ message: { version: 2 } })
|
|
553
|
+
.mockResolvedValueOnce({
|
|
554
|
+
message: {
|
|
555
|
+
btc_test_address: 'main-wallet-state',
|
|
556
|
+
session_id: 'main-wallet-session',
|
|
557
|
+
},
|
|
558
|
+
});
|
|
489
559
|
const promptPassphrase = jest.fn();
|
|
490
560
|
const method = new OpenWalletSession({
|
|
491
561
|
payload: { method: 'openWalletSession', connectId: 'connect-id', mode: 'standard' },
|
|
@@ -499,19 +569,29 @@ describe('openWalletSession', () => {
|
|
|
499
569
|
protocol: 'V2',
|
|
500
570
|
walletType: 'standard',
|
|
501
571
|
deviceId: 'device-1',
|
|
502
|
-
passphraseState:
|
|
572
|
+
passphraseState: 'main-wallet-state',
|
|
503
573
|
resumed: false,
|
|
504
574
|
});
|
|
505
|
-
expect(typedCall).toHaveBeenCalledTimes(
|
|
575
|
+
expect(typedCall).toHaveBeenCalledTimes(2);
|
|
506
576
|
expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {
|
|
507
577
|
eventless_wallet_session: true,
|
|
508
578
|
});
|
|
579
|
+
expect(device.unlockDevice).toHaveBeenCalledWith(DeviceSessionPinType.Main);
|
|
580
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
|
|
509
581
|
expect(promptPassphrase).not.toHaveBeenCalled();
|
|
510
582
|
expect(device.passphraseState).toBeUndefined();
|
|
511
583
|
});
|
|
512
584
|
|
|
513
585
|
test('opens a locked Protocol V2 standard wallet after deviceId becomes available', async () => {
|
|
514
|
-
const typedCall = jest
|
|
586
|
+
const typedCall = jest
|
|
587
|
+
.fn()
|
|
588
|
+
.mockResolvedValueOnce({ message: { version: 2 } })
|
|
589
|
+
.mockResolvedValueOnce({
|
|
590
|
+
message: {
|
|
591
|
+
btc_test_address: 'main-wallet-state',
|
|
592
|
+
session_id: 'main-wallet-session',
|
|
593
|
+
},
|
|
594
|
+
});
|
|
515
595
|
const method = new OpenWalletSession({
|
|
516
596
|
payload: { method: 'openWalletSession', connectId: 'connect-id', mode: 'standard' },
|
|
517
597
|
});
|
|
@@ -538,21 +618,21 @@ describe('openWalletSession', () => {
|
|
|
538
618
|
protocol: 'V2',
|
|
539
619
|
walletType: 'standard',
|
|
540
620
|
deviceId: 'device-1',
|
|
541
|
-
passphraseState:
|
|
621
|
+
passphraseState: 'main-wallet-state',
|
|
542
622
|
resumed: false,
|
|
543
623
|
});
|
|
544
|
-
expect(device.unlockDevice).
|
|
624
|
+
expect(device.unlockDevice).toHaveBeenCalledWith(DeviceSessionPinType.Main);
|
|
545
625
|
expect(device.getDeviceState).toHaveBeenCalledTimes(2);
|
|
546
626
|
});
|
|
547
627
|
|
|
548
|
-
test('
|
|
628
|
+
test('switches from Attach PIN to Main PIN before opening the standard wallet', async () => {
|
|
549
629
|
const typedCall = jest
|
|
550
630
|
.fn()
|
|
551
631
|
.mockResolvedValueOnce({ message: { version: 2 } })
|
|
552
632
|
.mockResolvedValueOnce({
|
|
553
633
|
message: {
|
|
554
|
-
btc_test_address: '
|
|
555
|
-
session_id: '
|
|
634
|
+
btc_test_address: 'main-wallet-state',
|
|
635
|
+
session_id: 'main-wallet-session',
|
|
556
636
|
},
|
|
557
637
|
});
|
|
558
638
|
const method = new OpenWalletSession({
|
|
@@ -561,20 +641,25 @@ describe('openWalletSession', () => {
|
|
|
561
641
|
method.init();
|
|
562
642
|
const device = createDevice({ typedCall });
|
|
563
643
|
device.features.unlockedAttachPin = true;
|
|
644
|
+
device.unlockDevice = jest.fn().mockImplementation(() => {
|
|
645
|
+
device.features.unlockedAttachPin = false;
|
|
646
|
+
return Promise.resolve(device.features);
|
|
647
|
+
});
|
|
564
648
|
method.device = device as any;
|
|
565
649
|
|
|
566
|
-
await expect(method.run()).
|
|
567
|
-
|
|
650
|
+
await expect(method.run()).resolves.toMatchObject({
|
|
651
|
+
walletType: 'standard',
|
|
652
|
+
passphraseState: 'main-wallet-state',
|
|
568
653
|
});
|
|
569
|
-
expect(device.
|
|
570
|
-
expect(
|
|
654
|
+
expect(device.unlockDevice).toHaveBeenCalledWith(DeviceSessionPinType.Main);
|
|
655
|
+
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
|
|
571
656
|
});
|
|
572
657
|
|
|
573
658
|
test('selects a hidden wallet without exposing the internal device session', async () => {
|
|
574
659
|
const typedCall = jest
|
|
575
660
|
.fn()
|
|
576
661
|
.mockResolvedValueOnce({ message: { version: 2 } })
|
|
577
|
-
.mockResolvedValueOnce({ message: {
|
|
662
|
+
.mockResolvedValueOnce({ message: {} })
|
|
578
663
|
.mockResolvedValueOnce({
|
|
579
664
|
message: {
|
|
580
665
|
btc_test_address: 'hidden-state',
|
|
@@ -606,11 +691,11 @@ describe('openWalletSession', () => {
|
|
|
606
691
|
expect(deviceWalletSessionStore.get('device-1', 'hidden-state')).toBe('hidden-session');
|
|
607
692
|
});
|
|
608
693
|
|
|
609
|
-
test('
|
|
694
|
+
test('gets the prepared session after on-device passphrase entry', async () => {
|
|
610
695
|
const typedCall = jest
|
|
611
696
|
.fn()
|
|
612
697
|
.mockResolvedValueOnce({ message: { version: 2 } })
|
|
613
|
-
.mockResolvedValueOnce({ message: {
|
|
698
|
+
.mockResolvedValueOnce({ message: {} })
|
|
614
699
|
.mockResolvedValueOnce({
|
|
615
700
|
message: { btc_test_address: 'device-state', session_id: 'device-session' },
|
|
616
701
|
});
|
|
@@ -635,7 +720,7 @@ describe('openWalletSession', () => {
|
|
|
635
720
|
const typedCall = jest
|
|
636
721
|
.fn()
|
|
637
722
|
.mockResolvedValueOnce({ message: { version: 2 } })
|
|
638
|
-
.mockResolvedValueOnce({ message: {
|
|
723
|
+
.mockResolvedValueOnce({ message: {} })
|
|
639
724
|
.mockResolvedValueOnce({
|
|
640
725
|
message: { btc_test_address: 'host-state', session_id: 'host-session' },
|
|
641
726
|
});
|
|
@@ -660,7 +745,7 @@ describe('openWalletSession', () => {
|
|
|
660
745
|
expect(promptPassphrase).toHaveBeenCalledWith(
|
|
661
746
|
{
|
|
662
747
|
existsAttachPinUser: false,
|
|
663
|
-
deviceOnly:
|
|
748
|
+
deviceOnly: false,
|
|
664
749
|
source: 'wallet-session-coordinator',
|
|
665
750
|
reason: 'open-wallet',
|
|
666
751
|
},
|
|
@@ -670,6 +755,52 @@ describe('openWalletSession', () => {
|
|
|
670
755
|
passphrase: 'host hidden wallet',
|
|
671
756
|
on_device: false,
|
|
672
757
|
});
|
|
758
|
+
expect(typedCall).toHaveBeenNthCalledWith(3, 'DeviceSessionGet', 'DeviceSession', {});
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
test('normalizes a Unicode Host passphrase before sending it to Pro2 firmware', async () => {
|
|
762
|
+
const typedCall = jest
|
|
763
|
+
.fn()
|
|
764
|
+
.mockResolvedValueOnce({ message: { version: 2 } })
|
|
765
|
+
.mockResolvedValueOnce({ message: {} })
|
|
766
|
+
.mockResolvedValueOnce({
|
|
767
|
+
message: { btc_test_address: 'host-state', session_id: 'host-session' },
|
|
768
|
+
});
|
|
769
|
+
const promptPassphrase = jest.fn().mockResolvedValue({ passphrase: 'caf\u00e9' });
|
|
770
|
+
const method = new OpenWalletSession({
|
|
771
|
+
payload: { method: 'openWalletSession', connectId: 'connect-id', mode: 'select-hidden' },
|
|
772
|
+
});
|
|
773
|
+
method.init();
|
|
774
|
+
method.device = createDevice({ typedCall, promptPassphrase }) as any;
|
|
775
|
+
|
|
776
|
+
await expect(method.run()).resolves.toMatchObject({ passphraseState: 'host-state' });
|
|
777
|
+
expect(typedCall).toHaveBeenNthCalledWith(2, 'DeviceSessionAskPassphrase', 'Success', {
|
|
778
|
+
passphrase: 'cafe\u0301',
|
|
779
|
+
on_device: false,
|
|
780
|
+
});
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
test('accepts a Host passphrase at the 50-byte firmware boundary', async () => {
|
|
784
|
+
const passphrase = 'a'.repeat(50);
|
|
785
|
+
const typedCall = jest
|
|
786
|
+
.fn()
|
|
787
|
+
.mockResolvedValueOnce({ message: { version: 2 } })
|
|
788
|
+
.mockResolvedValueOnce({ message: {} })
|
|
789
|
+
.mockResolvedValueOnce({
|
|
790
|
+
message: { btc_test_address: 'host-state', session_id: 'host-session' },
|
|
791
|
+
});
|
|
792
|
+
const promptPassphrase = jest.fn().mockResolvedValue({ passphrase });
|
|
793
|
+
const method = new OpenWalletSession({
|
|
794
|
+
payload: { method: 'openWalletSession', connectId: 'connect-id', mode: 'select-hidden' },
|
|
795
|
+
});
|
|
796
|
+
method.init();
|
|
797
|
+
method.device = createDevice({ typedCall, promptPassphrase }) as any;
|
|
798
|
+
|
|
799
|
+
await expect(method.run()).resolves.toMatchObject({ passphraseState: 'host-state' });
|
|
800
|
+
expect(typedCall).toHaveBeenNthCalledWith(2, 'DeviceSessionAskPassphrase', 'Success', {
|
|
801
|
+
passphrase,
|
|
802
|
+
on_device: false,
|
|
803
|
+
});
|
|
673
804
|
});
|
|
674
805
|
|
|
675
806
|
test('selects Attach PIN only when the device reports an existing binding', async () => {
|
|
@@ -695,7 +826,7 @@ describe('openWalletSession', () => {
|
|
|
695
826
|
expect(promptPassphrase).toHaveBeenCalledWith(
|
|
696
827
|
{
|
|
697
828
|
existsAttachPinUser: true,
|
|
698
|
-
deviceOnly:
|
|
829
|
+
deviceOnly: false,
|
|
699
830
|
source: 'wallet-session-coordinator',
|
|
700
831
|
reason: 'open-wallet',
|
|
701
832
|
},
|
|
@@ -705,6 +836,179 @@ describe('openWalletSession', () => {
|
|
|
705
836
|
expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
|
|
706
837
|
});
|
|
707
838
|
|
|
839
|
+
test('uses the complete Attach PIN wire flow without a main PIN unlock', async () => {
|
|
840
|
+
const typedCall = jest
|
|
841
|
+
.fn()
|
|
842
|
+
.mockResolvedValueOnce({ message: { version: 2 } })
|
|
843
|
+
.mockResolvedValueOnce({ message: { message: 'PIN verified' } })
|
|
844
|
+
.mockResolvedValueOnce({
|
|
845
|
+
message: { btc_test_address: 'attach-state', session_id: 'attach-session' },
|
|
846
|
+
});
|
|
847
|
+
const promptPassphrase = jest.fn().mockResolvedValue({ attachPinOnDevice: true });
|
|
848
|
+
const method = new OpenWalletSession({
|
|
849
|
+
payload: { method: 'openWalletSession', connectId: 'connect-id', mode: 'select-hidden' },
|
|
850
|
+
});
|
|
851
|
+
method.init();
|
|
852
|
+
const device = createDevice({ typedCall, promptPassphrase });
|
|
853
|
+
device.features.attachToPinEnabled = true;
|
|
854
|
+
device.unlockDevice = Device.prototype.unlockDevice.bind(device);
|
|
855
|
+
method.device = device as any;
|
|
856
|
+
|
|
857
|
+
await expect(method.run()).resolves.toMatchObject({
|
|
858
|
+
walletType: 'hidden',
|
|
859
|
+
passphraseState: 'attach-state',
|
|
860
|
+
});
|
|
861
|
+
expect(device.commands.typedCall.mock.calls).toEqual([
|
|
862
|
+
['ProtocolInfoRequest', 'ProtocolInfo', { eventless_wallet_session: true }],
|
|
863
|
+
['DeviceSessionAskPin', 'Success', { type: DeviceSessionPinType.AttachToPin }],
|
|
864
|
+
['DeviceStatusGet', 'DeviceStatus', {}],
|
|
865
|
+
['DeviceSessionGet', 'DeviceSession', {}],
|
|
866
|
+
]);
|
|
867
|
+
expect(device.commands.typedCall).not.toHaveBeenCalledWith('DeviceSessionAskPin', 'Success', {
|
|
868
|
+
type: DeviceSessionPinType.Main,
|
|
869
|
+
});
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
test.each([
|
|
873
|
+
['no selection', {}],
|
|
874
|
+
['an empty Host passphrase', { passphrase: '' }],
|
|
875
|
+
['more than one selection', { passphrase: 'host hidden wallet', passphraseOnDevice: true }],
|
|
876
|
+
])('rejects %s before sending a wallet selection command', async (_name, response) => {
|
|
877
|
+
const typedCall = jest.fn().mockResolvedValueOnce({ message: { version: 2 } });
|
|
878
|
+
const promptPassphrase = jest.fn().mockResolvedValue(response);
|
|
879
|
+
const method = new OpenWalletSession({
|
|
880
|
+
payload: { method: 'openWalletSession', connectId: 'connect-id', mode: 'select-hidden' },
|
|
881
|
+
});
|
|
882
|
+
method.init();
|
|
883
|
+
method.device = createDevice({ typedCall, promptPassphrase }) as any;
|
|
884
|
+
|
|
885
|
+
await expect(method.run()).rejects.toMatchObject({
|
|
886
|
+
errorCode: HardwareErrorCode.RuntimeError,
|
|
887
|
+
message: 'Wallet selection must contain exactly one passphrase access mode.',
|
|
888
|
+
});
|
|
889
|
+
expect(typedCall).toHaveBeenCalledTimes(1);
|
|
890
|
+
});
|
|
891
|
+
|
|
892
|
+
test('stops before sending a wallet command when the App cancels selection', async () => {
|
|
893
|
+
const cancellation = new Error('Wallet selection cancelled');
|
|
894
|
+
const typedCall = jest.fn().mockResolvedValueOnce({ message: { version: 2 } });
|
|
895
|
+
const promptPassphrase = jest.fn().mockRejectedValue(cancellation);
|
|
896
|
+
const method = new OpenWalletSession({
|
|
897
|
+
payload: { method: 'openWalletSession', connectId: 'connect-id', mode: 'select-hidden' },
|
|
898
|
+
});
|
|
899
|
+
method.init();
|
|
900
|
+
method.device = createDevice({ typedCall, promptPassphrase }) as any;
|
|
901
|
+
|
|
902
|
+
await expect(method.run()).rejects.toBe(cancellation);
|
|
903
|
+
expect(typedCall).toHaveBeenCalledTimes(1);
|
|
904
|
+
});
|
|
905
|
+
|
|
906
|
+
test.each(['User cancelled', 'Device disconnected'])(
|
|
907
|
+
'does not issue a resume request after the wallet selection reports %s',
|
|
908
|
+
async message => {
|
|
909
|
+
const typedCall = jest
|
|
910
|
+
.fn()
|
|
911
|
+
.mockResolvedValueOnce({ message: { version: 2 } })
|
|
912
|
+
.mockRejectedValueOnce(new Error(message));
|
|
913
|
+
const promptPassphrase = jest.fn().mockResolvedValue({ passphraseOnDevice: true });
|
|
914
|
+
const method = new OpenWalletSession({
|
|
915
|
+
payload: { method: 'openWalletSession', connectId: 'connect-id', mode: 'select-hidden' },
|
|
916
|
+
});
|
|
917
|
+
method.init();
|
|
918
|
+
method.device = createDevice({ typedCall, promptPassphrase }) as any;
|
|
919
|
+
|
|
920
|
+
await expect(method.run()).rejects.toThrow(message);
|
|
921
|
+
expect(typedCall).toHaveBeenCalledTimes(2);
|
|
922
|
+
expect(typedCall).not.toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
|
|
923
|
+
}
|
|
924
|
+
);
|
|
925
|
+
|
|
926
|
+
test.each([
|
|
927
|
+
['a NUL byte', 'hidden\0wallet'],
|
|
928
|
+
['more than 50 UTF-8 bytes', 'a'.repeat(51)],
|
|
929
|
+
['more than 50 UTF-8 bytes after NFKD normalization', '\u00e9'.repeat(17)],
|
|
930
|
+
['an unpaired UTF-16 surrogate', '\ud800'],
|
|
931
|
+
])('rejects a Host passphrase containing %s', async (_name, passphrase) => {
|
|
932
|
+
const typedCall = jest.fn().mockResolvedValueOnce({ message: { version: 2 } });
|
|
933
|
+
const promptPassphrase = jest.fn().mockResolvedValue({ passphrase });
|
|
934
|
+
const method = new OpenWalletSession({
|
|
935
|
+
payload: { method: 'openWalletSession', connectId: 'connect-id', mode: 'select-hidden' },
|
|
936
|
+
});
|
|
937
|
+
method.init();
|
|
938
|
+
method.device = createDevice({ typedCall, promptPassphrase }) as any;
|
|
939
|
+
|
|
940
|
+
await expect(method.run()).rejects.toMatchObject({
|
|
941
|
+
errorCode: HardwareErrorCode.RuntimeError,
|
|
942
|
+
message: 'Host passphrase must contain 1 to 50 valid UTF-8 bytes without NUL.',
|
|
943
|
+
});
|
|
944
|
+
expect(typedCall).toHaveBeenCalledTimes(1);
|
|
945
|
+
});
|
|
946
|
+
|
|
947
|
+
test('rejects an unavailable Attach PIN selection before asking the device for a PIN', async () => {
|
|
948
|
+
const typedCall = jest.fn().mockResolvedValueOnce({ message: { version: 2 } });
|
|
949
|
+
const promptPassphrase = jest.fn().mockResolvedValue({ attachPinOnDevice: true });
|
|
950
|
+
const method = new OpenWalletSession({
|
|
951
|
+
payload: { method: 'openWalletSession', connectId: 'connect-id', mode: 'select-hidden' },
|
|
952
|
+
});
|
|
953
|
+
method.init();
|
|
954
|
+
const device = createDevice({ typedCall, promptPassphrase });
|
|
955
|
+
device.features.attachToPinEnabled = false;
|
|
956
|
+
method.device = device as any;
|
|
957
|
+
|
|
958
|
+
await expect(method.run()).rejects.toMatchObject({
|
|
959
|
+
errorCode: HardwareErrorCode.RuntimeError,
|
|
960
|
+
message: 'Attach PIN wallet selection is unavailable on this device.',
|
|
961
|
+
});
|
|
962
|
+
expect(device.unlockDevice).not.toHaveBeenCalled();
|
|
963
|
+
expect(typedCall).toHaveBeenCalledTimes(1);
|
|
964
|
+
});
|
|
965
|
+
|
|
966
|
+
test('unlocks and retries hidden-wallet preparation when the device is locked', async () => {
|
|
967
|
+
const lockedError = { errorCode: HardwareErrorCode.DeviceLocked };
|
|
968
|
+
const typedCall = jest
|
|
969
|
+
.fn()
|
|
970
|
+
.mockResolvedValueOnce({ message: { version: 2 } })
|
|
971
|
+
.mockRejectedValueOnce(lockedError)
|
|
972
|
+
.mockResolvedValueOnce({ message: {} })
|
|
973
|
+
.mockResolvedValueOnce({
|
|
974
|
+
message: { btc_test_address: 'hidden-state', session_id: 'hidden-session' },
|
|
975
|
+
});
|
|
976
|
+
const promptPassphrase = jest.fn().mockResolvedValue({ passphraseOnDevice: true });
|
|
977
|
+
const method = new OpenWalletSession({
|
|
978
|
+
payload: { method: 'openWalletSession', connectId: 'connect-id', mode: 'select-hidden' },
|
|
979
|
+
});
|
|
980
|
+
method.init();
|
|
981
|
+
const device = createDevice({ typedCall, promptPassphrase });
|
|
982
|
+
method.device = device as any;
|
|
983
|
+
|
|
984
|
+
await expect(method.run()).resolves.toMatchObject({
|
|
985
|
+
walletType: 'hidden',
|
|
986
|
+
passphraseState: 'hidden-state',
|
|
987
|
+
});
|
|
988
|
+
expect(device.unlockDevice).toHaveBeenCalledWith(DeviceSessionPinType.Main);
|
|
989
|
+
expect(typedCall).toHaveBeenCalledTimes(4);
|
|
990
|
+
});
|
|
991
|
+
|
|
992
|
+
test('does not fall back when wallet preparation is rejected', async () => {
|
|
993
|
+
const invalidSessionError = { errorCode: HardwareErrorCode.WalletSessionInvalid };
|
|
994
|
+
const typedCall = jest
|
|
995
|
+
.fn()
|
|
996
|
+
.mockResolvedValueOnce({ message: { version: 2 } })
|
|
997
|
+
.mockRejectedValueOnce(invalidSessionError);
|
|
998
|
+
const promptPassphrase = jest.fn().mockResolvedValue({ passphraseOnDevice: true });
|
|
999
|
+
const method = new OpenWalletSession({
|
|
1000
|
+
payload: { method: 'openWalletSession', connectId: 'connect-id', mode: 'select-hidden' },
|
|
1001
|
+
});
|
|
1002
|
+
method.init();
|
|
1003
|
+
const device = createDevice({ typedCall, promptPassphrase });
|
|
1004
|
+
method.device = device as any;
|
|
1005
|
+
|
|
1006
|
+
await expect(method.run()).rejects.toBe(invalidSessionError);
|
|
1007
|
+
expect(promptPassphrase).toHaveBeenCalledTimes(1);
|
|
1008
|
+
expect(typedCall).toHaveBeenCalledTimes(2);
|
|
1009
|
+
expect(device.updateInternalState).not.toHaveBeenCalled();
|
|
1010
|
+
});
|
|
1011
|
+
|
|
708
1012
|
test.each([
|
|
709
1013
|
{
|
|
710
1014
|
missingField: 'session_id',
|
|
@@ -720,7 +1024,7 @@ describe('openWalletSession', () => {
|
|
|
720
1024
|
const typedCall = jest
|
|
721
1025
|
.fn()
|
|
722
1026
|
.mockResolvedValueOnce({ message: { version: 2 } })
|
|
723
|
-
.mockResolvedValueOnce({ message: {
|
|
1027
|
+
.mockResolvedValueOnce({ message: {} })
|
|
724
1028
|
.mockResolvedValueOnce({ message });
|
|
725
1029
|
const promptPassphrase = jest.fn().mockResolvedValue({ passphraseOnDevice: true });
|
|
726
1030
|
const method = new OpenWalletSession({
|
|
@@ -732,7 +1036,7 @@ describe('openWalletSession', () => {
|
|
|
732
1036
|
|
|
733
1037
|
await expect(method.run()).rejects.toMatchObject({
|
|
734
1038
|
errorCode: HardwareErrorCode.RuntimeError,
|
|
735
|
-
message: '
|
|
1039
|
+
message: 'Device returned an incomplete DeviceSession response.',
|
|
736
1040
|
});
|
|
737
1041
|
expect(device.clearInternalState).toHaveBeenCalled();
|
|
738
1042
|
expect(device.updateInternalState).not.toHaveBeenCalled();
|
|
@@ -12,7 +12,7 @@ describe('encodePro2Wallpaper', () => {
|
|
|
12
12
|
expect(Array.from(result.data.slice(0, 12))).toEqual([
|
|
13
13
|
0x19, 0x12, 0, 0, 2, 0, 1, 0, 4, 0, 0, 0,
|
|
14
14
|
]);
|
|
15
|
-
expect(Array.from(result.data.slice(12))).toEqual([0x00,
|
|
15
|
+
expect(Array.from(result.data.slice(12))).toEqual([0x00, 0xf8, 0xe0, 0x07]);
|
|
16
16
|
});
|
|
17
17
|
|
|
18
18
|
test('encodes transparent pixels as RGB565A8 with alpha plane after RGB data', () => {
|